Communitygithub.com

meshy-dev/meshy-3d-agent

Meshy AI 3D生成プラットフォームのAIエージェントスキル

meshy-3d-agent とは?

Meshy 3Dエージェントスキルは、Claude Code、Cursor、Codexなどの開発者向けAIエージェントが、Meshy AI APIを介して3Dアセットの生成と印刷を直接操作できるようにするためのツールです。APIキーの安全な設定、テキスト・画像からの3Dモデル生成、指数バックオフポーリングによるタスクステータス監視、生成されたモデルの自動ダウンロード、およびPBRテクスチャリングやリギング、アニメーションを含む多段階パイプラインの実行をカバーします。さらに、3Dプリントの準備として、印刷可能性の分析、メッシュ修復、マルチカラー印刷、スライサー統合のための出力生成もサポートしています。このスキルは、環境変数とカレントディレクトリの`.env`ファイルのみからAPIキーを読み取り、生成されたすべてのファイルを構造化された`meshy_output/`ディレクトリに整理するため、セキュリティとプロジェクト管理のベストプラクティスに従っています。ユーザーに対しては、クレジット消費前にコスト見積もりを提示し確認を求めることで、無駄なAPI利用を防ぎます。

対応~Claude Code~Codex CLI~Cursor
npx skills add meshy-dev/meshy-3d-agent

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

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リクエストには、MESHY_API_KEY がAuthorizationヘッダーに含まれ、ユーザーが提供したテキストプロンプトまたは画像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分かかります。create → poll → downloadのフロー全体を1つのPythonスクリプトとして記述し、単一のBash呼び出しで実行してください。バッファリングされていない出力のために python3 -u script.py を使用します。タスクが30〜120秒間99%で停止するのは通常の最終処理です — 中断しないでください。


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

現在のセッションの環境変数と、カレントワーキングディレクトリの .env ファイルのみを確認してください。ホームディレクトリやシェルプロファイルファイルをスキャンしないでください。

echo "=== Meshy APIキー検出 ==="

# 1. 現在の環境変数を確認
if [ -n "$MESHY_API_KEY" ]; then
  echo "ENV_VAR: 見つかりました (${MESHY_API_KEY:0:8}...)"
else
  echo "ENV_VAR: 見つかりません"
fi

