meshy-3d-generation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Meshy 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 to
meshy_output/
in the current working directory. Each project gets its own folder (
{YYYYMMDD_HHmmss}_{prompt}_{id}/
) with model files, textures, thumbnails, and metadata. History is tracked in
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
meshy_output/
directory in the current working directory. Do NOT scatter files randomly.
  • 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
    metadata.json
    per project, and global
    history.json
  • Auto-download thumbnails alongside models
The Reusable Script Template below includes
get_project_dir()
,
record_task()
, and
save_thumbnail()
helpers.

所有下载的文件必须存入当前工作目录下结构化的
meshy_output/
目录中。请勿随意分散存放文件
  • 每个项目对应独立文件夹:
    meshy_output/{YYYYMMDD_HHmmss}_{prompt_slug}_{task_id_prefix}/
  • 对于串联任务(预览→精修→骨骼绑定),请复用同一个
    project_dir
  • 每个项目的任务记录保存在
    metadata.json
    中,全局历史记录保存在
    history.json
  • 自动下载模型对应的缩略图
下方的可复用脚本模板包含
get_project_dir()
record_task()
save_thumbnail()
辅助函数。

IMPORTANT: Shell Command Rules

重要提示:Shell命令规则

Use only standard POSIX tools in shell commands. Do NOT use
rg
(ripgrep),
fd
, or other non-standard CLI tools — they may not be installed. Use these standard alternatives instead:
Do NOT useUse instead
rg
grep
fd
find
bat
cat
exa
/
eza
ls

仅在Shell命令中使用标准POSIX工具。请勿使用
rg
(ripgrep)、
fd
或其他非标准CLI工具——它们可能未预装。请使用以下标准替代工具:
禁止使用替代工具
rg
grep
fd
find
bat
cat
exa
/
eza
ls

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
    python3 -u script.py
    (unbuffered) so progress output is visible in real time.
  • 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 ==="
undefined
python3 -c "import requests; print('PYTHON_REQUESTS: OK')" 2>/dev/null || echo "PYTHON_REQUESTS: MISSING (run: pip install requests)"
echo "=== Detection Complete ==="
undefined

Decision 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:
  1. Go to https://www.meshy.ai/settings/api
  2. Click "Create API Key", give it a name, and copy the key (it starts with
    msy_
    )
  3. 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密钥。获取方式如下:
  1. 访问https://www.meshy.ai/settings/api
  2. 点击**"Create API Key"**,为密钥命名并复制(密钥以
    msy_
    开头)
  3. 密钥仅会显示一次,请妥善保存
注意: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)
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)
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)
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)
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:
StepAPICredits
PreviewText to 3D20
RefineText to 3D10
RigAuto-Rigging5
Total35
Note: Rigging automatically includes basic walking + running animations for free (in
result.basic_animations
). Only add
Animate
(3 credits) if the user needs a custom animation beyond walking/running.
Wait for user confirmation before executing.
关键:在创建任何任务前,向用户展示任务摘要并获取确认:
我将按照以下方案生成"<prompt>"的3D模型:

  1. 预览(网格生成)—— 20积分
  2. 精修(添加PBR纹理)—— 10积分
  3. 下载为.glb格式

  总消耗:30积分
  当前余额:<N>积分

  是否继续执行?
对于多步骤工作流(例如文本转3D→骨骼绑定→动画),请提前展示完整工作流的总消耗
步骤API积分
预览文本转3D20
精修文本转3D10
骨骼绑定自动骨骼绑定5
总计35
注意:骨骼绑定会自动包含基础的行走+跑步动画(在
result.basic_animations
中)。仅当用户需要行走/跑步之外的自定义动画时,才需要添加“动画”步骤(3积分)。
等待用户确认后再执行任务。

Intent → API Mapping

用户需求→API映射

User wants to...APIEndpointCredits
3D model from textText to 3D
POST /openapi/v2/text-to-3d
20 + 10
3D model from one imageImage to 3D
POST /openapi/v1/image-to-3d
20–30
3D model from multiple imagesMulti-Image to 3D
POST /openapi/v1/multi-image-to-3d
20–30
New textures on existing modelRetexture
POST /openapi/v1/retexture
10
Change mesh format/topologyRemesh
POST /openapi/v1/remesh
5
Add skeleton to characterAuto-Rigging
POST /openapi/v1/rigging
5 (includes walking + running)
Animate a rigged character (custom)Animation
POST /openapi/v1/animations
3
2D image from textText to Image
POST /openapi/v1/text-to-image
3–9
Transform a 2D imageImage to Image
POST /openapi/v1/image-to-image
3–9
Check credit balanceBalance
GET /openapi/v1/balance
0

