feat: full format compatibility adaptation for model downloads
- Add get_model_urls() and download_model() to api.py and api_complete.py supporting all 14 discovered urlResult format keys (glb, obj, pbr maps, etc.) - Update generator.py to extract full urlResult dict instead of just modelUrl - Add CLI subcommands: formats (list available formats) and download (fetch by key) - Update reverse engineering docs with complete format key table and CLI examples
This commit is contained in:
@@ -131,6 +131,16 @@ api = Hunyuan3DAPIComplete()
|
||||
result = api.generate_from_image(resource_url, title="My Model")
|
||||
```
|
||||
|
||||
### 4. CLI Download
|
||||
|
||||
```bash
|
||||
# List available formats for a creation
|
||||
hunyuan3dweb formats <creation_id>
|
||||
|
||||
# Download a specific format (default: glb)
|
||||
hunyuan3dweb download <creation_id> --format glb -o model.glb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
@@ -150,6 +160,27 @@ result = api.generate_from_image(resource_url, title="My Model")
|
||||
| Global config | GET | `/config` | Query params |
|
||||
| Animation templates | GET | `/workflow/action/templates` | Query params |
|
||||
|
||||
### Model Download Formats
|
||||
|
||||
The `urlResult` field in `/creations/detail` contains up to 14 format keys:
|
||||
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `glb` | GLB binary model with PBR textures |
|
||||
| `obj` | OBJ model (usually zipped) |
|
||||
| `mtl` | MTL material file |
|
||||
| `obj_url` | Standalone OBJ file URL |
|
||||
| `geometryGlb` | Geometry-only GLB (no materials) |
|
||||
| `textureGlb` | GLB texture pack |
|
||||
| `textureObj` | OBJ texture pack |
|
||||
| `image_url` | Preview / thumbnail image |
|
||||
| `pbrImage` | Combined PBR texture map |
|
||||
| `pbrMetallicImage` | PBR metallic map |
|
||||
| `pbrRoughnessImage` | PBR roughness map |
|
||||
| `pbrNormalImage` | PBR normal map |
|
||||
| `invisible_wall` | Invisible collision wall |
|
||||
| `air_wall` | Air wall (collision body) |
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
@@ -131,6 +131,16 @@ api = Hunyuan3DAPIComplete()
|
||||
result = api.generate_from_image(resource_url, title="我的模型")
|
||||
```
|
||||
|
||||
### 4. CLI 下载模型
|
||||
|
||||
```bash
|
||||
# 列出某个创作所有可用的下载格式
|
||||
hunyuan3dweb formats <creation_id>
|
||||
|
||||
# 下载指定格式(默认 glb)
|
||||
hunyuan3dweb download <creation_id> --format glb -o model.glb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 端点
|
||||
@@ -150,6 +160,27 @@ result = api.generate_from_image(resource_url, title="我的模型")
|
||||
| 全局配置 | GET | `/config` | 查询参数 |
|
||||
| 动画模板 | GET | `/workflow/action/templates` | 查询参数 |
|
||||
|
||||
### 模型下载格式
|
||||
|
||||
`/creations/detail` 返回的 `urlResult` 字段最多包含 14 种格式键:
|
||||
|
||||
| 键名 | 说明 |
|
||||
|-----|------|
|
||||
| `glb` | 带 PBR 贴图的 GLB 二进制模型 |
|
||||
| `obj` | OBJ 模型(通常打包在 zip 中) |
|
||||
| `mtl` | MTL 材质文件 |
|
||||
| `obj_url` | 独立 OBJ 文件 URL |
|
||||
| `geometryGlb` | 纯几何 GLB(无材质) |
|
||||
| `textureGlb` | GLB 纹理包 |
|
||||
| `textureObj` | OBJ 纹理包 |
|
||||
| `image_url` | 预览图/缩略图 |
|
||||
| `pbrImage` | PBR 综合贴图 |
|
||||
| `pbrMetallicImage` | PBR 金属度贴图 |
|
||||
| `pbrRoughnessImage` | PBR 粗糙度贴图 |
|
||||
| `pbrNormalImage` | PBR 法线贴图 |
|
||||
| `invisible_wall` | 不可见碰撞墙 |
|
||||
| `air_wall` | 空气墙(碰撞体) |
|
||||
|
||||
---
|
||||
|
||||
## 技术细节
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
from typing import Optional, Dict, Any
|
||||
from urllib.parse import urlparse, unquote
|
||||
from .sign import sign, sign_with_custom_nonce
|
||||
|
||||
BASE_URL = "https://3d.hunyuan.tencent.com"
|
||||
@@ -17,6 +19,24 @@ API_BASE = f"{BASE_URL}/api/3d"
|
||||
class Hunyuan3DAPI:
|
||||
"""腾讯混元3D API客户端"""
|
||||
|
||||
# 模型下载格式键名(与腾讯混元3D API返回的 urlResult 字段对应)
|
||||
MODEL_FORMAT_KEYS = [
|
||||
"glb", # 带PBR贴图的GLB二进制模型
|
||||
"obj", # OBJ模型(通常打包在zip中)
|
||||
"mtl", # MTL材质文件
|
||||
"obj_url", # 独立OBJ文件URL
|
||||
"geometryGlb", # 纯几何GLB(无材质)
|
||||
"textureGlb", # GLB纹理
|
||||
"textureObj", # OBJ纹理包
|
||||
"image_url", # 预览图/缩略图
|
||||
"pbrImage", # PBR综合贴图
|
||||
"pbrMetallicImage", # PBR金属度贴图
|
||||
"pbrRoughnessImage", # PBR粗糙度贴图
|
||||
"pbrNormalImage", # PBR法线贴图
|
||||
"invisible_wall", # 不可见碰撞墙
|
||||
"air_wall", # 空气墙(碰撞体)
|
||||
]
|
||||
|
||||
def __init__(self, cookies: Optional[str] = None):
|
||||
"""
|
||||
初始化API客户端
|
||||
@@ -180,6 +200,66 @@ class Hunyuan3DAPI:
|
||||
"creationsId": creation_id
|
||||
})
|
||||
|
||||
def get_model_urls(self, creation_id: str) -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
获取指定创作所有可用格式的下载URL
|
||||
|
||||
Returns:
|
||||
格式键名 -> 下载URL 的字典,空值已被过滤
|
||||
"""
|
||||
status = self.get_generation_status(creation_id)
|
||||
|
||||
# 兼容两种响应包装格式
|
||||
if "code" in status:
|
||||
data = status.get("data", {})
|
||||
else:
|
||||
data = status
|
||||
|
||||
result_list = data.get("result", [])
|
||||
if not result_list:
|
||||
return {}
|
||||
|
||||
url_result = result_list[0].get("urlResult", {})
|
||||
return {
|
||||
key: val
|
||||
for key in self.MODEL_FORMAT_KEYS
|
||||
if (val := url_result.get(key)) and val not in (None, "", {})
|
||||
}
|
||||
|
||||
def download_model(self, creation_id: str, format_key: str = "glb",
|
||||
output_path: Optional[str] = None) -> str:
|
||||
"""
|
||||
下载指定格式的模型文件
|
||||
|
||||
Args:
|
||||
creation_id: 创作ID
|
||||
format_key: 格式键名,如 'glb' / 'obj' / 'pbrNormalImage' 等
|
||||
output_path: 本地保存路径,为空时自动从URL推断文件名
|
||||
|
||||
Returns:
|
||||
实际保存的本地文件路径
|
||||
"""
|
||||
urls = self.get_model_urls(creation_id)
|
||||
if format_key not in urls:
|
||||
available = ", ".join(urls.keys())
|
||||
raise ValueError(
|
||||
f"格式 '{format_key}' 不可用。可用格式: {available}"
|
||||
)
|
||||
|
||||
url = urls[format_key]
|
||||
resp = self.session.get(url, timeout=120)
|
||||
resp.raise_for_status()
|
||||
|
||||
if output_path is None:
|
||||
parsed = urlparse(url)
|
||||
filename = unquote(os.path.basename(parsed.path)) or f"{creation_id}_{format_key}"
|
||||
output_path = filename
|
||||
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
|
||||
return os.path.abspath(output_path)
|
||||
|
||||
def wait_for_completion(self, creation_id: str, timeout: int = 600,
|
||||
poll_interval: int = 5) -> Dict[str, Any]:
|
||||
"""
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
支持所有生成模式:图生3D、文生3D、草图生3D、动画生成、纹理生成、智能拓扑
|
||||
"""
|
||||
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
from typing import Optional, Dict, Any, List
|
||||
from urllib.parse import urlparse, unquote
|
||||
from .sign import sign
|
||||
|
||||
BASE_URL = "https://3d.hunyuan.tencent.com"
|
||||
@@ -47,6 +49,24 @@ class Hunyuan3DAPI:
|
||||
TOPO_MEDIUM = 18000
|
||||
TOPO_HIGH = 30000
|
||||
|
||||
# 模型下载格式键名(与腾讯混元3D API返回的 urlResult 字段对应)
|
||||
MODEL_FORMAT_KEYS = [
|
||||
"glb", # 带PBR贴图的GLB二进制模型
|
||||
"obj", # OBJ模型(通常打包在zip中)
|
||||
"mtl", # MTL材质文件
|
||||
"obj_url", # 独立OBJ文件URL
|
||||
"geometryGlb", # 纯几何GLB(无材质)
|
||||
"textureGlb", # GLB纹理
|
||||
"textureObj", # OBJ纹理包
|
||||
"image_url", # 预览图/缩略图
|
||||
"pbrImage", # PBR综合贴图
|
||||
"pbrMetallicImage", # PBR金属度贴图
|
||||
"pbrRoughnessImage", # PBR粗糙度贴图
|
||||
"pbrNormalImage", # PBR法线贴图
|
||||
"invisible_wall", # 不可见碰撞墙
|
||||
"air_wall", # 空气墙(碰撞体)
|
||||
]
|
||||
|
||||
def __init__(self, cookies: Optional[str] = None):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
@@ -124,6 +144,66 @@ class Hunyuan3DAPI:
|
||||
"creationsId": creation_id
|
||||
})
|
||||
|
||||
def get_model_urls(self, creation_id: str) -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
获取指定创作所有可用格式的下载URL
|
||||
|
||||
Returns:
|
||||
格式键名 -> 下载URL 的字典,空值已被过滤
|
||||
"""
|
||||
status = self.get_generation_status(creation_id)
|
||||
|
||||
# 兼容两种响应包装格式
|
||||
if "code" in status:
|
||||
data = status.get("data", {})
|
||||
else:
|
||||
data = status
|
||||
|
||||
result_list = data.get("result", [])
|
||||
if not result_list:
|
||||
return {}
|
||||
|
||||
url_result = result_list[0].get("urlResult", {})
|
||||
return {
|
||||
key: val
|
||||
for key in self.MODEL_FORMAT_KEYS
|
||||
if (val := url_result.get(key)) and val not in (None, "", {})
|
||||
}
|
||||
|
||||
def download_model(self, creation_id: str, format_key: str = "glb",
|
||||
output_path: Optional[str] = None) -> str:
|
||||
"""
|
||||
下载指定格式的模型文件
|
||||
|
||||
Args:
|
||||
creation_id: 创作ID
|
||||
format_key: 格式键名,如 'glb' / 'obj' / 'pbrNormalImage' 等
|
||||
output_path: 本地保存路径,为空时自动从URL推断文件名
|
||||
|
||||
Returns:
|
||||
实际保存的本地文件路径
|
||||
"""
|
||||
urls = self.get_model_urls(creation_id)
|
||||
if format_key not in urls:
|
||||
available = ", ".join(urls.keys())
|
||||
raise ValueError(
|
||||
f"格式 '{format_key}' 不可用。可用格式: {available}"
|
||||
)
|
||||
|
||||
url = urls[format_key]
|
||||
resp = self.session.get(url, timeout=120)
|
||||
resp.raise_for_status()
|
||||
|
||||
if output_path is None:
|
||||
parsed = urlparse(url)
|
||||
filename = unquote(os.path.basename(parsed.path)) or f"{creation_id}_{format_key}"
|
||||
output_path = filename
|
||||
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
|
||||
return os.path.abspath(output_path)
|
||||
|
||||
def cancel_generation(self, creation_id: str) -> Dict[str, Any]:
|
||||
"""取消生成任务"""
|
||||
return self._make_request("POST", "/creations/cancel", data={
|
||||
|
||||
@@ -68,7 +68,7 @@ def generate_3d(image_path, wait_for_complete=False, timeout=300):
|
||||
page = context.new_page()
|
||||
|
||||
# 用于存储结果
|
||||
result = {"creationsId": None, "status": None, "modelUrl": None}
|
||||
result = {"creationsId": None, "status": None, "urlResult": {}}
|
||||
|
||||
# 监听响应
|
||||
def handle_response(response):
|
||||
@@ -79,7 +79,7 @@ def generate_3d(image_path, wait_for_complete=False, timeout=300):
|
||||
if "creationsId" in body:
|
||||
result["creationsId"] = body["creationsId"]
|
||||
print(f"✅ 生成已触发,creationsId: {body['creationsId']}")
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
elif "creations/detail" in url and response.status == 200:
|
||||
try:
|
||||
@@ -87,9 +87,13 @@ def generate_3d(image_path, wait_for_complete=False, timeout=300):
|
||||
result["status"] = body.get("status")
|
||||
if "result" in body and isinstance(body["result"], list) and len(body["result"]) > 0:
|
||||
model_data = body["result"][0]
|
||||
if "modelUrl" in model_data:
|
||||
result["modelUrl"] = model_data["modelUrl"]
|
||||
except:
|
||||
url_result = model_data.get("urlResult", {})
|
||||
if url_result:
|
||||
result["urlResult"] = {
|
||||
k: v for k, v in url_result.items()
|
||||
if v not in (None, "", {})
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
page.on("response", handle_response)
|
||||
@@ -147,12 +151,16 @@ def generate_3d(image_path, wait_for_complete=False, timeout=300):
|
||||
last_status = status
|
||||
|
||||
if status == "success":
|
||||
# 提取模型URL
|
||||
# 提取所有可用格式URL
|
||||
if "result" in status_result and len(status_result["result"]) > 0:
|
||||
model_data = status_result["result"][0]
|
||||
result["modelUrl"] = model_data.get("modelUrl")
|
||||
result["previewUrl"] = model_data.get("previewUrl")
|
||||
print(f"\n✅ 生成完成!")
|
||||
url_result = model_data.get("urlResult", {})
|
||||
if url_result:
|
||||
result["urlResult"] = {
|
||||
k: v for k, v in url_result.items()
|
||||
if v not in (None, "", {})
|
||||
}
|
||||
print(f"\n✅ 生成完成!可用格式: {', '.join(result['urlResult'].keys())}")
|
||||
break
|
||||
elif status == "fail":
|
||||
print(f"\n❌ 生成失败")
|
||||
|
||||
@@ -31,6 +31,18 @@ def main():
|
||||
status_parser.add_argument("creation_id", help="创作ID")
|
||||
status_parser.add_argument("--cookies", "-c", default=_DEFAULT_COOKIE, help="Cookie文件路径")
|
||||
|
||||
formats_parser = subparsers.add_parser("formats", help="列出创作可用下载格式")
|
||||
formats_parser.add_argument("creation_id", help="创作ID")
|
||||
formats_parser.add_argument("--cookies", "-c", default=_DEFAULT_COOKIE, help="Cookie文件路径")
|
||||
|
||||
download_parser = subparsers.add_parser("download", help="下载指定格式模型")
|
||||
download_parser.add_argument("creation_id", help="创作ID")
|
||||
download_parser.add_argument("--format", "-f", default="glb",
|
||||
help="格式键名 (默认: glb)")
|
||||
download_parser.add_argument("--output", "-o", default=None,
|
||||
help="本地保存路径 (默认自动推断)")
|
||||
download_parser.add_argument("--cookies", "-c", default=_DEFAULT_COOKIE, help="Cookie文件路径")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
@@ -49,6 +61,16 @@ def main():
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
elif args.command == "status":
|
||||
print(json.dumps(api.get_generation_status(args.creation_id), indent=2, ensure_ascii=False))
|
||||
elif args.command == "formats":
|
||||
urls = api.get_model_urls(args.creation_id)
|
||||
if not urls:
|
||||
print("暂无可用格式,可能生成未完成或失败。", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
for key, url in urls.items():
|
||||
print(f"{key}: {url}")
|
||||
elif args.command == "download":
|
||||
path = api.download_model(args.creation_id, args.format, args.output)
|
||||
print(f"已下载: {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user