diff --git a/README_REVERSE_ENGINEERING.md b/README_REVERSE_ENGINEERING.md index 936e0dc..479d27f 100644 --- a/README_REVERSE_ENGINEERING.md +++ b/README_REVERSE_ENGINEERING.md @@ -137,8 +137,14 @@ result = api.generate_from_image(resource_url, title="My Model") # List available formats for a creation hunyuan3dweb formats +# Include converted formats (fbx, stl, usdz, mp4, gif) +hunyuan3dweb formats --converted + # Download a specific format (default: glb) hunyuan3dweb download --format glb -o model.glb + +# Download a converted format +hunyuan3dweb download --format usdz --converted -o model.usdz ``` --- @@ -154,6 +160,7 @@ hunyuan3dweb download --format glb -o model.glb | Creation list | POST | `/creations/list` | Query params | | Creation count | POST | `/creations/count` | Query params | | Cancel generation | POST | `/creations/cancel` | Query params | +| Resource convert | POST | `/creations/resourceConvert` | Query params | | Upload credentials | POST | `/resource/genUploadInfo` | Query params | | Resource review | POST | `/resource/review` | Query params | | Create share | POST | `/share` | Query params | @@ -162,7 +169,9 @@ hunyuan3dweb download --format glb -o model.glb ### Model Download Formats -The `urlResult` field in `/creations/detail` contains up to 14 format keys: +#### Native Formats (from `urlResult`) + +The `urlResult` field in `/creations/detail` contains up to 14 native format keys: | Key | Description | |-----|-------------| @@ -181,6 +190,28 @@ The `urlResult` field in `/creations/detail` contains up to 14 format keys: | `invisible_wall` | Invisible collision wall | | `air_wall` | Air wall (collision body) | +#### Converted Formats (via `/creations/resourceConvert`) + +Additional formats are generated on-demand by converting the OBJ zip: + +| Key | Description | Source | +|-----|-------------|--------| +| `fbx` | Autodesk FBX format | OBJ zip conversion | +| `stl` | STL format (3D printing) | OBJ zip conversion | +| `usdz` | USDZ format (iOS AR Quick Look) | OBJ zip conversion | +| `mp4` | MP4 video format | OBJ zip conversion | +| `gif` | GIF animation format | OBJ zip conversion | + +**Conversion API**: +```python +POST /creations/resourceConvert +Body: { + "sourceResource": [{"format": "zip", "url": ""}], + "targetFormatList": ["usdz", "fbx"] +} +Response: {"convertResult": [{"format": "usdz", "url": "..."}]} +``` + --- ## Technical Details diff --git a/README_REVERSE_ENGINEERING_CN.md b/README_REVERSE_ENGINEERING_CN.md index ded3a42..d47395c 100644 --- a/README_REVERSE_ENGINEERING_CN.md +++ b/README_REVERSE_ENGINEERING_CN.md @@ -137,8 +137,14 @@ result = api.generate_from_image(resource_url, title="我的模型") # 列出某个创作所有可用的下载格式 hunyuan3dweb formats +# 包含转换格式(fbx, stl, usdz, mp4, gif) +hunyuan3dweb formats --converted + # 下载指定格式(默认 glb) hunyuan3dweb download --format glb -o model.glb + +# 下载转换格式(如 usdz) +hunyuan3dweb download --format usdz --converted -o model.usdz ``` --- @@ -154,6 +160,7 @@ hunyuan3dweb download --format glb -o model.glb | 作品列表 | POST | `/creations/list` | 查询参数 | | 作品数量 | POST | `/creations/count` | 查询参数 | | 取消生成 | POST | `/creations/cancel` | 查询参数 | +| 资源转换 | POST | `/creations/resourceConvert` | 查询参数 | | 上传凭证 | POST | `/resource/genUploadInfo` | 查询参数 | | 资源审核 | POST | `/resource/review` | 查询参数 | | 创建分享 | POST | `/share` | 查询参数 | @@ -162,7 +169,9 @@ hunyuan3dweb download --format glb -o model.glb ### 模型下载格式 -`/creations/detail` 返回的 `urlResult` 字段最多包含 14 种格式键: +#### 原生格式(来自 `urlResult`) + +`/creations/detail` 返回的 `urlResult` 字段包含 14 种原生格式键: | 键名 | 说明 | |-----|------| @@ -181,6 +190,28 @@ hunyuan3dweb download --format glb -o model.glb | `invisible_wall` | 不可见碰撞墙 | | `air_wall` | 空气墙(碰撞体) | +#### 转换格式(通过 `/creations/resourceConvert`) + +以下格式由网站按需将 OBJ zip 转换生成: + +| 键名 | 说明 | 来源 | +|-----|------|------| +| `fbx` | Autodesk FBX 格式 | OBJ zip 转换 | +| `stl` | STL 格式(3D打印) | OBJ zip 转换 | +| `usdz` | USDZ 格式(iOS AR) | OBJ zip 转换 | +| `mp4` | MP4 视频格式 | OBJ zip 转换 | +| `gif` | GIF 动图格式 | OBJ zip 转换 | + +**转换接口示例**: +```python +POST /creations/resourceConvert +Body: { + "sourceResource": [{"format": "zip", "url": ""}], + "targetFormatList": ["usdz", "fbx"] +} +Response: {"convertResult": [{"format": "usdz", "url": "..."}]} +``` + --- ## 技术细节 diff --git a/hunyuan3dweb/api.py b/hunyuan3dweb/api.py index a995818..173a06a 100644 --- a/hunyuan3dweb/api.py +++ b/hunyuan3dweb/api.py @@ -19,7 +19,9 @@ API_BASE = f"{BASE_URL}/api/3d" class Hunyuan3DAPI: """腾讯混元3D API客户端""" - # 模型下载格式键名(与腾讯混元3D API返回的 urlResult 字段对应) + # 模型下载格式键名 + # 原生格式:直接来自 API urlResult + # 转换格式:通过 /creations/resourceConvert 从 OBJ(zip) 转换得到 MODEL_FORMAT_KEYS = [ "glb", # 带PBR贴图的GLB二进制模型 "obj", # OBJ模型(通常打包在zip中) @@ -35,6 +37,11 @@ class Hunyuan3DAPI: "pbrNormalImage", # PBR法线贴图 "invisible_wall", # 不可见碰撞墙 "air_wall", # 空气墙(碰撞体) + "fbx", # FBX 格式(转换) + "stl", # STL 格式(转换) + "usdz", # USDZ 格式(转换,用于 iOS AR) + "mp4", # MP4 视频格式(转换) + "gif", # GIF 动图格式(转换) ] def __init__(self, cookies: Optional[str] = None): @@ -200,10 +207,41 @@ class Hunyuan3DAPI: "creationsId": creation_id }) - def get_model_urls(self, creation_id: str) -> Dict[str, Optional[str]]: + def resource_convert(self, source_url: str, target_formats: List[str]) -> Dict[str, str]: + """ + 调用资源转换接口将 OBJ(zip) 转换为其他格式 + + Args: + source_url: OBJ zip 文件的 URL(来自 urlResult['obj']) + target_formats: 目标格式列表,如 ["usdz", "fbx"] + + Returns: + 格式键名 -> 转换后下载URL 的字典 + """ + data = { + "sourceResource": [ + {"format": "zip", "url": source_url} + ], + "targetFormatList": target_formats + } + resp = self._make_request("POST", "/creations/resourceConvert", data=data) + result = {} + for item in resp.get("convertResult", []): + fmt = item.get("format") + url = item.get("url") + if fmt and url: + result[fmt] = url + return result + + def get_model_urls(self, creation_id: str, include_converted: bool = False) -> Dict[str, Optional[str]]: """ 获取指定创作所有可用格式的下载URL + Args: + creation_id: 创作ID + include_converted: 是否包含转换格式(fbx/stl/usdz/mp4/gif), + 启用时会调用 resourceConvert 接口 + Returns: 格式键名 -> 下载URL 的字典,空值已被过滤 """ @@ -220,14 +258,26 @@ class Hunyuan3DAPI: return {} url_result = result_list[0].get("urlResult", {}) - return { + urls = { key: val for key in self.MODEL_FORMAT_KEYS if (val := url_result.get(key)) and val not in (None, "", {}) } + if include_converted: + obj_url = urls.get("obj") + if obj_url: + converted = self.resource_convert( + obj_url, + ["fbx", "stl", "usdz", "mp4", "gif"] + ) + urls.update(converted) + + return urls + def download_model(self, creation_id: str, format_key: str = "glb", - output_path: Optional[str] = None) -> str: + output_path: Optional[str] = None, + include_converted: bool = False) -> str: """ 下载指定格式的模型文件 @@ -239,7 +289,7 @@ class Hunyuan3DAPI: Returns: 实际保存的本地文件路径 """ - urls = self.get_model_urls(creation_id) + urls = self.get_model_urls(creation_id, include_converted=include_converted) if format_key not in urls: available = ", ".join(urls.keys()) raise ValueError( diff --git a/hunyuan3dweb/api_complete.py b/hunyuan3dweb/api_complete.py index b1d6e8b..aa2b66e 100644 --- a/hunyuan3dweb/api_complete.py +++ b/hunyuan3dweb/api_complete.py @@ -49,7 +49,9 @@ class Hunyuan3DAPI: TOPO_MEDIUM = 18000 TOPO_HIGH = 30000 - # 模型下载格式键名(与腾讯混元3D API返回的 urlResult 字段对应) + # 模型下载格式键名 + # 原生格式:直接来自 API urlResult + # 转换格式:通过 /creations/resourceConvert 从 OBJ(zip) 转换得到 MODEL_FORMAT_KEYS = [ "glb", # 带PBR贴图的GLB二进制模型 "obj", # OBJ模型(通常打包在zip中) @@ -65,6 +67,11 @@ class Hunyuan3DAPI: "pbrNormalImage", # PBR法线贴图 "invisible_wall", # 不可见碰撞墙 "air_wall", # 空气墙(碰撞体) + "fbx", # FBX 格式(转换) + "stl", # STL 格式(转换) + "usdz", # USDZ 格式(转换,用于 iOS AR) + "mp4", # MP4 视频格式(转换) + "gif", # GIF 动图格式(转换) ] def __init__(self, cookies: Optional[str] = None): @@ -144,10 +151,41 @@ class Hunyuan3DAPI: "creationsId": creation_id }) - def get_model_urls(self, creation_id: str) -> Dict[str, Optional[str]]: + def resource_convert(self, source_url: str, target_formats: List[str]) -> Dict[str, str]: + """ + 调用资源转换接口将 OBJ(zip) 转换为其他格式 + + Args: + source_url: OBJ zip 文件的 URL(来自 urlResult['obj']) + target_formats: 目标格式列表,如 ["usdz", "fbx"] + + Returns: + 格式键名 -> 转换后下载URL 的字典 + """ + data = { + "sourceResource": [ + {"format": "zip", "url": source_url} + ], + "targetFormatList": target_formats + } + resp = self._make_request("POST", "/creations/resourceConvert", data=data) + result = {} + for item in resp.get("convertResult", []): + fmt = item.get("format") + url = item.get("url") + if fmt and url: + result[fmt] = url + return result + + def get_model_urls(self, creation_id: str, include_converted: bool = False) -> Dict[str, Optional[str]]: """ 获取指定创作所有可用格式的下载URL + Args: + creation_id: 创作ID + include_converted: 是否包含转换格式(fbx/stl/usdz/mp4/gif), + 启用时会调用 resourceConvert 接口 + Returns: 格式键名 -> 下载URL 的字典,空值已被过滤 """ @@ -164,14 +202,26 @@ class Hunyuan3DAPI: return {} url_result = result_list[0].get("urlResult", {}) - return { + urls = { key: val for key in self.MODEL_FORMAT_KEYS if (val := url_result.get(key)) and val not in (None, "", {}) } + if include_converted: + obj_url = urls.get("obj") + if obj_url: + converted = self.resource_convert( + obj_url, + ["fbx", "stl", "usdz", "mp4", "gif"] + ) + urls.update(converted) + + return urls + def download_model(self, creation_id: str, format_key: str = "glb", - output_path: Optional[str] = None) -> str: + output_path: Optional[str] = None, + include_converted: bool = False) -> str: """ 下载指定格式的模型文件 @@ -183,7 +233,7 @@ class Hunyuan3DAPI: Returns: 实际保存的本地文件路径 """ - urls = self.get_model_urls(creation_id) + urls = self.get_model_urls(creation_id, include_converted=include_converted) if format_key not in urls: available = ", ".join(urls.keys()) raise ValueError( diff --git a/hunyuan3dweb/cli.py b/hunyuan3dweb/cli.py index 248e76f..99a7bb5 100644 --- a/hunyuan3dweb/cli.py +++ b/hunyuan3dweb/cli.py @@ -33,6 +33,8 @@ def main(): formats_parser = subparsers.add_parser("formats", help="列出创作可用下载格式") formats_parser.add_argument("creation_id", help="创作ID") + formats_parser.add_argument("--converted", action="store_true", + help="包含转换格式 (fbx/stl/usdz/mp4/gif)") formats_parser.add_argument("--cookies", "-c", default=_DEFAULT_COOKIE, help="Cookie文件路径") download_parser = subparsers.add_parser("download", help="下载指定格式模型") @@ -41,6 +43,8 @@ def main(): help="格式键名 (默认: glb)") download_parser.add_argument("--output", "-o", default=None, help="本地保存路径 (默认自动推断)") + download_parser.add_argument("--converted", action="store_true", + help="若格式为转换格式,自动调用转换接口") download_parser.add_argument("--cookies", "-c", default=_DEFAULT_COOKIE, help="Cookie文件路径") args = parser.parse_args() @@ -62,14 +66,15 @@ def main(): 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) + urls = api.get_model_urls(args.creation_id, include_converted=args.converted) 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) + path = api.download_model(args.creation_id, args.format, args.output, + include_converted=args.converted) print(f"已下载: {path}")