用户需求API端点积分
从文本生成3D模型文本转3D
POST /openapi/v2/text-to-3d
20 + 10
从单张图片生成3D模型图片转3D
POST /openapi/v1/image-to-3d
20–30
从多张图片生成3D模型多图转3D
POST /openapi/v1/multi-image-to-3d
20–30
为现有模型添加新纹理重新纹理
POST /openapi/v1/retexture
10
更改网格格式/拓扑结构重拓扑
POST /openapi/v1/remesh
5
为角色添加骨骼自动骨骼绑定
POST /openapi/v1/rigging
5
为绑定骨骼的角色制作自定义动画动画生成
POST /openapi/v1/animations
3
从文本生成2D图像文本转图像
POST /openapi/v1/text-to-image
3–9
转换2D图像风格图像转图像
POST /openapi/v1/image-to-image
3–9
查看积分余额余额查询
GET /openapi/v1/balance
0

Step 2: Execute the Workflow

步骤2:执行工作流

CRITICAL: Async Task Model

关键:异步任务模型

All generation endpoints return
{"result": "<task_id>"}
, NOT the model. You MUST poll.
NEVER read
model_urls
from the POST response.
所有生成端点返回
{"result": "<task_id>"}
,而非直接返回模型。你必须轮询任务状态。
绝对不要从POST响应中直接读取
model_urls

Reusable 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
undefined
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) # 更新全局历史记录 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
undefined

Text 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 base64
python
import base64

For 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")
undefined
task_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")
undefined

Multi-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
pose_mode: "t-pose"
for best rigging results.
If the model was already generated without t-pose, recommend regenerating with
pose_mode: "t-pose"
first.
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)必须使用
pose_mode: "t-pose"
以获得最佳骨骼绑定效果。如果模型生成时未使用T姿势,建议用户先重新生成带
pose_mode: "t-pose"
的模型。
绑定骨骼前,请验证模型面数不超过300,000。脚本应自动检查并在超出时阻止执行:
python
undefined

Pre-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")

```python
source_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")

```python

Rig (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")

undefined
undefined

Text to Image / Image to Image

文本转图像/图像转图像

python
undefined
python
undefined

Text 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:
  1. Downloaded file paths and sizes
  2. Task IDs (for follow-up operations like refine, rig, retexture)
  3. Available formats (list
    model_urls
    keys)
  4. Thumbnail URL if present
  5. Credits consumed and remaining balance (run balance check)
  6. 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
      meshy-3d-printing
      skill)

任务成功后,向用户报告以下内容:
  1. 下载文件的路径和大小
  2. 任务ID(用于后续操作,如精修、绑定骨骼、重新纹理)
  3. 可用格式(列出
    model_urls
    的键)
  4. 缩略图URL(如果存在)
  5. 消耗的积分和剩余余额(运行余额查询)
  6. 建议的下一步操作
    • 预览完成 → "是否需要精修模型(添加纹理)?"
    • 模型完成 → "是否需要为该角色绑定骨骼以制作动画?"
    • 已绑定骨骼 → "是否需要添加动画效果?"
    • 任意模型 → "是否需要重拓扑/导出为其他格式?"
    • 任意模型 → "是否需要3D打印该模型?"(需要使用
      meshy-3d-printing
      技能)

Error Recovery

错误恢复

HTTP StatusMeaningAction
401Invalid API keyRe-run Step 0; ask user to check key
402Insufficient creditsAuto-query balance (
GET /openapi/v1/balance
), show current balance, link https://www.meshy.ai/pricing
422Cannot processExplain limitation (e.g., non-humanoid for rigging)
429Rate limitedAuto-retry after 5s (max 3 times)
5xxServer errorAuto-retry after 10s (once)
Task
FAILED
messages:
  • "The server is busy..."
    → retry with backoff (5s, 10s, 20s)
  • "Internal server error."
    → simplify prompt, retry once

HTTP状态码含义操作
401API密钥无效重新执行步骤0;请用户检查密钥
402积分不足自动查询余额(
GET /openapi/v1/balance
),显示当前余额并链接到https://www.meshy.ai/pricing
422无法处理请求说明限制条件(例如骨骼绑定仅支持人形角色)
429请求频率超限5秒后自动重试(最多3次)
5xx服务器错误10秒后自动重试(1次)
任务
FAILED
消息处理:
  • "The server is busy..."
    → 退避重试(5秒、10秒、20秒)
  • "Internal server error."
    → 简化提示词,重试1次

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
    enable_pbr: true
    explicitly.
  • Format availability: Check keys in
    model_urls
    before downloading — not all formats are always present.
  • 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
    python3 -u
    for unbuffered output
  • 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