diff --git a/PROGRESS.md b/PROGRESS.md index b3b4b19..4218297 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1,5 +1,36 @@ # MOCAPGOD progress ledger +## SESSION 8 (2026-07-25) — review + `--merge` hook. Three silent bugs found and fixed. +Re-verified the repo against its own claims instead of trusting the ledger. Review #2's "SHIPPED" +was right about the machinery but **three defects had passed sign-off**, all of the same species: +the *log said green, the artifact was wrong*. Each is now fixed and verified at the artifact level. + +1. **FBX take name was still `Armature|Scene`** — the previous "fix" renamed the baked *action*, but + the exporter builds the AnimStack from **`scene.name`**. Caught on a clip built *after* that fix + commit. Now sets `scene.name`; verified `Armature|wan_dig_t3` on import + 6 `strings` hits. +2. **Every FBX carried Blender's factory 1..250 scene range** (`bake_anim` exports the *scene* range, + and retarget only set it inside `qc_render`, which runs *after* export). Short clips gained a frozen + tail (98f → 250f); **long clips were truncated — the 317f gymnasts clip lost 67 frames / 2.2s**, and + that is the very clip Review #2 signed off as "317f green". Now pinned to the trimmed range. +3. **The bank merge silently resampled 30fps → 24fps** (factory scene fps; both importers convert + timing through it). Durations stayed right (glTF stores seconds) so nothing looked broken, but + every rebuild shaved ~20% temporal resolution off *all* clips, base bank included. Fixed in + `character_kit/scripts/merge_anims.py` (cross-repo — affects 90sDJsim/OPSHOPGAME/NPCFACTORY too). + +**`--merge` SHIPPED** (`./mocap … --merge`) → `clips_out/_bank.glb`. Verified from the +exported glTF JSON, not Blender (which resamples on read): **27 animations, no dupe names, all +30.0 keys/sec**, 317-frame clip keeps 317 keys over 10.567s, our clips drive Δ48–184u. That is the +"lands in 90sDJsim" step — point the game at that GLB. + +**Lesson for future reviews:** every one of these hid behind a green log line. Check the *artifact* +(import the FBX, parse the glTF), never the console output. + +⚠️ **Lane B is currently un-runnable on ultra:** `uv` pruned its managed CPython 3.10, so +`.engine/HSMR/.venv/bin/python` is a dangling symlink. Not caused by these changes. Fix: +`./setup_hsmr.sh` (re-runnable; SKEL + weights survive). Retarget/merge stages are unaffected. +**Still open:** John's real footage (quality verdict), the MPI Path-A email, `--to-unreal`, +foot-contact IK, male rig. + ## FABLE REVIEW #2 (2026-07-17, post lanes B–D) — VERDICT: SHIPPED, one bug, one cross-repo action Independently verified (not trusted): fresh end-to-end `./mocap` run on a renamed demo clip → diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index eaa9809..e27ff68 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -39,17 +39,31 @@ Timing: pose engine dominates (~1.6× realtime on ultra/MPS); a 10s clip ≈ a c - Male rig (`--character male` → `hum_character.glb`) is **unverified** until a male clip is run; female (`female_game.glb`) is the proven path. -## Deferred hooks — build these when real footage lands (not faked in the CLI) -Each is off by default and needs a real clip to verify, so none ship as dead flags today. -1. **Clip-bank GLB merge** (`--merge`, → 90sDJsim / three.js games). Games load GLB, so this is the - "lands in-game" step. Needs a small merge script first: `character_kit/scripts/merge_anims.py` - has **no CLI and is pre-Baklava** — it must add the Blender-5 slot-bind - (`animation_data.action_slot = act.slots[0]`, verified in Lane C) or the merged GLB won't animate. - Then merge `clips_out/.fbx` onto the character's bank GLB and export. -2. **`--to-unreal`** — import via the A_3D_STORE deferred-import pattern (memory: +## `--merge` — the clip-bank GLB (BUILT + verified 2026-07-25) +``` +./mocap capture/inbox/move.mp4 --name dj_scratch_01 --merge +``` +Folds **every** `clips_out/*.fbx` onto the character's bank GLB via +`character_kit/scripts/merge_anims.py` → `clips_out/_bank.glb`. Games load GLB, so this +is the "lands in 90sDJsim" step: point the game at that file. + +- Rebuilt from scratch each run (base rig + current `clips_out/` contents) — **idempotent, no + compounding**. Prune `clips_out/` to control what ships. +- Verified from the exported glTF JSON: female bank = **27 animations** (24 base + 3 ours), no + duplicate names, all **30.0 keys/sec**, our clips drive real bone motion (Δ 48–184u). +- Needs the Blender-5 slot-bind and the 30fps pin — both now **in** `merge_anims.py`. Without the + slot-bind the GLB animates nothing; without the fps pin every rebuild resampled 30→24fps. + +## Deferred hooks — build when real footage lands (not faked in the CLI) +1. **`--to-unreal`** — import via the A_3D_STORE deferred-import pattern (memory: `unreal-asset-convergence` — slate post-tick, NOT a direct AssetImportTask through the bridge). 3. **`--publish-3god`** — depot pattern from procity `publish.py`. +## Environment gotcha (bit us 2026-07-25) +`uv` pruned its managed **CPython 3.10**, leaving `.engine/HSMR/.venv/bin/python` a *dangling +symlink* — so Lane B dies and `Path.exists()` reports the venv "missing". `mocap` now says so +explicitly. Rebuild with **`./setup_hsmr.sh`** (re-runnable; SKEL files + weights survive). + ## John's homework (blocks the real quality verdict) Film **walk / dance / pick-up-object** per `docs/CAPTURE.md`, drop in `capture/inbox/`, run `mocap`. Until then the pipeline is proven on the HSMR `gymnasts` demo only — machinery works; the honest diff --git a/mocap b/mocap index 50b0862..b284383 100755 --- a/mocap +++ b/mocap @@ -12,9 +12,11 @@ Chains the three proven stages, all on ultra: Result: clips_out/.fbx (anim-only, Mixamo "Without Skin") + _qc.mp4 turntable. House rule: eyeball the QC mp4 before the clip enters a bank. -Deferred hooks (off by default — built when John's real footage lands; see docs/RUNBOOK.md): - clip-bank GLB merge, --to-unreal, --publish-3god. merge_anims.py needs a CLI + the Blender-5 - slot-bind first, and none of them are verifiable without a real clip, so they're not faked here. +--merge additionally folds clips_out/*.fbx onto the character's bank GLB (games load GLB, so +this is the "lands in 90sDJsim" step) via character_kit/scripts/merge_anims.py. + +Deferred hooks (off by default — see docs/RUNBOOK.md): --to-unreal, --publish-3god. Neither is +verifiable without a real clip in the target project, so they're not faked here. """ import argparse, datetime, re, subprocess, sys from pathlib import Path @@ -26,6 +28,7 @@ CLIPS = REPO / "clips_out" VENV_PY = REPO / ".engine/HSMR/.venv/bin/python" # Lane B lives in its own env BLENDER = Path("/Applications/Blender.app/Contents/MacOS/Blender") KIT = Path.home() / "Documents/character_kit" +MERGE = KIT / "scripts/merge_anims.py" # CLI: -- BASE CLIP_DIR OUT (Blender-5 slot-bind inside) RIGS = { # --character → (pose gender, retarget rig) "female": ("female", KIT / "female/female_game.glb"), # v1 proven path "male": ("male", KIT / "hum_character.glb"), # ponytail: male rig unverified until a male clip exists @@ -59,7 +62,10 @@ def selfcheck(): assert slug("DJ Scratch #1!") == "DJ_Scratch_1", slug("DJ Scratch #1!") for c in RIGS.values(): assert c[0] in ("male", "female") - print("selfcheck OK — name threads inbox→queue→out→clips, final clip name stays clean") + # --merge writes one bank per character, and must not glob its own output back in + for ch in RIGS: + assert (CLIPS / f"{ch}_bank.glb").suffix == ".glb", ch + print("selfcheck OK — name threads inbox→queue→out→clips→bank, final clip name stays clean") return 0 @@ -71,6 +77,9 @@ def main(): ap.add_argument("--gender", choices=["male", "female"], help="override pose gender (default: from --character)") ap.add_argument("--travel", action="store_true", help="keep world root translation (default: in-place)") ap.add_argument("--no-qc", action="store_true", help="skip the QC turntable render") + ap.add_argument("--merge", action="store_true", + help="also merge ALL clips_out/*.fbx onto the character's bank GLB " + "→ clips_out/_bank.glb (the 'lands in-game' step)") ap.add_argument("--selfcheck", action="store_true", help="verify the path-threading logic and exit") a = ap.parse_args() @@ -79,7 +88,11 @@ def main(): if not a.video or not a.name: ap.error("video and --name are required (or use --selfcheck)") if not VENV_PY.exists(): - sys.exit(f"✗ HSMR venv missing: {VENV_PY} — run .engine/setup_hsmr.sh (Lane B).") + # .exists() is False for a DANGLING symlink too — uv pruning its managed CPython 3.10 + # breaks the venv this way (seen 2026-07-25). Re-running setup rebuilds it. + sys.exit(f"✗ HSMR venv missing or broken: {VENV_PY}\n" + f" (a dangling interpreter symlink counts — check: ls -la {VENV_PY})\n" + f" rebuild with: ./setup_hsmr.sh") if not BLENDER.exists(): sys.exit(f"✗ Blender not found: {BLENDER} (Lane C needs it).") @@ -111,7 +124,21 @@ def main(): print(f"\n✅ clip ready: {fbx}") if qc.exists(): print(f" eyeball before it enters a bank: {qc}") - print(" merge-to-GLB / Unreal / 3god hooks are deferred — docs/RUNBOOK.md.") + + bank = CLIPS / f"{a.character}_bank.glb" + if a.merge: + if not MERGE.exists(): + sys.exit(f"✗ merge script missing: {MERGE}") + # the bank is rebuilt from scratch each time = base rig + EVERY fbx currently in clips_out/ + # (idempotent, no compounding). Prune clips_out to control what ships. + print(f"\n bank = {rig.name} + all {len(list(CLIPS.glob('*.fbx')))} clip(s) in {CLIPS.name}/") + stage("Lane D · merge to clip-bank GLB", [BLENDER, "--background", "--python", MERGE, "--", + rig, CLIPS, bank]) + if not bank.exists(): + sys.exit(f"✗ no {bank} after merge.") + print(f"\n✅ bank ready: {bank} → load this in 90sDJsim / three.js games") + else: + print(f" --merge folds it into {bank.name}; --to-unreal / --publish-3god: docs/RUNBOOK.md.") return 0