Files
hunyuan3dweb/hunyuan3dweb/api.py
Claude bf4e1e5755 feat: add pure Python COS upload and improve login detection
- feat(cos): add cos_upload.py for direct file upload without browser
  - implements COS V1 signature algorithm with temporary credentials
  - upload_image() pipeline: get_upload_info → sign → PUT to COS
- feat(api): auto-load cookies from file when cookies arg is omitted
  - both Hunyuan3DAPI and Hunyuan3DAPIComplete now fall back to
    ~/.config/hunyuan3dweb/cookies.txt automatically
- fix(login): strengthen login-state detection using both URL and DOM
  - checks "login" not in page.url AND no login button on page
- docs: update README / README_CN with COS upload examples

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:47:11 +08:00

252 lines
8.2 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纯Python API客户端
无需浏览器直接HTTP调用
"""
import requests
import json
import time
from typing import Optional, Dict, Any
from .sign import sign, sign_with_custom_nonce
BASE_URL = "https://3d.hunyuan.tencent.com"
API_BASE = f"{BASE_URL}/api/3d"
class Hunyuan3DAPI:
"""腾讯混元3D API客户端"""
def __init__(self, cookies: Optional[str] = None):
"""
初始化API客户端
Args:
cookies: 浏览器Cookie字符串包含hunyuan_user和hunyuan_token
"""
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"
})
try:
cookie_value = cookies if cookies is not None else load_cookies_from_file()
except FileNotFoundError:
cookie_value = None
if cookie_value:
self.set_cookies(cookie_value)
def set_cookies(self, cookies: str):
"""设置Cookie"""
self.session.headers["Cookie"] = cookies
def set_cookie_dict(self, cookie_dict: Dict[str, str]):
"""从字典设置Cookie"""
cookie_str = "; ".join(f"{k}={v}" for k, v in cookie_dict.items())
self.session.headers["Cookie"] = cookie_str
def _make_request(self, method: str, endpoint: str, params: Optional[Dict] = None,
data: Optional[Dict] = None) -> Dict[str, Any]:
"""
发送带签名的API请求
注意: 腾讯混元3D的签名只包含URL查询参数不包含请求体数据
"""
url = f"{API_BASE}{endpoint}"
# 签名只针对查询参数,不包含请求体
sign_params = {}
if params:
sign_params.update(params)
# 生成签名
signed_params = sign(sign_params)
# 分离签名参数
timestamp = signed_params.pop("timestamp")
nonce = signed_params.pop("nonce")
sign_value = signed_params.pop("sign")
# 构建最终URL查询参数只包含签名相关
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:
# POST请求签名在URL参数中数据在body中
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 generate_3d(self, image_url: str, scene_type: str = "playGround3D-2.0",
model_type: str = "image2ModelV3.1", title: str = "",
enable_pbr: bool = True, enable_low_poly: bool = False,
face_count: int = 1500000, count: int = 1) -> Dict[str, Any]:
"""
生成3D模型
Args:
image_url: 图片URL需要先上传到腾讯云COS
scene_type: 场景类型
model_type: 模型类型
title: 标题
enable_pbr: 是否启用PBR材质
enable_low_poly: 是否启用低多边形
face_count: 面数
count: 生成数量
Returns:
包含creationsId的响应
"""
data = {
"sceneType": scene_type,
"count": count,
"modelType": model_type,
"title": title,
"style": "",
"imageList": [image_url],
"enable_pbr": enable_pbr,
"enableLowPoly": enable_low_poly,
"faceCount": face_count
}
return self._make_request("POST", "/creations/generations", data=data)
def generate_text(self, prompt: str, scene_type: str = "playGround3D-2.0",
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: 文本描述/提示词
scene_type: 场景类型
title: 作品标题
style: 纹理风格
enable_pbr: 是否启用PBR材质
enable_low_poly: 是否低多边形
face_count: 面数
count: 生成数量文生3D固定为4
Returns:
包含creationsId的响应
"""
data = {
"sceneType": scene_type,
"count": count,
"modelType": "text2ModelV3.1",
"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)
def get_generation_status(self, creation_id: str) -> Dict[str, Any]:
"""获取生成状态"""
return self._make_request("GET", "/creations/detail", params={
"creationsId": creation_id
})
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: 轮询间隔(秒)
Returns:
完成的创作信息
"""
start_time = time.time()
last_error = None
while time.time() - start_time < timeout:
try:
status = self.get_generation_status(creation_id)
except Exception as e:
last_error = e
print(f"查询状态失败: {e},将在 {poll_interval} 秒后重试...")
time.sleep(poll_interval)
continue
# Handle both wrapped {code, data} and flat response formats
if "code" in status:
data = status.get("data", {})
else:
data = status
state = data.get("status") or data.get("state")
if state == "success":
return status
elif state == "fail":
raise Exception(f"Generation failed: {data.get('errorMsg', 'Unknown error')}")
print(f"State: {state}, progress: {data.get('progress', 0)}%")
time.sleep(poll_interval)
if last_error:
raise TimeoutError(f"Generation timeout after {timeout} seconds. Last error: {last_error}")
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
if len(sys.argv) > 1 and sys.argv[1] == "--cookie-string":
cookies = sys.argv[2]
else:
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))