meshy-3d-agent

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Meshy 3D — Generation + Printing

Meshy 3D — 生成 + 打印

Directly communicate with the Meshy AI API to generate and print 3D assets. Covers the complete lifecycle: API key setup, task creation, exponential backoff polling, downloading, multi-step pipelines, and 3D print preparation with slicer integration.

可直接与Meshy AI API通信,生成并打印3D资产,覆盖完整的生命周期:API密钥配置、任务创建、指数退避轮询、下载、多步流程,以及整合了切片工具的3D打印准备功能。

SECURITY MANIFEST

安全声明

Environment variables accessed:
  • MESHY_API_KEY
    — API authentication token sent in HTTP
    Authorization: Bearer
    header only. Never logged, never written to any file except
    .env
    in the current working directory when explicitly requested by the user.
External network endpoints:
  • https://api.meshy.ai
    — Meshy AI API (task creation, status polling, model/image downloads)
File system access:
  • Read:
    .env
    in the current working directory only (API key lookup)
  • Write:
    .env
    in the current working directory only (API key storage, only on user request)
  • Write:
    ./meshy_output/
    in the current working directory (downloaded model files, metadata)
  • Read: files explicitly provided by the user (e.g., local images passed for image-to-3D conversion), accessed only at the exact path the user specifies
  • No access to home directories, shell profiles, or any path outside the above
Data leaving this machine:
  • API requests to
    api.meshy.ai
    include the
    MESHY_API_KEY
    in the Authorization header and user-provided text prompts or image URLs. No other local data is transmitted. Downloaded model files are saved locally only.

访问的环境变量:
  • MESHY_API_KEY
    — 仅在HTTP
    Authorization: Bearer
    请求头中携带的API认证令牌。除非用户明确要求,否则不会记录日志,也不会写入任何文件,仅会保存在当前工作目录的
    .env
    文件中。
外部网络端点:
  • https://api.meshy.ai
    — Meshy AI API(用于任务创建、状态轮询、模型/图片下载)
文件系统访问权限:
  • 读取:仅读取当前工作目录下的
    .env
    文件(用于查找API密钥)
  • 写入:仅写入当前工作目录下的
    .env
    文件(仅在用户要求时存储API密钥)
  • 写入:当前工作目录下的
    ./meshy_output/
    文件夹(存储下载的模型文件、元数据)
  • 读取:仅读取用户明确提供的文件(比如用于图片转3D的本地图片),仅会访问用户指定的精确路径
  • 无权访问home目录、shell配置文件,以及上述路径之外的任何位置
流出本机的数据:
  • 发往
    api.meshy.ai
    的API请求包含Authorization头中的
    MESHY_API_KEY
    ,以及用户提供的文本提示词或图片URL,不会传输其他本地数据。下载的模型文件仅保存在本地。

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.

当本技能在会话中首次激活时,告知用户:
所有生成的文件都将保存在当前工作目录的
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:
    meshy_output/{YYYYMMDD_HHmmss}_{prompt_slug}_{task_id_prefix}/
  • 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

所有下载的文件必须存储到当前工作目录下结构化的
meshy_output/
目录中,禁止随意散落文件
  • 每个项目路径:
    meshy_output/{YYYYMMDD_HHmmss}_{prompt_slug}_{task_id_prefix}/
  • 链式任务(预览→优化→绑定骨骼)复用同一个
    project_dir
  • 每个项目的任务记录在
    metadata.json
    中,全局历史记录在
    history.json
  • 缩略图会和模型一起自动下载

IMPORTANT: Shell Command Rules

重要提示:Shell命令规则

Use only standard POSIX tools. Do NOT use
rg
,
fd
,
bat
,
exa
/
eza
.

仅使用标准POSIX工具,禁止使用
rg
fd
bat
exa
/
eza

IMPORTANT: Run Long Tasks Properly

重要提示:正确执行长耗时任务

Meshy generation takes 1–5 minutes. Write the entire create → poll → download flow as ONE Python script and execute in a single Bash call. Use
python3 -u script.py
for unbuffered output. Tasks sitting at 99% for 30–120s is normal finalization — do NOT interrupt.

Meshy生成耗时1-5分钟,请将完整的创建→轮询→下载流程编写为单个Python脚本,通过单次Bash调用执行。使用
python3 -u script.py
获取无缓冲输出。任务进度停留在99%持续30-120秒是正常的收尾阶段,请勿中断

Step 0: API Key Detection (ALWAYS RUN FIRST)

步骤0:API密钥检测(始终优先执行)

Only check the current session environment and the
.env
file in the current working directory. Do NOT scan home directories or shell profile files.
bash
echo "=== Meshy API Key Detection ==="
仅检查当前会话环境变量和当前工作目录下的
.env
文件,禁止扫描home目录或shell配置文件。
bash
echo "=== Meshy API Key Detection ==="

1. Check current env var

1. Check current env var

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 in current working directory only

2. Check .env in current working directory only

if [ -f ".env" ] && grep -q "MESHY_API_KEY" ".env" 2>/dev/null; then echo "DOTENV(.env): FOUND" export MESHY_API_KEY=$(grep "^MESHY_API_KEY=" ".env" | head -1 | cut -d'=' -f2- | tr -d '"'"'" ) fi
if [ -f ".env" ] && grep -q "MESHY_API_KEY" ".env" 2>/dev/null; then echo "DOTENV(.env): FOUND" export MESHY_API_KEY=$(grep "^MESHY_API_KEY=" ".env" | head -1 | cut -d'=' -f2- | tr -d '"'"'" ) fi

3. Final status

3. Final status

