feat: add on-demand format conversion support (FBX, STL, USDZ, MP4, GIF)

- Discover /creations/resourceConvert endpoint via browser inspection
- Add resource_convert() method to api.py and api_complete.py
- Extend MODEL_FORMAT_KEYS with fbx, stl, usdz, mp4, gif
- Update get_model_urls() and download_model() with include_converted flag
- Update CLI with --converted flag for formats and download commands
- Update reverse engineering docs with native vs converted format tables
This commit is contained in:
KawasakiAkasei
2026-05-27 13:42:29 +08:00
parent ad3c86b8ba
commit 5328c213fe
5 changed files with 181 additions and 14 deletions
+55 -5
View File
@@ -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(
+55 -5
View File
@@ -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(
+7 -2
View File
@@ -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}")