Files
tripocli/tripo/features/motion.py
T
Mrpzx 977f97995a tripocli v2.0.0:Tripo AI 3D 生成与加工 CLI(面向 Agent 与开发者,纯 Python 标准库、零第三方依赖)
覆盖文生 3D、图生 3D、多视图、批量生成,以及重拓扑、绑定、动画、智能分割、
纹理、PBR、智能 UV、部件补全、格式导出与下载。

代码组织:一个功能一个模块(tripo/features/),入口薄壳 tripocli.py。
2026-10-13 19:22:54 +08:00

152 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""文字生成动作 — 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