tripocli v2.0.0:Tripo AI 3D 生成与加工 CLI(面向 Agent 与开发者,纯 Python 标准库、零第三方依赖)
覆盖文生 3D、图生 3D、多视图、批量生成,以及重拓扑、绑定、动画、智能分割、 纹理、PBR、智能 UV、部件补全、格式导出与下载。 代码组织:一个功能一个模块(tripo/features/),入口薄壳 tripocli.py。
This commit is contained in:
commit
977f97995a
60 files changed
+5622
No files matched your search
@@ -0,0 +1,151 @@
|
||||
"""文字生成动作 — text to motion(AI 动画),并把动作应用到角色。
|
||||
|
||||
tripocli motion -p "让角色挥手打招呼" --duration 5 --wait # 生成动作,得到 motion_asset_id
|
||||
tripocli motion -p "让角色走一段" --project <pid> --wait # 生成并直接应用到角色
|
||||
tripocli motion --list # 列出我的动作资产
|
||||
tripocli motion -p "..." --segment "3:跳一下" --segment "2:落地" # 多阶段动作
|
||||
|
||||
实测流程(在动画页「AI 动画」标签里点生成抓到):
|
||||
|
||||
POST /v2/studio/motion/generate
|
||||
{"segments": [{"duration_seconds": 5, "prompt": "让角色挥手打招呼"}]}
|
||||
→ {task_id, status:"queued"}
|
||||
|
||||
POST /v2/studio/motion/get_task {"task_id": "..."}
|
||||
→ 轮询直到 status="success",取 asset_id
|
||||
|
||||
POST /v2/studio/operation/retarget_model
|
||||
{"motion_asset_id": "<asset_id>", "model_version":"default",
|
||||
"project_id": "...", "rig_type": "biped"} → 把动作套到模型上
|
||||
|
||||
其它已验证的 motion 接口:`/motion/list_assets`(我的动作资产)、`/motion/get_asset`、
|
||||
`/motion/list_active_tasks`(正在跑的任务)。
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from . import _common, animate
|
||||
from ..errors import TripoAPIError, TripoTimeout
|
||||
|
||||
NAME = "motion"
|
||||
HELP = "文字生成动作:用一句话生成动画并(可选)应用到已绑定的角色"
|
||||
|
||||
|
||||
def add_arguments(parser):
|
||||
parser.add_argument("-p", "--prompt", help="动作描述,如「让角色挥手打招呼」")
|
||||
parser.add_argument("--duration", type=float, default=5, help="动作时长(秒,默认 5)")
|
||||
parser.add_argument(
|
||||
"--segment", action="append",
|
||||
help="多阶段动作:\"时长:描述\",可重复(如 --segment \"3:跳一下\" --segment \"2:落地\")",
|
||||
)
|
||||
parser.add_argument("--list", action="store_true", help="列出我的动作资产后退出")
|
||||
parser.add_argument("--project", help="生成后直接应用到该项目(需已绑定骨骼)")
|
||||
parser.add_argument("--type", default="biped", help="骨骼类型,默认 biped(配合 --project 使用)")
|
||||
parser.add_argument("-w", "--wait", action="store_true", help="等待动作生成完成")
|
||||
parser.add_argument("-o", "--output", help="应用动作后的模型保存路径(配合 --project)")
|
||||
parser.add_argument("--timeout", type=int, default=300, help="等待超时秒数(默认 300)")
|
||||
|
||||
|
||||
def _segments(args):
|
||||
if args.segment:
|
||||
out = []
|
||||
for raw in args.segment:
|
||||
if ":" in raw:
|
||||
duration, text = raw.split(":", 1)
|
||||
out.append({"duration_seconds": float(duration), "prompt": text.strip()})
|
||||
else:
|
||||
out.append({"duration_seconds": args.duration, "prompt": raw.strip()})
|
||||
return out
|
||||
if not args.prompt:
|
||||
return []
|
||||
return [{"duration_seconds": args.duration, "prompt": args.prompt}]
|
||||
|
||||
|
||||
def _poll_task(client, task_id, timeout):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
data = client.get_data("POST", "/v2/studio/motion/get_task", body={"task_id": task_id}) or {}
|
||||
status = data.get("status")
|
||||
if status == "success":
|
||||
return data
|
||||
if status in ("failed", "banned", "expired"):
|
||||
raise TripoAPIError(f"动作生成失败: status={status} error={data.get('error')}")
|
||||
time.sleep(3)
|
||||
raise TripoTimeout(f"动作生成在 {timeout}s 内未完成(task_id={task_id})")
|
||||
|
||||
|
||||
def _list_assets(ctx):
|
||||
data = ctx.client.get_data("POST", "/v2/studio/motion/list_assets", body={}) or {}
|
||||
assets = data.get("assets") or []
|
||||
ctx.out.data(
|
||||
data,
|
||||
human=lambda: [
|
||||
ctx.out.line(f"共 {len(assets)} 个动作资产"),
|
||||
*[ctx.out.line(
|
||||
f" {a.get('asset_id') or a.get('id')} [{a.get('action_type') or a.get('category') or ''}] "
|
||||
f"{a.get('title') or a.get('name') or a.get('prompt') or ''}"
|
||||
) for a in assets],
|
||||
],
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
def run(ctx):
|
||||
args = ctx.args
|
||||
|
||||
if args.list:
|
||||
return _list_assets(ctx)
|
||||
|
||||
segments = _segments(args)
|
||||
if not segments:
|
||||
raise ValueError("需要 -p/--prompt 描述动作(或用 --segment 组合多阶段动作,--list 查看已有资产)")
|
||||
|
||||
ctx.out.line("🎬 生成动作: " + " | ".join(f"{s['duration_seconds']}s {s['prompt']}" for s in segments))
|
||||
task = ctx.client.get_data("POST", "/v2/studio/motion/generate", body={"segments": segments}) or {}
|
||||
task_id = task.get("task_id")
|
||||
if not task_id:
|
||||
raise TripoAPIError(f"motion/generate 未返回 task_id: {task}")
|
||||
|
||||
if not args.wait:
|
||||
ctx.out.data(
|
||||
task,
|
||||
human=lambda: (
|
||||
ctx.out.line(f"✅ 已提交 task_id={task_id}"),
|
||||
ctx.out.line(f"取结果: tripocli motion --wait ... 或轮询 /motion/get_task"),
|
||||
),
|
||||
)
|
||||
return task
|
||||
|
||||
result = _poll_task(ctx.client, task_id, args.timeout)
|
||||
asset_id = result.get("asset_id")
|
||||
|
||||
payload = {"task": result, "motion_asset_id": asset_id}
|
||||
if args.project and asset_id:
|
||||
ctx.out.line(f"🦴 应用到项目 {args.project} ...")
|
||||
body = {
|
||||
"motion_asset_id": asset_id,
|
||||
"model_version": "default",
|
||||
"project_id": args.project,
|
||||
"rig_type": args.type,
|
||||
}
|
||||
applied = ctx.client.get_data("POST", "/v2/studio/operation/retarget_model", body=body) or {}
|
||||
op_id = applied.get("operator_id")
|
||||
if op_id:
|
||||
_common.wait_and_report(ctx, op_id, timeout=900)
|
||||
entry = animate._retarget_entry(ctx.client, args.project, op_id) or {}
|
||||
model_url = entry.get("model_url")
|
||||
output_path = None
|
||||
if args.output and model_url:
|
||||
ctx.client.download(model_url, args.output)
|
||||
output_path = os.path.abspath(args.output)
|
||||
payload["applied"] = {"result": applied, "model_url": model_url, "output_file": output_path}
|
||||
|
||||
ctx.out.data(
|
||||
payload,
|
||||
human=lambda: (
|
||||
ctx.out.line(f"🎉 动作已生成 motion_asset_id={asset_id}"),
|
||||
ctx.out.line(f" 应用到角色: tripocli animate -p <project_id> --motion-asset {asset_id} --wait"),
|
||||
),
|
||||
)
|
||||
return payload
|
||||
Reference in new issue
Block a user