mcpField
M
meshy-3d-agent

meshy-3d-agent

github.com/meshy-devクリエイティブ・メディア106908 回インストール2 週間前に更新
リポジトリ

meshy-3d-agent について

meshy-3d-agentは、Meshy AI APIで3Dアセットを生成・加工し、印刷用に準備するスキルです。タスク作成、状態確認、ダウンロードから複数段階のモデル処理までを案内し、費用確認とファイル管理のルールも含みます。

主な機能

  1. 01テキスト・画像からの3D生成
  2. 02リテクスチャ・リメッシュ・形式変換
  3. 03モデルのサイズ変更とUV展開
  4. 04リギングとアニメーションの手順
  5. 05印刷適性の分析と修復
  6. 06ローカルへの整理済み保存と履歴管理

活用シーン

  1. 01プロンプトや画像から3Dモデルを生成
  2. 02モデルを別形式に変換
  3. 03キャラクターのリギングとアニメーション
  4. 04印刷適性の確認や修復

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/meshy-dev/meshy-3d-agent --skill meshy-3d-agent

インストール後、Claude Code で/meshy-3d-agentを入力して読み込まれたか確認します。

同梱ファイルreference.md

詳細

Meshy 3D — 生成と印刷

Meshy AI API と直接通信して、3D アセットを生成し、印刷します。API キーの設定、タスクの作成、指数バックオフによるポーリング、ダウンロード、複数ステップのパイプライン、スライサーとの連携による3Dプリントの準備まで、ライフサイクル全体をカバーします。


セキュリティマニフェスト

アクセスする環境変数:

  • MESHY_API_KEY — HTTP の Authorization: Bearer ヘッダーでのみ送信される API 認証トークンです。ログに記録されず、ユーザーから明示的に要求された場合に現在の作業ディレクトリにある .env に書き込まれる場合を除き、ファイルに書き込まれることもありません。

外部ネットワークエンドポイント:

  • https://api.meshy.ai — Meshy AI API(タスクの作成、ステータスのポーリング、モデル/画像のダウンロード)

ファイルシステムへのアクセス:

  • 読み取り:現在の作業ディレクトリにある .env のみ(API キーの検索)
  • 書き込み:現在の作業ディレクトリにある .env のみ(ユーザーの要求があった場合に限り、API キーを保存)
  • 書き込み:現在の作業ディレクトリにある ./meshy_output/(ダウンロードしたモデルファイル、メタデータ)
  • 読み取り:ユーザーが明示的に指定したファイルのみ(例:画像から3Dへの変換に渡されたローカル画像)。アクセスするのは、ユーザーが指定した正確なパスのみです。
  • ホームディレクトリ、シェルプロファイル、上記以外のパスにはアクセスしません。

このマシンから送信されるデータ:

  • api.meshy.ai への API リクエストには、Authorization ヘッダー内の MESHY_API_KEY と、ユーザーが指定したテキストプロンプトまたは画像 URL が含まれます。それ以外のローカルデータは送信されません。ダウンロードしたモデルファイルはローカルにのみ保存されます。

重要:初回利用時のセッション通知

このスキルがセッションで初めて有効化されたときは、ユーザーに次のように伝えてください。

生成されたファイルはすべて、現在の作業ディレクトリにある meshy_output/ に保存されます。各プロジェクトには、モデルファイル、テクスチャ、サムネイル、メタデータを含む専用フォルダー({YYYYMMDD_HHmmss}_{prompt}_{id}/)が作成されます。履歴は meshy_output/history.json に記録されます。

これはセッションごとに1回伝えれば十分です。


重要:ファイルの整理

ダウンロードしたファイルはすべて、現在の作業ディレクトリにある構造化された meshy_output/ ディレクトリに保存しなければなりません。ファイルを無造作に別々の場所へ保存しないでください。

  • 各プロジェクト:meshy_output/{YYYYMMDD_HHmmss}_{prompt_slug}_{task_id_prefix}/
  • 連続するタスク(プレビュー → リファイン → リグ)では、同じ project_dir を再利用します。
  • 各プロジェクトの metadata.json と全体の history.json にタスクを記録します。
  • モデルと一緒にサムネイルを自動ダウンロードします。

