MIRPAMO/bin/mirpamo
jing a148b4a543 MIRPAMO v0: local Mixamo — autorig + retarget via headless Blender
- autorig: landmark detection from vertex cloud (markers JSON override),
  mixamorig 22-bone skeleton fit, bone-heat weights + nearest-bone rescue
- retarget: world-space quaternion delta transfer, hip-height scale
  compensation, fuzzy/explicit bone mapping (CMU map bundled)
- smoke: procedural test humanoid + synthetic BVH -> rig -> retarget -> verify
- deploy: push to gitea, pull + smoke on m3ultra and m1ultra

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 01:26:13 +10:00

167 lines
5.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""MIRPAMO — local Mixamo-style character pipeline.
Orchestrates headless Blender. Subcommands:
mirpamo rig <mesh> -o rigged.glb [--markers m.json] [--name N]
mirpamo anim <rigged.glb> <clip> [<clip> ...] -o out.glb
[--bonemap map.json] [--inplace] [--fps 30]
mirpamo batch <rigged.glb> <clips_dir> -o out_dir [--bonemap ...] [--inplace]
mirpamo verify <file.glb> [--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()