Lane D: --merge clip-bank GLB hook + honest ledger of the three silent bugs

--merge folds clips_out/*.fbx onto the character's bank GLB via character_kit's
merge_anims.py (already CLI'd + slot-bound; fps pin added there this session) ->
clips_out/<character>_bank.glb. Rebuilt from scratch each run, so it's idempotent.
This is the 'lands in 90sDJsim' step: games load GLB.

Verified from the exported glTF JSON rather than Blender (which resamples on read):
27 animations (24 base + 3 ours), no duplicate names, all 30.0 keys/sec, the 317-frame
clip keeps 317 keys over 10.567s, our clips drive real bone motion (delta 48-184u).

Also: mocap now reports a DANGLING venv symlink honestly (uv pruned its managed
CPython 3.10, which makes Path.exists() read as 'missing'), selfcheck covers the
bank path, RUNBOOK documents --merge + the env gotcha, PROGRESS records all three
bugs and the lesson (check artifacts, not green log lines).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-25 21:50:52 +10:00
parent 1b72fa8196
commit 4af9033b68
3 changed files with 86 additions and 14 deletions

View File

@ -1,5 +1,36 @@
# MOCAPGOD progress ledger # 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/<character>_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 Δ48184u. 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 BD) — VERDICT: SHIPPED, one bug, one cross-repo action ## FABLE REVIEW #2 (2026-07-17, post lanes BD) — VERDICT: SHIPPED, one bug, one cross-repo action
Independently verified (not trusted): fresh end-to-end `./mocap` run on a renamed demo clip → Independently verified (not trusted): fresh end-to-end `./mocap` run on a renamed demo clip →

View File

@ -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; - Male rig (`--character male` → `hum_character.glb`) is **unverified** until a male clip is run;
female (`female_game.glb`) is the proven path. female (`female_game.glb`) is the proven path.
## Deferred hooks — build these when real footage lands (not faked in the CLI) ## `--merge` — the clip-bank GLB (BUILT + verified 2026-07-25)
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 ./mocap capture/inbox/move.mp4 --name dj_scratch_01 --merge
"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 Folds **every** `clips_out/*.fbx` onto the character's bank GLB via
(`animation_data.action_slot = act.slots[0]`, verified in Lane C) or the merged GLB won't animate. `character_kit/scripts/merge_anims.py``clips_out/<character>_bank.glb`. Games load GLB, so this
Then merge `clips_out/<name>.fbx` onto the character's bank GLB and export. is the "lands in 90sDJsim" step: point the game at that file.
2. **`--to-unreal`** — import via the A_3D_STORE deferred-import pattern (memory:
- 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 (Δ 48184u).
- 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). `unreal-asset-convergence` — slate post-tick, NOT a direct AssetImportTask through the bridge).
3. **`--publish-3god`** — depot pattern from procity `publish.py`. 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) ## John's homework (blocks the real quality verdict)
Film **walk / dance / pick-up-object** per `docs/CAPTURE.md`, drop in `capture/inbox/`, run `mocap`. 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 Until then the pipeline is proven on the HSMR `gymnasts` demo only — machinery works; the honest

39
mocap
View File

@ -12,9 +12,11 @@ Chains the three proven stages, all on ultra:
Result: clips_out/<name>.fbx (anim-only, Mixamo "Without Skin") + <name>_qc.mp4 turntable. Result: clips_out/<name>.fbx (anim-only, Mixamo "Without Skin") + <name>_qc.mp4 turntable.
House rule: eyeball the QC mp4 before the clip enters a bank. 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): --merge additionally folds clips_out/*.fbx onto the character's bank GLB (games load GLB, so
clip-bank GLB merge, --to-unreal, --publish-3god. merge_anims.py needs a CLI + the Blender-5 this is the "lands in 90sDJsim" step) via character_kit/scripts/merge_anims.py.
slot-bind first, and none of them are verifiable without a real clip, so they're not faked here.
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 import argparse, datetime, re, subprocess, sys
from pathlib import Path 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 VENV_PY = REPO / ".engine/HSMR/.venv/bin/python" # Lane B lives in its own env
BLENDER = Path("/Applications/Blender.app/Contents/MacOS/Blender") BLENDER = Path("/Applications/Blender.app/Contents/MacOS/Blender")
KIT = Path.home() / "Documents/character_kit" 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) RIGS = { # --character → (pose gender, retarget rig)
"female": ("female", KIT / "female/female_game.glb"), # v1 proven path "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 "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!") assert slug("DJ Scratch #1!") == "DJ_Scratch_1", slug("DJ Scratch #1!")
for c in RIGS.values(): for c in RIGS.values():
assert c[0] in ("male", "female") 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 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("--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("--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("--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/<character>_bank.glb (the 'lands in-game' step)")
ap.add_argument("--selfcheck", action="store_true", help="verify the path-threading logic and exit") ap.add_argument("--selfcheck", action="store_true", help="verify the path-threading logic and exit")
a = ap.parse_args() a = ap.parse_args()
@ -79,7 +88,11 @@ def main():
if not a.video or not a.name: if not a.video or not a.name:
ap.error("video and --name are required (or use --selfcheck)") ap.error("video and --name are required (or use --selfcheck)")
if not VENV_PY.exists(): 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(): if not BLENDER.exists():
sys.exit(f"✗ Blender not found: {BLENDER} (Lane C needs it).") sys.exit(f"✗ Blender not found: {BLENDER} (Lane C needs it).")
@ -111,7 +124,21 @@ def main():
print(f"\n✅ clip ready: {fbx}") print(f"\n✅ clip ready: {fbx}")
if qc.exists(): if qc.exists():
print(f" eyeball before it enters a bank: {qc}") 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 return 0