tripocli v2.0.0:Tripo AI 3D 生成与加工 CLI(面向 Agent 与开发者,纯 Python 标准库、零第三方依赖)

覆盖文生 3D、图生 3D、多视图、批量生成,以及重拓扑、绑定、动画、智能分割、
纹理、PBR、智能 UV、部件补全、格式导出与下载。

代码组织:一个功能一个模块(tripo/features/),入口薄壳 tripocli.py。
This commit is contained in:
Mrpzx committed 2026-10-13 19:22:54 +08:00
commit 977f97995a
60 files changed
+5622

No files matched your search

+159
View File
@@ -0,0 +1,159 @@
"""文生图 — text to image(游戏资产流程的第一步)。
tripocli img -p "a cute cartoon wooden barrel" -m nano-banana-2 --amount 1 --resolution 2K --wait -o ./refs
实测流程(页面上真操作抓到的请求):
POST /v2/studio/image/gen_image_v2
{"if_upload":true,"sketch_to_render":false,"t_pose":false,"amount":4,
"model_version":"gemini_3.1_flash_image_preview","prompt":"...","resolution":"4K","scale":"1:1"}
→ {task_id, asset_id, remaining_free_count}
POST /v2/studio/image/get_image_asset {"asset_id": "..."}
→ {status, type:"generate_image", output:{data:[{bucket,key,url}]}}
异步:先 gen_image_v2 拿 asset_id,再轮询 get_image_asset 直到 status=success,从 output.data 取图。
"""
import os
import time
from ..errors import TripoAPIError, TripoTimeout
NAME = "img"
HELP = "文生图:用文本生成参考图(游戏资产流程的第一步)"
# 界面上的模型名 → API 的 model_version(逐个在页面上选中并抓包确认)
IMAGE_MODELS = {
"nano-banana-2": "gemini_3.1_flash_image_preview",
"nano-banana-pro": "gemini_3_pro_image_preview",
"gpt-image-2": "gpt_image_2",
"gpt-image-2.5": "gpt_image_2.5_sunburst",
"midjourney": "midjourney",
}
DEFAULT_MODEL = "nano-banana-2"
SCALES = ["1:1", "16:9", "9:16", "4:3", "3:4"]
RESOLUTIONS = ["1K", "2K", "4K"]
def resolve_model(name):
"""Accept either a friendly alias or a raw model_version string."""
if not name:
return IMAGE_MODELS[DEFAULT_MODEL]
key = name.strip().lower()
return IMAGE_MODELS.get(key, name.strip())
def add_arguments(parser):
parser.add_argument("-p", "--prompt", help="图片描述提示词(用 --asset 取已有结果时可省略)")
parser.add_argument("--asset", help="直接取已有 asset_id 的结果,不发起新生成")
parser.add_argument("--templates", action="store_true", help="列出官方模板(角色拆分/T姿势/3D增强…)后退出")
parser.add_argument("--amount", type=int, default=1, choices=[1, 2, 3, 4], help="生成数量 1-4(默认 1,省额度)")
parser.add_argument("--scale", default="1:1", choices=SCALES, help="图片比例(默认 1:1)")
parser.add_argument("--resolution", default="1K", choices=RESOLUTIONS, help="分辨率 1K/2K/4K(默认 1K)")
parser.add_argument(
"-m", "--model", default=DEFAULT_MODEL,
help="图像模型: " + " / ".join(IMAGE_MODELS) + f"(默认 {DEFAULT_MODEL}),也可直接传 model_version",
)
parser.add_argument("--t-pose", action="store_true", help="人形角色输出 T 姿势(便于后续绑骨)")
parser.add_argument("-w", "--wait", action="store_true", help="等待生成完成")
parser.add_argument("-o", "--output", help="图片保存目录(配合 --wait)")
parser.add_argument("--timeout", type=int, default=300, help="等待超时秒数(默认 300)")
def _poll_asset(client, asset_id, timeout):
deadline = time.time() + timeout
while time.time() < deadline:
data = client.get_data("POST", "/v2/studio/image/get_image_asset", body={"asset_id": asset_id}) or {}
status = data.get("status")
if status == "success":
return data
if status in ("failed", "banned"):
raise TripoAPIError(f"图片生成失败: status={status}")
time.sleep(3)
raise TripoTimeout(f"图片生成在 {timeout}s 内未完成(asset_id={asset_id})")
def _report(ctx, asset_id, asset, output_dir):
images = ((asset.get("output") or {}).get("data")) or []
saved = []
if output_dir and images:
os.makedirs(output_dir, exist_ok=True)
for index, item in enumerate(images):
target = os.path.abspath(os.path.join(output_dir, f"image_{index}.png"))
ctx.client.download(item["url"], target)
saved.append(target)
payload = {"asset_id": asset_id, "status": asset.get("status"), "images": images, "saved": saved}
ctx.out.data(
payload,
human=lambda: (
ctx.out.line(f"🎉 生成完成(status={asset.get('status')}),共 {len(images)} 张"),
*[ctx.out.line(f" {path}") for path in saved],
*([] if saved else [ctx.out.line(f" {item['url'][:90]}") for item in images]),
),
)
return payload
def run(ctx):
args = ctx.args
if args.templates:
# 模板是客户端的「提示词+参数」预设,接口只给元数据(实测:请求体里没有 template_id)。
data = ctx.client.get_data("GET", "/v2/studio/image/available_templates") or {}
templates = data.get("templates") or []
def show():
for item in templates:
tags = ", ".join(item.get("secondary_tag") or [])
ctx.out.line(f" {item.get('title', ''):<22} [{item.get('category', '')}] {tags}")
if item.get("description"):
ctx.out.line(f" {item['description'].splitlines()[0]}")
ctx.out.data({"templates": templates}, human=show)
return data
if args.asset:
if args.wait:
asset = _poll_asset(ctx.client, args.asset, args.timeout)
else:
asset = ctx.client.get_data(
"POST", "/v2/studio/image/get_image_asset", body={"asset_id": args.asset}
) or {}
return _report(ctx, args.asset, asset, args.output)
if not args.prompt:
raise ValueError("需要 -p/--prompt 发起生成,或用 --asset 取已有结果")
model_version = resolve_model(args.model)
body = {
"if_upload": True,
"sketch_to_render": False,
"t_pose": args.t_pose,
"amount": args.amount,
"model_version": model_version,
"prompt": args.prompt,
"resolution": args.resolution,
"scale": args.scale,
}
ctx.out.line(f"🎨 生成图片: \"{args.prompt}\" (x{args.amount}, {args.scale}, {args.resolution}, {model_version})")
data = ctx.client.get_data("POST", "/v2/studio/image/gen_image_v2", body=body) or {}
asset_id = data.get("asset_id") or data.get("task_id")
if not asset_id:
raise TripoAPIError(f"gen_image_v2 未返回 asset_id: {data}")
if not args.wait:
ctx.out.data(
data,
human=lambda: (
ctx.out.line(f"✅ 已提交 asset_id={asset_id}(免费剩余 {data.get('remaining_free_count')})"),
ctx.out.line(f"取图: tripocli img --asset {asset_id} --wait -o ./refs"),
),
)
return data
asset = _poll_asset(ctx.client, asset_id, args.timeout)
return _report(ctx, asset_id, asset, args.output)