if [ -n "$MESHY_API_KEY" ]; then echo "READY: key=${MESHY_API_KEY:0:8}..." else echo "READY: NO_KEY_FOUND" fi
if [ -n "$MESHY_API_KEY" ]; then echo "READY: key=${MESHY_API_KEY:0:8}..." else echo "READY: NO_KEY_FOUND" fi

4. Python requests check

4. Python requests check

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:
  1. Go to https://www.meshy.ai/settings/api
  2. Click "Create API Key", name it, and copy the key (starts with
    msy_
    )
  3. The key is shown only once — save it somewhere safe
Note: API access requires a Pro plan or above. Free-tier accounts cannot create API keys.
Once the user provides the key, set it for the current session and optionally persist to
.env
:
bash
undefined
告知用户:
使用Meshy API需要API密钥:
  1. 访问 https://www.meshy.ai/settings/api
  2. 点击 "Create API Key",命名后复制密钥(以
    msy_
    开头)
  3. 密钥仅显示一次,请妥善保管
注意: API访问需要Pro及以上套餐,免费版账户无法创建API密钥。
用户提供密钥后,为当前会话设置密钥,也可选择持久化到
.env
bash
undefined

Set for current session only

Set for current session only

export MESHY_API_KEY="msy_PASTE_KEY_HERE"
export MESHY_API_KEY="msy_PASTE_KEY_HERE"

Verify the key

Verify the key

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" else echo "Key invalid (HTTP $STATUS). Please check the key and try again." fi

**To persist the key (current project only):**

```bash
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" else echo "Key invalid (HTTP $STATUS). Please check the key and try again." fi

**持久化密钥(仅当前项目生效):**

```bash

Write to .env in current working directory

Write to .env in current working directory

echo 'MESHY_API_KEY=msy_PASTE_KEY_HERE' >> .env echo "Saved to .env"
echo 'MESHY_API_KEY=msy_PASTE_KEY_HERE' >> .env echo "Saved to .env"

IMPORTANT: add .env to .gitignore to avoid leaking the key

IMPORTANT: add .env to .gitignore to avoid leaking the key

grep -q "^.env" .gitignore 2>/dev/null || echo ".env" >> .gitignore echo ".env added to .gitignore"

> **Security reminder:** The key is stored only in `.env` in your current project directory. Never commit this file to version control. `.env` has been automatically added to `.gitignore`.

---
grep -q "^.env" .gitignore 2>/dev/null || echo ".env" >> .gitignore echo ".env added to .gitignore"

> **安全提醒:** 密钥仅存储在当前项目目录的`.env`文件中,请勿将该文件提交到版本控制。`.env`已自动添加到`.gitignore`中。

---

Step 1: Confirm Plan With User Before Spending Credits

步骤1:消耗积分前先和用户确认方案

CRITICAL: Before creating any task, present the user with a cost summary and wait for 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 (text-to-3d → rig → animate), show the FULL pipeline cost upfront.
Note: Rigging automatically includes walking + running animations at no extra cost. Only add
Animate
(3 credits) for custom animations beyond those.
至关重要: 创建任何任务前,先向用户展示费用汇总,等待用户确认:
我将使用以下方案生成"<prompt>"的3D模型:

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

  总费用:30积分
  当前余额:<N> 积分

  是否继续?
对于多步流程(文生3D→绑定骨骼→动画),请提前展示完整流程的总费用。
注意: 骨骼绑定默认赠送走路+跑步动画,无需额外付费。仅当需要自定义动画时才需要添加
Animate
(3积分)。

Intent → API Mapping

需求→API映射

User wants to...APIEndpointCredits
3D model from textText to 3D
POST /openapi/v2/text-to-3d
5–20 (preview) + 10 (refine)
3D model from one imageImage to 3D
POST /openapi/v1/image-to-3d
5–30
3D model from multiple imagesMulti-Image to 3D
POST /openapi/v1/multi-image-to-3d
5–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
Animate a rigged characterAnimation
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
3D print a model (white)→ See Print Pipeline section20
Multi-color 3D printMulti-Color Print
POST /openapi/v1/print/multi-color
10 (+ generation)

用户需要...对应API端点消耗积分
文本生成3D模型文生3D
POST /openapi/v2/text-to-3d
5–20(预览) + 10(优化)
单张图片生成3D模型图生3D
POST /openapi/v1/image-to-3d
5–30
多张图片生成3D模型多图生3D
POST /openapi/v1/multi-image-to-3d
5–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
3D打印模型(单色)→ 见打印流程章节20
多色3D打印多色打印
POST /openapi/v1/print/multi-color
10(+生成费用)

Step 2: Execute the Workflow

步骤2:执行工作流

Reusable Script Template

可复用脚本模板

Use this as the base for ALL workflows. It loads the API key securely from environment or
.env
in the current directory only:
python
#!/usr/bin/env python3
"""Meshy API task runner. Handles create → poll → download."""
import requests, time, os, sys, re, json
from datetime import datetime
所有工作流都基于此模板开发,它仅从环境变量或当前目录的
.env
中安全加载API密钥:
python
#!/usr/bin/env python3
"""Meshy API task runner. Handles create → poll → download."""
import requests, time, os, sys, re, json
from datetime import datetime

--- Secure API key loading ---

--- Secure API key loading ---

