#!/usr/bin/env python3 """MIRPAMO — local Mixamo-style character pipeline. Orchestrates headless Blender. Subcommands: mirpamo rig -o rigged.glb [--markers m.json] [--name N] mirpamo anim [ ...] -o out.glb [--bonemap map.json] [--inplace] [--fps 30] mirpamo batch -o out_dir [--bonemap ...] [--inplace] mirpamo verify [--expect-anim] mirpamo smoke [--keep] Blender binary resolution: $MIRPAMO_BLENDER, then /Applications/Blender.app/Contents/MacOS/Blender, then `blender` on PATH. """ import argparse import glob import os import shutil import subprocess import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BPY = os.path.join(ROOT, "bpy") def blender_bin(): cand = os.environ.get("MIRPAMO_BLENDER") if cand and os.path.exists(cand): return cand mac_default = "/Applications/Blender.app/Contents/MacOS/Blender" if os.path.exists(mac_default): return mac_default path = shutil.which("blender") if path: return path sys.exit("mirpamo: Blender not found. Install Blender or set MIRPAMO_BLENDER.") def run_stage(script, stage_argv): cmd = [blender_bin(), "-b", "--python", os.path.join(BPY, script), "--", *stage_argv] proc = subprocess.run(cmd) if proc.returncode != 0: sys.exit(f"mirpamo: stage {script} failed (exit {proc.returncode})") def cmd_rig(a): argv = ["--in", a.mesh, "--out", a.out] if a.markers: argv += ["--markers", a.markers] if a.name: argv += ["--name", a.name] run_stage("autorig.py", argv) def anim_argv(rig, clips, out, a): argv = ["--rig", rig, "--out", out] for c in clips: argv += ["--clip", c] if a.bonemap: argv += ["--bonemap", a.bonemap] if a.inplace: argv += ["--inplace"] if getattr(a, "fps", None): argv += ["--fps", str(a.fps)] return argv def cmd_anim(a): run_stage("retarget.py", anim_argv(a.rig, a.clips, a.out, a)) def cmd_batch(a): clips = sorted( p for ext in ("*.bvh", "*.fbx", "*.glb") for p in glob.glob(os.path.join(a.clips_dir, ext))) if not clips: sys.exit(f"mirpamo: no clips (.bvh/.fbx/.glb) in {a.clips_dir}") os.makedirs(a.out, exist_ok=True) base = os.path.splitext(os.path.basename(a.rig))[0] for c in clips: cn = os.path.splitext(os.path.basename(c))[0] out = os.path.join(a.out, f"{base}@{cn}.glb") print(f"== {c} -> {out}") run_stage("retarget.py", anim_argv(a.rig, [c], out, a)) def cmd_verify(a): argv = ["--in", a.file] if a.expect_anim: argv += ["--expect-anim"] run_stage("verify_glb.py", argv) def cmd_smoke(a): out = os.path.join(ROOT, "out", "smoke") os.makedirs(out, exist_ok=True) mesh = os.path.join(out, "test_human.glb") clip = os.path.join(out, "test_clip.bvh") rigged = os.path.join(out, "test_human_rigged.glb") final = os.path.join(out, "test_human_walking.glb") print("== smoke 1/5: generate test mesh") run_stage("smoke_mesh.py", ["--out", mesh]) print("== smoke 2/5: generate test clip") subprocess.run([sys.executable, os.path.join(ROOT, "tests", "gen_test_bvh.py"), clip], check=True) print("== smoke 3/5: autorig") run_stage("autorig.py", ["--in", mesh, "--out", rigged]) print("== smoke 4/5: retarget") run_stage("retarget.py", ["--rig", rigged, "--clip", clip, "--out", final]) print("== smoke 5/5: verify") run_stage("verify_glb.py", ["--in", final, "--expect-anim", "--min-frames", "60"]) if not a.keep: pass # outputs stay in out/smoke for inspection; gitignored print("\nMIRPAMO SMOKE TEST PASSED") print(f" inspect: {final}") def main(): ap = argparse.ArgumentParser(prog="mirpamo") sub = ap.add_subparsers(dest="cmd", required=True) p = sub.add_parser("rig", help="auto-rig a raw mesh") p.add_argument("mesh") p.add_argument("-o", "--out", required=True) p.add_argument("--markers") p.add_argument("--name") p.set_defaults(fn=cmd_rig) p = sub.add_parser("anim", help="retarget clip(s) onto a rig") p.add_argument("rig") p.add_argument("clips", nargs="+") p.add_argument("-o", "--out", required=True) p.add_argument("--bonemap") p.add_argument("--inplace", action="store_true") p.add_argument("--fps", type=int) p.set_defaults(fn=cmd_anim) p = sub.add_parser("batch", help="retarget every clip in a dir") p.add_argument("rig") p.add_argument("clips_dir") p.add_argument("-o", "--out", required=True) p.add_argument("--bonemap") p.add_argument("--inplace", action="store_true") p.add_argument("--fps", type=int) p.set_defaults(fn=cmd_batch) p = sub.add_parser("verify", help="verify an output GLB") p.add_argument("file") p.add_argument("--expect-anim", action="store_true") p.set_defaults(fn=cmd_verify) p = sub.add_parser("smoke", help="end-to-end self test") p.add_argument("--keep", action="store_true") p.set_defaults(fn=cmd_smoke) a = ap.parse_args() a.fn(a) if __name__ == "__main__": main()