重要:シェルコマンドのルール

標準的な POSIX ツールのみを使用してください。rg、fd、bat、exa/eza は使用しないでください。


重要:時間のかかるタスクを適切に実行する

Meshy の生成には1~5分かかります。作成 → ポーリング → ダウンロードの一連の処理全体を1つの Python スクリプトに記述し、1回の Bash 呼び出しで実行してください。バッファーなしで出力するには python3 -u script.py を使用します。タスクが99%のまま30~120秒かかるのは、最終処理中によくあることです。中断しないでください。


ステップ0:API キーの検出(必ず最初に実行)

現在のセッション環境と、現在の作業ディレクトリにある .env ファイルのみを確認してください。ホームディレクトリやシェルプロファイルファイルを検索しないでください。

echo "=== Meshy API Key Detection ==="

# 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

# 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

# 3. Final status
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
python3 -c "import requests; print('PYTHON_REQUESTS: OK')" 2>/dev/null || echo "PYTHON_REQUESTS: MISSING (run: pip install requests)"

echo "=== Detection Complete ==="
検出後の判断
  • キーが見つかった場合 → ステップ1に進みます。
  • キーが見つからない場合 → ステップ0aに進みます。
  • Python の requests が見つからない場合 → pip install requests を実行します。

ステップ0a:API キーの設定(キーが見つからない場合のみ)

ユーザーに次のように伝えてください。

Meshy API を使用するには、API キーが必要です。

  1. https://www.meshy.ai/settings/api にアクセスします。
  2. **「Create API Key」**をクリックし、名前を付けて、キー(msy_ で始まります)をコピーします。
  3. キーが表示されるのは一度だけです。安全な場所に保存してください。

注: API にアクセスするには、Pro プラン以上が必要です。無料プランのアカウントでは API キーを作成できません。

ユーザーからキーを受け取ったら、現在のセッションで設定し、必要に応じて .env に保存します。

# Set for current session only
export MESHY_API_KEY="msy_PASTE_KEY_HERE"

# 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

キーを保存する場合(現在のプロジェクトのみ):

# Write to .env in current working directory
echo 'MESHY_API_KEY=msy_PASTE_KEY_HERE' >> .env
echo "Saved to .env"

# 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"

セキュリティに関する注意: キーは現在のプロジェクトディレクトリにある .env にのみ保存されます。このファイルをバージョン管理にコミットしないでください。.env は自動的に .gitignore に追加されています。


Step 1: クレジットを消費する前にユーザーと計画を確認する

重要:タスクを作成する前に、ユーザーに費用の概要を提示し、確認を待ってください。

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?

複数ステップのパイプライン(text-to-3d → rig → animate)では、パイプライン全体の費用を事前に提示してください。

注:リギングには、追加費用なしで歩行と走行のアニメーションが自動的に含まれます。これら以外のカスタムアニメーションを追加する場合にのみ、Animate(3 クレジット)を追加してください。