def load_api_key(): """Load MESHY_API_KEY from environment, then .env in cwd only.""" key = os.environ.get("MESHY_API_KEY", "").strip() if key: return key env_path = os.path.join(os.getcwd(), ".env") if os.path.exists(env_path): with open(env_path) as f: for line in f: line = line.strip() if line.startswith("MESHY_API_KEY=") and not line.startswith("#"): val = line.split("=", 1)[1].strip().strip('"').strip("'") if val: return val return ""
API_KEY = load_api_key() if not API_KEY: sys.exit("ERROR: MESHY_API_KEY not set. Run Step 0a to configure it.")
def load_api_key(): """Load MESHY_API_KEY from environment, then .env in cwd only.""" key = os.environ.get("MESHY_API_KEY", "").strip() if key: return key env_path = os.path.join(os.getcwd(), ".env") if os.path.exists(env_path): with open(env_path) as f: for line in f: line = line.strip() if line.startswith("MESHY_API_KEY=") and not line.startswith("#"): val = line.split("=", 1)[1].strip().strip('"').strip("'") if val: return val return ""
API_KEY = load_api_key() if not API_KEY: sys.exit("ERROR: MESHY_API_KEY not set. Run Step 0a to configure it.")

Never log the full key — only first 8 chars for traceability

Never log the full key — only first 8 chars for traceability

print(f"API key loaded: {API_KEY[:8]}...")
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). Re-run Step 0a.") 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). 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=300): """Poll with exponential backoff (5s→30s, fixed 15s at 95%+).""" elapsed, delay, max_delay, backoff, finalize_delay, poll_count = 0, 5, 30, 1.5, 15, 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) bar = f"[{'█' * int(progress/5)}{'░' * (20 - int(progress/5))}] {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 into a project directory (within cwd/meshy_output/).""" 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) print(f"DOWNLOADED: {filepath} ({os.path.getsize(filepath)/1024/1024:.1f} MB)")
print(f"API key loaded: {API_KEY[:8]}...")
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). Re-run Step 0a.") 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). 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=300): """Poll with exponential backoff (5s→30s, fixed 15s at 95%+).""" elapsed, delay, max_delay, backoff, finalize_delay, poll_count = 0, 5, 30, 1.5, 15, 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) bar = f"[{'█' * int(progress/5)}{'░' * (20 - int(progress/5))}] {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 into a project directory (within cwd/meshy_output/).""" 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) print(f"DOWNLOADED: {filepath} ({os.path.getsize(filepath)/1024/1024:.1f} MB)")

--- File organization helpers ---

