Files
hy3d/hunyuan3dweb/api_complete.py
T
Claude 734d53dafb fix: prevent browser process leaks and improve robustness
- fix(browser): wrap context lifecycle in try/finally to ensure browser
  closes on exceptions and KeyboardInterrupt (login, sniffer, generator)
- fix(browser): replace time.sleep with Playwright native waits
  (wait_for, wait_for_timeout) for more reliable element interaction
- fix(browser): use parameterized page.evaluate instead of f-string
  JS injection in generator polling
- fix(api): add retry logic in wait_for_completion to survive transient
  network errors
- fix(config): add prepare_profile_dir to copy profile to temp dir,
  preventing Chromium SingletonLock conflicts when tools run concurrently
- fix(sniffer): stream API logs to tempfile instead of unbounded memory
  list to avoid OOM
- fix(api): specify encoding='utf-8' when loading cookies from file

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 23:08:46 +08:00

433 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
腾讯混元3D 完整API客户端
支持所有生成模式:图生3D、文生3D、草图生3D、动画生成、纹理生成、智能拓扑
"""
import requests
import json
import time
from typing import Optional, Dict, Any, List
from .sign import sign
BASE_URL = "https://3d.hunyuan.tencent.com"
API_BASE = f"{BASE_URL}/api/3d"
class Hunyuan3DAPI:
"""腾讯混元3D 完整API客户端"""
# 生成模式常量
MODEL_TYPE_IMAGE = "image2ModelV3.1"
MODEL_TYPE_TEXT = "text2ModelV3.1"
MODEL_TYPE_SKETCH = "sketch2ModelV3.1"
MODEL_TYPE_ANIMATION = "animation3dV2"
MODEL_TYPE_TEXTURE = "textureTo3DV2"
MODEL_TYPE_LOWPOLY = "lowpolyV2"
# 动作类型
MOTION_CAPOEIRA = 9
MOTION_FALLING = 10
MOTION_JUMPING = 11
MOTION_KICKING = 12
MOTION_SWORD = 13
MOTION_RUNNING = 15
MOTION_DANCING = 16
# 纹理风格
STYLE_DEFAULT = ""
STYLE_SCULPTURE = "sculpture"
STYLE_QINGHUA = "qinghuaci"
STYLE_CHINA = "china_style"
STYLE_CARTOON = "cartoon"
STYLE_CYBERPUNK = "cyberpunk"
# 拓扑面数
TOPO_LOW = 5000
TOPO_MEDIUM = 18000
TOPO_HIGH = 30000
def __init__(self, cookies: Optional[str] = None):
self.session = requests.Session()
self.session.headers.update({
"Content-Type": "application/json",
"x-source": "web",
"x-product": "hunyuan3d",
"Origin": BASE_URL,
"Referer": f"{BASE_URL}/",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
})
if cookies:
self.set_cookies(cookies)
def set_cookies(self, cookies: str):
self.session.headers["Cookie"] = cookies
def _make_request(self, method: str, endpoint: str, params: Optional[Dict] = None,
data: Optional[Dict] = None) -> Dict[str, Any]:
url = f"{API_BASE}{endpoint}"
sign_params = dict(params) if params else {}
signed_params = sign(sign_params)
timestamp = signed_params.pop("timestamp")
nonce = signed_params.pop("nonce")
sign_value = signed_params.pop("sign")
query_params = {}
if params:
query_params.update(params)
query_params.update({
"timestamp": timestamp,
"nonce": nonce,
"sign": sign_value
})
if method.upper() == "GET":
response = self.session.get(url, params=query_params, timeout=30)
else:
response = self.session.post(url, params=query_params, json=data, timeout=30)
response.raise_for_status()
return response.json()
# ========== 用户接口 ==========
def get_user_info(self) -> Dict[str, Any]:
"""获取用户信息"""
return self._make_request("GET", "/getuserinfo")
def get_quota_info(self, scene_type: str = "3dCreations") -> Dict[str, Any]:
"""获取配额信息"""
return self._make_request("POST", "/quotainfo", data={"sceneType": scene_type})
# ========== 作品管理 ==========
def get_creation_list(self, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
"""获取作品列表"""
return self._make_request("POST", "/creations/list", data={
"page": page,
"pageSize": page_size
})
def get_creation_count(self, status_list: Optional[List[str]] = None) -> Dict[str, Any]:
"""获取作品数量"""
data = {"statusList": status_list or ["wait", "processing", "success", "fail"]}
return self._make_request("POST", "/creations/count", data=data)
def get_generation_status(self, creation_id: str) -> Dict[str, Any]:
"""获取生成状态"""
return self._make_request("GET", "/creations/detail", params={
"creationsId": creation_id
})
def cancel_generation(self, creation_id: str) -> Dict[str, Any]:
"""取消生成任务"""
return self._make_request("POST", "/creations/cancel", data={
"creationsId": creation_id
})
# ========== 核心生成接口 ==========
def _generate(self, model_type: str, scene_type: str = "playGround3D-2.0",
title: str = "", style: str = "", text: str = "",
image_list: Optional[List[str]] = None,
enable_pbr: bool = True, enable_low_poly: bool = False,
face_count: int = 1500000, motion_type: Optional[int] = None,
topology_format: Optional[str] = None) -> Dict[str, Any]:
"""通用生成接口"""
data = {
"sceneType": scene_type,
"count": 1,
"modelType": model_type,
"title": title,
"style": style,
"enable_pbr": enable_pbr,
"enableLowPoly": enable_low_poly,
"faceCount": face_count
}
if text:
data["prompt"] = text
if image_list:
data["imageList"] = image_list
if motion_type is not None:
data["motionType"] = motion_type
if topology_format:
data["topology_format"] = topology_format
return self._make_request("POST", "/creations/generations", data=data)
# ========== 图生3D ==========
def generate_from_image(self, image_url: str, title: str = "",
style: str = "", enable_pbr: bool = True,
enable_low_poly: bool = False,
face_count: int = 1500000) -> Dict[str, Any]:
"""
图生3D
Args:
image_url: 图片URL(需先上传获取resourceId
title: 作品标题
style: 纹理风格
enable_pbr: 是否启用PBR材质
enable_low_poly: 是否低多边形
face_count: 面数
"""
return self._generate(
model_type=self.MODEL_TYPE_IMAGE,
title=title,
style=style,
image_list=[image_url],
enable_pbr=enable_pbr,
enable_low_poly=enable_low_poly,
face_count=face_count
)
def generate_from_multi_view(self, image_urls: List[str], title: str = "",
style: str = "", enable_pbr: bool = True,
enable_low_poly: bool = False,
face_count: int = 1500000) -> Dict[str, Any]:
"""
多图视角生3D(多视图)
Args:
image_urls: 多角度图片URL列表(通常2-4张不同视角)
title: 作品标题
style: 纹理风格
enable_pbr: 是否启用PBR材质
enable_low_poly: 是否低多边形
face_count: 面数
"""
return self._generate(
model_type=self.MODEL_TYPE_IMAGE,
title=title,
style=style,
image_list=image_urls,
enable_pbr=enable_pbr,
enable_low_poly=enable_low_poly,
face_count=face_count
)
# ========== 文生3D ==========
def generate_from_text(self, prompt: str, title: str = "",
style: str = "", enable_pbr: bool = True,
enable_low_poly: bool = False,
face_count: int = 1500000, count: int = 4) -> Dict[str, Any]:
"""
文生3D
Args:
prompt: 文本描述/提示词
title: 作品标题
style: 纹理风格
enable_pbr: 是否启用PBR材质
enable_low_poly: 是否低多边形
face_count: 面数
count: 生成数量,文生3D固定为4
"""
data = {
"sceneType": "playGround3D-2.0",
"count": count,
"modelType": self.MODEL_TYPE_TEXT,
"title": title or prompt,
"style": style,
"prompt": prompt,
"enable_pbr": enable_pbr,
"enableLowPoly": enable_low_poly,
"faceCount": face_count
}
return self._make_request("POST", "/creations/generations", data=data)
# ========== 草图生3D ==========
def generate_from_sketch(self, sketch_url: str, prompt: str,
title: str = "", style: str = "",
enable_pbr: bool = True,
enable_low_poly: bool = False,
face_count: int = 1500000) -> Dict[str, Any]:
"""
草图生3D
Args:
sketch_url: 草图图片URL
prompt: 草图描述提示词
title: 作品标题
style: 纹理风格
enable_pbr: 是否启用PBR材质
enable_low_poly: 是否低多边形
face_count: 面数
"""
return self._generate(
model_type=self.MODEL_TYPE_SKETCH,
title=title,
style=style,
text=prompt,
image_list=[sketch_url],
enable_pbr=enable_pbr,
enable_low_poly=enable_low_poly,
face_count=face_count
)
# ========== 3D动画生成 ==========
def generate_animation(self, model_image_url: str, motion_type: int,
title: str = "") -> Dict[str, Any]:
"""
3D动画生成
Args:
model_image_url: 3D模型图片URL
motion_type: 动作类型ID
title: 作品标题
"""
return self._generate(
model_type=self.MODEL_TYPE_ANIMATION,
title=title,
image_list=[model_image_url],
motion_type=motion_type
)
# ========== 3D纹理生成 ==========
def generate_texture(self, white_model_url: str, prompt: str,
title: str = "") -> Dict[str, Any]:
"""
3D纹理生成
Args:
white_model_url: 白模图片URL
prompt: 纹理描述
title: 作品标题
"""
return self._generate(
model_type=self.MODEL_TYPE_TEXTURE,
title=title,
text=prompt,
image_list=[white_model_url]
)
# ========== 3D智能拓扑 ==========
def generate_lowpoly(self, model_url: str, face_count: int = 5000,
topology_format: str = "glb",
title: str = "") -> Dict[str, Any]:
"""
3D智能拓扑(减面)
Args:
model_url: 模型图片URL
face_count: 目标面数(5000/18000/30000
topology_format: 输出格式(glb/obj
title: 作品标题
"""
return self._generate(
model_type=self.MODEL_TYPE_LOWPOLY,
title=title,
image_list=[model_url],
enable_low_poly=True,
face_count=face_count,
topology_format=topology_format
)
# ========== 资源上传 ==========
def get_upload_info(self, filename: str) -> Dict[str, Any]:
"""获取上传凭证"""
return self._make_request("POST", "/resource/genUploadInfo", data={
"fileName": filename
})
def review_resource(self, resource_url: str, scene_type: str = "playGround3D-2.0",
resource_type: str = "image", text: str = "") -> Dict[str, Any]:
"""资源审核"""
return self._make_request("POST", "/resource/review", data={
"sceneType": scene_type,
"text": text,
"resourceType": resource_type,
"resourceUrl": resource_url
})
# ========== 分享 ==========
def create_share(self, creation_id: str, platform: str = "3dPlayground") -> Dict[str, Any]:
"""创建分享"""
return self._make_request("POST", "/share", data={
"contentType": "creation",
"contentId": creation_id,
"sharedContent": "",
"platform": platform
})
# ========== 配置 ==========
def get_config(self) -> Dict[str, Any]:
"""获取全局配置"""
return self._make_request("GET", "/config")
def get_action_templates(self) -> Dict[str, Any]:
"""获取动画动作模板"""
return self._make_request("GET", "/workflow/action/templates")
# ========== 轮询等待 ==========
def wait_for_completion(self, creation_id: str, timeout: int = 600,
poll_interval: int = 5) -> Dict[str, Any]:
"""
等待生成完成
Args:
creation_id: 创作ID
timeout: 超时时间(秒)
poll_interval: 轮询间隔(秒)
"""
start_time = time.time()
while time.time() - start_time < timeout:
status = self.get_generation_status(creation_id)
# Handle both wrapped {code, data} and flat response formats
if "code" in status:
data = status.get("data", {})
else:
data = status
state = data.get("status")
if state == "success":
return status
elif state == "fail":
raise Exception(f"Generation failed: {data}")
progress = data.get("progress", 0)
print(f"State: {state}, progress: {progress}%")
time.sleep(poll_interval)
raise TimeoutError(f"Generation timeout after {timeout} seconds")
from .config import get_cookie_path
def load_cookies_from_file(filepath: Optional[str] = None) -> str:
"""从文件加载Cookie,默认读取用户配置目录的 cookies.txt"""
path = filepath or str(get_cookie_path())
with open(path, 'r', encoding='utf-8') as f:
return f.read().strip()
if __name__ == "__main__":
import sys
cookies = load_cookies_from_file(sys.argv[1] if len(sys.argv) > 1 else None)
api = Hunyuan3DAPI(cookies)
# 测试配额查询
print("查询配额...")
quota = api.get_quota_info()
print(json.dumps(quota, indent=2, ensure_ascii=False))
# 测试获取配置
print("\n获取配置...")
config = api.get_config()
print(f"Config keys: {list(config.keys())}")