インテント → API の対応表
ユーザーの目的APIエンドポイントクレジット
テキストから 3D モデルを作成するText to 3DPOST /openapi/v2/text-to-3d5~20(プレビュー)+ 10(仕上げ)
1 枚の画像から 3D モデルを作成するImage to 3DPOST /openapi/v1/image-to-3d5~30
複数の画像から 3D モデルを作成するMulti-Image to 3DPOST /openapi/v1/multi-image-to-3d5~30
既存モデルに新しいテクスチャを適用するRetexturePOST /openapi/v1/retexture10
メッシュ形式/トポロジーを変更するRemeshPOST /openapi/v1/remesh5
モデルを別の形式に変換する(リメッシュなし)ConvertPOST /openapi/v1/convert1
モデルを実寸に合わせて拡大縮小するResizePOST /openapi/v1/resize1
外部でテクスチャを適用する前に新しい UV を生成する(GLB、面数 40,000 以下)UV UnwrapPOST /openapi/v1/uv-unwrap5
キャラクターにスケルトンを追加するAuto-RiggingPOST /openapi/v1/rigging5
リギング済みキャラクターをアニメーション化するAnimationPOST /openapi/v1/animations3
テキストから 2D 画像を作成する(image-to-3d の前処理として推奨)Text to ImagePOST /openapi/v1/text-to-image3 / 6 / 9 / 9
2D 画像を最適化/編集する(image-to-3d の前処理として推奨)Image to ImagePOST /openapi/v1/image-to-image3 / 6 / 9 / 12
写真をスタイル付きの実物製品にする(フィギュア/ランプ/キーホルダー/冷蔵庫用マグネット)Creative LabPOST /openapi/creative-lab/{product}/v1/prototype then .../build6 + 30
FDM 方式での印刷適性を確認するAnalyze PrintabilityPOST /openapi/v1/print/analyze0(無料)
非多様体/縮退面/穴のあるトポロジーを修復するRepair PrintabilityPOST /openapi/v1/print/repair10
マルチカラー 3D プリントMulti-Color PrintPOST /openapi/v1/print/multi-color10(+生成)
モデルを 3D プリントする(白)→「Print Pipeline」セクションを参照—20
クレジット残高を確認するBalanceGET /openapi/v1/balance0

Step 2: ワークフローを実行する

再利用可能なスクリプトテンプレート

すべてのワークフローのベースとして使用してください。API キーは、環境変数または現在のディレクトリにある .env から安全に読み込みます。

#!/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 ---
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
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 ---
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(プレビュー+仕上げ)

上記のテンプレートに追加してください。

PROMPT = "USER_PROMPT"

# 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
    # "hd_texture": True,      # 4K base color on refine (meshy-6/latest only)
    # "target_formats": ["glb", "3mf"],  # 3mf must be explicitly requested
    # NOTE: symmetry_mode / art_style / is_a_t_pose are deprecated (symmetry_mode & art_style ignored; use pose_mode)
})
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_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(= Meshy 6)が使用できます。一貫性を保つため、プレビューと同じモデルファミリーを選んでください。仕上げの費用はモデルにかかわらず 10 クレジットです。(meshy-4 は廃止されており、400 を返します。)

(任意ですが、強く推奨)2D 最適化の事前ステップ

テキストから直接 3D にするより、画像から 3D にする方法を優先してください。品質が高く、制御しやすいため、テキストのみのリクエストでは、まずデザイン画像を作成してから 3D 化してください。

画像の品質が 3D モデルの品質を直接左右します。/openapi/v1/image-to-3d または /openapi/v1/multi-image-to-3d を呼び出す前に、ユーザーの入力を評価し、2D の処理を事前に提案してください。

ユーザーの入力推奨される事前ステップ
テキストによる説明のみで、参照画像がないnano-banana-pro を使用して /openapi/v1/text-to-image を実行します。キャラクターの場合は、リグに適した出力にするため、generate_multi_view: True と pose_mode: "a-pose" または "t-pose" を追加します。
参照画像の解像度が低い/背景が雑然としている/被写体が不明瞭/照明が悪いnano-banana-pro を使用して /openapi/v1/image-to-image でクリーンアップします。
スタイル/色/細部を調整したい/openapi/v1/image-to-image でスタイルを変換してから、3D 化します。

通常、追加の 3~9 クレジットで目に見える品質向上が得られます。すでにスタジオ撮影風のきれいな画像が提供されている場合は省略してください。また、Creative Lab 製品(フィギュア/ランプ/キーホルダー/冷蔵庫用マグネット)も省略してください。独自の組み込みスタイル処理が適用されるため、加工せずに写真(ランプの場合はテキスト)をそのまま Creative Lab に入力してください。最初に text-to-image/image-to-image を通さないでください。

画像から 3D へ
import base64

# For local files: convert to data URI
# with open("photo.jpg", "rb") as f:
#     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"])

複数画像から 3D へ
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"))

再テクスチャ

