Directly communicate with the Meshy AI API to generate 3D assets. This skill handles the complete lifecycle: environment setup, API key detection, task creation, polling, downloading, and chaining multi-step pipelines.
For full endpoint reference (all parameters, response schemas, error codes), read reference.md.
直接与Meshy AI API交互以生成3D资产。本技能支持完整的生命周期管理:环境配置、API密钥检测、任务创建、状态轮询、资源下载,以及多步骤工作流的串联执行。
如需完整的端点参考(包含所有参数、响应结构、错误码),请查看reference.md。
import re, json
from datetime import datetime
OUTPUT_ROOT = os.path.join(os.getcwd(), "meshy_output")
os.makedirs(OUTPUT_ROOT, exist_ok=True)
HISTORY_FILE = os.path.join(OUTPUT_ROOT, "history.json")
def get_project_dir(task_id, prompt="", task_type="model"):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
slug = re.sub(r'[^a-z0-9]+', '-', (prompt or task_type).lower())[:30].strip('-')
folder = f"{timestamp}{slug}{task_id[:8]}"
project_dir = os.path.join(OUTPUT_ROOT, folder)
os.makedirs(project_dir, exist_ok=True)
return project_dir
def record_task(project_dir, task_id, task_type, stage, prompt="", files=None):
meta_path = os.path.join(project_dir, "metadata.json")
if os.path.exists(meta_path):
meta = json.load(open(meta_path))
else:
meta = {"project_name": prompt or task_type, "folder": os.path.basename(project_dir),
"root_task_id": task_id, "created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(), "tasks": []}
meta["tasks"].append({"task_id": task_id, "task_type": task_type, "stage": stage,
"files": files or [], "created_at": datetime.now().isoformat()})
meta["updated_at"] = datetime.now().isoformat()
json.dump(meta, open(meta_path, "w"), indent=2)
# Update global history
if os.path.exists(HISTORY_FILE):
history = json.load(open(HISTORY_FILE))
else:
history = {"version": 1, "projects": []}
folder = os.path.basename(project_dir)
entry = next((p for p in history["projects"] if p["folder"] == folder), None)
if entry:
entry["task_count"] = len(meta["tasks"])
entry["updated_at"] = meta["updated_at"]
else:
history["projects"].append({"folder": folder, "prompt": prompt, "task_type": task_type,
"root_task_id": task_id, "created_at": meta["created_at"],
"updated_at": meta["updated_at"], "task_count": len(meta["tasks"])})
json.dump(history, open(HISTORY_FILE, "w"), indent=2)
def save_thumbnail(project_dir, url):
path = os.path.join(project_dir, "thumbnail.png")
if os.path.exists(path): return
try:
r = SESSION.get(url, timeout=15); r.raise_for_status()
open(path, "wb").write(r.content)
except Exception: pass
import re, json
from datetime import datetime
OUTPUT_ROOT = os.path.join(os.getcwd(), "meshy_output")
os.makedirs(OUTPUT_ROOT, exist_ok=True)
HISTORY_FILE = os.path.join(OUTPUT_ROOT, "history.json")
def get_project_dir(task_id, prompt="", task_type="model"):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
slug = re.sub(r'[^a-z0-9]+', '-', (prompt or task_type).lower())[:30].strip('-')
folder = f"{timestamp}{slug}{task_id[:8]}"
project_dir = os.path.join(OUTPUT_ROOT, folder)
os.makedirs(project_dir, exist_ok=True)
return project_dir
def record_task(project_dir, task_id, task_type, stage, prompt="", files=None):
meta_path = os.path.join(project_dir, "metadata.json")
if os.path.exists(meta_path):
meta = json.load(open(meta_path))
else:
meta = {"project_name": prompt or task_type, "folder": os.path.basename(project_dir),
"root_task_id": task_id, "created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(), "tasks": []}
meta["tasks"].append({"task_id": task_id, "task_type": task_type, "stage": stage,
"files": files or [], "created_at": datetime.now().isoformat()})
meta["updated_at"] = datetime.now().isoformat()
json.dump(meta, open(meta_path, "w"), indent=2)
# 更新全局历史记录
if os.path.exists(HISTORY_FILE):
history = json.load(open(HISTORY_FILE))
else:
history = {"version": 1, "projects": []}
folder = os.path.basename(project_dir)
entry = next((p for p in history["projects"] if p["folder"] == folder), None)
if entry:
entry["task_count"] = len(meta["tasks"])
entry["updated_at"] = meta["updated_at"]
else:
history["projects"].append({"folder": folder, "prompt": prompt, "task_type": task_type,
"root_task_id": task_id, "created_at": meta["created_at"],
"updated_at": meta["updated_at"], "task_count": len(meta["tasks"])})
json.dump(history, open(HISTORY_FILE, "w"), indent=2)
def save_thumbnail(project_dir, url):
path = os.path.join(project_dir, "thumbnail.png")
if os.path.exists(path): return
try:
r = SESSION.get(url, timeout=15); r.raise_for_status()
open(path, "wb").write(r.content)
except Exception: pass
preview_id = create_task("/openapi/v2/text-to-3d", {
"mode": "preview",
"prompt": PROMPT,
"ai_model": "latest",
# "model_type": "standard", # "standard" | "lowpoly"
# "topology": "triangle", # "triangle" | "quad"
# "target_polycount": 30000, # 100–300000
# "should_remesh": False,
# "symmetry_mode": "auto", # "auto" | "on" | "off"
# "pose_mode": "t-pose", # "" | "a-pose" | "t-pose" (use "t-pose" if rigging/animating later)
})
task = poll_task("/openapi/v2/text-to-3d", preview_id)
project_dir = get_project_dir(preview_id, prompt=PROMPT)
download(task["model_urls"]["glb"], os.path.join(project_dir, "preview.glb"))
record_task(project_dir, preview_id, "text-to-3d", "preview", prompt=PROMPT, files=["preview.glb"])
if task.get("thumbnail_url"):
save_thumbnail(project_dir, task["thumbnail_url"])
print(f"\nPREVIEW COMPLETE")
print(f" Task ID: {preview_id}")
print(f" Project: {project_dir}")
print(f" Formats: {', '.join(task['model_urls'].keys())}")
preview_id = create_task("/openapi/v2/text-to-3d", {
"mode": "preview",
"prompt": PROMPT,
"ai_model": "latest",
# "model_type": "standard", # "standard" | "lowpoly"
# "topology": "triangle", # "triangle" | "quad"
# "target_polycount": 30000, # 100–300000
# "should_remesh": False,
# "symmetry_mode": "auto", # "auto" | "on" | "off"
# "pose_mode": "t-pose", # "" | "a-pose" | "t-pose"(后续需要绑定骨骼/动画时请使用"t-pose")
})
task = poll_task("/openapi/v2/text-to-3d", preview_id)
project_dir = get_project_dir(preview_id, prompt=PROMPT)
download(task["model_urls"]["glb"], os.path.join(project_dir, "preview.glb"))
record_task(project_dir, preview_id, "text-to-3d", "preview", prompt=PROMPT, files=["preview.glb"])
if task.get("thumbnail_url"):
save_thumbnail(project_dir, task["thumbnail_url"])
print(f"\nPREVIEW COMPLETE")
print(f" Task ID: {preview_id}")
print(f" Project: {project_dir}")
print(f" Formats: {', '.join(task['model_urls'].keys())}")
refine_id = create_task("/openapi/v2/text-to-3d", {
"mode": "refine",
"preview_task_id": preview_id,
"enable_pbr": True,
"ai_model": "latest",
# "texture_prompt": "",
# "remove_lighting": True, # Remove baked lighting (meshy-6/latest only, default True)
})
task = poll_task("/openapi/v2/text-to-3d", refine_id)
download(task["model_urls"]["glb"], os.path.join(project_dir, "refined.glb"))
record_task(project_dir, refine_id, "text-to-3d", "refined", prompt=PROMPT, files=["refined.glb"])
print(f"\nREFINE COMPLETE")
print(f" Task ID: {refine_id}")
print(f" Project: {project_dir}")
print(f" Formats: {', '.join(task['model_urls'].keys())}")
> **Refine compatibility**: Only previews generated with `meshy-5` or `latest` can be refined. `meshy-6` previews do NOT support refine (API returns 400). If the user wants to refine later, always use `meshy-5` or `latest` for the preview step.
refine_id = create_task("/openapi/v2/text-to-3d", {
"mode": "refine",
"preview_task_id": preview_id,
"enable_pbr": True,
"ai_model": "latest",
# "texture_prompt": "",
# "remove_lighting": True, # 移除烘焙光照(仅meshy-6/latest支持,默认开启)
})
task = poll_task("/openapi/v2/text-to-3d", refine_id)
download(task["model_urls"]["glb"], os.path.join(project_dir, "refined.glb"))
record_task(project_dir, refine_id, "text-to-3d", "refined", prompt=PROMPT, files=["refined.glb"])
print(f"\nREFINE COMPLETE")
print(f" Task ID: {refine_id}")
print(f" Project: {project_dir}")
print(f" Formats: {', '.join(task['model_urls'].keys())}")
> **精修兼容性**:仅使用`meshy-5`或`latest`生成的预览模型支持精修。`meshy-6`生成的预览模型不支持精修(API会返回400错误)。如果用户后续需要精修,预览步骤请始终使用`meshy-5`或`latest`。
task_id = create_task("/openapi/v1/image-to-3d", {
"image_url": "IMAGE_URL_OR_DATA_URI",
"should_texture": True,
"enable_pbr": True, # Default is False; set True for metallic/roughness/normal maps
"ai_model": "latest",
# "image_enhancement": True, # Optimize input image (meshy-6/latest only, default True)
# "remove_lighting": True, # Remove baked lighting from texture (meshy-6/latest only, default True)
})
task = poll_task("/openapi/v1/image-to-3d", task_id)
download(task["model_urls"]["glb"], "model.glb")
task_id = create_task("/openapi/v1/image-to-3d", {
"image_url": "IMAGE_URL_OR_DATA_URI",
"should_texture": True,
"enable_pbr": True, # 默认关闭;开启后会生成金属度/粗糙度/法线贴图
"ai_model": "latest",
# "image_enhancement": True, # 优化输入图像(仅meshy-6/latest支持,默认开启)
# "remove_lighting": True, # 从纹理中移除烘焙光照(仅meshy-6/latest支持,默认开启)
})
task = poll_task("/openapi/v1/image-to-3d", task_id)
download(task["model_urls"]["glb"], "model.glb")
source_endpoint = "/openapi/v2/text-to-3d" # adjust to match the source task's endpoint
source_task_id = "TASK_ID"
check_resp = SESSION.get(f"{BASE}{source_endpoint}/{source_task_id}", headers=HEADERS, timeout=30)
check_resp.raise_for_status()
source = check_resp.json()
face_count = source.get("face_count", 0)
if face_count > 300000:
print(f"ERROR: Model has {face_count:,} faces (limit: 300,000). Remesh first:")
print(f" create_task('/openapi/v1/remesh', {{'input_task_id': '{source_task_id}', 'target_polycount': 100000}})")
sys.exit("Rigging blocked: face count too high")
source_endpoint = "/openapi/v2/text-to-3d" # 根据源任务的端点调整
source_task_id = "TASK_ID"
check_resp = SESSION.get(f"{BASE}{source_endpoint}/{source_task_id}", headers=HEADERS, timeout=30)
check_resp.raise_for_status()
source = check_resp.json()
face_count = source.get("face_count", 0)
if face_count > 300000:
print(f"ERROR: Model has {face_count:,} faces (limit: 300,000). Remesh first:")
print(f" create_task('/openapi/v1/remesh', {{'input_task_id': '{source_task_id}', 'target_polycount': 100000}})")
sys.exit("Rigging blocked: face count too high")
rig_id = create_task("/openapi/v1/rigging", {
"input_task_id": "TASK_ID",
"height_meters": 1.7,
})
rig_task = poll_task("/openapi/v1/rigging", rig_id)
download(rig_task["result"]["rigged_character_glb_url"], "rigged.glb")
rig_id = create_task("/openapi/v1/rigging", {
"input_task_id": "TASK_ID",
"height_meters": 1.7,
})
rig_task = poll_task("/openapi/v1/rigging", rig_id)
download(rig_task["result"]["rigged_character_glb_url"], "rigged.glb")
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)