Files
hy3d/hunyuan3dweb/browser/generator.py
T
KawasakiAkasei ad3c86b8ba 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
2026-05-27 11:59:48 +08:00

249 lines
8.6 KiB
Python

#!/usr/bin/env python3
"""
腾讯混元3D 图生3D 完整自动化脚本
使用 cloakbrowser 在浏览器内完成所有操作(包括签名生成)
"""
import contextlib
import json
import os
import shutil
import sys
import tempfile
import time
from datetime import datetime
from cloakbrowser import launch_persistent_context
from ..config import get_profile_dir
PROFILE_DIR = str(get_profile_dir())
@contextlib.contextmanager
def _temp_persistent_context(headless=True):
"""
将标准 profile 复制到临时目录后启动持久化上下文,
避免与 login 进程发生 Chromium SingletonLock 冲突。
"""
standard = str(get_profile_dir())
if not os.path.exists(standard):
raise FileNotFoundError(f"未找到登录状态目录: {standard}")
temp_root = tempfile.mkdtemp(prefix="hunyuan3dweb-profile-")
temp_profile = os.path.join(temp_root, "profile")
shutil.copytree(standard, temp_profile, dirs_exist_ok=True)
context = None
try:
context = launch_persistent_context(temp_profile, headless=headless)
yield context
finally:
if context is not None:
try:
context.close()
except Exception:
pass
try:
shutil.rmtree(temp_root)
except Exception:
pass
def generate_3d(image_path, wait_for_complete=False, timeout=300):
"""
上传图片并触发生成3D模型
Args:
image_path: 图片路径
wait_for_complete: 是否等待生成完成
timeout: 最长等待时间(秒)
Returns:
dict: 包含 creationsId 和状态信息
"""
if not os.path.exists(image_path):
raise FileNotFoundError(f"图片不存在: {image_path}")
with _temp_persistent_context(headless=True) as context:
page = context.new_page()
# 用于存储结果
result = {"creationsId": None, "status": None, "urlResult": {}}
# 监听响应
def handle_response(response):
url = response.url
if "creations/generations" in url and response.status == 200:
try:
body = response.json()
if "creationsId" in body:
result["creationsId"] = body["creationsId"]
print(f"✅ 生成已触发,creationsId: {body['creationsId']}")
except Exception:
pass
elif "creations/detail" in url and response.status == 200:
try:
body = response.json()
result["status"] = body.get("status")
if "result" in body and isinstance(body["result"], list) and len(body["result"]) > 0:
model_data = body["result"][0]
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)
print("[1/6] 打开首页...")
page.goto("https://3d.hunyuan.tencent.com/", timeout=60000)
page.wait_for_timeout(3000)
print("[2/6] 点击 AI创作...")
page.locator("button").filter(has_text="AI创作").first.click()
page.locator("text=图生3D").wait_for(state="visible", timeout=10000)
print("[3/6] 点击 图生3D...")
page.locator("text=图生3D").first.click()
page.locator("text=单张图片").wait_for(state="visible", timeout=10000)
print("[4/6] 点击 单张图片...")
page.locator("text=单张图片").first.click()
page.locator('input[type=file]').first.wait_for(state="visible", timeout=10000)
print("[5/6] 上传图片...")
page.locator('input[type=file]').first.set_input_files(image_path)
page.locator("text=立即生成").wait_for(state="visible", timeout=10000)
print("[6/6] 点击 立即生成...")
page.locator("text=立即生成").first.click()
# 等待生成触发
page.wait_for_timeout(5000)
if not result["creationsId"]:
print("⚠️ 未获取到 creationsId,可能生成失败")
return result
if wait_for_complete:
print(f"\n⏳ 等待生成完成(最长 {timeout} 秒)...")
start = time.time()
last_status = None
while time.time() - start < timeout:
# 轮询状态
status_result = page.evaluate('''
async (creationsId) => {
const resp = await fetch(`https://3d.hunyuan.tencent.com/api/3d/creations/detail?creationsId=${creationsId}`, {
headers: {'X-Source': 'web', 'Referer': 'https://3d.hunyuan.tencent.com/'}
});
return await resp.json();
}
''', result["creationsId"])
status = status_result.get("status", "unknown")
progress = status_result.get("progress", 0)
if status != last_status:
print(f" 状态: {status} (进度: {progress}%)")
last_status = status
if status == "success":
# 提取所有可用格式URL
if "result" in status_result and len(status_result["result"]) > 0:
model_data = status_result["result"][0]
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❌ 生成失败")
break
time.sleep(3)
else:
print(f"\n⏰ 超时,当前状态: {last_status}")
return result
def get_quota():
"""获取当前配额信息"""
with _temp_persistent_context(headless=True) as context:
page = context.new_page()
page.goto("https://3d.hunyuan.tencent.com/", timeout=60000)
page.wait_for_timeout(3000)
result = page.evaluate('''
async () => {
const resp = await fetch('https://3d.hunyuan.tencent.com/api/3d/quotainfo', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Source': 'web', 'Referer': 'https://3d.hunyuan.tencent.com/'},
body: '{"sceneType":"3dCreations"}'
});
return await resp.json();
}
''')
return result
def get_creations_list():
"""获取作品列表"""
with _temp_persistent_context(headless=True) as context:
page = context.new_page()
page.goto("https://3d.hunyuan.tencent.com/", timeout=60000)
page.wait_for_timeout(3000)
result = page.evaluate('''
async () => {
const resp = await fetch('https://3d.hunyuan.tencent.com/api/3d/creations/list', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Source': 'web', 'Referer': 'https://3d.hunyuan.tencent.com/'},
body: '{"page":1,"pageSize":20}'
});
return await resp.json();
}
''')
return result
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法:")
print(f" python {sys.argv[0]} quota # 查询配额")
print(f" python {sys.argv[0]} list # 查询作品列表")
print(f" python {sys.argv[0]} generate <图片路径> [wait] # 生成3D模型")
sys.exit(1)
cmd = sys.argv[1]
if cmd == "quota":
quota = get_quota()
print(json.dumps(quota, indent=2, ensure_ascii=False))
elif cmd == "list":
creations = get_creations_list()
print(json.dumps(creations, indent=2, ensure_ascii=False))
elif cmd == "generate":
if len(sys.argv) < 3:
print("请提供图片路径")
sys.exit(1)
image_path = sys.argv[2]
wait = len(sys.argv) > 3 and sys.argv[3] == "wait"
result = generate_3d(image_path, wait_for_complete=wait)
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
print(f"未知命令: {cmd}")