重要:最初にユーザーにテクスチャのスタイルを尋ねてください。text_style_prompt または 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/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}"))

メッシュユーティリティ(変換/サイズ変更/UV 展開)

完成したモデルに対する軽量な後処理です(input_task_id または model_url を使用します)。

# Convert to other formats without remeshing (1 credit). Cheapest way to get 3MF/STL.
conv_id = create_task("/openapi/v1/convert", {
    "input_task_id": "TASK_ID",         # or "model_url": "URL"
    "target_formats": ["stl", "3mf"],   # required: glb/fbx/obj/usdz/blend/stl/3mf
})
poll_task("/openapi/v1/convert", conv_id)

# Resize to a real-world size (1 credit). Give EXACTLY ONE resize mode.
resize_id = create_task("/openapi/v1/resize", {
    "input_task_id": "TASK_ID",         # or "model_url": "URL"
    "resize_height": 0.15,              # meters — OR "resize_longest_side": 0.2  OR "auto_size": True
    # "origin_at": "bottom",            # "bottom" | "center"
})
poll_task("/openapi/v1/resize", resize_id)

# UV Unwrap a GLB (5 credits). GLB only, ≤ 40,000 faces (else 400 → remesh down first).
# Output: a GLB "UV white model" (fresh UVs + placeholder grey material) for external texturing.
uv_id = create_task("/openapi/v1/uv-unwrap", {
    "input_task_id": "TASK_ID",         # or "model_url": "GLB_URL"
})
poll_task("/openapi/v1/uv-unwrap", uv_id)

Creative Lab の一般消費者向け製品

写真を、スタイル加工された印刷可能な実物製品に変換します。製品:figure、lamp、keychain、fridge-magnet。次の 2 段階で処理します({product} をいずれかの製品に置き換えてください)。

  1. プロトタイプ(6 クレジット):写真 → スタイル加工されたコンセプト画像。
  2. 作成(30 クレジット):SUCCEEDED 状態のプロトタイプに対して image-to-3d を実行 → テクスチャ付き GLB/OBJ+MTL。
PRODUCT = "figure"  # figure | lamp | keychain | fridge-magnet

# Stage 1 — prototype (6 credits)
proto_id = create_task(f"/openapi/creative-lab/{PRODUCT}/v1/prototype", {
    "image_url": "PHOTO_URL_OR_DATA_URI",   # jpg/jpeg/png/webp
    # "name": "My figure",                  # optional, ≤ 100 chars
})
poll_task(f"/openapi/creative-lab/{PRODUCT}/v1/prototype", proto_id)

# Stage 2 — build (30 credits). Must reference a SUCCEEDED prototype of the SAME product.
# Web-app prototypes are rejected with 404 — only API-created prototypes are accepted.
build_id = create_task(f"/openapi/creative-lab/{PRODUCT}/v1/build", {
    "input_task_id": proto_id,
})
build = poll_task(f"/openapi/creative-lab/{PRODUCT}/v1/build", build_id)
project_dir = get_project_dir(build_id, task_type=f"creative-lab-{PRODUCT}")
download(build["model_urls"]["glb"], os.path.join(project_dir, "creative-lab.glb"))

# To MULTICOLOR a Creative Lab result: a Creative Lab model can only be sent to multi-color
# as model_url — pass the build's GLB URL (or a data URI of the downloaded GLB):
#   create_task("/openapi/v1/print/multi-color", {"model_url": build["model_urls"]["glb"], "max_colors": 4})

自動リギング+アニメーション

ユーザーがリギングまたはアニメーションを依頼した場合、生成ステップでは必ず pose_mode: "t-pose" を使用してください。

# 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.")

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

# 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_task = poll_task("/openapi/v1/animations", anim_id)
# download(anim_task["result"]["animation_glb_url"], os.path.join(project_dir, "animated.glb"))

テキストから画像へ/画像から画像へ
# 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)
# Result 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)

3D プリントのワークフロー

重要:ユーザーのリクエストに 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。

選択:白色モデルかマルチカラーか
  1. 最初にインストール済みのスライサーを検出します(下記のスクリプトを参照)。
  2. ユーザーに「白色モデル(単色)とマルチカラーのどちらにしますか?」と尋ねます。
  3. マルチカラーの場合:マルチカラー対応スライサー(OrcaSlicer、Bambu Studio、Creality Print、Elegoo Slicer、Anycubic Slicer Next)の有無を確認し、max_colors(1~16、デフォルト 4)と max_depth(3~6、デフォルト 4)を尋ねて、料金を確認します:40 クレジット(修復が必要な場合は追加で 10 クレジット)。
  4. (推奨)生成後にプリント適性分析(POST /openapi/v1/print/analyze、無料)を実行します。ステータスが error の場合に限り、POST /openapi/v1/print/repair(10 クレジット)を実行します。
プリント適性の分析と修復
# After the textured/final mesh is ready:
INPUT_TASK_ID = refine_id  # or whatever produced the print-ready mesh

# input_task_id MUST refer to a Meshy 6 / Preview task. For Meshy 4/5 outputs,
# pass `model_url` (the GLB download URL) instead.
analyze_id = create_task("/openapi/v1/print/analyze", {
    "input_task_id": INPUT_TASK_ID,
})
analyze_task = poll_task("/openapi/v1/print/analyze", analyze_id)

p = analyze_task.get("printability") or {}
metrics = p.get("metrics", {})
print(f"Printability: {p.get('status')}: {metrics}")

if p.get("status") == "error":
    repair_id = create_task("/openapi/v1/print/repair", {
        "input_task_id": INPUT_TASK_ID,   # output is GLB
    })
    repair_task = poll_task("/openapi/v1/print/repair", repair_id)
    repaired_url = next((u for u in repair_task["model_urls"].values() if u), None)
    # Use repaired_url for the next step (download / multicolor / slicer)

ステータスの意味:healthy(そのままプリント可能)|warning(縮退面/穴あり — 修復は任意)|error(非ウォータータイト/非多様体 — 修復を推奨)|unknown(分析失敗)。修復で保持されるのはジオメトリのみで、テクスチャは保持されません。マルチカラー用に必要な場合は、再テクスチャしてください。

スライサーの検出と起動
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}")

# Several SEPARATE/unrelated models (e.g. results from different tasks)? Call this once per
# model with a short gap — Bambu Studio especially may respond to only one if fired
# back-to-back. (Parts of ONE model belong in a single project — not spaced.)
#     import time
#     for f in [model_a, model_b, model_c]:
#         open_in_slicer(f, slicer_name); time.sleep(2)

slicers = detect_slicers()
for s in slicers:
    mc = " [multicolor]" if s["multicolor"] else ""
    print(f"  - {s['name']}{mc}: {s['path']}")
ホワイトモデルパイプライン
ステップアクションクレジット
1テクスチャなしモデルを生成20
2OBJ をダウンロード0
3OBJ を修正(fix_obj_for_printing)0
4スライサーで開く0

target_formats に "obj" を含めて生成し、印刷用に修正します。

