Initial commit

This commit is contained in:
2026-05-24 21:47:44 +08:00
commit 2ce35563c4
20 changed files with 3698 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Sensitive user data
cookies.txt
hunyuan3d_profile/
# Python build artifacts
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
dist/
build/
.eggs/
# IDE
.vscode/
.idea/
*.swp
*.swo
# Logs
*.log
api_requests.log.json
api_image_to_3d.json

291
README.md Normal file
View File

@@ -0,0 +1,291 @@
# hunyuan3dweb
Python API client and browser automation tools for Tencent Hunyuan 3D (https://3d.hunyuan.tencent.com/).
## Installation
```bash
pip install -e .
```
For browser automation features:
```bash
pip install -e ".[browser]"
```
## Login and Setup
First-time users need to log in via the browser. Cookies will be saved automatically:
```bash
hunyuan3dweb-login
```
After successful login:
- Browser profile is saved to `~/.config/hunyuan3dweb/profile`
- Cookies are automatically extracted to `~/.config/hunyuan3dweb/cookies.txt`
Subsequent Python scripts will read cookies automatically — no manual configuration needed.
## Python Script Usage
### Quick Start (Auto-read Default Cookie)
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
# Check quota
quota = api.get_quota_info()
print(f"Remaining: {quota['remainQuota']}/{quota['totalQuota']}")
```
### 1. Text-to-3D
The simplest mode. No image upload required.
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
# Submit generation job
result = api.generate_from_text("a ceramic vase", title="Test")
cid = result["creationsId"]
# Poll until completion (built-in polling with progress output)
final = api.wait_for_completion(cid)
print("Generation complete")
```
**Full pipeline: Text-to-3D → Extract Download Links**
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
job = api.generate_from_text(
"cyberpunk style mechanical dog",
style=api.STYLE_CYBERPUNK
)
cid = job["creationsId"]
result = api.wait_for_completion(cid, timeout=600, poll_interval=5)
if result["status"] == "success":
models = result.get("result", [])
for m in models:
if m["status"] == "success":
urls = m["urlResult"]
print(f"GLB: {urls.get('glb')}")
print(f"OBJ: {urls.get('obj')}")
print(f"Image: {urls.get('image_url')}")
```
### 2. Image-to-3D (Local File)
For local image files, use the browser automation tool. The pure API client cannot upload images directly because Tencent COS requires browser-side decryption of temporary credentials.
```bash
hunyuan3dweb-generate /path/to/image.png wait
```
Or programmatically via browser automation:
```python
from hunyuan3dweb.browser.generator import generate_3d
result = generate_3d("/path/to/image.png", wait_for_complete=True)
print(f"Model URL: {result.get('modelUrl')}")
```
### 3. Image-to-3D (API with Existing resourceUrl)
If you already have a `resourceUrl` (e.g. from a previous browser upload), use the pure API client:
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
result = api.generate_from_image(
image_url="https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=...",
title="My Model"
)
cid = result["creationsId"]
final = api.wait_for_completion(cid)
```
### 4. Multi-View Image-to-3D
Use multiple images from different angles to generate a more accurate 3D model.
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
result = api.generate_from_multi_view(
image_urls=[
"https://.../front.png",
"https://.../side.png",
"https://.../back.png",
],
title="Multi-View Model"
)
cid = result["creationsId"]
final = api.wait_for_completion(cid)
```
### 5. Sketch-to-3D
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
result = api.generate_from_sketch(
sketch_url="https://...",
prompt="a sketch of a robot",
title="Sketch Robot"
)
cid = result["creationsId"]
final = api.wait_for_completion(cid)
```
### 6. Animation Generation
Requires a model image URL and a motion type.
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
# Available motions: MOTION_CAPOEIRA, MOTION_FALLING, MOTION_JUMPING,
# MOTION_KICKING, MOTION_SWORD, MOTION_RUNNING, MOTION_DANCING
result = api.generate_animation(
model_image_url="https://...",
motion_type=api.MOTION_DANCING,
title="Dancing Model"
)
cid = result["creationsId"]
final = api.wait_for_completion(cid)
```
### 7. Texture Generation
Apply texture to a white/untexured model.
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
result = api.generate_texture(
white_model_url="https://...",
prompt="red metallic texture with rust",
title="Textured Model"
)
cid = result["creationsId"]
final = api.wait_for_completion(cid)
```
### 8. Smart Topology (Decimation / Low Poly)
Reduce polygon count of an existing model.
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
result = api.generate_lowpoly(
model_url="https://...",
face_count=api.TOPO_LOW, # 5000, 18000, or 30000
topology_format="glb", # "glb" or "obj"
title="Low Poly Model"
)
cid = result["creationsId"]
final = api.wait_for_completion(cid)
```
### Basic Client (Lightweight)
For users who only need image-to-3D and text-to-3D:
```python
from hunyuan3dweb import Hunyuan3DAPI
api = Hunyuan3DAPI()
# Text-to-3D
api.generate_text("a cat")
# Image-to-3D (requires existing resourceUrl)
api.generate_3d(image_url="...")
```
### Explicit Cookie Path (Multi-account or Custom Path)
```python
from hunyuan3dweb import Hunyuan3DAPIComplete, load_cookies_from_file
cookies = load_cookies_from_file("/path/to/cookies.txt")
api = Hunyuan3DAPIComplete(cookies=cookies)
```
## CLI Commands
- `hunyuan3dweb` - API CLI tool (supports quota / list / text / status subcommands)
- `hunyuan3dweb-login` - Browser login (auto-saves cookies)
- `hunyuan3dweb-sniffer` - API request/response sniffer
- `hunyuan3dweb-generate` - Browser automation for generation
### CLI Examples
```bash
# Check quota
hunyuan3dweb quota
# List creations
hunyuan3dweb list
# Text-to-3D
hunyuan3dweb text "a red apple"
# Check generation status
hunyuan3dweb status <creationsId>
# Browser login
hunyuan3dweb-login
# Generate from local image (browser automation)
hunyuan3dweb-generate /path/to/image.png wait
# Sniff API traffic
hunyuan3dweb-sniffer
```
## File Reference
| File | Description |
|------|-------------|
| `hunyuan3dweb/api.py` | Basic API client (image2model, text2model) |
| `hunyuan3dweb/api_complete.py` | Full API client (all generation modes) |
| `hunyuan3dweb/sign.py` | Tencent Hunyuan 3D signing algorithm |
| `hunyuan3dweb/config.py` | User config path management |
| `hunyuan3dweb/cli.py` | CLI entry point |
| `hunyuan3dweb/browser/login.py` | Browser login tool |
| `hunyuan3dweb/browser/sniffer.py` | API sniffer tool |
| `hunyuan3dweb/browser/generator.py` | Browser automation generator |
| `doc/api.md` | API endpoint documentation |

291
README_CN.md Normal file
View File

@@ -0,0 +1,291 @@
# hunyuan3dweb
腾讯混元3D (https://3d.hunyuan.tencent.com/) 的 Python API 客户端与浏览器自动化工具。
## 安装
```bash
pip install -e .
```
如需浏览器自动化功能:
```bash
pip install -e ".[browser]"
```
## 登录与配置
第一次使用需要先通过浏览器登录,系统会自动保存 Cookie
```bash
hunyuan3dweb-login
```
登录成功后:
- 浏览器 Profile 保存到 `~/.config/hunyuan3dweb/profile`
- Cookie 自动提取到 `~/.config/hunyuan3dweb/cookies.txt`
之后写 Python 脚本无需再处理 Cookie库会自动读取。
## Python 脚本调用
### 快速开始(自动读取默认 Cookie
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
# 查配额
quota = api.get_quota_info()
print(f"剩余: {quota['remainQuota']}/{quota['totalQuota']}")
```
### 1. 文生3D
最简单的模式,无需上传图片。
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
# 提交生成任务
result = api.generate_from_text("一只陶瓷花瓶", title="测试")
cid = result["creationsId"]
# 轮询直到完成(内置轮询,自动打印进度)
final = api.wait_for_completion(cid)
print("生成完成")
```
**完整流水线文生3D → 提取下载链接**
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
job = api.generate_from_text(
"赛博朋克风格的机械狗",
style=api.STYLE_CYBERPUNK
)
cid = job["creationsId"]
result = api.wait_for_completion(cid, timeout=600, poll_interval=5)
if result["status"] == "success":
models = result.get("result", [])
for m in models:
if m["status"] == "success":
urls = m["urlResult"]
print(f"GLB: {urls.get('glb')}")
print(f"OBJ: {urls.get('obj')}")
print(f"图片: {urls.get('image_url')}")
```
### 2. 图生3D本地文件
本地图片文件需通过浏览器自动化工具上传。纯 API 客户端无法直接上传图片,因为腾讯 COS 临时凭证需要浏览器端解密。
```bash
hunyuan3dweb-generate /path/to/image.png wait
```
或通过浏览器自动化脚本:
```python
from hunyuan3dweb.browser.generator import generate_3d
result = generate_3d("/path/to/image.png", wait_for_complete=True)
print(f"模型地址: {result.get('modelUrl')}")
```
### 3. 图生3DAPI已有 resourceUrl
如果你已经有 `resourceUrl`(例如之前通过浏览器上传过),可直接调用 API
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
result = api.generate_from_image(
image_url="https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=...",
title="我的模型"
)
cid = result["creationsId"]
final = api.wait_for_completion(cid)
```
### 4. 多图视角生3D多视图
使用多张不同角度的图片生成更精确的 3D 模型。
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
result = api.generate_from_multi_view(
image_urls=[
"https://.../front.png",
"https://.../side.png",
"https://.../back.png",
],
title="多视图模型"
)
cid = result["creationsId"]
final = api.wait_for_completion(cid)
```
### 5. 草图生3D
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
result = api.generate_from_sketch(
sketch_url="https://...",
prompt="一个机器人的草图",
title="草图机器人"
)
cid = result["creationsId"]
final = api.wait_for_completion(cid)
```
### 6. 动画生成
需要模型图片 URL 和动作类型。
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
# 可选动作MOTION_CAPOEIRA, MOTION_FALLING, MOTION_JUMPING,
# MOTION_KICKING, MOTION_SWORD, MOTION_RUNNING, MOTION_DANCING
result = api.generate_animation(
model_image_url="https://...",
motion_type=api.MOTION_DANCING,
title="跳舞的模型"
)
cid = result["creationsId"]
final = api.wait_for_completion(cid)
```
### 7. 纹理生成
为白模/无纹理模型生成贴图。
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
result = api.generate_texture(
white_model_url="https://...",
prompt="红色金属锈迹纹理",
title="纹理模型"
)
cid = result["creationsId"]
final = api.wait_for_completion(cid)
```
### 8. 智能拓扑(减面 / Low Poly
降低现有模型的面数。
```python
from hunyuan3dweb import Hunyuan3DAPIComplete
api = Hunyuan3DAPIComplete()
result = api.generate_lowpoly(
model_url="https://...",
face_count=api.TOPO_LOW, # 5000 / 18000 / 30000
topology_format="glb", # "glb" 或 "obj"
title="低模模型"
)
cid = result["creationsId"]
final = api.wait_for_completion(cid)
```
### 基础客户端(轻量)
仅需图生3D和文生3D的用户可使用简化客户端
```python
from hunyuan3dweb import Hunyuan3DAPI
api = Hunyuan3DAPI()
# 文生3D
api.generate_text("一只猫")
# 图生3D需已有 resourceUrl
api.generate_3d(image_url="...")
```
### 显式指定 Cookie多账户或自定义路径
```python
from hunyuan3dweb import Hunyuan3DAPIComplete, load_cookies_from_file
cookies = load_cookies_from_file("/path/to/cookies.txt")
api = Hunyuan3DAPIComplete(cookies=cookies)
```
## CLI 命令
- `hunyuan3dweb` - API CLI 工具(支持 quota / list / text / status 子命令)
- `hunyuan3dweb-login` - 浏览器登录(自动保存 Cookie
- `hunyuan3dweb-sniffer` - API 拦截分析
- `hunyuan3dweb-generate` - 浏览器自动化生成
### CLI 示例
```bash
# 查询配额
hunyuan3dweb quota
# 查询作品列表
hunyuan3dweb list
# 文生3D
hunyuan3dweb text "一只红色的苹果"
# 查询生成状态
hunyuan3dweb status <creationsId>
# 浏览器登录
hunyuan3dweb-login
# 本地图片生成(浏览器自动化)
hunyuan3dweb-generate /path/to/image.png wait
# API 拦截
hunyuan3dweb-sniffer
```
## 文件说明
| 文件 | 说明 |
|------|------|
| `hunyuan3dweb/api.py` | 基础 API 客户端图生3D、文生3D |
| `hunyuan3dweb/api_complete.py` | 完整 API 客户端(所有生成模式) |
| `hunyuan3dweb/sign.py` | 腾讯混元3D 签名算法 |
| `hunyuan3dweb/config.py` | 用户配置路径管理 |
| `hunyuan3dweb/cli.py` | CLI 入口 |
| `hunyuan3dweb/browser/login.py` | 浏览器登录工具 |
| `hunyuan3dweb/browser/sniffer.py` | API 拦截工具 |
| `hunyuan3dweb/browser/generator.py` | 浏览器自动化生成 |
| `doc/api.md` | API 接口文档 |

View File

@@ -0,0 +1,206 @@
# Tencent Hunyuan 3D API Reverse Engineering Document
## Project Overview
This project reverse-engineers the frontend signing algorithm of Tencent Hunyuan 3D (https://3d.hunyuan.tencent.com/), enabling **pure Python HTTP calls** without a browser environment for features like image-to-3D generation and quota queries.
---
## Core Achievements
### 1. Signing Algorithm Cracked
**Algorithm Location**: webpack Chunk 3057, Module 47436
**Signing Flow**:
```
1. nonce generation: 16 iterations of Math.random(), selecting from 62-char set (A-Za-z0-9)
2. timestamp: Math.floor(Date.now() / 1000) (second-level timestamp)
3. key derivation: hardcoded byte array → XOR → circular left shift → permutation → truncation
4. signature: HMAC-SHA256(sorted query param string, derived key) → Hex
```
**Key Constants**:
```python
C = bytes([122, 59, 92, 165, 30, 79, 166, 139, 142, 129, 139, 89, 219, 131, 101, 204])
D = bytes([122, 59, 92, 45, 30, 79, 106, 139, 156, 13, 46, 63, 74, 91, 108, 125])
U = [3, 5, 2, 7, 1, 4, 6, 2, 5, 3, 1, 4, 2, 6, 3, 5] # left shift bits
M = [14, 11, 13, 9, 15, 10, 12, 8, 6, 3, 5, 1, 7, 2, 4, 0] # permutation table
```
**Derived Key**: `Hf6d6KFB3D` (10 characters)
### 2. Signing Scope (Important Finding)
⚠️ **The signature only covers URL query parameters, NOT the request body**
Actual browser request:
```
POST /api/3d/creations/generations?timestamp=xxx&nonce=yyy&sign=zzz
Body: {"sceneType":"playGround3D-2.0",...}
```
Signature computation:
```python
# Only sign query params (timestamp + nonce)
param_str = "nonce=yyy&timestamp=xxx"
sign = HMAC-SHA256(param_str, key="Hf6d6KFB3D")
```
### 3. Image URL Format
Must use Tencent's internal resource format:
```
https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=<32-digit-hex>
```
You cannot directly use a COS URL; you must upload through the browser to obtain a resourceId.
---
## File Reference
| File | Description |
|------|-------------|
| `hunyuan3dweb/sign.py` | Signing algorithm in pure Python |
| `hunyuan3dweb/api.py` | Pure Python API client |
| `hunyuan3dweb/browser/login.py` | Browser automation login tool |
| `doc/api.md` | API endpoint documentation |
---
## Usage Flow
### 1. Login to Obtain Cookie
```bash
hunyuan3dweb-login
# Follow prompts to enter email and verification code
# Login state is automatically saved to ~/.config/hunyuan3dweb/profile
```
### 2. Pure Python 3D Generation
```python
from hunyuan3dweb import Hunyuan3DAPI
api = Hunyuan3DAPI()
# Check quota
quota = api.get_quota_info()
# Generate 3D model
result = api.generate_3d(
image_url="https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=...",
scene_type="playGround3D-2.0",
model_type="image2ModelV3.1"
)
# Query status
status = api.get_generation_status(result["creationsId"])
```
---
## API Endpoints
| Feature | Method | Endpoint | Signature Scope |
|---------|--------|----------|-----------------|
| User info | GET | `/getuserinfo` | Query params |
| Quota query | POST | `/quotainfo` | Query params |
| Generate 3D | POST | `/creations/generations` | Query params |
| Query status | GET | `/creations/detail` | Query params |
---
## Technical Details
### Signing Algorithm Implementation
```python
def derive_key(c: bytes) -> str:
"""Derive signing key from hardcoded constants"""
# 1. XOR with D
t = bytearray(16)
for i in range(16):
t[i] = c[i] ^ D[i]
# 2. Circular left shift
o = bytearray(16)
for i in range(16):
n = U[i]
val = t[i]
o[i] = (val << n | val >> (8 - n)) & 0xFF
# 3. Permutation
n = bytearray(16)
for i in range(16):
n[i] = o[M[i]]
# 4. Truncate (find first zero byte)
try:
r = n.index(0)
except ValueError:
r = 16
return n[:r].decode('utf-8')
```
### Request Signing
```python
def sign(params: dict) -> dict:
result = dict(params)
result["timestamp"] = int(time.time())
result["nonce"] = generate_nonce(16)
# Sort and join (JSON format for lists/dicts)
sorted_items = sort_params(result)
param_str = join_params(sorted_items)
# HMAC-SHA256
key = derive_key(C)
signature = hmac.new(
key.encode('utf-8'),
param_str.encode('utf-8'),
hashlib.sha256
).hexdigest()
result["sign"] = signature
return result
```
---
## Verification Results
| Test Item | Browser Signature | Python Signature | Match |
|-----------|-------------------|------------------|-------|
| Fixed params | `2214e55a...` | `2214e55a...` | ✅ |
| Quota query | Works | Works | ✅ |
| Generate request | Works | Works | ✅ |
| Status query | Works | Works | ✅ |
---
## Limitations and Notes
1. **Image Upload**: Still requires a browser environment to upload images and obtain resourceId (Tencent COS requires temporary signatures)
2. **Cookie Expiration**: Login sessions expire and need periodic re-login
3. **Quota Limit**: Each account has a generation quota limit (default 20/day)
4. **Rate Limiting**: Frequent calls may trigger anti-bot measures
---
## Dependencies
```
requests
cloakbrowser (for login and upload)
```
---
## Legal Notice
This project is for educational and research purposes only. Use of this code is subject to Tencent Hunyuan 3D's Terms of Service. Do not use for commercial purposes or large-scale automated calling.

View File

@@ -0,0 +1,222 @@
# 腾讯混元3D API 逆向工程文档
## 项目概述
本项目通过逆向工程破解了腾讯混元3Dhttps://3d.hunyuan.tencent.com/)的前端签名算法,实现了**纯 Python HTTP 调用**无需浏览器环境即可使用图生3D、查询配额等功能。
---
## 核心成果
### 1. 签名算法破解
**算法位置**: webpack Chunk 3057, Module 47436
**签名流程**:
```
1. nonce 生成: 16次 Math.random(),从 62 字符集 (A-Za-z0-9) 选取
2. timestamp: Math.floor(Date.now() / 1000)(秒级时间戳)
3. 密钥派生: 硬编码字节数组 → XOR → 循环左移 → 置换 → 截断
4. 签名: HMAC-SHA256(排序后的查询参数字符串, 派生密钥) → Hex
```
**关键常量**:
```python
C = bytes([122, 59, 92, 165, 30, 79, 166, 139, 142, 129, 139, 89, 219, 131, 101, 204])
D = bytes([122, 59, 92, 45, 30, 79, 106, 139, 156, 13, 46, 63, 74, 91, 108, 125])
U = [3, 5, 2, 7, 1, 4, 6, 2, 5, 3, 1, 4, 2, 6, 3, 5] # 左移位数
M = [14, 11, 13, 9, 15, 10, 12, 8, 6, 3, 5, 1, 7, 2, 4, 0] # 置换表
```
**派生密钥**: `Hf6d6KFB3D`10字符
### 2. 签名范围(重要发现)
⚠️ **签名只包含 URL 查询参数,不包含请求体数据**
浏览器实际请求:
```
POST /api/3d/creations/generations?timestamp=xxx&nonce=yyy&sign=zzz
Body: {"sceneType":"playGround3D-2.0",...}
```
签名计算:
```python
# 只签查询参数timestamp + nonce
param_str = "nonce=yyy&timestamp=xxx"
sign = HMAC-SHA256(param_str, key="Hf6d6KFB3D")
```
### 3. 图片URL格式
必须使用腾讯内部资源格式:
```
https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=<32位十六进制>
```
不能直接上传 COS URL需要通过浏览器上传获取 resourceId。
---
## 文件说明
| 文件 | 说明 |
|------|------|
| `hunyuan3d_sign.py` | 签名算法纯 Python 实现 |
| `hunyuan3d_api.py` | 纯 Python API 客户端 |
| `hunyuan3d_login.py` | 浏览器自动化登录工具 |
| `get_resource_id.py` | 获取图片 resourceId 工具 |
| `cookies.txt` | Cookie 存储文件 |
| `uploaded_image_url.txt` | 上传后的图片 URL |
---
## 使用流程
### 1. 登录获取 Cookie
```bash
python hunyuan3d_login.py
# 按提示输入邮箱和验证码
# 登录状态自动保存到 ./hunyuan3d_profile
```
### 2. 提取 Cookie
```bash
python get_cookie_persistent.py
# 生成 cookies.txt
```
### 3. 上传图片获取 resourceId
```bash
python get_resource_id_v2.py
# 生成 uploaded_image_url.txt
```
### 4. 纯 Python 生成3D模型
```python
from hunyuan3d_api import Hunyuan3DAPI
api = Hunyuan3DAPI("hunyuan_user=xxx; hunyuan_token=yyy")
# 查询配额
quota = api.get_quota_info()
# 生成3D模型
result = api.generate_3d(
image_url="https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=...",
scene_type="playGround3D-2.0",
model_type="image2ModelV3.1"
)
# 查询状态
status = api.get_generation_status(result["creationsId"])
```
---
## API 端点
| 功能 | 方法 | 端点 | 签名范围 |
|------|------|------|----------|
| 用户信息 | GET | `/getuserinfo` | 查询参数 |
| 配额查询 | POST | `/quotainfo` | 查询参数 |
| 生成3D | POST | `/creations/generations` | 查询参数 |
| 查询状态 | GET | `/creations/detail` | 查询参数 |
---
## 技术细节
### 签名算法实现
```python
def derive_key(c: bytes) -> str:
"""从硬编码常量派生签名密钥"""
# 1. XOR with D
t = bytearray(16)
for i in range(16):
t[i] = c[i] ^ D[i]
# 2. 循环左移
o = bytearray(16)
for i in range(16):
n = U[i]
val = t[i]
o[i] = (val << n | val >> (8 - n)) & 0xFF
# 3. 置换
n = bytearray(16)
for i in range(16):
n[i] = o[M[i]]
# 4. 截断找到第一个0字节
try:
r = n.index(0)
except ValueError:
r = 16
return n[:r].decode('utf-8')
```
### 请求签名
```python
def sign(params: dict) -> dict:
result = dict(params)
result["timestamp"] = int(time.time())
result["nonce"] = generate_nonce(16)
# 排序并拼接JSON格式用于列表/字典)
sorted_items = sort_params(result)
param_str = join_params(sorted_items)
# HMAC-SHA256
key = derive_key(C)
signature = hmac.new(
key.encode('utf-8'),
param_str.encode('utf-8'),
hashlib.sha256
).hexdigest()
result["sign"] = signature
return result
```
---
## 验证结果
| 测试项 | 浏览器签名 | Python 签名 | 匹配 |
|--------|-----------|-------------|------|
| 固定参数 | `2214e55a...` | `2214e55a...` | ✅ |
| 配额查询 | 可用 | 可用 | ✅ |
| 生成请求 | 可用 | 可用 | ✅ |
| 状态查询 | 可用 | 可用 | ✅ |
---
## 限制与注意事项
1. **图片上传**: 仍需浏览器环境上传图片获取 resourceId腾讯 COS 需要临时签名)
2. **Cookie 有效期**: 登录状态会过期,需要定期重新登录
3. **配额限制**: 每个账号有生成配额限制默认20次/天)
4. **风控检测**: 频繁调用可能触发风控
---
## 依赖
```
requests
cloakbrowser (用于登录和上传)
```
---
## 法律声明
本项目仅供学习和研究使用。使用本代码需遵守腾讯混元3D的服务条款。请勿用于商业用途或大规模自动化调用。

216
UNIVERSALITY_ANALYSIS.md Normal file
View File

@@ -0,0 +1,216 @@
# 腾讯混元3D 签名算法泛用性分析
## 核心结论
**该签名算法具有"有限泛用性"** —— 只能在腾讯混元3D产品线内复用不能直接用于其他腾讯产品或第三方平台。
---
## 一、算法特征分析
### 1. 硬编码密钥派生
```python
C = bytes([122, 59, 92, 165, 30, 79, 166, 139, 142, 129, 139, 89, 219, 131, 101, 204])
D = bytes([122, 59, 92, 45, 30, 79, 106, 139, 156, 13, 46, 63, 74, 91, 108, 125])
U = [3, 5, 2, 7, 1, 4, 6, 2, 5, 3, 1, 4, 2, 6, 3, 5]
M = [14, 11, 13, 9, 15, 10, 12, 8, 6, 3, 5, 1, 7, 2, 4, 0]
```
**特征**:
- 使用 16 字节硬编码种子 `C`
- 通过 XOR + 循环左移 + 置换 派生密钥
- 最终密钥: `Hf6d6KFB3D`
**泛用性**: ❌ **低** — 这是腾讯混元3D特有的密钥派生方案其他产品使用不同的密钥。
### 2. 签名范围
```
签名只包含 URL 查询参数,不包含请求体
```
**特征**:
- 仅对 `timestamp` + `nonce` 签名
- 请求体数据不参与签名计算
- 使用 HMAC-SHA256输出 Hex
**泛用性**: ⚠️ **中等** — 这种"轻量级签名"模式在腾讯部分产品中有复用,但具体实现不同。
### 3. 参数排序规则
```python
# 过滤空值
# 按键名字母顺序排序
# 列表/字典使用 JSON 格式
```
**泛用性**: ⚠️ **中等** — 这是常见的签名参数处理模式,但不同产品的过滤规则和序列化方式可能不同。
---
## 二、可复用场景
### ✅ 可直接复用
| 场景 | 说明 |
|------|------|
| 腾讯混元3D所有API | 同一套签名算法适用于所有端点 |
| 混元3D的不同环境 | dev/pre/prod 使用相同算法(可能密钥不同)|
| 同一账号的不同请求 | 签名算法与账号无关 |
### ⚠️ 需要修改后复用
| 场景 | 需要修改的内容 |
|------|---------------|
| 腾讯其他产品(如混元大模型)| 硬编码常量 C/D/U/M 不同 |
| 其他公司的类似产品 | 整套算法可能完全不同 |
| 混元3D的移动端API | 可能使用不同的密钥或签名方式 |
### ❌ 无法复用
| 场景 | 原因 |
|------|------|
| 标准 OAuth/JWT 接口 | 完全不同的认证机制 |
| 使用 AK/SK 签名的云服务 | 使用 HMAC-SHA256 但密钥和消息格式不同 |
| 无签名的公开 API | 不需要签名 |
---
## 三、方法论的可复用性
虽然具体算法不能泛用,但**逆向方法论**可以复用:
### 1. webpack 模块提取
```javascript
// 适用于所有 webpack 打包的前端应用
const chunks = self.webpackChunk<app_name>;
for (const chunk of chunks) {
const modules = chunk[1];
// 搜索包含关键词的模块
}
```
**复用价值**: ⭐⭐⭐⭐⭐ — 所有 webpack 应用都适用
### 2. 签名定位技巧
```javascript
// Hook Math.random 和 Date.now
Math.random = () => { console.trace(); return 0.123; };
Date.now = () => { console.trace(); return 1779599999000; };
```
**复用价值**: ⭐⭐⭐⭐⭐ — 适用于所有前端签名逆向
### 3. 请求拦截分析
```javascript
// 拦截 fetch/XHR
const origFetch = fetch;
fetch = (...args) => {
console.log('Fetch:', args);
return origFetch(...args);
};
```
**复用价值**: ⭐⭐⭐⭐⭐ — 通用的前端请求分析技术
### 4. 密钥派生分析
```python
# 从字节数组派生密钥的模式
def derive_key(c, d, u, m):
# XOR -> 移位 -> 置换 -> 截断
...
```
**复用价值**: ⭐⭐⭐ — 密钥派生是常见模式,但具体变换不同
---
## 四、与其他腾讯产品的对比
| 产品 | 签名方式 | 密钥来源 | 与混元3D的相似度 |
|------|----------|----------|-----------------|
| 腾讯混元3D | HMAC-SHA256 | 硬编码派生 | 100%(基准)|
| 腾讯云 API | HMAC-SHA256 | 用户 SecretKey | 低(密钥不同)|
| 微信小程序 | SHA256 | 固定字符串+token | 中(算法类似)|
| QQ 音乐 | 未知 | 未知 | 未知 |
| 腾讯视频 | 未知 | 未知 | 未知 |
---
## 五、实际应用建议
### 场景1: 开发混元3D的自动化工具
**适用性**: ⭐⭐⭐⭐⭐
直接使用本项目的代码,无需修改。
### 场景2: 逆向其他腾讯产品
**适用性**: ⭐⭐⭐
可以复用方法论,但需要:
1. 重新提取 webpack 模块
2. 重新定位签名函数
3. 重新分析密钥派生逻辑
### 场景3: 学习前端逆向技术
**适用性**: ⭐⭐⭐⭐⭐
本项目是完整的前端逆向案例,涵盖:
- JS Bundle 分析
- webpack 模块提取
- 签名算法逆向
- 浏览器自动化测试
- Python 重写验证
### 场景4: 构建通用签名破解框架
**适用性**: ⭐⭐
可以构建半自动化工具:
```python
class SignatureCracker:
def extract_webpack_modules(self, url): ...
def locate_sign_function(self, modules, keywords): ...
def analyze_key_derivation(self, func_code): ...
def verify_signature(self, params, expected_sign): ...
```
但每个产品仍需要人工分析和调整。
---
## 六、技术价值评估
| 维度 | 评分 | 说明 |
|------|------|------|
| 算法复杂度 | ⭐⭐⭐ | 中等XOR+移位+置换)|
| 逆向难度 | ⭐⭐⭐⭐ | 较高webpack混淆|
| 代码质量 | ⭐⭐⭐⭐⭐ | 清晰可维护 |
| 文档完整性 | ⭐⭐⭐⭐⭐ | 详细文档 |
| 泛用性 | ⭐⭐ | 仅限混元3D |
| 方法论价值 | ⭐⭐⭐⭐⭐ | 可复用的逆向流程 |
---
## 七、总结
### 算法本身
- **不可泛用**: 硬编码常量和密钥派生方案是混元3D特有的
- **不可迁移**: 不能直接用于其他产品
### 方法论
- **高度可复用**: 逆向流程、工具、技巧适用于所有前端应用
- **学习价值高**: 完整展示了从 JS 混淆代码到 Python 实现的完整过程
### 建议
1. **直接使用**: 如果你需要调用混元3D API
2. **参考学习**: 如果你需要逆向其他前端应用
3. **扩展改进**: 可以构建更通用的前端逆向工具链

430
doc/api.md Normal file
View File

@@ -0,0 +1,430 @@
# 腾讯混元3D API 文档
> 基础地址: `https://3d.hunyuan.tencent.com`
---
## 认证方式
Cookie 会话认证。登录后由浏览器自动携带 `sessionid` 等 Cookie。
持久化数据保存在 `./hunyuan3d_profile`CloakBrowser 复用该目录即可保持登录态。
---
## 通用响应格式
**注意**:接口响应格式不统一,分为两类:
**A. 扁平格式**(大多数 GET 及数据查询接口):
```json
{
"key": "value"
}
```
**B. 包装格式**(部分 POST 操作接口):
```json
{
"code": 0,
"message": "success",
"data": {}
}
```
- `code`: 业务状态码,`0` 表示成功
- `message` / `msg`: 提示信息(字段名不统一)
- `data`: 实际返回数据
---
## 接口列表
### 1. 获取用户信息
- **URL**: `/api/3d/getuserinfo`
- **Method**: `GET`
- **描述**: 获取当前登录用户的基本信息
**响应示例**(扁平格式):
```json
{
"userId": "...",
"nickname": "...",
"imageUrl": "...",
"loginType": "email",
"registered": true
}
```
---
### 2. 获取用户配额
- **URL**: `/api/3d/quotainfo`
- **Method**: `GET`
- **描述**: 获取当前用户的生成配额/剩余次数
**响应示例**(扁平格式):
```json
{
"date": "20260524",
"totalQuota": 20,
"remainQuota": 17,
"consumeQuota": 3
}
```
---
### 3. 获取工作流模板
- **URL**: `/api/3d/workflow/action/templates`
- **Method**: `GET`
- **描述**: 获取可用的3D生成工作流模板列表
**响应示例**(扁平格式):
```json
{
"templates": [
{
"name": "跨步",
"value": "9",
"fbx": "https://...",
"glb": "https://...",
"image": "https://...",
"preview": "https://...",
"visible": true
}
]
}
```
---
### 4. 获取通知列表
- **URL**: `/api/3d/notice/list`
- **Method**: `GET`
- **描述**: 获取系统通知/公告列表
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": [
{
"id": "...",
"title": "...",
"content": "...",
"create_time": "..."
}
]
}
```
---
### 5. 获取作品数量
- **URL**: `/api/3d/creations/count`
- **Method**: `POST`
- **描述**: 获取当前用户已生成的作品总数
**请求体**:
```json
{
"statusList": ["wait", "processing", "success", "fail"]
}
```
**响应示例**(扁平格式):
```json
{
"count": 42
}
```
---
### 6. 分享相关
- **URL**: `/api/3d/share`
- **Method**: `POST`
- **描述**: 创建分享链接或获取分享信息
**请求体**:
```json
{
"creation_id": "..."
}
```
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"share_url": "https://3d.hunyuan.tencent.com/share/..."
}
}
```
---
## 图生3D 完整流程
### 7. 获取图片上传凭证
- **URL**: `/api/3d/resource/genUploadInfo`
- **Method**: `POST`
- **描述**: 获取腾讯云 COS 临时上传凭证
**请求体**:
```json
{
"fileName": "your_image.png"
}
```
**响应示例**:
```json
{
"error": {
"code": "0",
"message": ""
},
"bucketName": "hunyuan-base-prod-1258344703",
"region": "ap-guangzhou",
"location": "hunyuan3d/default/xxx/xxx.png",
"encryptTmpSecretId": "...",
"encryptTmpSecretKey": "...",
"encryptToken": "...",
"startTime": 1779562912,
"expiredTime": 1811098912,
"resourceUrl": "https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=..."
}
```
---
### 8. 图片内容审核
- **URL**: `/api/3d/resource/review`
- **Method**: `POST`
- **描述**: 上传前对图片进行安全审核
**请求体**:
```json
{
"sceneType": "playGround3D-2.0",
"text": "",
"resourceType": "image",
"resourceUrl": "https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=..."
}
```
**响应示例**:
```json
{
"code": 0,
"msg": "",
"result": {
"text": { "code": 0 },
"name": { "code": 0 },
"resource": { "code": 0 }
},
"id": "..."
}
```
---
### 9. 触发3D生成
- **URL**: `/api/3d/creations/generations?timestamp={ts}&nonce={nonce}&sign={sign}`
- **Method**: `POST`
- **描述**: 提交图片触发3D模型生成任务。URL 带有签名参数防重放。
**请求体**:
```json
{
"sceneType": "playGround3D-2.0",
"count": 1,
"modelType": "image2ModelV3.1",
"title": "",
"style": "",
"imageList": [
"https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=..."
],
"enable_pbr": true,
"enableLowPoly": false,
"faceCount": 1500000
}
```
**参数说明**:
- `modelType`: `image2ModelV3.1` (图生3D V3.1)
- `faceCount`: 模型面数,`1500000` 对应 1.5m`1000000` 对应 1m`500000` 对应 500k`50000` 对应 50k
- `enable_pbr`: 是否启用 PBR 材质
- `imageList`: 已上传图片的 `resourceUrl` 列表
**响应**: 返回 `creationsId`,用于后续轮询生成状态。
---
### 10. 文生3D
- **URL**: `/api/3d/creations/generations?timestamp={ts}&nonce={nonce}&sign={sign}`
- **Method**: `POST`
- **描述**: 输入文本描述触发3D模型生成。接口地址和图生3D相同仅请求体参数不同。
**请求体**:
```json
{
"sceneType": "playGround3D-2.0",
"count": 4,
"modelType": "text2ModelV3.1",
"title": "一只银白色的鲑鱼",
"style": "",
"prompt": "一只银白色的鲑鱼",
"enable_pbr": true,
"enableLowPoly": false,
"faceCount": 1500000
}
```
**参数说明**:
- `modelType`: `text2ModelV3.1` (文生3D V3.1)
- `count`: 一次生成的候选数量,默认 `4`
- `title` / `prompt`: 文本描述,建议以单主体为主
- `faceCount`: 模型面数,`1500000`(1.5m) / `1000000`(1m) / `500000`(500k) / `50000`(50k)
- `enable_pbr`: 是否启用 PBR 材质
- `style`: 纹理风格,可选 `通用``石雕``青花瓷``中国风``卡通``赛博朋克`
**响应**: 返回 `creationsId`,用于后续轮询生成状态。
---
### 11. 查询生成状态(轮询)
- **URL**: `/api/3d/creations/detail?creationsId={id}`
- **Method**: `GET`
- **描述**: 轮询查询3D生成任务的进度和结果。前端约每 2~3 秒轮询一次。
**响应示例**(扁平格式):
```json
{
"id": "355e2c8b-eaab-4e22-a521-375c7c2fcc2e",
"status": "processing",
"prompt": "...",
"result": [
{
"taskId": "...",
"status": "processing",
"progress": 21,
"urlResult": {
"glb": "https://...",
"obj": "https://...",
"image_url": "https://..."
}
}
]
}
```
**状态说明**:
- `wait`: 排队中
- `processing`: 生成中
- `success`: 生成完成
- `fail`: 生成失败
---
### 12. 获取作品列表(资产)
- **URL**: `/api/3d/creations/list`
- **Method**: `POST`
- **描述**: 获取当前用户的3D作品列表对应「资产」页面。
**响应示例**(扁平格式):
```json
{
"totalCount": 10,
"creations": [
{
"id": "...",
"title": "一只银白色的鲑鱼",
"status": "success",
"modelType": "text2ModelV3.1",
"result": [...]
}
]
}
```
---
### 13. 获取作品数量
- **URL**: `/api/3d/creations/count`
- **Method**: `POST`
- **描述**: 获取当前用户的作品总数及各状态数量
---
### 14. 下载资源文件
- **URL**: `/api/3d/resource/download?resourceId={id}`
- **Method**: `GET`
- **描述**: 下载图片或模型文件(二进制流)
---
## 完整调用流程文生3D
```
1. POST /api/3d/creations/generations
→ 提交文本描述,得到 creationsId
2. GET /api/3d/creations/detail?creationsId=xxx
→ 轮询直到 status == "success"
3. 从 detail 响应中提取 modelUrl / previewUrl 下载模型
```
**与图生3D的区别**文生3D无需图片上传和审核直接调用 generations 接口即可。
---
## 完整调用流程图生3D
```
1. POST /api/3d/resource/genUploadInfo
→ 获取 COS 临时凭证
2. 用临时凭证把图片上传到腾讯云 COS
→ 得到 resourceUrl
3. POST /api/3d/resource/review
→ 图片安全审核通过
4. POST /api/3d/creations/generations
→ 触发3D生成得到 creationsId
5. GET /api/3d/creations/detail?creationsId=xxx
→ 轮询直到 status == "success"
6. 从 detail 响应中提取 modelUrl / previewUrl 下载模型
```
---
## 使用建议
1. 运行 `hunyuan3d_login.py` 完成登录,持久化 Cookie
2. 运行 `hunyuan3d_api_sniffer.py` 拦截实际操作中的完整请求和响应
3. 在浏览器中手动触发各种操作(生成、分享、删除等),终端会实时打印 API 调用
4. 结果保存到 `api_requests.log.json`,可据此补充本文档

