"""动画 — retarget a rigged model onto animation presets. tripocli animate -p --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 (示例: {', '.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