Initial commit
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal 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
291
README.md
Normal 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
291
README_CN.md
Normal 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. 图生3D(API,已有 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 接口文档 |
|
||||
206
README_REVERSE_ENGINEERING.md
Normal file
206
README_REVERSE_ENGINEERING.md
Normal 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×tamp=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.
|
||||
222
README_REVERSE_ENGINEERING_CN.md
Normal file
222
README_REVERSE_ENGINEERING_CN.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# 腾讯混元3D API 逆向工程文档
|
||||
|
||||
## 项目概述
|
||||
|
||||
本项目通过逆向工程破解了腾讯混元3D(https://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×tamp=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
216
UNIVERSALITY_ANALYSIS.md
Normal 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
430
doc/api.md
Normal 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
599
doc/api_complete.md
Normal 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
11
hunyuan3dweb/__init__.py
Normal 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
238
hunyuan3dweb/api.py
Normal 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))
|
||||
432
hunyuan3dweb/api_complete.py
Normal file
432
hunyuan3dweb/api_complete.py
Normal 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())}")
|
||||
0
hunyuan3dweb/browser/__init__.py
Normal file
0
hunyuan3dweb/browser/__init__.py
Normal file
221
hunyuan3dweb/browser/generator.py
Normal file
221
hunyuan3dweb/browser/generator.py
Normal 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}")
|
||||
139
hunyuan3dweb/browser/login.py
Normal file
139
hunyuan3dweb/browser/login.py
Normal 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 相关 Cookie,API 客户端可能无法使用")
|
||||
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)
|
||||
110
hunyuan3dweb/browser/sniffer.py
Normal file
110
hunyuan3dweb/browser/sniffer.py
Normal 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
55
hunyuan3dweb/cli.py
Normal 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
23
hunyuan3dweb/config.py
Normal 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
162
hunyuan3dweb/sign.py
Normal 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
29
pyproject.toml
Normal 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*"]
|
||||
Reference in New Issue
Block a user