--- File organization helpers ---

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"): slug = re.sub(r'[^a-z0-9]+', '-', (prompt or task_type).lower())[:30].strip('-') folder = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}{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") meta = json.load(open(meta_path)) if os.path.exists(meta_path) else { "project_name": prompt or task_type, "folder": os.path.basename(project_dir), "root_task_id": task_id, "created_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) history = json.load(open(HISTORY_FILE)) if os.path.exists(HISTORY_FILE) else {"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.update({"task_count": len(meta["tasks"]), "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

---
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"): slug = re.sub(r'[^a-z0-9]+', '-', (prompt or task_type).lower())[:30].strip('-') folder = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}{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") meta = json.load(open(meta_path)) if os.path.exists(meta_path) else { "project_name": prompt or task_type, "folder": os.path.basename(project_dir), "root_task_id": task_id, "created_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) history = json.load(open(HISTORY_FILE)) if os.path.exists(HISTORY_FILE) else {"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.update({"task_count": len(meta["tasks"]), "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

---

Text to 3D (Preview + Refine)

文生3D(预览+优化)

Append to the template above:
python
PROMPT = "USER_PROMPT"
追加到上述模板中:
python
PROMPT = "USER_PROMPT"

Preview

Preview

preview_id = create_task("/openapi/v2/text-to-3d", { "mode": "preview", "prompt": PROMPT, "ai_model": "latest", # "pose_mode": "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 — Task: {preview_id} | Project: {project_dir}")
preview_id = create_task("/openapi/v2/text-to-3d", { "mode": "preview", "prompt": PROMPT, "ai_model": "latest", # "pose_mode": "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 — Task: {preview_id} | Project: {project_dir}")

Refine

Refine

refine_id = create_task("/openapi/v2/text-to-3d", { "mode": "refine", "preview_task_id": preview_id, "enable_pbr": True, "ai_model": "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 — Task: {refine_id} | Formats: {', '.join(task['model_urls'].keys())}")

> **Note:** All models (meshy-5, meshy-6, latest) support both preview and refine. The preview and refine ai_model should match to avoid 400 errors.

---
refine_id = create_task("/openapi/v2/text-to-3d", { "mode": "refine", "preview_task_id": preview_id, "enable_pbr": True, "ai_model": "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 — Task: {refine_id} | Formats: {', '.join(task['model_urls'].keys())}")

> **注意:** 所有模型(meshy-5、meshy-6、latest)都支持预览和优化。预览和优化阶段的ai_model需要保持一致,避免出现400错误。

---

Image to 3D

图生3D

python
import base64
python
import base64

For local files: convert to data URI

For local files: convert to data 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, "ai_model": "latest", }) task = poll_task("/openapi/v1/image-to-3d", task_id) project_dir = get_project_dir(task_id, task_type="image-to-3d") download(task["model_urls"]["glb"], os.path.join(project_dir, "model.glb")) record_task(project_dir, task_id, "image-to-3d", "complete", files=["model.glb"])

---
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", }) task = poll_task("/openapi/v1/image-to-3d", task_id) project_dir = get_project_dir(task_id, task_type="image-to-3d") download(task["model_urls"]["glb"], os.path.join(project_dir, "model.glb")) record_task(project_dir, task_id, "image-to-3d", "complete", files=["model.glb"])

---

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,
    "ai_model": "latest",
})
task = poll_task("/openapi/v1/multi-image-to-3d", task_id)
project_dir = get_project_dir(task_id, task_type="multi-image-to-3d")
download(task["model_urls"]["glb"], os.path.join(project_dir, "model.glb"))

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,
    "ai_model": "latest",
})
task = poll_task("/openapi/v1/multi-image-to-3d", task_id)
project_dir = get_project_dir(task_id, task_type="multi-image-to-3d")
download(task["model_urls"]["glb"], os.path.join(project_dir, "model.glb"))

Retexture

重纹理

IMPORTANT: Ask user for texture style first —
text_style_prompt
OR
image_style_url
(one required, image takes precedence if both given).
python
undefined
重要: 先询问用户纹理风格,需要提供
text_style_prompt
image_style_url
(二选一,同时提供时优先使用图片)。
python
undefined

REQUIRED: ask user for text_style_prompt OR image_style_url

REQUIRED: ask user for text_style_prompt OR image_style_url

task_id = create_task("/openapi/v1/retexture", { "input_task_id": "PREVIOUS_TASK_ID", "text_style_prompt": "wooden texture", # REQUIRED if no image_style_url # "image_style_url": "URL", # REQUIRED if no prompt (takes precedence) "enable_pbr": True, # "target_formats": ["glb", "3mf"], # 3mf must be explicitly requested }) task = poll_task("/openapi/v1/retexture", task_id) project_dir = get_project_dir(task_id, task_type="retexture") download(task["model_urls"]["glb"], os.path.join(project_dir, "retextured.glb"))

---
task_id = create_task("/openapi/v1/retexture", { "input_task_id": "PREVIOUS_TASK_ID", "text_style_prompt": "wooden texture", # REQUIRED if no image_style_url # "image_style_url": "URL", # REQUIRED if no prompt (takes precedence) "enable_pbr": True, # "target_formats": ["glb", "3mf"], # 3mf must be explicitly requested }) task = poll_task("/openapi/v1/retexture", task_id) project_dir = get_project_dir(task_id, task_type="retexture") download(task["model_urls"]["glb"], os.path.join(project_dir, "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)
project_dir = get_project_dir(task_id, task_type="remesh")
for fmt, url in task["model_urls"].items():
    download(url, os.path.join(project_dir, 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)
project_dir = get_project_dir(task_id, task_type="remesh")
for fmt, url in task["model_urls"].items():
    download(url, os.path.join(project_dir, f"remeshed.{fmt}"))

Auto-Rigging + Animation

自动绑定+动画

When the user asks to rig or animate, the generation step MUST use
pose_mode: "t-pose"
.
python
undefined
当用户要求绑定骨骼或制作动画时,生成阶段必须使用
pose_mode: "t-pose"
python
undefined

Pre-rig check: polycount must be ≤ 300,000

Pre-rig check: polycount must be ≤ 300,000

source_endpoint = "/openapi/v2/text-to-3d" # adjust to match source task endpoint source_task_id = "TASK_ID" check = SESSION.get(f"{BASE}{source_endpoint}/{source_task_id}", headers=HEADERS, timeout=30) check.raise_for_status() face_count = check.json().get("face_count", 0) if face_count > 300000: sys.exit(f"ERROR: {face_count:,} faces exceeds 300,000 limit. Remesh first.")
source_endpoint = "/openapi/v2/text-to-3d" # adjust to match source task endpoint source_task_id = "TASK_ID" check = SESSION.get(f"{BASE}{source_endpoint}/{source_task_id}", headers=HEADERS, timeout=30) check.raise_for_status() face_count = check.json().get("face_count", 0) if face_count > 300000: sys.exit(f"ERROR: {face_count:,} faces exceeds 300,000 limit. Remesh first.")

Rig

Rig

rig_id = create_task("/openapi/v1/rigging", { "input_task_id": source_task_id, "height_meters": 1.7, }) rig_task = poll_task("/openapi/v1/rigging", rig_id) project_dir = get_project_dir(rig_id, task_type="rigging") download(rig_task["result"]["rigged_character_glb_url"], os.path.join(project_dir, "rigged.glb")) download(rig_task["result"]["basic_animations"]["walking_glb_url"], os.path.join(project_dir, "walking.glb")) download(rig_task["result"]["basic_animations"]["running_glb_url"], os.path.join(project_dir, "running.glb"))
rig_id = create_task("/openapi/v1/rigging", { "input_task_id": source_task_id, "height_meters": 1.7, }) rig_task = poll_task("/openapi/v1/rigging", rig_id) project_dir = get_project_dir(rig_id, task_type="rigging") download(rig_task["result"]["rigged_character_glb_url"], os.path.join(project_dir, "rigged.glb")) download(rig_task["result"]["basic_animations"]["walking_glb_url"], os.path.join(project_dir, "walking.glb")) download(rig_task["result"]["basic_animations"]["running_glb_url"], os.path.join(project_dir, "running.glb"))

Custom animation (optional, 3 credits — only if user needs beyond walking/running)

Custom animation (optional, 3 credits — only if user needs beyond walking/running)

anim_id = create_task("/openapi/v1/animations", {"rig_task_id": rig_id, "action_id": 1})

anim_id = create_task("/openapi/v1/animations", {"rig_task_id": rig_id, "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"], os.path.join(project_dir, "animated.glb"))

download(anim_task["result"]["animation_glb_url"], os.path.join(project_dir, "animated.glb"))


---

---

Text to Image / Image to Image

文生图/图生图

python
undefined
python
undefined

Text to Image

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 URL: task["image_url"]

Result URL: task["image_url"]

Image to Image

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)

---

3D Printing Workflow

3D打印工作流

IMPORTANT: When the user's request involves 3D printing, use this section for the ENTIRE workflow — including model generation. Do NOT run the generation workflows above and then come here. This section controls
target_formats
and other print-specific parameters from the start.
Trigger when the user mentions: print, 3d print, slicer, slice, bambu, orca, prusa, cura, multicolor, multi-color, 3mf, figurine, miniature, statue, physical model, desk toy, phone stand.
重要:当用户的需求涉及3D打印时,整个流程都使用本节内容,包括模型生成。 请勿先执行上面的生成流程再跳转至此,本节从一开始就会控制
target_formats
和其他打印专用参数。
当用户提到以下关键词时触发本流程:print、3d print、slicer、slice、bambu、orca、prusa、cura、multicolor、multi-color、3mf、figurine、miniature、statue、physical model、desk toy、phone stand。

Decision: White Model vs Multicolor

决策:单色模型还是多色模型

  1. Detect installed slicers first (see script below)
  2. Ask the user: "White model (single-color) or multicolor?"
  3. If multicolor: check for multicolor-capable slicer (OrcaSlicer, Bambu Studio, Creality Print, Elegoo Slicer, Anycubic Slicer Next), ask max_colors (1-16, default 4) and max_depth (3-6, default 4), confirm cost: 40 credits
  1. 首先检测已安装的切片软件(见下方脚本)
  2. 询问用户:"需要单色模型(单颜色)还是多色模型?"
  3. 如果选择多色:检查是否支持多色的切片软件(OrcaSlicer、Bambu Studio、Creality Print、Elegoo Slicer、Anycubic Slicer Next),询问max_colors(1-16,默认4)和max_depth(3-6,默认4),确认费用:40积分

Slicer Detection + Opening

切片软件检测+打开

python
import subprocess, shutil, platform, os, glob as glob_mod

SLICER_MAP = {
    "OrcaSlicer":           {"mac_app": "OrcaSlicer",          "win_exe": "orca-slicer.exe",         "win_dir": "OrcaSlicer",          "linux_exe": "orca-slicer"},
    "Bambu Studio":         {"mac_app": "BambuStudio",         "win_exe": "bambu-studio.exe",        "win_dir": "BambuStudio",         "linux_exe": "bambu-studio"},
    "Creality Print":       {"mac_app": "Creality Print",      "win_exe": "CrealityPrint.exe",       "win_dir": "Creality Print*",     "linux_exe": None},
    "Elegoo Slicer":        {"mac_app": "ElegooSlicer",        "win_exe": "elegoo-slicer.exe",       "win_dir": "ElegooSlicer",        "linux_exe": None},
    "Anycubic Slicer Next": {"mac_app": "AnycubicSlicerNext",  "win_exe": "AnycubicSlicerNext.exe",  "win_dir": "AnycubicSlicerNext",  "linux_exe": None},
    "PrusaSlicer":          {"mac_app": "PrusaSlicer",         "win_exe": "prusa-slicer.exe",        "win_dir": "PrusaSlicer",         "linux_exe": "prusa-slicer"},
    "UltiMaker Cura":       {"mac_app": "UltiMaker Cura",      "win_exe": "UltiMaker-Cura.exe",     "win_dir": "UltiMaker Cura*",     "linux_exe": None},
}
MULTICOLOR_SLICERS = {"OrcaSlicer", "Bambu Studio", "Creality Print", "Elegoo Slicer", "Anycubic Slicer Next"}

def detect_slicers():
    found = []
    system = platform.system()
    for name, info in SLICER_MAP.items():
        path = None
        if system == "Darwin":
            app = info.get("mac_app")
            if app and os.path.exists(f"/Applications/{app}.app"):
                path = f"/Applications/{app}.app"
        elif system == "Windows":
            win_dir, win_exe = info.get("win_dir", ""), info.get("win_exe", "")
            for base in [os.environ.get("ProgramFiles", r"C:\Program Files"),
                         os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")]:
                if "*" in win_dir:
                    matches = glob_mod.glob(os.path.join(base, win_dir, win_exe))
                    if matches: path = matches[0]; break
                else:
                    candidate = os.path.join(base, win_dir, win_exe)
                    if os.path.exists(candidate): path = candidate; break
        else:
            exe = info.get("linux_exe")
            if exe: path = shutil.which(exe)
        if path:
            found.append({"name": name, "path": path, "multicolor": name in MULTICOLOR_SLICERS})
    return found

def open_in_slicer(file_path, slicer_name):
    info = SLICER_MAP.get(slicer_name, {})
    system, abs_path = platform.system(), os.path.abspath(file_path)
    if system == "Darwin":
        subprocess.run(["open", "-a", info.get("mac_app", slicer_name), abs_path])
    elif system == "Windows":
        exe_path = shutil.which(info.get("win_exe", ""))
        (subprocess.Popen([exe_path, abs_path]) if exe_path else os.startfile(abs_path))
    else:
        exe_path = shutil.which(info.get("linux_exe", ""))
        (subprocess.Popen([exe_path, abs_path]) if exe_path else subprocess.run(["xdg-open", abs_path]))
    print(f"Opened {abs_path} in {slicer_name}")

slicers = detect_slicers()
for s in slicers:
    mc = " [multicolor]" if s["multicolor"] else ""
    print(f"  - {s['name']}{mc}: {s['path']}")
python
import subprocess, shutil, platform, os, glob as glob_mod

SLICER_MAP = {
    "OrcaSlicer":           {"mac_app": "OrcaSlicer",          "win_exe": "orca-slicer.exe",         "win_dir": "OrcaSlicer",          "linux_exe": "orca-slicer"},
    "Bambu Studio":         {"mac_app": "BambuStudio",         "win_exe": "bambu-studio.exe",        "win_dir": "BambuStudio",         "linux_exe": "bambu-studio"},
    "Creality Print":       {"mac_app": "Creality Print",      "win_exe": "CrealityPrint.exe",       "win_dir": "Creality Print*",     "linux_exe": None},
    "Elegoo Slicer":        {"mac_app": "ElegooSlicer",        "win_exe": "elegoo-slicer.exe",       "win_dir": "ElegooSlicer",        "linux_exe": None},
    "Anycubic Slicer Next": {"mac_app": "AnycubicSlicerNext",  "win_exe": "AnycubicSlicerNext.exe",  "win_dir": "AnycubicSlicerNext",  "linux_exe": None},
    "PrusaSlicer":          {"mac_app": "PrusaSlicer",         "win_exe": "prusa-slicer.exe",        "win_dir": "PrusaSlicer",         "linux_exe": "prusa-slicer"},
    "UltiMaker Cura":       {"mac_app": "UltiMaker Cura",      "win_exe": "UltiMaker-Cura.exe",     "win_dir": "UltiMaker Cura*",     "linux_exe": None},
}
MULTICOLOR_SLICERS = {"OrcaSlicer", "Bambu Studio", "Creality Print", "Elegoo Slicer", "Anycubic Slicer Next"}

def detect_slicers():
    found = []
    system = platform.system()
    for name, info in SLICER_MAP.items():
        path = None
        if system == "Darwin":
            app = info.get("mac_app")
            if app and os.path.exists(f"/Applications/{app}.app"):
                path = f"/Applications/{app}.app"
        elif system == "Windows":
            win_dir, win_exe = info.get("win_dir", ""), info.get("win_exe", "")
            for base in [os.environ.get("ProgramFiles", r"C:\Program Files"),
                         os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")]:
                if "*" in win_dir:
                    matches = glob_mod.glob(os.path.join(base, win_dir, win_exe))
                    if matches: path = matches[0]; break
                else:
                    candidate = os.path.join(base, win_dir, win_exe)
                    if os.path.exists(candidate): path = candidate; break
        else:
            exe = info.get("linux_exe")
            if exe: path = shutil.which(exe)
        if path:
            found.append({"name": name, "path": path, "multicolor": name in MULTICOLOR_SLICERS})
    return found

def open_in_slicer(file_path, slicer_name):
    info = SLICER_MAP.get(slicer_name, {})
    system, abs_path = platform.system(), os.path.abspath(file_path)
    if system == "Darwin":
        subprocess.run(["open", "-a", info.get("mac_app", slicer_name), abs_path])
    elif system == "Windows":
        exe_path = shutil.which(info.get("win_exe", ""))
        (subprocess.Popen([exe_path, abs_path]) if exe_path else os.startfile(abs_path))
    else:
        exe_path = shutil.which(info.get("linux_exe", ""))
        (subprocess.Popen([exe_path, abs_path]) if exe_path else subprocess.run(["xdg-open", abs_path]))
    print(f"Opened {abs_path} in {slicer_name}")

slicers = detect_slicers()
for s in slicers:
    mc = " [multicolor]" if s["multicolor"] else ""
    print(f"  - {s['name']}{mc}: {s['path']}")

White Model Pipeline

单色模型打印流程

StepActionCredits
1Generate untextured model20
2Download OBJ0
3Fix OBJ (
fix_obj_for_printing
)
0
4Open in slicer0
Generate with
target_formats
including
"obj"
, then fix for printing:
python
undefined
步骤操作消耗积分
1生成无纹理模型20
2下载OBJ格式0
3修复OBJ(
fix_obj_for_printing
0
4在切片软件中打开0
生成时指定
target_formats
包含
"obj"
,然后进行打印前修复:
python
undefined

--- Generate for white model printing ---

--- Generate for white model printing ---

Text to 3D:

Text to 3D:

task_id = create_task("/openapi/v2/text-to-3d", { "mode": "preview", "prompt": "USER_PROMPT", "ai_model": "latest", "target_formats": ["obj"], # Only OBJ for white model printing })
task_id = create_task("/openapi/v2/text-to-3d", { "mode": "preview", "prompt": "USER_PROMPT", "ai_model": "latest", "target_formats": ["obj"], # Only OBJ for white model printing })

OR Image to 3D:

OR Image to 3D:

task_id = create_task("/openapi/v1/image-to-3d", {

task_id = create_task("/openapi/v1/image-to-3d", {

"image_url": "URL", "should_texture": False,

"image_url": "URL", "should_texture": False,

"target_formats": ["glb", "obj"],

"target_formats": ["glb", "obj"],

})

})

task = poll_task("/openapi/v2/text-to-3d", task_id) project_dir = get_project_dir(task_id, "print")
obj_url = task["model_urls"].get("obj") or task["model_urls"].get("glb") obj_path = os.path.join(project_dir, "model.obj") download(obj_url, obj_path)
def fix_obj_for_printing(input_path, output_path=None, target_height_mm=75.0): if output_path is None: output_path = input_path lines = open(input_path, "r").readlines() rotated, min_x, max_x, min_y, max_y, min_z, max_z = [], float("inf"), float("-inf"), float("inf"), float("-inf"), float("inf"), float("-inf") for line in lines: if line.startswith("v "): parts = line.split() x, y, z = float(parts[1]), float(parts[2]), float(parts[3]) rx, ry, rz = x, -z, y min_x, max_x = min(min_x, rx), max(max_x, rx) min_y, max_y = min(min_y, ry), max(max_y, ry) min_z, max_z = min(min_z, rz), max(max_z, rz) rotated.append(("v", rx, ry, rz, parts[4:])) elif line.startswith("vn "): parts = line.split() rotated.append(("vn", float(parts[1]), -float(parts[3]), float(parts[2]), [])) else: rotated.append(("line", line)) h = max_z - min_z s = target_height_mm / h if h > 1e-6 else 1.0 xo, yo, zo = -(min_x+max_x)/2s, -(min_y+max_y)/2s, -(min_zs) with open(output_path, "w") as f: for item in rotated: if item[0] == "v": _, rx, ry, rz, extra = item e = " "+" ".join(extra) if extra else "" f.write(f"v {rxs+xo:.6f} {rys+yo:.6f} {rzs+zo:.6f}{e}\n") elif item[0] == "vn": f.write(f"vn {item[1]:.6f} {item[2]:.6f} {item[3]:.6f}\n") else: f.write(item[1]) print(f"OBJ fixed: scaled to {target_height_mm:.0f}mm, Z-up, centered")
fix_obj_for_printing(obj_path, target_height_mm=75.0) if slicers: open_in_slicer(obj_path, slicers[0]["name"])
undefined
task = poll_task("/openapi/v2/text-to-3d", task_id) project_dir = get_project_dir(task_id, "print")
obj_url = task["model_urls"].get("obj") or task["model_urls"].get("glb") obj_path = os.path.join(project_dir, "model.obj") download(obj_url, obj_path)
def fix_obj_for_printing(input_path, output_path=None, target_height_mm=75.0): if output_path is None: output_path = input_path lines = open(input_path, "r").readlines() rotated, min_x, max_x, min_y, max_y, min_z, max_z = [], float("inf"), float("-inf"), float("inf"), float("-inf"), float("inf"), float("-inf") for line in lines: if line.startswith("v "): parts = line.split() x, y, z = float(parts[1]), float(parts[2]), float(parts[3]) rx, ry, rz = x, -z, y min_x, max_x = min(min_x, rx), max(max_x, rx) min_y, max_y = min(min_y, ry), max(max_y, ry) min_z, max_z = min(min_z, rz), max(max_z, rz) rotated.append(("v", rx, ry, rz, parts[4:])) elif line.startswith("vn "): parts = line.split() rotated.append(("vn", float(parts[1]), -float(parts[3]), float(parts[2]), [])) else: rotated.append(("line", line)) h = max_z - min_z s = target_height_mm / h if h > 1e-6 else 1.0 xo, yo, zo = -(min_x+max_x)/2s, -(min_y+max_y)/2s, -(min_zs) with open(output_path, "w") as f: for item in rotated: if item[0] == "v": _, rx, ry, rz, extra = item e = " "+" ".join(extra) if extra else "" f.write(f"v {rxs+xo:.6f} {rys+yo:.6f} {rzs+zo:.6f}{e}\n") elif item[0] == "vn": f.write(f"vn {item[1]:.6f} {item[2]:.6f} {item[3]:.6f}\n") else: f.write(item[1]) print(f"OBJ fixed: scaled to {target_height_mm:.0f}mm, Z-up, centered")
fix_obj_for_printing(obj_path, target_height_mm=75.0) if slicers: open_in_slicer(obj_path, slicers[0]["name"])
undefined

Multicolor Pipeline

多色模型打印流程

StepActionCredits
1Generate + texture30
2Multi-color processing10
3Download 3MF0
4Open in multicolor slicer0
python
mc_slicers = [s for s in slicers if s["multicolor"]]
if not mc_slicers:
    print("WARNING: No multicolor slicer detected. Install: OrcaSlicer, Bambu Studio, etc.")
步骤操作消耗积分
1生成+纹理制作30
2多色处理10
3下载3MF0
4在多色切片软件中打开0
python
mc_slicers = [s for s in slicers if s["multicolor"]]
if not mc_slicers:
    print("WARNING: No multicolor slicer detected. Install: OrcaSlicer, Bambu Studio, etc.")

--- Generate + texture with target_formats including 3mf ---

--- Generate + texture with target_formats including 3mf ---

preview_id = create_task("/openapi/v2/text-to-3d", { "mode": "preview", "prompt": "USER_PROMPT", "ai_model": "latest", # No target_formats needed — 3MF comes from multi-color API }) poll_task("/openapi/v2/text-to-3d", preview_id)
refine_id = create_task("/openapi/v2/text-to-3d", { "mode": "refine", "preview_task_id": preview_id, "enable_pbr": True, }) poll_task("/openapi/v2/text-to-3d", refine_id) project_dir = get_project_dir(preview_id, "multicolor-print")
preview_id = create_task("/openapi/v2/text-to-3d", { "mode": "preview", "prompt": "USER_PROMPT", "ai_model": "latest", # No target_formats needed — 3MF comes from multi-color API }) poll_task("/openapi/v2/text-to-3d", preview_id)
refine_id = create_task("/openapi/v2/text-to-3d", { "mode": "refine", "preview_task_id": preview_id, "enable_pbr": True, }) poll_task("/openapi/v2/text-to-3d", refine_id) project_dir = get_project_dir(preview_id, "multicolor-print")

--- Multi-color processing ---

--- Multi-color processing ---

mc_task_id = create_task("/openapi/v1/print/multi-color", { "input_task_id": refine_id, "max_colors": 4, # 1-16, ask user "max_depth": 4, # 3-6, ask user }) task = poll_task("/openapi/v1/print/multi-color", mc_task_id) threemf_path = os.path.join(project_dir, "multicolor.3mf") download(task["model_urls"]["3mf"], threemf_path) if mc_slicers: open_in_slicer(threemf_path, mc_slicers[0]["name"])
undefined
mc_task_id = create_task("/openapi/v1/print/multi-color", { "input_task_id": refine_id, "max_colors": 4, # 1-16, ask user "max_depth": 4, # 3-6, ask user }) task = poll_task("/openapi/v1/print/multi-color", mc_task_id) threemf_path = os.path.join(project_dir, "multicolor.3mf") download(task["model_urls"]["3mf"], threemf_path) if mc_slicers: open_in_slicer(threemf_path, mc_slicers[0]["name"])
undefined

Printability Checklist

可打印性检查清单

CheckRecommendation
Wall thicknessMin 1.2mm FDM, 0.8mm resin
OverhangsKeep below 45° or add supports
Manifold meshWatertight, no holes
Minimum detail0.4mm FDM, 0.05mm resin
Base stabilityFlat base or add brim/raft in slicer
Floating partsAll parts connected or printed separately

检查项建议
壁厚FDM最小1.2mm,树脂最小0.8mm
悬垂角度保持在45°以下,否则添加支撑
流形网格水密无孔洞
最小细节FDM 0.4mm,树脂0.05mm
底座稳定性底座平整,或在切片软件中添加裙边/raft
浮动部件所有部件需连接,或分开打印

Step 3: Report Results

步骤3:结果汇报

After task succeeds:
  1. Downloaded file paths and sizes
  2. Task IDs (for follow-up: refine, rig, retexture)
  3. Available formats (list
    model_urls
    keys)
  4. Credits consumed + current balance
  5. Suggested next steps:
    • Preview done → "Want to refine (add textures)?"
    • Model done → "Want to rig this character?"
    • Rigged → "Want to apply a custom animation?"
    • Any textured model → "Want to 3D print this? Multicolor printing is available!"
    • Any model → "Want to 3D print this?"

任务成功后,汇报以下内容:
  1. 下载的文件路径和大小
  2. 任务ID(用于后续操作:优化、绑定骨骼、重纹理)
  3. 可用格式(列出
    model_urls
    的键)
  4. 消耗的积分+当前余额
  5. 建议的下一步操作:
    • 预览完成 → "是否需要优化(添加纹理)?"
    • 模型生成完成 → "是否需要为这个角色绑定骨骼?"
    • 绑定完成 → "是否需要添加自定义动画?"
    • 任意带纹理的模型 → "是否需要3D打印?支持多色打印!"
    • 任意模型 → "是否需要3D打印?"

Error Recovery

错误恢复

HTTP StatusMeaningAction
401Invalid API keyRe-run Step 0; ask user to check key
402Insufficient creditsShow balance, link https://www.meshy.ai/pricing
422Cannot processExplain (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状态码含义处理方式
401无效API密钥重新运行步骤0,要求用户检查密钥
402积分不足展示余额,提供链接https://www.meshy.ai/pricing
422无法处理请求解释原因(例如绑定骨骼仅支持人形角色)
429触发限流5秒后自动重试(最多3次)
5xx服务端错误10秒后自动重试(1次)
任务
FAILED
消息处理:
  • "The server is busy..."
    → 退避重试(5s、10s、20s)
  • "Internal server error."
    → 简化提示词,重试1次

Known Behaviors & Constraints

已知行为与限制

  • 99% stall: Normal finalization (30–120s). Do NOT interrupt.
  • Asset retention: Files deleted after 3 days (non-Enterprise). Download immediately.
  • PBR maps: Must set
    enable_pbr: true
    explicitly.
  • Refine: All models support both preview and refine. Preview and refine ai_model should match.
  • Rigging: Humanoid bipedal only, polycount ≤ 300,000.
  • Printing formats: White model → OBJ with
    fix_obj_for_printing()
    . Multicolor → 3MF from Multi-Color Print API. Always detect slicer first.
  • Download format: Ask the user which format they need before downloading. GLB (viewing), OBJ (printing), 3MF (multicolor), FBX (games), USDZ (AR). Do NOT download all formats.
  • 3MF for multicolor: Multi-Color Print API outputs 3MF directly — no need to request 3MF from generate/refine. For non-print use cases needing 3MF, pass
    "3mf"
    in
    target_formats
    .
  • Timestamps: All API timestamps are Unix epoch milliseconds.

  • 99%进度停滞:属于正常收尾阶段(30-120秒),请勿中断
  • 资产保留期限:非企业版用户文件将在3天后删除,请立即下载
  • PBR贴图:必须显式设置
    enable_pbr: true
    才会生成
  • 优化阶段:所有模型都支持预览和优化,预览和优化阶段的ai_model需保持一致
  • 骨骼绑定:仅支持双足人形角色,面数≤300,000
  • 打印格式:单色模型→OBJ+
    fix_obj_for_printing()
    处理;多色模型→多色打印API输出的3MF。始终优先检测切片软件
  • 下载格式:下载前询问用户需要的格式,GLB(预览)、OBJ(打印)、3MF(多色)、FBX(游戏)、USDZ(AR),请勿下载所有格式
  • 多色3MF:多色打印API直接输出3MF,无需在生成/优化阶段请求3MF。非打印场景需要3MF时,在
    target_formats
    中添加
    "3mf"
    即可
  • 时间戳:所有API时间戳为Unix epoch 毫秒

Execution Checklist

执行检查清单

  • Ran API key detection (Step 0) — checked env var and
    .env
    only
  • API key verified (never printed in full)
  • Presented cost summary and got user confirmation
  • Wrote complete workflow as single Python script
  • Ran with
    python3 -u
    for unbuffered output
  • Reported file paths, formats, task IDs, and balance
  • Suggested next steps

  • 已执行API密钥检测(步骤0)—— 仅检查环境变量和
    .env
    文件
  • API密钥已验证(从未完整打印)
  • 已向用户展示费用汇总并获得确认
  • 已将完整工作流编写为单个Python脚本
  • 使用
    python3 -u
    执行以获取无缓冲输出
  • 已汇报文件路径、格式、任务ID和余额
  • 已给出下一步操作建议

Additional Resources

额外资源

For the complete API endpoint reference including all parameters, response schemas, and error codes, read reference.md.
完整的API端点参考(包含所有参数、响应结构和错误码)请查看reference.md