124 lines
3.9 KiB
Python
124 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
||
"""Meshy text-to-3d CLI(Python 3 标准库,无需额外依赖)。
|
||
|
||
用法:
|
||
MESHY_API_KEY=msy_xxx python3 meshy_gen.py "<prompt>" "<输出.glb>" [texture_prompt]
|
||
|
||
流程:preview -> 轮询 -> refine(PBR) -> 轮询 -> 下载 GLB。
|
||
"""
|
||
import argparse
|
||
import json
|
||
import os
|
||
import shutil
|
||
import sys
|
||
import time
|
||
import urllib.error
|
||
import urllib.request
|
||
|
||
API_URL = "https://api.meshy.ai/openapi/v2/text-to-3d"
|
||
|
||
|
||
def load_key():
|
||
key = os.environ.get("MESHY_API_KEY", "").strip()
|
||
if key:
|
||
return key
|
||
config_path = os.path.expanduser("~/.config/meshy_api_key")
|
||
if os.path.exists(config_path):
|
||
key = open(config_path).read().strip()
|
||
if key:
|
||
return key
|
||
sys.exit("没有 Meshy API Key。设置环境变量 MESHY_API_KEY,或写入 ~/.config/meshy_api_key")
|
||
|
||
|
||
def request(method, url, body=None, key=None):
|
||
req = urllib.request.Request(url, method=method)
|
||
req.add_header("Authorization", "Bearer " + key)
|
||
req.add_header("Content-Type", "application/json")
|
||
data = json.dumps(body).encode() if body is not None else None
|
||
try:
|
||
with urllib.request.urlopen(req, data=data) as resp:
|
||
return json.loads(resp.read().decode())
|
||
except urllib.error.HTTPError as e:
|
||
text = e.read().decode()
|
||
try:
|
||
text = json.loads(text).get("message", text)
|
||
except json.JSONDecodeError:
|
||
pass
|
||
sys.exit(f"HTTP {e.code}: {text}")
|
||
|
||
|
||
def create_task(mode, prompt, key, preview_task_id=None, texture_prompt=None):
|
||
body = {
|
||
"mode": mode,
|
||
"prompt": prompt,
|
||
"model_type": "lowpoly",
|
||
"target_formats": ["glb"],
|
||
"pose_mode": "",
|
||
"should_remesh": False,
|
||
"art_style": "realistic",
|
||
}
|
||
if mode == "refine":
|
||
body["preview_task_id"] = preview_task_id
|
||
body["enable_pbr"] = True
|
||
body["remove_lighting"] = True
|
||
if texture_prompt:
|
||
body["texture_prompt"] = texture_prompt
|
||
return request("POST", API_URL, body, key)["result"]
|
||
|
||
|
||
def poll_task(task_id, key):
|
||
url = f"{API_URL}/{task_id}"
|
||
while True:
|
||
result = request("GET", url, None, key)
|
||
status = result.get("status")
|
||
if status == "SUCCEEDED":
|
||
return result
|
||
if status == "FAILED":
|
||
message = result.get("task_error", {}).get("message", "unknown error")
|
||
sys.exit(f"任务失败:{message}")
|
||
print(f" {status} ...", flush=True)
|
||
time.sleep(5)
|
||
|
||
|
||
def download(url, output):
|
||
req = urllib.request.Request(url)
|
||
with urllib.request.urlopen(req) as resp, open(output, "wb") as f:
|
||
shutil.copyfileobj(resp, f)
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="Meshy text-to-3d 生成 GLB")
|
||
parser.add_argument("prompt", help="模型描述,必须包含 stylized, low-poly, game asset")
|
||
parser.add_argument("output", help="输出 .glb 路径")
|
||
parser.add_argument("texture_prompt", nargs="?", default="", help="可选贴图提示")
|
||
args = parser.parse_args()
|
||
|
||
key = load_key()
|
||
print("创建 preview 任务 ...", flush=True)
|
||
preview_id = create_task("preview", args.prompt, key)
|
||
print(f" preview task: {preview_id}", flush=True)
|
||
preview = poll_task(preview_id, key)
|
||
print(" preview 完成", flush=True)
|
||
|
||
print("创建 refine 任务(PBR 贴图)...", flush=True)
|
||
refine_id = create_task(
|
||
"refine",
|
||
args.prompt,
|
||
key,
|
||
preview_task_id=preview_id,
|
||
texture_prompt=args.texture_prompt,
|
||
)
|
||
print(f" refine task: {refine_id}", flush=True)
|
||
refine = poll_task(refine_id, key)
|
||
print(" refine 完成", flush=True)
|
||
|
||
glb_url = refine.get("model_urls", {}).get("glb")
|
||
if not glb_url:
|
||
sys.exit("响应中没有 model_urls.glb")
|
||
download(glb_url, args.output)
|
||
print(f"已保存: {args.output}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|