# --- Generate for white model printing ---
# 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
})
# OR Image to 3D:
# task_id = create_task("/openapi/v1/image-to-3d", {
#     "image_url": "URL", "should_texture": False,
#     "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)/2*s, -(min_y+max_y)/2*s, -(min_z*s)
    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 {rx*s+xo:.6f} {ry*s+yo:.6f} {rz*s+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"])
マルチカラーパイプライン
ステップアクションクレジット
1生成+テクスチャ適用30
2マルチカラー処理10
33MF をダウンロード0
4マルチカラー対応スライサーで開く0
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 ---
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 ---
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"])
印刷可能性のチェックリスト
チェック項目推奨事項
壁の厚さFDM は最小 1.2mm、レジンは 0.8mm
オーバーハング45°未満にするか、サポートを追加
マニホールドメッシュ水密で、穴がないこと
最小ディテールFDM は 0.4mm、レジンは 0.05mm
ベースの安定性平らなベースにするか、スライサーでブリム/ラフトを追加
浮遊パーツすべてのパーツを接続するか、個別に印刷

ステップ 3:結果を報告する

タスクが成功したら、以下を報告します。

  1. ダウンロードしたファイルのパスとサイズ
  2. タスク ID(後続の操作:リファイン、リギング、テクスチャの再適用用)
  3. 利用可能な形式(model_urls のキーを一覧表示)
  4. 消費したクレジット+現在の残高
  5. 推奨する次のステップ:
    • プレビュー完了 → 「リファイン(テクスチャを追加)しますか?」
    • モデル完成 → 「このキャラクターにリギングしますか?」
    • リギング済み → 「カスタムアニメーションを適用しますか?」
    • テクスチャ付きモデル → 「これを 3D プリントしますか?マルチカラー印刷も利用できます!」
    • モデル → 「これを 3D プリントしますか?」

エラーからの復旧

HTTP ステータス意味対応
401API キーが無効ステップ 0 を再実行し、キーを確認するようユーザーに依頼
402クレジット不足残高を表示し、https://www.meshy.ai/pricing へのリンクを提示
422処理できない理由を説明(例:リギング対象が非ヒューマノイド)
429レート制限5 秒後に自動再試行(最大 3 回)
5xxサーバーエラー10 秒後に自動再試行(1 回)

タスクの FAILED メッセージ:

  • "The server is busy..." → 待機時間を延ばしながら再試行(5 秒、10 秒、20 秒)
  • "Internal server error." → プロンプトを簡略化し、1 回再試行

既知の動作と制約

  • 99% で停滞:通常の最終処理です(30~120 秒)。中断しないでください。
  • アセットの保持期間:ファイルは 3 日後に削除されます(Enterprise 以外)。すぐにダウンロードしてください。
  • PBR マップ:enable_pbr: true を明示的に設定する必要があります。
  • リファイン:meshy-5、meshy-6、または latest で動作します。一貫性を保つため、プレビューと同じ系列を選んでください。モデルにかかわらず 10 クレジットです。(meshy-4 は廃止済みのため、400 が返されます。)
  • 非推奨のパラメーター:symmetry_mode は出力に影響しなくなりました。art_style は Meshy-6 では無視されます。古い is_a_t_pose フラグの代わりに pose_mode を使用してください。
  • consumed_credits:すべてのタスク GET 応答に consumed_credits が含まれます。推定ではなく、実際の消費クレジットを報告するために読み取ってください。
  • リギング:ヒューマノイドの二足歩行モデルのみ対応し、ポリゴン数は 300,000 以下です。
  • 印刷形式:ホワイトモデルには OBJ と fix_obj_for_printing() を使用します。マルチカラーには Multi-Color Print API の 3MF を使用します。必ず先にスライサーを確認してください。
  • ダウンロード形式:ダウンロードする前に、必要な形式をユーザーに確認してください。GLB(表示用)、OBJ(印刷用)、3MF(マルチカラー用)、FBX(ゲーム用)、USDZ(AR 用)があります。すべての形式をダウンロードしないでください。
  • マルチカラー用の 3MF:Multi-Color Print API は 3MF を直接出力するため、generate/refine に 3MF を要求する必要はありません。印刷以外の用途で 3MF が必要な場合は、target_formats に "3mf" を渡してください。
  • タイムスタンプ:API のタイムスタンプはすべて Unix エポックの ミリ秒です。

実行チェックリスト

  • API キー検出を実行した(ステップ 0)— 環境変数と .env のみを確認
  • API キーを検証した(全文を表示していない)
  • コストの概要を提示し、ユーザーの確認を得た
  • ワークフロー全体を単一の Python スクリプトとして記述した
  • バッファーなしの出力で実行した(python3 -u)
  • ファイルのパス、形式、タスク ID、残高を報告した
  • 次のステップを提案した

追加リソース

すべてのパラメーター、応答スキーマ、エラーコードを含む API エンドポイントの完全なリファレンスについては、reference.md をお読みください。

コメント