Files
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

121 lines
4.7 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.
"""动画 — retarget a rigged model onto animation presets.
tripocli animate -p <project_id> --preset idle --wait -o anim_idle.glb
tripocli animate --list
"""
import os
from . import _common
from .. import motions
NAME = "animate"
HELP = "动画:为已绑定骨骼的模型套用动作预设"
PRESET_EXAMPLES = ["idle", "walk", "run", "slash", "cast_a_spell", "jump"]
def add_arguments(parser):
parser.add_argument("-p", "--project", help="项目 ID(--list 时可省略)")
parser.add_argument("-m", "--model", help="覆盖 model_version(默认 default)")
_common.add_wait_options(parser, "动画结果保存到该路径(可选)")
parser.add_argument("--type", default="biped", help="骨骼类型,默认 biped")
parser.add_argument(
"--preset", action="append",
help="动作预设名(如 idle / walk / run / slash),可重复;用 --list 查看全部",
)
parser.add_argument(
"--animation", action="append",
help="完整动作标识(如 preset:biped:idle),可重复",
)
parser.add_argument(
"--motion-asset",
help="套用「文字生成动作」产出的动作资产 ID(来自 tripocli motion)",
)
parser.add_argument("--list", action="store_true", help="列出全部可用动作预设(按骨骼类型)")
def run(ctx):
args = ctx.args
if args.list:
data = {
rig: [
{"name": item["name"], "action_type": item.get("action_type"),
"category": item.get("category"), "id": motions.identifier(rig, item["name"])}
for item in motions.catalog().get(rig, [])
]
for rig in motions.rig_types()
}
def human():
for rig, items in data.items():
ctx.out.line(f"== {rig} ({len(items)}) ==")
ctx.out.line(" " + ", ".join(item["name"] for item in items))
ctx.out.data(data, human=human)
return data
animations = list(args.animation or [])
for preset in (args.preset or []):
animations.append(preset if ":" in preset else motions.identifier(args.type, preset))
if not args.project:
raise ValueError("需要 -p/--project 指定项目(用 --list 只查看动作清单)")
body = {
"project_id": args.project,
"rig_type": args.type,
"model_version": args.model or "default",
}
if args.motion_asset:
# 文字生成动作产出的资产:retarget_model 用 motion_asset_id(而不是 animations)
body["motion_asset_id"] = args.motion_asset
elif animations:
body["animations"] = animations
else:
raise ValueError(
"至少需要一个动作:用 --preset afraid、--animation preset:biped:afraid,"
f"或 --motion-asset <id>(示例: {', '.join(PRESET_EXAMPLES)};用 --list 看全部)"
)
data = ctx.client.get_data("POST", "/v2/studio/operation/retarget_model", body=body)
if not args.wait:
return _common.run_operator(ctx, data, output=args.output, wait=False, label="动画重定向")
operator_id = (data or {}).get("operator_id")
project_id = (data or {}).get("project_id") or args.project
if operator_id:
_common.wait_and_report(ctx, operator_id, timeout=900)
# 动画结果不在 project.model_url(那是绑骨结果),而在 operator.retarget[] 里:
# 每个元素 {operator_id, name:"preset:biped:xxx", status, model_url}(实测确认)。
entry = _retarget_entry(ctx.client, project_id, operator_id)
model_url = (entry or {}).get("model_url")
output_path = args.output
if output_path and model_url:
ctx.client.download(model_url, output_path)
output_path = os.path.abspath(output_path)
payload = {"result": data, "animation": (entry or {}).get("name"), "model_url": model_url,
"output_file": output_path if model_url else None}
ctx.out.data(
payload,
human=lambda: (
ctx.out.line(f"🎉 动画重定向完成({(entry or {}).get('name')})"),
ctx.out.line(f"模型地址: {model_url}"),
ctx.out.line(f"已保存 : {output_path}") if model_url else ctx.out.line("⚠️ 未取到动画 model_url"),
),
)
return payload
def _retarget_entry(client, project_id, operator_id=None):
"""Pick the retarget result from the project's operator.retarget list."""
detail = client.project_detail(project_id) or {}
entries = (detail.get("operator") or {}).get("retarget") or []
if operator_id:
for item in entries:
if item.get("operator_id") == operator_id:
return item
return entries[-1] if entries else None