599
doc/api_complete.md Normal file
View File

@@ -0,0 +1,599 @@
# 腾讯混元3D API 完整文档
> 基础地址: `https://3d.hunyuan.tencent.com`
> 签名算法: HMAC-SHA256已破解见 `hunyuan3d_sign.py`
---
## 目录
1. [认证方式](#认证方式)
2. [通用响应格式](#通用响应格式)
3. [用户相关接口](#用户相关接口)
4. [作品管理接口](#作品管理接口)
5. [图生3D接口](#图生3d接口)
6. [文生3D接口](#文生3d接口)
7. [草图生3D接口](#草图生3d接口)
8. [3D动画生成接口](#3d动画生成接口)
9. [3D纹理生成接口](#3d纹理生成接口)
10. [3D智能拓扑接口](#3d智能拓扑接口)
11. [资源上传接口](#资源上传接口)
12. [分享接口](#分享接口)
13. [配置接口](#配置接口)
14. [生成模式参数总表](#生成模式参数总表)
---
## 认证方式
Cookie 会话认证。需要携带 `hunyuan_user``hunyuan_token` Cookie。
所有 API 请求需要在 URL 查询参数中附带签名:
- `timestamp`: 秒级时间戳
- `nonce`: 16位随机字符串
- `sign`: HMAC-SHA256签名
---
## 通用响应格式
```json
{
"code": 0,
"message": "success",
"data": {}
}
```
- `code`: 业务状态码,`0` 表示成功
- `message`: 提示信息
- `data`: 实际返回数据
---
## 用户相关接口
### 1. 获取用户信息
- **URL**: `/api/3d/getuserinfo`
- **Method**: `GET`
- **描述**: 获取当前登录用户的基本信息
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"userId": "abb5e2f51b08416fb5459a5082504ea0",
"userType": "external",
"loginType": "email",
"registered": true,
"status": 2
}
}
```
### 2. 获取用户配额
- **URL**: `/api/3d/quotainfo`
- **Method**: `POST`
- **描述**: 获取当前用户的生成配额
**请求体**:
```json
{"sceneType": "3dCreations"}
```
**响应示例**:
```json
{
"date": "20260524",
"totalQuota": 20,
"remainQuota": 0,
"consumeQuota": 20,
"alarmQuota": 1,
"userInviteQuota": 0,
"perUserInviteQuotaCount": 30,
"maxUserInviteQuota": 6
}
```
---
## 作品管理接口
### 3. 获取作品列表
- **URL**: `/api/3d/creations/list`
- **Method**: `POST`
- **描述**: 获取当前用户的3D作品列表
**请求体**:
```json
{
"page": 1,
"pageSize": 20
}
```
### 4. 获取作品数量
- **URL**: `/api/3d/creations/count`
- **Method**: `POST`
- **描述**: 获取作品数量
**请求体**:
```json
{"statusList": ["wait", "processing"]}
```
### 5. 查询作品详情/生成状态
- **URL**: `/api/3d/creations/detail`
- **Method**: `GET`
- **描述**: 查询3D生成任务的进度和结果
**查询参数**:
```
?creationsId={id}
```
**状态说明**:
- `wait`: 排队中
- `processing`: 生成中
- `success`: 生成完成
- `fail`: 生成失败
### 6. 取消任务
- **URL**: `/api/3d/creations/cancel`
- **Method**: `POST`
- **描述**: 取消进行中的生成任务
---
## 图生3D接口
### 7. 触发图生3D
- **URL**: `/api/3d/creations/generations`
- **Method**: `POST`
- **描述**: 提交图片触发3D模型生成
**请求体**:
```json
{
"sceneType": "playGround3D-2.0",
"count": 1,
"modelType": "image2ModelV3.1",
"title": "",
"style": "",
"imageList": [
"https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=..."
],
"enable_pbr": true,
"enableLowPoly": false,
"faceCount": 1500000
}
```
**参数说明**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `sceneType` | string | 是 | 场景类型,固定 `playGround3D-2.0` |
| `count` | int | 是 | 生成数量,固定 `1` |
| `modelType` | string | 是 | 模型类型,`image2ModelV3.1` |
| `title` | string | 否 | 作品标题 |
| `style` | string | 否 | 风格,见下方风格表 |
| `imageList` | array | 是 | 图片URL列表需先上传获取resourceId |
| `enable_pbr` | bool | 否 | 是否启用PBR材质默认 `true` |
| `enableLowPoly` | bool | 否 | 是否低多边形,默认 `false` |
| `faceCount` | int | 否 | 面数,`1500000` |
**风格(style)选项**:
- `""` - 默认
- `"sculpture"` - 石雕
- `"qinghuaci"` - 青花瓷
- `"china_style"` - 中国风
- `"cartoon"` - 卡通
- `"cyberpunk"` - 赛博朋克
**几何风格(geo_type)选项**:
- `"default"` - 默认
- `"voxel"` - 体素
- `"low_poly"` - 低多边形
---
## 文生3D接口
### 8. 触发文生3D
- **URL**: `/api/3d/creations/generations`
- **Method**: `POST`
- **描述**: 输入文本描述生成3D模型
**请求体**:
```json
{
"sceneType": "playGround3D-2.0",
"count": 1,
"modelType": "text2ModelV3.1",
"title": "银色的长剑",
"style": "",
"text": "银色的长剑",
"enable_pbr": true,
"enableLowPoly": false,
"faceCount": 1500000
}
```
**参数说明**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `modelType` | string | 是 | `text2ModelV3.1` |
| `text` | string | 是 | 文本描述/提示词 |
| `title` | string | 否 | 作品标题 |
| `style` | string | 否 | 风格 |
| 其他 | - | - | 同图生3D |
**提示词示例**(来自配置):
- "银色的长剑"
- "猫头鹰,大眼睛,深棕色"
- "粉色蝴蝶结"
- "橙色颈挂式耳机"
- "歼-20威龙"
- "装甲机械风格逼真4K"
- "汉堡里面有酸黄瓜 生菜 牛肉饼和芝士"
---
## 草图生3D接口
### 9. 触发草图生3D
- **URL**: `/api/3d/creations/generations`
- **Method**: `POST`
- **描述**: 上传草图+提示词生成3D模型
**请求体**:
```json
{
"sceneType": "playGround3D-2.0",
"count": 1,
"modelType": "sketch2ModelV3.1",
"title": "",
"style": "",
"text": "一个穿着蓝色衣服的小男孩",
"imageList": [
"https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=..."
],
"enable_pbr": true,
"enableLowPoly": false,
"faceCount": 1500000
}
```
**参数说明**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `modelType` | string | 是 | `sketch2ModelV3.1` |
| `text` | string | 是 | 草图描述提示词 |
| `imageList` | array | 是 | 草图图片URL |
---
## 3D动画生成接口
### 10. 获取动画动作模板
- **URL**: `/api/3d/workflow/action/templates`
- **Method**: `GET`
- **描述**: 获取可用的动作模板列表
**动作类型(motionType)**:
| ID | 名称 | 说明 |
|----|------|------|
| 9 | 跨步 | Capoeira |
| 10 | 摔倒 | FallingBackDeath |
| 11 | 跳跃 | Jumping |
| 12 | 踢腿 | Kicking |
| 13 | 挥击 | OneHandSwordCombo |
| 15 | 跑步 | TreadmillRunning |
| 16 | 跳舞 | TwistDance |
### 11. 触发3D动画生成
- **URL**: `/api/3d/creations/generations`
- **Method**: `POST`
- **描述**: 为3D模型绑定骨骼并生成动画
**请求体**:
```json
{
"sceneType": "playGround3D-2.0",
"count": 1,
"modelType": "animation3dV2",
"title": "",
"style": "",
"imageList": [
"https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=..."
],
"motionType": 11,
"enable_pbr": true,
"enableLowPoly": false,
"faceCount": 1500000
}
```
**参数说明**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `modelType` | string | 是 | `animation3dV2` |
| `motionType` | int | 是 | 动作类型ID |
| `imageList` | array | 是 | 要绑定的3D模型图片 |
---
## 3D纹理生成接口
### 12. 触发3D纹理生成
- **URL**: `/api/3d/creations/generations`
- **Method**: `POST`
- **描述**: 为白模生成纹理贴图
**请求体**:
```json
{
"sceneType": "playGround3D-2.0",
"count": 1,
"modelType": "textureTo3DV2",
"title": "",
"style": "",
"text": "木质纹理,棕色",
"imageList": [
"https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=..."
],
"enable_pbr": true,
"enableLowPoly": false,
"faceCount": 1500000
}
```
**参数说明**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `modelType` | string | 是 | `textureTo3DV2` |
| `text` | string | 是 | 纹理描述 |
| `imageList` | array | 是 | 白模图片URL |
---
## 3D智能拓扑接口
### 13. 触发3D智能拓扑减面
- **URL**: `/api/3d/creations/generations`
- **Method**: `POST`
- **描述**: 输入文本/图片/白模,生成布线规整的低面数模型
**请求体**:
```json
{
"sceneType": "playGround3D-2.0",
"count": 1,
"modelType": "lowpolyV2",
"title": "",
"style": "",
"text": "",
"imageList": [
"https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=..."
],
"enable_pbr": true,
"enableLowPoly": true,
"faceCount": 5000,
"topology_format": "glb"
}
```
**参数说明**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `modelType` | string | 是 | `lowpolyV2` |
| `faceCount` | int | 否 | 目标面数:`5000`(低), `18000`(中), `30000`(高) |
| `topology_format` | string | 否 | 输出格式:`glb``obj` |
| `enableLowPoly` | bool | 否 | 必须设为 `true` |
---
## 资源上传接口
### 14. 获取上传凭证
- **URL**: `/api/3d/resource/genUploadInfo`
- **Method**: `POST`
- **描述**: 获取腾讯云COS临时上传凭证
**请求体**:
```json
{"fileName": "your_image.png"}
```
**响应示例**:
```json
{
"bucketName": "hunyuan-base-prod-1258344703",
"region": "ap-guangzhou",
"location": "hunyuan3d/default/...",
"encryptTmpSecretId": "...",
"encryptTmpSecretKey": "...",
"encryptToken": "...",
"startTime": 1779562912,
"expiredTime": 1811098912,
"resourceUrl": "https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=..."
}
```
### 15. 图片内容审核
- **URL**: `/api/3d/resource/review`
- **Method**: `POST`
- **描述**: 上传前对图片进行安全审核
**请求体**:
```json
{
"sceneType": "playGround3D-2.0",
"text": "",
"resourceType": "image",
"resourceUrl": "https://3d.hunyuan.tencent.com/api/3d/resource/download?resourceId=..."
}
```
### 16. 下载资源
- **URL**: `/api/3d/resource/download`
- **Method**: `GET`
- **描述**: 下载图片或模型文件
**查询参数**:
```
?resourceId={id}
```
---
## 分享接口
### 17. 创建分享
- **URL**: `/api/3d/share`
- **Method**: `POST`
- **描述**: 创建分享链接
**请求体**:
```json
{
"contentType": "creation",
"contentId": "creationsId",
"sharedContent": "",
"platform": "3dPlayground"
}
```
---
## 配置接口
### 18. 获取全局配置
- **URL**: `/api/3d/config`
- **Method**: `GET`
- **描述**: 获取前端配置,包含提示词示例、风格配置、模板等
**包含内容**:
- `promptExamples` - 文生3D提示词示例
- `styleConfig` - 风格配置(几何风格+纹理风格)
- `animation3dConfig` - 动画模板配置
- `sketchImageConfig` - 草图示例配置
- `gameTemplate` - 游戏角色模板
- `retopologize` - 拓扑减面配置
---
## 生成模式参数总表
| 生成模式 | modelType | sceneType | 特有参数 | 说明 |
|----------|-----------|-----------|----------|------|
| **图生3D** | `image2ModelV3.1` | `playGround3D-2.0` | `imageList` | 图片生成3D模型 |
| **文生3D** | `text2ModelV3.1` | `playGround3D-2.0` | `text` | 文本描述生成3D |
| **草图生3D** | `sketch2ModelV3.1` | `playGround3D-2.0` | `text`, `imageList` | 草图+提示词生成3D |
| **3D动画** | `animation3dV2` | `playGround3D-2.0` | `motionType` | 为模型绑定骨骼动画 |
| **3D纹理** | `textureTo3DV2` | `playGround3D-2.0` | `text`, `imageList` | 为白模生成纹理 |
| **智能拓扑** | `lowpolyV2` | `playGround3D-2.0` | `faceCount`(5000/18000/30000) | 减面优化 |
### 通用参数(所有模式)
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `count` | int | 1 | 生成数量固定1 |
| `title` | string | "" | 作品标题 |
| `style` | string | "" | 纹理风格 |
| `enable_pbr` | bool | true | PBR材质 |
| `enableLowPoly` | bool | false | 低多边形 |
| `faceCount` | int | 1500000 | 面数 |
### 风格参数
**纹理风格(style)**:
- `""` - 通用
- `"sculpture"` - 石雕
- `"qinghuaci"` - 青花瓷
- `"china_style"` - 中国风
- `"cartoon"` - 卡通
- `"cyberpunk"` - 赛博朋克
**几何风格(geo_type)**:
- `"default"` - 默认
- `"voxel"` - 体素
- `"low_poly"` - 低多边形
### 动作类型(motionType)
| ID | 动作 |
|----|------|
| 9 | 跨步(Capoeira) |
| 10 | 摔倒(FallingBackDeath) |
| 11 | 跳跃(Jumping) |
| 12 | 踢腿(Kicking) |
| 13 | 挥击(OneHandSwordCombo) |
| 15 | 跑步(TreadmillRunning) |
| 16 | 跳舞(TwistDance) |
---
## 完整调用流程
### 图生3D流程
```
1. POST /resource/genUploadInfo → 获取COS凭证
2. 上传图片到COS → 获取resourceUrl
3. POST /resource/review → 图片审核
4. POST /creations/generations → 触发生成
5. GET /creations/detail → 轮询状态
6. 下载模型文件(glb/obj)
```
### 文生3D流程
```
1. POST /creations/generations (带text参数) → 触发生成
2. GET /creations/detail → 轮询状态
3. 下载模型文件
```
### 动画生成流程
```
1. 先生成/上传3D模型
2. POST /creations/generations (modelType=animation3dV2, motionType=X)
3. GET /creations/detail → 轮询状态
4. 下载带动画的模型
```
---
## 限制与注意事项
1. **配额限制**: 每日20次生成配额
2. **频率限制**: 频繁调用会触发"今天操作频繁,请明天再试"
3. **count参数**: 必须固定为1其他值会报错
4. **faceCount**: 图生3D固定1500000拓扑减面用5000/18000/30000
5. **图片格式**: 必须通过腾讯COS上传获取resourceId

11
hunyuan3dweb/__init__.py Normal file
View File

@@ -0,0 +1,11 @@
from .api import Hunyuan3DAPI
from .api_complete import Hunyuan3DAPI as Hunyuan3DAPIComplete
from .sign import sign, sign_with_custom_nonce
__version__ = "0.1.0"
__all__ = [
"Hunyuan3DAPI",
"Hunyuan3DAPIComplete",
"sign",
"sign_with_custom_nonce",
]

238
hunyuan3dweb/api.py Normal file
View File

@@ -0,0 +1,238 @@
#!/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"
})
if cookies:
self.set_cookies(cookies)
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()
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") 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)
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') 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))

View File

@@ -0,0 +1,432 @@
#!/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') 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())}")

View File

View File

@@ -0,0 +1,221 @@
#!/usr/bin/env python3
"""
腾讯混元3D 图生3D 完整自动化脚本
使用 cloakbrowser 在浏览器内完成所有操作(包括签名生成)
"""
import json
import os
import time
from datetime import datetime
from cloakbrowser import launch_persistent_context
from ..config import get_profile_dir
PROFILE_DIR = str(get_profile_dir())
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}")
if not os.path.exists(PROFILE_DIR):
raise FileNotFoundError(f"未找到登录状态目录: {PROFILE_DIR}")
context = launch_persistent_context(PROFILE_DIR, headless=True)
page = context.new_page()
# 用于存储结果
result = {"creationsId": None, "status": None, "modelUrl": None}
# 监听响应
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:
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]
if "modelUrl" in model_data:
result["modelUrl"] = model_data["modelUrl"]
except:
pass
page.on("response", handle_response)
try:
print("[1/6] 打开首页...")
page.goto("https://3d.hunyuan.tencent.com/")
page.wait_for_timeout(3000)
print("[2/6] 点击 AI创作...")
page.locator("button").filter(has_text="AI创作").first.click()
page.wait_for_timeout(2000)
print("[3/6] 点击 图生3D...")
page.locator("text=图生3D").first.click()
page.wait_for_timeout(2000)
print("[4/6] 点击 单张图片...")
page.locator("text=单张图片").first.click()
page.wait_for_timeout(2000)
print("[5/6] 上传图片...")
page.locator('input[type=file]').first.set_input_files(image_path)
page.wait_for_timeout(3000)
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(f'''
async () => {{
const resp = await fetch('https://3d.hunyuan.tencent.com/api/3d/creations/detail?creationsId={result["creationsId"]}', {{
headers: {{'X-Source': 'web', 'Referer': 'https://3d.hunyuan.tencent.com/'}}
}});
return await resp.json();
}}
''')
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]
result["modelUrl"] = model_data.get("modelUrl")
result["previewUrl"] = model_data.get("previewUrl")
print(f"\n✅ 生成完成!")
break
elif status == "fail":
print(f"\n❌ 生成失败")
break
time.sleep(3)
else:
print(f"\n⏰ 超时,当前状态: {last_status}")
return result
finally:
context.close()
def get_quota():
"""获取当前配额信息"""
context = launch_persistent_context(PROFILE_DIR, headless=True)
page = context.new_page()
try:
page.goto("https://3d.hunyuan.tencent.com/")
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
finally:
context.close()
def get_creations_list():
"""获取作品列表"""
context = launch_persistent_context(PROFILE_DIR, headless=True)
page = context.new_page()
try:
page.goto("https://3d.hunyuan.tencent.com/")
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
finally:
context.close()
if __name__ == "__main__":
import sys
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}")

View File

@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""
腾讯混元3D 邮箱验证码登录 CLI 工具 (CloakBrowser 持久化版本)
第一次输入邮箱回车后自动发送验证码
第二次输入验证码后回车自动点击登录按钮
登录状态会自动保存到 ./hunyuan3d_profile下次运行无需重新登录
"""
import os
import sys
import time
from cloakbrowser import launch_persistent_context
from ..config import get_profile_dir, get_cookie_path
PROFILE_DIR = str(get_profile_dir())
def _extract_and_save_cookies(context):
"""从浏览器上下文提取 Cookie 并保存到文件"""
try:
cookies = context.cookies()
relevant_names = {"hunyuan_user", "hunyuan_token", "hunyuan_source", "hy_user"}
cookie_dict = {c["name"]: c["value"] for c in cookies if c["name"] in relevant_names}
if cookie_dict:
cookie_str = "; ".join(f"{k}={v}" for k, v in cookie_dict.items())
cookie_path = get_cookie_path()
cookie_path.write_text(cookie_str, encoding="utf-8")
print(f"Cookie 已自动保存到: {cookie_path}")
return True
else:
print("警告: 未找到 hunyuan 相关 CookieAPI 客户端可能无法使用")
except Exception as e:
print(f"自动提取 Cookie 失败: {e}")
return False
def main():
# 使用持久化上下文cookie 和登录状态会保存到 PROFILE_DIR
context = launch_persistent_context(PROFILE_DIR, headless=False)
page = context.new_page()
print("正在打开腾讯混元3D...")
page.goto("https://3d.hunyuan.tencent.com/")
time.sleep(2)
# 检查是否已经是登录状态(没有登录按钮说明已登录)
login_btn = page.locator("button").filter(has_text="登录").first
if login_btn.count() == 0:
print("检测到已有登录状态,无需重新登录。")
print(f"当前页面: {page.url}")
else:
# 未登录,走登录流程
login_btn.click()
time.sleep(1.5)
# 切换到邮箱登录
email_tab = page.locator("text=邮箱").first
if email_tab.count() > 0:
email_tab.click()
time.sleep(1.5)
else:
print("未找到邮箱登录选项")
context.close()
return
# 定位元素
email_input = page.locator('input[placeholder="请输入邮箱地址"]')
code_input = page.locator('input[type="number"][placeholder="请输入邮箱验证码"]')
send_code_btn = page.locator("a.hyc-email-login__send-code")
login_submit_btn = page.locator("button.hyc-email-login__btn")
checkbox = page.locator(".t-checkbox__former")
# 第一次交互:输入邮箱并发送验证码
print("\n============================================")
email = input("请输入邮箱地址,按回车发送验证码: ").strip()
if not email:
print("邮箱不能为空,退出")
context.close()
return
email_input.fill(email)
time.sleep(0.5)
# 勾选协议(如果未勾选)
if checkbox.count() > 0:
is_checked = checkbox.evaluate("el => el.checked")
if not is_checked:
checkbox.evaluate("el => el.click()")
time.sleep(0.3)
if send_code_btn.count() > 0:
send_code_btn.click()
print("已点击发送验证码,请查收邮件...")
else:
print("未找到发送验证码按钮")
context.close()
return
# 第二次交互:输入验证码并登录
print("\n============================================")
code = input("请输入邮箱验证码,按回车登录: ").strip()
if not code:
print("验证码不能为空,退出")
context.close()
return
code_input.fill(code)
time.sleep(0.5)
# 点击登录
if login_submit_btn.count() > 0:
login_submit_btn.click()
print("已点击登录按钮,等待跳转...")
else:
print("未找到登录提交按钮")
context.close()
return
# 等待登录成功跳转
try:
page.wait_for_url(lambda url: "login" not in url, timeout=30000)
print(f"\n登录成功!当前页面: {page.url}")
print(f"登录状态已保存到: {os.path.abspath(PROFILE_DIR)}")
_extract_and_save_cookies(context)
except Exception:
print("\n登录可能仍在处理中,或出现错误。请检查浏览器状态。")
# 保持浏览器打开
print("\n按 Enter 键关闭浏览器并退出...")
input()
context.close()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n已取消")
sys.exit(0)

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""
腾讯混元3D API 拦截分析工具 (CloakBrowser)
利用持久化登录状态,自动捕获所有 API 请求和响应
"""
import json
import os
import sys
import time
from datetime import datetime
from cloakbrowser import launch_persistent_context
from ..config import get_profile_dir
PROFILE_DIR = str(get_profile_dir())
API_LOG_FILE = "./api_requests.log.json"
def main():
logs = []
def log_request(request):
url = request.url
# 只关注同域 API 和关键第三方接口
if "/api/" in url or "hunyuan" in url:
entry = {
"time": datetime.now().isoformat(),
"type": "request",
"method": request.method,
"url": url,
"headers": dict(request.headers) if hasattr(request, "headers") else {},
}
# 尝试获取 POST body
if request.method in ("POST", "PUT", "PATCH") and hasattr(request, "post_data"):
try:
entry["body"] = request.post_data
except Exception:
pass
logs.append(entry)
print(f"[REQ] {request.method} {url}")
def log_response(response):
url = response.url
if "/api/" in url or "hunyuan" in url:
entry = {
"time": datetime.now().isoformat(),
"type": "response",
"status": response.status,
"url": url,
}
# 尝试读取响应体
try:
# 只读取 JSON 响应
content_type = response.headers.get("content-type", "")
if "json" in content_type:
body = response.json()
entry["body"] = body
else:
text = response.text()
# 限制文本长度,避免过大
entry["body_preview"] = text[:500]
except Exception as e:
entry["body_error"] = str(e)
logs.append(entry)
print(f"[RES] {response.status} {url}")
if not os.path.exists(PROFILE_DIR):
print(f"错误: 未找到持久化目录 {PROFILE_DIR}")
print("请先运行 hunyuan3dweb-login 完成登录")
sys.exit(1)
context = launch_persistent_context(PROFILE_DIR, headless=False)
page = context.new_page()
page.on("request", log_request)
page.on("response", log_response)
print("正在打开腾讯混元3D并捕获 API...")
page.goto("https://3d.hunyuan.tencent.com/")
time.sleep(3)
# 检查是否已登录
login_btn = page.locator("button").filter(has_text="登录").first
if login_btn.count() > 0:
print("\n警告: 当前未检测到登录状态API 可能返回未授权")
else:
print("\n已检测到登录状态,开始捕获 API...")
print("\n你可以手动在浏览器中操作切换页面、生成3D等")
print("所有 API 请求会实时打印在终端中")
print("按 Enter 停止捕获并保存结果...\n")
input()
# 保存日志
with open(API_LOG_FILE, "w", encoding="utf-8") as f:
json.dump(logs, f, ensure_ascii=False, indent=2)
print(f"\n共捕获 {len([l for l in logs if l['type'] == 'request'])} 个请求")
print(f"结果已保存到: {os.path.abspath(API_LOG_FILE)}")
context.close()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n已取消")
sys.exit(0)

55
hunyuan3dweb/cli.py Normal file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""
Hunyuan3D Web CLI
"""
import argparse
import json
import sys
from .api import Hunyuan3DAPI, load_cookies_from_file
from .config import get_cookie_path
_DEFAULT_COOKIE = str(get_cookie_path())
def main():
parser = argparse.ArgumentParser(description="Hunyuan3D Web CLI")
subparsers = parser.add_subparsers(dest="command")
quota_parser = subparsers.add_parser("quota", help="查询配额")
quota_parser.add_argument("--cookies", "-c", default=_DEFAULT_COOKIE, help="Cookie文件路径")
list_parser = subparsers.add_parser("list", help="查询作品列表")
list_parser.add_argument("--cookies", "-c", default=_DEFAULT_COOKIE, help="Cookie文件路径")
text_parser = subparsers.add_parser("text", help="文生3D")
text_parser.add_argument("prompt", help="文本描述")
text_parser.add_argument("--cookies", "-c", default=_DEFAULT_COOKIE, help="Cookie文件路径")
text_parser.add_argument("--title", "-t", default="", help="作品标题")
status_parser = subparsers.add_parser("status", help="查询生成状态")
status_parser.add_argument("creation_id", help="创作ID")
status_parser.add_argument("--cookies", "-c", default=_DEFAULT_COOKIE, help="Cookie文件路径")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
cookies = load_cookies_from_file(args.cookies)
api = Hunyuan3DAPI(cookies)
if args.command == "quota":
print(json.dumps(api.get_quota_info(), indent=2, ensure_ascii=False))
elif args.command == "list":
print(json.dumps(api.get_creation_list(), indent=2, ensure_ascii=False))
elif args.command == "text":
result = api.generate_text(args.prompt, title=args.title)
print(json.dumps(result, indent=2, ensure_ascii=False))
elif args.command == "status":
print(json.dumps(api.get_generation_status(args.creation_id), indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()

23
hunyuan3dweb/config.py Normal file
View File

@@ -0,0 +1,23 @@
import os
from pathlib import Path
def get_config_dir() -> Path:
"""Return the user configuration directory for hunyuan3dweb."""
xdg = os.environ.get("XDG_CONFIG_HOME")
base = Path(xdg) if xdg else Path.home() / ".config"
path = base / "hunyuan3dweb"
path.mkdir(parents=True, exist_ok=True)
return path
def get_cookie_path() -> Path:
"""Return the path to the cookies file."""
return get_config_dir() / "cookies.txt"
def get_profile_dir() -> Path:
"""Return the path to the browser profile directory."""
path = get_config_dir() / "profile"
path.mkdir(parents=True, exist_ok=True)
return path

162
hunyuan3dweb/sign.py Normal file
View File

@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""
腾讯混元3D签名算法实现
从 webpack 模块逆向提取
"""
import hmac
import hashlib
import time
import random
import string
# 密钥派生相关常量(从 JS 提取)
C = bytes([122, 59, 92, 165, 30, 79, 166, 139, 142, 129, 139, 89, 219, 131, 101, 204])
D = bytes([122, 59, 92, 45, 30, 79, 106, 139, 156, 13, 46, 63, 74, 91, 108, 125])
U = [3, 5, 2, 7, 1, 4, 6, 2, 5, 3, 1, 4, 2, 6, 3, 5]
M = [14, 11, 13, 9, 15, 10, 12, 8, 6, 3, 5, 1, 7, 2, 4, 0]
def derive_key(c: bytes) -> str:
"""密钥派生函数 - 从硬编码常量派生签名密钥"""
if len(c) != 16:
raise ValueError("输入必须是一个16字节的数组")
# 步骤1: XOR with D
t = bytearray(16)
for i in range(16):
t[i] = c[i] ^ D[i]
# 步骤2: 循环左移
o = bytearray(16)
for i in range(16):
n = U[i]
# Python 的移位和 JS 不同,需要模拟 8 位无符号整数
val = t[i]
o[i] = (val << n | val >> (8 - n)) & 0xFF
# 步骤3: 置换
n = bytearray(16)
for i in range(16):
n[i] = o[M[i]]
# 步骤4: 找到第一个0字节截断
try:
r = n.index(0)
except ValueError:
r = 16
return n[:r].decode('utf-8')
def generate_nonce(length: int = 16) -> str:
"""生成随机 nonce"""
chars = string.ascii_letters + string.digits # 62字符
return ''.join(random.choice(chars) for _ in range(length))
def get_timestamp() -> int:
"""获取当前时间戳(秒)"""
return int(time.time())
def sort_params(params: dict) -> list:
"""排序参数,过滤空值"""
import json
items = []
for k, v in params.items():
if v is not None and v != "":
# 对列表和字典使用 JSON 格式
if isinstance(v, (list, dict, bool)):
items.append((k, json.dumps(v, separators=(',', ':'), ensure_ascii=False)))
else:
items.append((k, str(v)))
return sorted(items, key=lambda x: x[0])
def join_params(items: list) -> str:
"""拼接参数为查询字符串"""
return '&'.join(f"{k}={v}" for k, v in items)
def sign(params: dict,
nonce_length: int = 16,
timestamp_field: str = "timestamp",
nonce_field: str = "nonce",
sign_field: str = "sign") -> dict:
"""
主签名函数
Args:
params: 请求参数
nonce_length: nonce 长度
timestamp_field: 时间戳字段名
nonce_field: nonce 字段名
sign_field: 签名字段名
Returns:
包含签名的新参数字典
"""
result = dict(params)
result[timestamp_field] = get_timestamp()
result[nonce_field] = generate_nonce(nonce_length)
# 排序并拼接
sorted_items = sort_params(result)
param_str = join_params(sorted_items)
# 派生密钥
key = derive_key(C)
# HMAC-SHA256 签名,然后转为 Hex
signature = hmac.new(
key.encode('utf-8'),
param_str.encode('utf-8'),
hashlib.sha256
).hexdigest()
result[sign_field] = signature
return result
def sign_with_custom_nonce(params: dict, timestamp: int, nonce: str,
timestamp_field: str = "timestamp",
nonce_field: str = "nonce",
sign_field: str = "sign") -> dict:
"""使用指定的 timestamp 和 nonce 生成签名(用于验证)"""
result = dict(params)
result[timestamp_field] = timestamp
result[nonce_field] = nonce
sorted_items = sort_params(result)
param_str = join_params(sorted_items)
key = derive_key(C)
signature = hmac.new(
key.encode('utf-8'),
param_str.encode('utf-8'),
hashlib.sha256
).hexdigest()
result[sign_field] = signature
return result
if __name__ == "__main__":
# 测试密钥派生
key = derive_key(C)
print(f"派生密钥: {key}")
print(f"密钥长度: {len(key)}")
# 测试签名
test_params = {
"sceneType": "playGround3D-2.0",
"count": 1,
"modelType": "image2ModelV3.1"
}
signed = sign(test_params)
print(f"\n测试签名:")
for k, v in sorted(signed.items()):
print(f" {k}: {v}")

29
pyproject.toml Normal file
View File

@@ -0,0 +1,29 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "hunyuan3dweb"
version = "0.1.0"
description = "Tencent Hunyuan 3D Web API client and browser automation tools"
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"requests",
]
[project.optional-dependencies]
browser = [
"cloakbrowser",
"playwright",
]
[project.scripts]
hunyuan3dweb = "hunyuan3dweb.cli:main"
hunyuan3dweb-login = "hunyuan3dweb.browser.login:main"
hunyuan3dweb-sniffer = "hunyuan3dweb.browser.sniffer:main"
hunyuan3dweb-generate = "hunyuan3dweb.browser.generator:main"
[tool.setuptools.packages.find]
where = ["."]
include = ["hunyuan3dweb*"]

BIN
test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB