meshy-3d-generation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMeshy 3D Generation
Meshy 3D 生成
Directly communicate with the Meshy AI API to generate 3D assets. This skill handles the complete lifecycle: environment setup, API key detection, task creation, polling, downloading, and chaining multi-step pipelines.
For full endpoint reference (all parameters, response schemas, error codes), read reference.md.
直接与Meshy AI API交互以生成3D资产。本技能支持完整的生命周期管理:环境配置、API密钥检测、任务创建、状态轮询、资源下载,以及多步骤工作流的串联执行。
如需完整的端点参考(包含所有参数、响应结构、错误码),请查看reference.md。
IMPORTANT: First-Use Session Notice
重要提示:首次使用会话通知
When this skill is first activated in a session, inform the user:
All generated files will be saved toin the current working directory. Each project gets its own folder (meshy_output/) with model files, textures, thumbnails, and metadata. History is tracked in{YYYYMMDD_HHmmss}_{prompt}_{id}/.meshy_output/history.json
This only needs to be said once per session, at the beginning.
当本技能在会话中首次激活时,请告知用户:
所有生成的文件将保存至当前工作目录下的文件夹。每个项目会有独立的文件夹(格式为meshy_output/),包含模型文件、纹理、缩略图和元数据。历史记录将保存在{YYYYMMDD_HHmmss}_{prompt}_{id}/中。meshy_output/history.json
此提示仅需在每会话开始时说明一次。
IMPORTANT: File Organization
重要提示:文件组织规范
All downloaded files MUST go into a structured directory in the current working directory. Do NOT scatter files randomly.
meshy_output/- Each project gets its own folder:
meshy_output/{YYYYMMDD_HHmmss}_{prompt_slug}_{task_id_prefix}/ - For chained tasks (preview → refine → rig), reuse the same
project_dir - Track tasks in per project, and global
metadata.jsonhistory.json - Auto-download thumbnails alongside models
The Reusable Script Template below includes , , and helpers.
get_project_dir()record_task()save_thumbnail()所有下载的文件必须存入当前工作目录下结构化的目录中。请勿随意分散存放文件。
meshy_output/- 每个项目对应独立文件夹:
meshy_output/{YYYYMMDD_HHmmss}_{prompt_slug}_{task_id_prefix}/ - 对于串联任务(预览→精修→骨骼绑定),请复用同一个
project_dir - 每个项目的任务记录保存在中,全局历史记录保存在
metadata.jsonhistory.json - 自动下载模型对应的缩略图
下方的可复用脚本模板包含、和辅助函数。
get_project_dir()record_task()save_thumbnail()IMPORTANT: Shell Command Rules
重要提示:Shell命令规则
Use only standard POSIX tools in shell commands. Do NOT use (ripgrep), , or other non-standard CLI tools — they may not be installed. Use these standard alternatives instead:
rgfd| Do NOT use | Use instead |
|---|---|
| |
| |
| |
| |
仅在Shell命令中使用标准POSIX工具。请勿使用(ripgrep)、或其他非标准CLI工具——它们可能未预装。请使用以下标准替代工具:
rgfd| 禁止使用 | 替代工具 |
|---|---|
| |
| |
| |
| |
IMPORTANT: Run Long Tasks Properly
重要提示:长任务运行规范
Meshy generation tasks take 1–5 minutes. When running Python scripts that poll for completion:
- Write the entire create → poll → download flow as ONE Python script and execute it in a single Bash call. Do NOT split into multiple commands. This keeps the API key, task IDs, and session in one process context.
- Use (unbuffered) so progress output is visible in real time.
python3 -u script.py - Be patient with long-running scripts — do NOT interrupt or kill them prematurely. Tasks at 99% for 30–120s is normal finalization, not a failure.
Meshy生成任务需要1-5分钟。运行用于轮询任务完成状态的Python脚本时:
- 将完整的创建→轮询→下载流程编写为单个Python脚本,并通过一次Bash调用执行。请勿拆分为多个命令,这样可以在同一个进程上下文中保留API密钥、任务ID和会话信息。
- 使用(无缓冲模式),以便实时查看进度输出。
python3 -u script.py - 对长时间运行的脚本保持耐心——请勿提前中断或终止。任务在99%进度停留30-120秒属于正常的最终处理阶段,并非失败。
Step 0: Environment Detection (ALWAYS RUN FIRST)
步骤0:环境检测(必须首先执行)
Before any API call, detect whether the environment is ready:
bash
echo "=== Meshy API Key Detection ==="在进行任何API调用前,先检测环境是否就绪:
bash
echo "=== Meshy API Key Detection ==="1. Check current env var
1. 检查当前环境变量
if [ -n "$MESHY_API_KEY" ]; then
echo "ENV_VAR: FOUND (${MESHY_API_KEY:0:8}...)"
else
echo "ENV_VAR: NOT_FOUND"
fi
if [ -n "$MESHY_API_KEY" ]; then
echo "ENV_VAR: FOUND (${MESHY_API_KEY:0:8}...)"
else
echo "ENV_VAR: NOT_FOUND"
fi
2. Check .env files in workspace
2. 检查工作区中的.env文件
for f in .env .env.local; do
if [ -f "$f" ] && grep -q "MESHY_API_KEY" "$f" 2>/dev/null; then
echo "DOTENV($f): FOUND"
export $(grep "MESHY_API_KEY" "$f" | head -1)
fi
done
for f in .env .env.local; do
if [ -f "$f" ] && grep -q "MESHY_API_KEY" "$f" 2>/dev/null; then
echo "DOTENV($f): FOUND"
export $(grep "MESHY_API_KEY" "$f" | head -1)
fi
done
3. Check shell profiles
3. 检查Shell配置文件
for f in ~/.zshrc ~/.bashrc ~/.bash_profile ~/.profile; do
if [ -f "$f" ] && grep -q "MESHY_API_KEY" "$f" 2>/dev/null; then
echo "SHELL_PROFILE: FOUND in $f"
fi
done
for f in ~/.zshrc ~/.bashrc ~/.bash_profile ~/.profile; do
if [ -f "$f" ] && grep -q "MESHY_API_KEY" "$f" 2>/dev/null; then
echo "SHELL_PROFILE: FOUND in $f"
fi
done
4. Final status
4. 最终状态
if [ -n "$MESHY_API_KEY" ]; then
echo "READY: key=${MESHY_API_KEY:0:12}..."
else
echo "READY: NO_KEY_FOUND"
fi
if [ -n "$MESHY_API_KEY" ]; then
echo "READY: key=${MESHY_API_KEY:0:12}..."
else
echo "READY: NO_KEY_FOUND"
fi
5. Python requests check
5. 检查Python requests库
python3 -c "import requests; print('PYTHON_REQUESTS: OK')" 2>/dev/null || echo "PYTHON_REQUESTS: MISSING (run: pip install requests)"
echo "=== Detection Complete ==="
undefinedpython3 -c "import requests; print('PYTHON_REQUESTS: OK')" 2>/dev/null || echo "PYTHON_REQUESTS: MISSING (run: pip install requests)"
echo "=== Detection Complete ==="
undefinedDecision After Detection
检测后的决策
- Key found → Proceed to Step 1.
- Key NOT found → Go to Step 0a.
- Python requests missing → Run .
pip install requests
- 已找到密钥 → 继续执行步骤1。
- 未找到密钥 → 执行步骤0a。
- 缺少Python requests库 → 运行。
pip install requests
Step 0a: API Key Setup (Only If No Key Found)
步骤0a:API密钥配置(仅当未找到密钥时执行)
Tell the user:
To use the Meshy API, you need an API key. Here's how to get one:
- Go to https://www.meshy.ai/settings/api
- Click "Create API Key", give it a name, and copy the key (it starts with
)msy_- The key is only shown once — save it somewhere safe
Note: API access requires a Pro plan or above. Free-tier accounts cannot create API keys. If you see "Please upgrade to a premium plan to create API tasks", you'll need to upgrade at https://www.meshy.ai/pricing first.
Once the user provides their key, set it and verify:
macOS (zsh):
bash
export MESHY_API_KEY="msy_PASTE_KEY_HERE"告知用户:
要使用Meshy API,你需要一个API密钥。获取方式如下:
- 访问https://www.meshy.ai/settings/api
- 点击**"Create API Key"**,为密钥命名并复制(密钥以
开头)msy_- 密钥仅会显示一次,请妥善保存
注意:API访问需要Pro及以上套餐。免费版账户无法创建API密钥。如果看到"Please upgrade to a premium plan to create API tasks"提示,你需要先在https://www.meshy.ai/pricing升级套餐。
用户提供密钥后,进行配置和验证:
macOS(zsh):
bash
export MESHY_API_KEY="msy_PASTE_KEY_HERE"Verify
验证
STATUS=$(curl -s -o /dev/null -w "%{http_code}"
-H "Authorization: Bearer $MESHY_API_KEY"
https://api.meshy.ai/openapi/v1/balance)
-H "Authorization: Bearer $MESHY_API_KEY"
https://api.meshy.ai/openapi/v1/balance)
if [ "$STATUS" = "200" ]; then
BALANCE=$(curl -s -H "Authorization: Bearer $MESHY_API_KEY" https://api.meshy.ai/openapi/v1/balance)
echo "Key valid. $BALANCE"
echo 'export MESHY_API_KEY="msy_PASTE_KEY_HERE"' >> ~/.zshrc
echo "Persisted to ~/.zshrc"
else
echo "Key invalid (HTTP $STATUS). Check the key and try again."
fi
**Linux (bash):**
```bash
export MESHY_API_KEY="msy_PASTE_KEY_HERE"STATUS=$(curl -s -o /dev/null -w "%{http_code}"
-H "Authorization: Bearer $MESHY_API_KEY"
https://api.meshy.ai/openapi/v1/balance)
-H "Authorization: Bearer $MESHY_API_KEY"
https://api.meshy.ai/openapi/v1/balance)
if [ "$STATUS" = "200" ]; then
BALANCE=$(curl -s -H "Authorization: Bearer $MESHY_API_KEY" https://api.meshy.ai/openapi/v1/balance)
echo "Key valid. $BALANCE"
echo 'export MESHY_API_KEY="msy_PASTE_KEY_HERE"' >> ~/.zshrc
echo "Persisted to ~/.zshrc"
else
echo "Key invalid (HTTP $STATUS). Check the key and try again."
fi
**Linux(bash):**
```bash
export MESHY_API_KEY="msy_PASTE_KEY_HERE"Verify (same as above), then persist to ~/.bashrc
验证(与上方相同),然后持久化到~/.bashrc
STATUS=$(curl -s -o /dev/null -w "%{http_code}"
-H "Authorization: Bearer $MESHY_API_KEY"
https://api.meshy.ai/openapi/v1/balance)
-H "Authorization: Bearer $MESHY_API_KEY"
https://api.meshy.ai/openapi/v1/balance)
if [ "$STATUS" = "200" ]; then
BALANCE=$(curl -s -H "Authorization: Bearer $MESHY_API_KEY" https://api.meshy.ai/openapi/v1/balance)
echo "Key valid. $BALANCE"
echo 'export MESHY_API_KEY="msy_PASTE_KEY_HERE"' >> ~/.bashrc
echo "Persisted to ~/.bashrc"
else
echo "Key invalid (HTTP $STATUS). Check the key and try again."
fi
**Windows (PowerShell):**
```powershell
$env:MESHY_API_KEY = "msy_PASTE_KEY_HERE"STATUS=$(curl -s -o /dev/null -w "%{http_code}"
-H "Authorization: Bearer $MESHY_API_KEY"
https://api.meshy.ai/openapi/v1/balance)
-H "Authorization: Bearer $MESHY_API_KEY"
https://api.meshy.ai/openapi/v1/balance)
if [ "$STATUS" = "200" ]; then
BALANCE=$(curl -s -H "Authorization: Bearer $MESHY_API_KEY" https://api.meshy.ai/openapi/v1/balance)
echo "Key valid. $BALANCE"
echo 'export MESHY_API_KEY="msy_PASTE_KEY_HERE"' >> ~/.bashrc
echo "Persisted to ~/.bashrc"
else
echo "Key invalid (HTTP $STATUS). Check the key and try again."
fi
**Windows(PowerShell):**
```powershell
$env:MESHY_API_KEY = "msy_PASTE_KEY_HERE"Verify
验证
$status = (Invoke-WebRequest -Uri "https://api.meshy.ai/openapi/v1/balance" -Headers @{Authorization="Bearer $env:MESHY_API_KEY"} -UseBasicParsing).StatusCode
if ($status -eq 200) {
Write-Host "Key valid."
# Persist permanently
[System.Environment]::SetEnvironmentVariable("MESHY_API_KEY", $env:MESHY_API_KEY, "User")
Write-Host "Persisted to user environment variables. Restart terminal to take effect."
} else {
Write-Host "Key invalid (HTTP $status). Check the key and try again."
}
**Alternative (all platforms):** Create a `.env` file in your project root:MESHY_API_KEY=msy_PASTE_KEY_HERE
---$status = (Invoke-WebRequest -Uri "https://api.meshy.ai/openapi/v1/balance" -Headers @{Authorization="Bearer $env:MESHY_API_KEY"} -UseBasicParsing).StatusCode
if ($status -eq 200) {
Write-Host "Key valid."
# 永久保存
[System.Environment]::SetEnvironmentVariable("MESHY_API_KEY", $env:MESHY_API_KEY, "User")
Write-Host "Persisted to user environment variables. Restart terminal to take effect."
} else {
Write-Host "Key invalid (HTTP $status). Check the key and try again."
}
**替代方案(全平台):** 在项目根目录创建`.env`文件:MESHY_API_KEY=msy_PASTE_KEY_HERE
---Step 1: Confirm Plan With User Before Spending Credits
步骤1:执行前与用户确认方案(避免消耗积分)
CRITICAL: Before creating any task, present the user with a summary and get confirmation:
I'll generate a 3D model of "<prompt>" using the following plan:
1. Preview (mesh generation) — 20 credits
2. Refine (texturing with PBR) — 10 credits
3. Download as .glb
Total cost: 30 credits
Current balance: <N> credits
Shall I proceed?For multi-step pipelines (e.g., text-to-3d → rig → animate), present the FULL pipeline cost upfront:
| Step | API | Credits |
|---|---|---|
| Preview | Text to 3D | 20 |
| Refine | Text to 3D | 10 |
| Rig | Auto-Rigging | 5 |
| Total | 35 |
Note: Rigging automatically includes basic walking + running animations for free (in). Only addresult.basic_animations(3 credits) if the user needs a custom animation beyond walking/running.Animate
Wait for user confirmation before executing.
关键:在创建任何任务前,向用户展示任务摘要并获取确认:
我将按照以下方案生成"<prompt>"的3D模型:
1. 预览(网格生成)—— 20积分
2. 精修(添加PBR纹理)—— 10积分
3. 下载为.glb格式
总消耗:30积分
当前余额:<N>积分
是否继续执行?对于多步骤工作流(例如文本转3D→骨骼绑定→动画),请提前展示完整工作流的总消耗:
| 步骤 | API | 积分 |
|---|---|---|
| 预览 | 文本转3D | 20 |
| 精修 | 文本转3D | 10 |
| 骨骼绑定 | 自动骨骼绑定 | 5 |
| 总计 | 35 |
注意:骨骼绑定会自动包含基础的行走+跑步动画(在中)。仅当用户需要行走/跑步之外的自定义动画时,才需要添加“动画”步骤(3积分)。result.basic_animations
等待用户确认后再执行任务。
Intent → API Mapping
用户需求→API映射
| User wants to... | API | Endpoint | Credits |
|---|---|---|---|
| 3D model from text | Text to 3D | | 20 + 10 |
| 3D model from one image | Image to 3D | | 20–30 |
| 3D model from multiple images | Multi-Image to 3D | | 20–30 |
| New textures on existing model | Retexture | | 10 |
| Change mesh format/topology | Remesh | | 5 |
| Add skeleton to character | Auto-Rigging | | 5 (includes walking + running) |
| Animate a rigged character (custom) | Animation | | 3 |
| 2D image from text | Text to Image | | 3–9 |
| Transform a 2D image | Image to Image | | 3–9 |
| Check credit balance | Balance | | 0 |
| 用户需求 | API | 端点 | 积分 |
|---|---|---|---|
| 从文本生成3D模型 | 文本转3D | | 20 + 10 |
| 从单张图片生成3D模型 | 图片转3D | | 20–30 |
| 从多张图片生成3D模型 | 多图转3D | | 20–30 |
| 为现有模型添加新纹理 | 重新纹理 | | 10 |
| 更改网格格式/拓扑结构 | 重拓扑 | | 5 |
| 为角色添加骨骼 | 自动骨骼绑定 | | 5 |
| 为绑定骨骼的角色制作自定义动画 | 动画生成 | | 3 |
| 从文本生成2D图像 | 文本转图像 | | 3–9 |
| 转换2D图像风格 | 图像转图像 | | 3–9 |
| 查看积分余额 | 余额查询 | | 0 |
Step 2: Execute the Workflow
步骤2:执行工作流
CRITICAL: Async Task Model
关键:异步任务模型
All generation endpoints return , NOT the model. You MUST poll.
{"result": "<task_id>"}NEVER read from the POST response.
model_urls所有生成端点返回,而非直接返回模型。你必须轮询任务状态。
{"result": "<task_id>"}绝对不要从POST响应中直接读取。
model_urlsReusable Script Template
可复用脚本模板
Use this as the base for ALL generation workflows:
python
#!/usr/bin/env python3
"""Meshy API task runner. Handles create → poll → download."""
import requests, time, os, sys
API_KEY = os.environ.get("MESHY_API_KEY", "")
if not API_KEY:
sys.exit("ERROR: MESHY_API_KEY not set")
BASE = "https://api.meshy.ai"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
SESSION = requests.Session()
SESSION.trust_env = False # bypass any system proxy settings
def create_task(endpoint, payload):
resp = SESSION.post(f"{BASE}{endpoint}", headers=HEADERS, json=payload, timeout=30)
if resp.status_code == 401:
sys.exit("ERROR: Invalid API key (401)")
if resp.status_code == 402:
try:
bal = SESSION.get(f"{BASE}/openapi/v1/balance", headers=HEADERS, timeout=10)
balance = bal.json().get("balance", "unknown")
sys.exit(f"ERROR: Insufficient credits (402). Current balance: {balance}. Top up at https://www.meshy.ai/pricing")
except Exception:
sys.exit("ERROR: Insufficient credits (402). Check balance at https://www.meshy.ai/pricing")
if resp.status_code == 429:
sys.exit("ERROR: Rate limited (429). Wait and retry.")
resp.raise_for_status()
task_id = resp.json()["result"]
print(f"TASK_CREATED: {task_id}")
return task_id
def poll_task(endpoint, task_id, timeout=600):
"""Poll task with exponential backoff (5s→30s, fixed 15s at 95%+)."""
elapsed = 0
delay = 5 # Initial delay: 5s
max_delay = 30 # Cap: 30s
backoff = 1.5 # Backoff multiplier
finalize_delay = 15 # Fixed delay during finalization (95%+)
poll_count = 0
while elapsed < timeout:
poll_count += 1
resp = SESSION.get(f"{BASE}{endpoint}/{task_id}", headers=HEADERS, timeout=30)
resp.raise_for_status()
task = resp.json()
status = task["status"]
progress = task.get("progress", 0)
filled = int(progress / 5)
bar = f"[{'█' * filled}{'░' * (20 - filled)}] {progress}%"
print(f" {bar} — {status} ({elapsed}s, poll #{poll_count})", flush=True)
if status == "SUCCEEDED":
return task
if status in ("FAILED", "CANCELED"):
msg = task.get("task_error", {}).get("message", "Unknown")
sys.exit(f"TASK_{status}: {msg}")
current_delay = finalize_delay if progress >= 95 else delay
time.sleep(current_delay)
elapsed += current_delay
if progress < 95:
delay = min(delay * backoff, max_delay)
sys.exit(f"TIMEOUT after {timeout}s ({poll_count} polls)")
def download(url, filepath):
"""Download a file to the given path (within a project directory)."""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
print(f"Downloading {filepath}...", flush=True)
resp = SESSION.get(url, timeout=300, stream=True)
resp.raise_for_status()
with open(filepath, "wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
size_mb = os.path.getsize(filepath) / (1024 * 1024)
print(f"DOWNLOADED: {filepath} ({size_mb:.1f} MB)")以下模板可作为所有生成工作流的基础:
python
#!/usr/bin/env python3
"""Meshy API任务运行器。处理创建→轮询→下载流程。"""
import requests, time, os, sys
API_KEY = os.environ.get("MESHY_API_KEY", "")
if not API_KEY:
sys.exit("ERROR: MESHY_API_KEY not set")
BASE = "https://api.meshy.ai"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
SESSION = requests.Session()
SESSION.trust_env = False # 绕过系统代理设置
def create_task(endpoint, payload):
resp = SESSION.post(f"{BASE}{endpoint}", headers=HEADERS, json=payload, timeout=30)
if resp.status_code == 401:
sys.exit("ERROR: Invalid API key (401)")
if resp.status_code == 402:
try:
bal = SESSION.get(f"{BASE}/openapi/v1/balance", headers=HEADERS, timeout=10)
balance = bal.json().get("balance", "unknown")
sys.exit(f"ERROR: Insufficient credits (402). Current balance: {balance}. Top up at https://www.meshy.ai/pricing")
except Exception:
sys.exit("ERROR: Insufficient credits (402). Check balance at https://www.meshy.ai/pricing")
if resp.status_code == 429:
sys.exit("ERROR: Rate limited (429). Wait and retry.")
resp.raise_for_status()
task_id = resp.json()["result"]
print(f"TASK_CREATED: {task_id}")
return task_id
def poll_task(endpoint, task_id, timeout=600):
"""使用指数退避策略轮询任务(5秒→30秒,进度95%+时固定15秒)。"""
elapsed = 0
delay = 5 # 初始延迟:5秒
max_delay = 30 # 最大延迟:30秒
backoff = 1.5 # 退避乘数
finalize_delay = 15 # 最终处理阶段(95%+)的固定延迟
poll_count = 0
while elapsed < timeout:
poll_count += 1
resp = SESSION.get(f"{BASE}{endpoint}/{task_id}", headers=HEADERS, timeout=30)
resp.raise_for_status()
task = resp.json()
status = task["status"]
progress = task.get("progress", 0)
filled = int(progress / 5)
bar = f"[{'█' * filled}{'░' * (20 - filled)}] {progress}%"
print(f" {bar} — {status} ({elapsed}s, poll #{poll_count})", flush=True)
if status == "SUCCEEDED":
return task
if status in ("FAILED", "CANCELED"):
msg = task.get("task_error", {}).get("message", "Unknown")
sys.exit(f"TASK_{status}: {msg}")
current_delay = finalize_delay if progress >= 95 else delay
time.sleep(current_delay)
elapsed += current_delay
if progress < 95:
delay = min(delay * backoff, max_delay)
sys.exit(f"TIMEOUT after {timeout}s ({poll_count} polls)")
def download(url, filepath):
"""将文件下载到指定路径(项目目录内)。"""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
print(f"Downloading {filepath}...", flush=True)
resp = SESSION.get(url, timeout=300, stream=True)
resp.raise_for_status()
with open(filepath, "wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
size_mb = os.path.getsize(filepath) / (1024 * 1024)
print(f"DOWNLOADED: {filepath} ({size_mb:.1f} MB)")--- File organization helpers (see File Organization section above) ---
--- 文件组织辅助函数(参考上方文件组织规范) ---
import re, json
from datetime import datetime
OUTPUT_ROOT = os.path.join(os.getcwd(), "meshy_output")
os.makedirs(OUTPUT_ROOT, exist_ok=True)
HISTORY_FILE = os.path.join(OUTPUT_ROOT, "history.json")
def get_project_dir(task_id, prompt="", task_type="model"):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
slug = re.sub(r'[^a-z0-9]+', '-', (prompt or task_type).lower())[:30].strip('-')
folder = f"{timestamp}{slug}{task_id[:8]}"
project_dir = os.path.join(OUTPUT_ROOT, folder)
os.makedirs(project_dir, exist_ok=True)
return project_dir
def record_task(project_dir, task_id, task_type, stage, prompt="", files=None):
meta_path = os.path.join(project_dir, "metadata.json")
if os.path.exists(meta_path):
meta = json.load(open(meta_path))
else:
meta = {"project_name": prompt or task_type, "folder": os.path.basename(project_dir),
"root_task_id": task_id, "created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(), "tasks": []}
meta["tasks"].append({"task_id": task_id, "task_type": task_type, "stage": stage,
"files": files or [], "created_at": datetime.now().isoformat()})
meta["updated_at"] = datetime.now().isoformat()
json.dump(meta, open(meta_path, "w"), indent=2)
# Update global history
if os.path.exists(HISTORY_FILE):
history = json.load(open(HISTORY_FILE))
else:
history = {"version": 1, "projects": []}
folder = os.path.basename(project_dir)
entry = next((p for p in history["projects"] if p["folder"] == folder), None)
if entry:
entry["task_count"] = len(meta["tasks"])
entry["updated_at"] = meta["updated_at"]
else:
history["projects"].append({"folder": folder, "prompt": prompt, "task_type": task_type,
"root_task_id": task_id, "created_at": meta["created_at"],
"updated_at": meta["updated_at"], "task_count": len(meta["tasks"])})
json.dump(history, open(HISTORY_FILE, "w"), indent=2)
def save_thumbnail(project_dir, url):
path = os.path.join(project_dir, "thumbnail.png")
if os.path.exists(path): return
try:
r = SESSION.get(url, timeout=15); r.raise_for_status()
open(path, "wb").write(r.content)
except Exception: pass
undefinedimport re, json
from datetime import datetime
OUTPUT_ROOT = os.path.join(os.getcwd(), "meshy_output")
os.makedirs(OUTPUT_ROOT, exist_ok=True)
HISTORY_FILE = os.path.join(OUTPUT_ROOT, "history.json")
def get_project_dir(task_id, prompt="", task_type="model"):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
slug = re.sub(r'[^a-z0-9]+', '-', (prompt or task_type).lower())[:30].strip('-')
folder = f"{timestamp}{slug}{task_id[:8]}"
project_dir = os.path.join(OUTPUT_ROOT, folder)
os.makedirs(project_dir, exist_ok=True)
return project_dir
def record_task(project_dir, task_id, task_type, stage, prompt="", files=None):
meta_path = os.path.join(project_dir, "metadata.json")
if os.path.exists(meta_path):
meta = json.load(open(meta_path))
else:
meta = {"project_name": prompt or task_type, "folder": os.path.basename(project_dir),
"root_task_id": task_id, "created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(), "tasks": []}
meta["tasks"].append({"task_id": task_id, "task_type": task_type, "stage": stage,
"files": files or [], "created_at": datetime.now().isoformat()})
meta["updated_at"] = datetime.now().isoformat()
json.dump(meta, open(meta_path, "w"), indent=2)
# 更新全局历史记录
if os.path.exists(HISTORY_FILE):
history = json.load(open(HISTORY_FILE))
else:
history = {"version": 1, "projects": []}
folder = os.path.basename(project_dir)
entry = next((p for p in history["projects"] if p["folder"] == folder), None)
if entry:
entry["task_count"] = len(meta["tasks"])
entry["updated_at"] = meta["updated_at"]
else:
history["projects"].append({"folder": folder, "prompt": prompt, "task_type": task_type,
"root_task_id": task_id, "created_at": meta["created_at"],
"updated_at": meta["updated_at"], "task_count": len(meta["tasks"])})
json.dump(history, open(HISTORY_FILE, "w"), indent=2)
def save_thumbnail(project_dir, url):
path = os.path.join(project_dir, "thumbnail.png")
if os.path.exists(path): return
try:
r = SESSION.get(url, timeout=15); r.raise_for_status()
open(path, "wb").write(r.content)
except Exception: pass
undefinedText to 3D (Preview + Refine)
文本转3D(预览+精修)
Append this to the template above and run as one script:
python
PROMPT = "USER_PROMPT" # max 600 chars将以下代码追加到上述模板后,作为单个脚本运行:
python
PROMPT = "USER_PROMPT" # 最大600字符--- Preview ---
--- 预览 ---
preview_id = create_task("/openapi/v2/text-to-3d", {
"mode": "preview",
"prompt": PROMPT,
"ai_model": "latest",
# "model_type": "standard", # "standard" | "lowpoly"
# "topology": "triangle", # "triangle" | "quad"
# "target_polycount": 30000, # 100–300000
# "should_remesh": False,
# "symmetry_mode": "auto", # "auto" | "on" | "off"
# "pose_mode": "t-pose", # "" | "a-pose" | "t-pose" (use "t-pose" if rigging/animating later)
})
task = poll_task("/openapi/v2/text-to-3d", preview_id)
project_dir = get_project_dir(preview_id, prompt=PROMPT)
download(task["model_urls"]["glb"], os.path.join(project_dir, "preview.glb"))
record_task(project_dir, preview_id, "text-to-3d", "preview", prompt=PROMPT, files=["preview.glb"])
if task.get("thumbnail_url"):
save_thumbnail(project_dir, task["thumbnail_url"])
print(f"\nPREVIEW COMPLETE")
print(f" Task ID: {preview_id}")
print(f" Project: {project_dir}")
print(f" Formats: {', '.join(task['model_urls'].keys())}")
preview_id = create_task("/openapi/v2/text-to-3d", {
"mode": "preview",
"prompt": PROMPT,
"ai_model": "latest",
# "model_type": "standard", # "standard" | "lowpoly"
# "topology": "triangle", # "triangle" | "quad"
# "target_polycount": 30000, # 100–300000
# "should_remesh": False,
# "symmetry_mode": "auto", # "auto" | "on" | "off"
# "pose_mode": "t-pose", # "" | "a-pose" | "t-pose"(后续需要绑定骨骼/动画时请使用"t-pose")
})
task = poll_task("/openapi/v2/text-to-3d", preview_id)
project_dir = get_project_dir(preview_id, prompt=PROMPT)
download(task["model_urls"]["glb"], os.path.join(project_dir, "preview.glb"))
record_task(project_dir, preview_id, "text-to-3d", "preview", prompt=PROMPT, files=["preview.glb"])
if task.get("thumbnail_url"):
save_thumbnail(project_dir, task["thumbnail_url"])
print(f"\nPREVIEW COMPLETE")
print(f" Task ID: {preview_id}")
print(f" Project: {project_dir}")
print(f" Formats: {', '.join(task['model_urls'].keys())}")
--- Refine ---
--- 精修 ---
refine_id = create_task("/openapi/v2/text-to-3d", {
"mode": "refine",
"preview_task_id": preview_id,
"enable_pbr": True,
"ai_model": "latest",
# "texture_prompt": "",
# "remove_lighting": True, # Remove baked lighting (meshy-6/latest only, default True)
})
task = poll_task("/openapi/v2/text-to-3d", refine_id)
download(task["model_urls"]["glb"], os.path.join(project_dir, "refined.glb"))
record_task(project_dir, refine_id, "text-to-3d", "refined", prompt=PROMPT, files=["refined.glb"])
print(f"\nREFINE COMPLETE")
print(f" Task ID: {refine_id}")
print(f" Project: {project_dir}")
print(f" Formats: {', '.join(task['model_urls'].keys())}")
> **Refine compatibility**: Only previews generated with `meshy-5` or `latest` can be refined. `meshy-6` previews do NOT support refine (API returns 400). If the user wants to refine later, always use `meshy-5` or `latest` for the preview step.refine_id = create_task("/openapi/v2/text-to-3d", {
"mode": "refine",
"preview_task_id": preview_id,
"enable_pbr": True,
"ai_model": "latest",
# "texture_prompt": "",
# "remove_lighting": True, # 移除烘焙光照(仅meshy-6/latest支持,默认开启)
})
task = poll_task("/openapi/v2/text-to-3d", refine_id)
download(task["model_urls"]["glb"], os.path.join(project_dir, "refined.glb"))
record_task(project_dir, refine_id, "text-to-3d", "refined", prompt=PROMPT, files=["refined.glb"])
print(f"\nREFINE COMPLETE")
print(f" Task ID: {refine_id}")
print(f" Project: {project_dir}")
print(f" Formats: {', '.join(task['model_urls'].keys())}")
> **精修兼容性**:仅使用`meshy-5`或`latest`生成的预览模型支持精修。`meshy-6`生成的预览模型不支持精修(API会返回400错误)。如果用户后续需要精修,预览步骤请始终使用`meshy-5`或`latest`。Image to 3D
图片转3D
python
import base64python
import base64For local files, convert to data URI:
对于本地文件,转换为数据URI:
with open("photo.jpg", "rb") as f:
with open("photo.jpg", "rb") as f:
image_url = "data:image/jpeg;base64," + base64.b64encode(f.read()).decode()
image_url = "data:image/jpeg;base64," + base64.b64encode(f.read()).decode()
task_id = create_task("/openapi/v1/image-to-3d", {
"image_url": "IMAGE_URL_OR_DATA_URI",
"should_texture": True,
"enable_pbr": True, # Default is False; set True for metallic/roughness/normal maps
"ai_model": "latest",
# "image_enhancement": True, # Optimize input image (meshy-6/latest only, default True)
# "remove_lighting": True, # Remove baked lighting from texture (meshy-6/latest only, default True)
})
task = poll_task("/openapi/v1/image-to-3d", task_id)
download(task["model_urls"]["glb"], "model.glb")
undefinedtask_id = create_task("/openapi/v1/image-to-3d", {
"image_url": "IMAGE_URL_OR_DATA_URI",
"should_texture": True,
"enable_pbr": True, # 默认关闭;开启后会生成金属度/粗糙度/法线贴图
"ai_model": "latest",
# "image_enhancement": True, # 优化输入图像(仅meshy-6/latest支持,默认开启)
# "remove_lighting": True, # 从纹理中移除烘焙光照(仅meshy-6/latest支持,默认开启)
})
task = poll_task("/openapi/v1/image-to-3d", task_id)
download(task["model_urls"]["glb"], "model.glb")
undefinedMulti-Image to 3D
多图转3D
python
task_id = create_task("/openapi/v1/multi-image-to-3d", {
"image_urls": ["URL_1", "URL_2", "URL_3"], # 1–4 images
"should_texture": True,
"enable_pbr": True, # Default is False; set True for metallic/roughness/normal maps
"ai_model": "latest",
# "image_enhancement": True, # Optimize input images (meshy-6/latest only, default True)
# "remove_lighting": True, # Remove baked lighting from texture (meshy-6/latest only, default True)
})
task = poll_task("/openapi/v1/multi-image-to-3d", task_id)
download(task["model_urls"]["glb"], "model.glb")python
task_id = create_task("/openapi/v1/multi-image-to-3d", {
"image_urls": ["URL_1", "URL_2", "URL_3"], # 1-4张图片
"should_texture": True,
"enable_pbr": True, # 默认关闭;开启后会生成金属度/粗糙度/法线贴图
"ai_model": "latest",
# "image_enhancement": True, # 优化输入图像(仅meshy-6/latest支持,默认开启)
# "remove_lighting": True, # 从纹理中移除烘焙光照(仅meshy-6/latest支持,默认开启)
})
task = poll_task("/openapi/v1/multi-image-to-3d", task_id)
download(task["model_urls"]["glb"], "model.glb")Retexture
重新纹理
python
task_id = create_task("/openapi/v1/retexture", {
"input_task_id": "PREVIOUS_TASK_ID", # or "model_url": "URL"
"text_style_prompt": "wooden texture", # or "image_style_url": "URL"
"enable_pbr": True,
# "remove_lighting": True, # Remove baked lighting (meshy-6/latest only, default True)
})
task = poll_task("/openapi/v1/retexture", task_id)
download(task["model_urls"]["glb"], "retextured.glb")python
task_id = create_task("/openapi/v1/retexture", {
"input_task_id": "PREVIOUS_TASK_ID", # 或使用"model_url": "URL"
"text_style_prompt": "wooden texture", # 或使用"image_style_url": "URL"
"enable_pbr": True,
# "remove_lighting": True, # 移除烘焙光照(仅meshy-6/latest支持,默认开启)
})
task = poll_task("/openapi/v1/retexture", task_id)
download(task["model_urls"]["glb"], "retextured.glb")Remesh / Format Conversion
重拓扑/格式转换
python
task_id = create_task("/openapi/v1/remesh", {
"input_task_id": "TASK_ID",
"target_formats": ["glb", "fbx", "obj"],
"topology": "quad",
"target_polycount": 10000,
})
task = poll_task("/openapi/v1/remesh", task_id)
for fmt, url in task["model_urls"].items():
download(url, f"remeshed.{fmt}")python
task_id = create_task("/openapi/v1/remesh", {
"input_task_id": "TASK_ID",
"target_formats": ["glb", "fbx", "obj"],
"topology": "quad",
"target_polycount": 10000,
})
task = poll_task("/openapi/v1/remesh", task_id)
for fmt, url in task["model_urls"].items():
download(url, f"remeshed.{fmt}")Auto-Rigging + Animation
自动骨骼绑定+动画
IMPORTANT: When the user explicitly asks to rig or animate, the generation step (text-to-3d / image-to-3d) MUST use for best rigging results. If the model was already generated without t-pose, recommend regenerating with first.
pose_mode: "t-pose"pose_mode: "t-pose"Before rigging, verify the model's polygon count is under 300,000. The script should auto-check and block if exceeded:
python
undefined重要提示:当用户明确要求绑定骨骼或制作动画时,生成步骤(文本转3D/图片转3D)必须使用以获得最佳骨骼绑定效果。如果模型生成时未使用T姿势,建议用户先重新生成带的模型。
pose_mode: "t-pose"pose_mode: "t-pose"绑定骨骼前,请验证模型面数不超过300,000。脚本应自动检查并在超出时阻止执行:
python
undefinedPre-rig check: verify face count (MUST be ≤ 300,000)
绑定骨骼前检查:验证面数(必须≤300,000)
source_endpoint = "/openapi/v2/text-to-3d" # adjust to match the source task's endpoint
source_task_id = "TASK_ID"
check_resp = SESSION.get(f"{BASE}{source_endpoint}/{source_task_id}", headers=HEADERS, timeout=30)
check_resp.raise_for_status()
source = check_resp.json()
face_count = source.get("face_count", 0)
if face_count > 300000:
print(f"ERROR: Model has {face_count:,} faces (limit: 300,000). Remesh first:")
print(f" create_task('/openapi/v1/remesh', {{'input_task_id': '{source_task_id}', 'target_polycount': 100000}})")
sys.exit("Rigging blocked: face count too high")
```pythonsource_endpoint = "/openapi/v2/text-to-3d" # 根据源任务的端点调整
source_task_id = "TASK_ID"
check_resp = SESSION.get(f"{BASE}{source_endpoint}/{source_task_id}", headers=HEADERS, timeout=30)
check_resp.raise_for_status()
source = check_resp.json()
face_count = source.get("face_count", 0)
if face_count > 300000:
print(f"ERROR: Model has {face_count:,} faces (limit: 300,000). Remesh first:")
print(f" create_task('/openapi/v1/remesh', {{'input_task_id': '{source_task_id}', 'target_polycount': 100000}})")
sys.exit("Rigging blocked: face count too high")
```pythonRig (humanoid bipedal characters only, polycount must be ≤ 300,000)
绑定骨骼(仅支持双足人形角色,面数必须≤300,000)
rig_id = create_task("/openapi/v1/rigging", {
"input_task_id": "TASK_ID",
"height_meters": 1.7,
})
rig_task = poll_task("/openapi/v1/rigging", rig_id)
download(rig_task["result"]["rigged_character_glb_url"], "rigged.glb")
rig_id = create_task("/openapi/v1/rigging", {
"input_task_id": "TASK_ID",
"height_meters": 1.7,
})
rig_task = poll_task("/openapi/v1/rigging", rig_id)
download(rig_task["result"]["rigged_character_glb_url"], "rigged.glb")
Rigging automatically includes basic walking + running animations — download them:
骨骼绑定会自动包含基础的行走+跑步动画——请下载这些动画:
download(rig_task["result"]["basic_animations"]["walking_glb_url"], "walking.glb")
download(rig_task["result"]["basic_animations"]["running_glb_url"], "running.glb")
download(rig_task["result"]["basic_animations"]["walking_glb_url"], "walking.glb")
download(rig_task["result"]["basic_animations"]["running_glb_url"], "running.glb")
Only call meshy_animate if you need a CUSTOM animation beyond walking/running:
仅当用户需要行走/跑步之外的自定义动画时,才调用动画生成API:
anim_id = create_task("/openapi/v1/animations", {
anim_id = create_task("/openapi/v1/animations", {
"rig_task_id": rig_id,
"rig_task_id": rig_id,
"action_id": 1, # from Animation Library
"action_id": 1, # 来自动画库
})
})
anim_task = poll_task("/openapi/v1/animations", anim_id)
anim_task = poll_task("/openapi/v1/animations", anim_id)
download(anim_task["result"]["animation_glb_url"], "animated.glb")
download(anim_task["result"]["animation_glb_url"], "animated.glb")
undefinedundefinedText to Image / Image to Image
文本转图像/图像转图像
python
undefinedpython
undefinedText to Image
文本转图像
task_id = create_task("/openapi/v1/text-to-image", {
"ai_model": "nano-banana-pro",
"prompt": "a futuristic spaceship",
})
task = poll_task("/openapi/v1/text-to-image", task_id)
task_id = create_task("/openapi/v1/text-to-image", {
"ai_model": "nano-banana-pro",
"prompt": "a futuristic spaceship",
})
task = poll_task("/openapi/v1/text-to-image", task_id)
Result: task["image_url"]
结果:task["image_url"]
Image to Image
图像转图像
task_id = create_task("/openapi/v1/image-to-image", {
"ai_model": "nano-banana-pro",
"prompt": "make it look cyberpunk",
"reference_image_urls": ["URL"],
})
task = poll_task("/openapi/v1/image-to-image", task_id)
---task_id = create_task("/openapi/v1/image-to-image", {
"ai_model": "nano-banana-pro",
"prompt": "make it look cyberpunk",
"reference_image_urls": ["URL"],
})
task = poll_task("/openapi/v1/image-to-image", task_id)
---Step 3: Report Results
步骤3:报告结果
After task succeeds, report:
- Downloaded file paths and sizes
- Task IDs (for follow-up operations like refine, rig, retexture)
- Available formats (list keys)
model_urls - Thumbnail URL if present
- Credits consumed and remaining balance (run balance check)
- Suggested next steps:
- Preview done → "Want to refine (add textures)?"
- Model done → "Want to rig this character for animation?"
- Rigged → "Want to apply an animation?"
- Any model → "Want to remesh / export to another format?"
- Any model → "Want to 3D print this model?" (requires skill)
meshy-3d-printing
任务成功后,向用户报告以下内容:
- 下载文件的路径和大小
- 任务ID(用于后续操作,如精修、绑定骨骼、重新纹理)
- 可用格式(列出的键)
model_urls - 缩略图URL(如果存在)
- 消耗的积分和剩余余额(运行余额查询)
- 建议的下一步操作:
- 预览完成 → "是否需要精修模型(添加纹理)?"
- 模型完成 → "是否需要为该角色绑定骨骼以制作动画?"
- 已绑定骨骼 → "是否需要添加动画效果?"
- 任意模型 → "是否需要重拓扑/导出为其他格式?"
- 任意模型 → "是否需要3D打印该模型?"(需要使用技能)
meshy-3d-printing
Error Recovery
错误恢复
| HTTP Status | Meaning | Action |
|---|---|---|
| 401 | Invalid API key | Re-run Step 0; ask user to check key |
| 402 | Insufficient credits | Auto-query balance ( |
| 422 | Cannot process | Explain limitation (e.g., non-humanoid for rigging) |
| 429 | Rate limited | Auto-retry after 5s (max 3 times) |
| 5xx | Server error | Auto-retry after 10s (once) |
Task messages:
FAILED- → retry with backoff (5s, 10s, 20s)
"The server is busy..." - → simplify prompt, retry once
"Internal server error."
| HTTP状态码 | 含义 | 操作 |
|---|---|---|
| 401 | API密钥无效 | 重新执行步骤0;请用户检查密钥 |
| 402 | 积分不足 | 自动查询余额( |
| 422 | 无法处理请求 | 说明限制条件(例如骨骼绑定仅支持人形角色) |
| 429 | 请求频率超限 | 5秒后自动重试(最多3次) |
| 5xx | 服务器错误 | 10秒后自动重试(1次) |
任务消息处理:
FAILED- → 退避重试(5秒、10秒、20秒)
"The server is busy..." - → 简化提示词,重试1次
"Internal server error."
Known Behaviors & Constraints
已知行为与限制
- 99% progress stall: Tasks commonly sit at 99% for 30–120s during finalization. This is normal. Do NOT kill or restart.
- CORS: API blocks browser requests. Always server-side.
- Asset retention: Files deleted after 3 days (non-Enterprise). Download immediately.
- PBR maps: Must set explicitly.
enable_pbr: true - Format availability: Check keys in before downloading — not all formats are always present.
model_urls - Timestamps: All API timestamps are Unix epoch milliseconds.
- Large files: Refined models can be 50–200 MB. Use streaming downloads with timeouts.
- 99%进度停滞:任务在最终处理阶段经常会在99%进度停留30-120秒,这属于正常现象。请勿终止或重启任务。
- CORS限制:API会阻止浏览器端请求,请始终在服务器端调用。
- 资产保留期限:文件会在3天后删除(非企业版)。请立即下载。
- PBR贴图:必须显式设置才能生成。
enable_pbr: true - 格式可用性:下载前请检查中的键——并非所有格式都会始终可用。
model_urls - 时间戳:所有API返回的时间戳为Unix时间戳(毫秒)。
- 大文件:精修后的模型大小可能为50-200 MB。请使用流式下载并设置超时时间。
Execution Checklist
执行检查清单
- Ran environment detection (Step 0)
- API key present and verified
- Presented cost summary and got user confirmation
- Wrote complete workflow as single Python script
- Ran script with for unbuffered output
python3 -u - Reported file paths, formats, task IDs, and balance
- Suggested next steps
- 已运行环境检测(步骤0)
- API密钥已存在且验证有效
- 已向用户展示成本摘要并获得确认
- 已将完整工作流编写为单个Python脚本
- 已使用运行脚本以获得无缓冲输出
python3 -u - 已报告文件路径、格式、任务ID和余额
- 已建议下一步操作
Additional Resources
额外资源
For the complete API endpoint reference including all parameters, response schemas, deprecated fields, and detailed error codes, read reference.md.
如需包含所有参数、响应结构、废弃字段和详细错误码的完整API端点参考,请查看reference.md。