# 2. カレントワーキングディレクトリの .env のみを確認
if [ -f ".env" ] && grep -q "MESHY_API_KEY" ".env" 2>/dev/null; then
  echo "DOTENV(.env): 見つかりました"
  export MESHY_API_KEY=$(grep "^MESHY_API_KEY=" ".env" | head -1 | cut -d'=' -f2- | tr -d '"\''" )
fi

# 3. 最終状態
if [ -n "$MESHY_API_KEY" ]; then
  echo "準備完了: キー=${MESHY_API_KEY:0:8}..."
else
  echo "準備完了: キーが見つかりません"
fi

# 4. Python requestsの確認
python3 -c "import requests; print('PYTHON_REQUESTS: OK')" 2>/dev/null || echo "PYTHON_REQUESTS: 見つかりません (実行: pip install requests)"

echo "=== 検出完了 ==="

検出後の判断

  • キーが見つかった場合 → ステップ1に進みます。
  • キーが見つからない場合 → ステップ0aに進みます。
  • Python requestsがない場合 → pip install requests を実行します。

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

ユーザーに伝えます:

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

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

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

ユーザーがキーを提供したら、現在のセッションに設定し、オプションで .env に永続化します:

# 現在のセッションのみに設定
エクスポート MESHY_API_KEY="msy_PASTE_KEY_HERE"

# キーを検証
ステータス=$(curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $MESHY_API_KEY" \
  https://api.meshy.ai/openapi/v1/balance)

if [ "$ステータス" = "200" ]; then
  残高=$(curl -s -H "Authorization: Bearer $MESHY_API_KEY" https://api.meshy.ai/openapi/v1/balance)
  echo "キーは有効です。$残高"
else
  echo "キーが無効です (HTTP $ステータス)。キーを確認してもう一度試してください。"
fi

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

# カレントワーキングディレクトリの .env に書き込む
echo 'MESHY_API_KEY=msy_PASTE_KEY_HERE' >> .env
echo ".env に保存しました"

# 重要: キーの漏洩を防ぐために .env を .gitignore に追加
grep -q "^\.env" .gitignore 2>/dev/null || echo ".env" >> .gitignore
echo ".env を .gitignore に追加しました"

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


ステップ1: クレジットを使用する前にユーザーと計画を確認する

重要: タスクを作成する前に、ユーザーにコストの概要を提示し、確認を待ちます:

「<プロンプト>」の3Dモデルを次の計画で生成します:

  1. プレビュー(メッシュ生成) — 20クレジット
  2. リファイン(PBRテクスチャリング) — 10クレジット
  3. .glbとしてダウンロード

  合計コスト: 30クレジット
  現在の残高: <N>クレジット

  続行しますか?

マルチステップパイプライン(テキスト→3D → リグ → アニメーション)の場合は、完全なパイプラインコストを事前に表示します。

注: リギングには、追加費用なしで歩行とランニングのアニメーションが自動的に含まれます。カスタムアニメーションには、それらに加えて 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、≤40k面)UV UnwrapPOST /openapi/v1/uv-unwrap5
キャラクターにスケルトンを追加Auto-RiggingPOST /openapi/v1/rigging5
リギングされたキャラクターをアニメーション化AnimationPOST /openapi/v1/animations3
テキストから2D画像(画像→3Dの前段階として推奨)Text to ImagePOST /openapi/v1/text-to-image3 / 6 / 9 / 9
2D画像の最適化/編集(画像→3Dの前段階として推奨)Image to ImagePOST /openapi/v1/image-to-image3 / 6 / 9 / 12
写真 → スタイル化された物理製品(フィギュア/ランプ/キーホルダー/冷蔵庫マグネット)Creative LabPOST /openapi/creative-lab/{product}/v1/prototype その後 .../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印刷(白色)→ 印刷パイプラインセクションを参照—20
クレジット残高の確認BalanceGET /openapi/v1/balance0

ステップ2: ワークフローを実行する

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

これをすべてのワークフローのベースとして使用します。環境変数またはカレントディレクトリの .env から安全にAPIキーを読み込みます:

#!/usr/bin/env python3
"""Meshy APIタスクランナー。create → poll → download を処理します。"""
import requests, time, os, sys, re, json
from datetime import datetime

# --- 安全なAPIキーの読み込み ---
def load_api_key():
    """環境変数、次にカレントディレクトリの .env から MESHY_API_KEY を読み込みます。"""
    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("エラー: MESHY_API_KEY が設定されていません。ステップ0aを実行して設定してください。")

# 完全なキーを決してログに記録しない — トレーサビリティのために最初の8文字のみ
print(f"APIキーが読み込まれました: {API_KEY[:8]}...")

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("エラー: APIキーが無効です (401)。ステップ0aを再実行してください。")
    if resp.status_code == 402:
        try:
            bal = SESSION.get(f"{BASE}/openapi/v1/balance", headers=HEADERS, timeout=10)
            balance = bal.json().get("balance", "不明")
            sys.exit(f"エラー: クレジットが不足しています (402)。残高: {balance}。https://www.meshy.ai/pricing でチャージしてください")
        except Exception:
            sys.exit("エラー: クレジットが不足しています (402)。https://www.meshy.ai/pricing で残高を確認してください")
    if resp.status_code == 429:
        sys.exit("エラー: レート制限に達しました (429)。待機してから再試行してください。")
    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):
    """指数バックオフでポーリングします(5秒→30秒、95%以上で固定15秒)。"""
    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_count})", flush=True)
        if status == "SUCCEEDED":
            return task
        if status in ("FAILED", "CANCELED"):
            msg = task.get("task_error", {}).get("message", "不明")
            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}s, {poll_count} ポーリング)")

def download(url, filepath):
    """プロジェクトディレクトリ(cwd/meshy_output/ 内)にファイルをダウンロードします。"""
    os.makedirs(os.path.dirname(filepath), exist_ok=True)
    print(f"{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"ダウンロード完了: {filepath} ({os.path.getsize(filepath)/1024/1024:.1f} MB)")

# --- ファイル整理ヘルパー ---
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

関連スキル