commit 2ce35563c44a9a9706a0847742e605a32bd19329 Author: KawasakiAkasei Date: Sun May 24 21:47:44 2026 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a246506 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4aa505 --- /dev/null +++ b/README.md @@ -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 + +# 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 | diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..47e617e --- /dev/null +++ b/README_CN.md @@ -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 + +# 浏览器登录 +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 接口文档 | diff --git a/README_REVERSE_ENGINEERING.md b/README_REVERSE_ENGINEERING.md new file mode 100644 index 0000000..a83db3b --- /dev/null +++ b/README_REVERSE_ENGINEERING.md @@ -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. diff --git a/README_REVERSE_ENGINEERING_CN.md b/README_REVERSE_ENGINEERING_CN.md new file mode 100644 index 0000000..27ffc74 --- /dev/null +++ b/README_REVERSE_ENGINEERING_CN.md @@ -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的服务条款。请勿用于商业用途或大规模自动化调用。 diff --git a/UNIVERSALITY_ANALYSIS.md b/UNIVERSALITY_ANALYSIS.md new file mode 100644 index 0000000..93f8b79 --- /dev/null +++ b/UNIVERSALITY_ANALYSIS.md @@ -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; +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. **扩展改进**: 可以构建更通用的前端逆向工具链 diff --git a/doc/api.md b/doc/api.md new file mode 100644 index 0000000..364692d --- /dev/null +++ b/doc/api.md @@ -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`,可据此补充本文档 diff --git a/doc/api_complete.md b/doc/api_complete.md new file mode 100644 index 0000000..aef1f36 --- /dev/null +++ b/doc/api_complete.md @@ -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 diff --git a/hunyuan3dweb/__init__.py b/hunyuan3dweb/__init__.py new file mode 100644 index 0000000..08c77e4 --- /dev/null +++ b/hunyuan3dweb/__init__.py @@ -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", +] diff --git a/hunyuan3dweb/api.py b/hunyuan3dweb/api.py new file mode 100644 index 0000000..05bad10 --- /dev/null +++ b/hunyuan3dweb/api.py @@ -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)) diff --git a/hunyuan3dweb/api_complete.py b/hunyuan3dweb/api_complete.py new file mode 100644 index 0000000..0371db6 --- /dev/null +++ b/hunyuan3dweb/api_complete.py @@ -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())}") diff --git a/hunyuan3dweb/browser/__init__.py b/hunyuan3dweb/browser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hunyuan3dweb/browser/generator.py b/hunyuan3dweb/browser/generator.py new file mode 100644 index 0000000..2025d32 --- /dev/null +++ b/hunyuan3dweb/browser/generator.py @@ -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}") diff --git a/hunyuan3dweb/browser/login.py b/hunyuan3dweb/browser/login.py new file mode 100644 index 0000000..c7d4f58 --- /dev/null +++ b/hunyuan3dweb/browser/login.py @@ -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) diff --git a/hunyuan3dweb/browser/sniffer.py b/hunyuan3dweb/browser/sniffer.py new file mode 100644 index 0000000..e1dfce2 --- /dev/null +++ b/hunyuan3dweb/browser/sniffer.py @@ -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) diff --git a/hunyuan3dweb/cli.py b/hunyuan3dweb/cli.py new file mode 100644 index 0000000..e712c14 --- /dev/null +++ b/hunyuan3dweb/cli.py @@ -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() diff --git a/hunyuan3dweb/config.py b/hunyuan3dweb/config.py new file mode 100644 index 0000000..169d8ee --- /dev/null +++ b/hunyuan3dweb/config.py @@ -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 diff --git a/hunyuan3dweb/sign.py b/hunyuan3dweb/sign.py new file mode 100644 index 0000000..91aeba5 --- /dev/null +++ b/hunyuan3dweb/sign.py @@ -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}") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b669d6b --- /dev/null +++ b/pyproject.toml @@ -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*"] diff --git a/test.png b/test.png new file mode 100644 index 0000000..9ff857c Binary files /dev/null and b/test.png differ