From 866551f266d68c5adf5824f107699c176c049d32 Mon Sep 17 00:00:00 2001 From: type-two Date: Sat, 18 Jul 2026 21:33:53 +1000 Subject: [PATCH] [sync5] viseme matcher side-suffix fix, v1 complete: README, git-hygiene rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stage.js detectVisemes: NONMOUTH blocklist + side-stripped matching — /(^|_)l($|_)/ was swallowing every ARKit left-side morph (eyeblink_l, browdown_l), so setMorph('L') would blink the eye; self-check hardened to assert group contents (L === tongue only, smile lands on E) - CLAUDE.md: lanes stage own files explicitly, never git add -A (laneB's attribution finding) - README.md: run + feature summary; lane files: session 4 accepted for all, v1 declared complete, optional backlog recorded Verified: node + 9 server test groups green, /rhubarb 503 gate live, audio lane (tone.wav block, gain) + cut markers + sun keys on screen, zero console errors. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 5 ++++- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ lanes/A-stage.md | 15 +++++++++++++++ lanes/B-timeline.md | 11 +++++++++++ scenegod/web/stage.js | 16 ++++++++++++---- 5 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 README.md diff --git a/CLAUDE.md b/CLAUDE.md index 656ae4d..3f7f1ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,10 @@ three.js, same conventions as MESHGOD (`~/Documents/MESHGOD`). 4. **Commit early, commit often**, on `main`, prefixed `[laneA]`/`[laneB]`/ `[laneC]`. `git pull --rebase` before every commit — other lanes commit to the same branch. File ownership makes conflicts near-zero; if you hit one - anyway, resolve only your own files and log it. + anyway, resolve only your own files and log it. **Stage your own files + explicitly (`git add `) — never `git add -A`/`.`**: another lane's + uncommitted work may be sitting in the shared working dir, and -A sweeps + it into your commit (happened once; attribution mess). 5. **Don't exceed your lane's current milestone.** Milestones and sync points are in PLAN.md §5. Finishing early = polish + tests + log, not starting the next milestone uninvited. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a090b2 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# SCENEGOD — machinima director's sandbox + +Drop characters, Mixamo clips, and photo backdrops onto a 3D stage; keyframe +transforms, cameras (with cuts), and lights on a timeline; render an mp4 with +muxed audio. The Movies / Source Filmmaker energy on the GODVERSE stack. + +## Run + +``` +.venv/bin/uvicorn scenegod.server:app --port 8020 +# → http://127.0.0.1:8020 +``` + +Assets live in `assets/{characters,animations,props,backdrops,audio}/` +(plain folders, scanned live — files sharing dir+stem are one entry; a +same-stem image becomes the thumbnail). Scenes save to `scenes/*.json`, +renders to `renders/` (reaped after 48h). + +## What works (v1) + +- GLB/FBX/OBJ characters & props; spec-gloss (Character Creator) GLBs are + converted on load. Image backdrops: flat plane, corner, or 360 dome. +- Mixamo-style clip retarget onto any mixamorig-named skeleton, client-side + (world-space rotation-delta bake, ported from MESHGOD rigroom). +- Timeline: transform + param keyframes with easing, clip blocks with + trim/loop/crossfade, multiple cameras + cut list, keyframable sun/ambient, + audio tracks (Web Audio preview; server-side ffmpeg mux in the final mp4), + viseme/morph track with Rhubarb JSON import (`GET /rhubarb?path=` if the + rhubarb binary is installed). +- Render: draft webm (MediaRecorder) or deterministic PNG-sequence → + ffmpeg h264 (+aac) mp4 at any resolution. +- `scripts/graft_limb.py`: headless-Blender armature graft (add a rigged + hand/limb to a rigged body; refuses when the bones already exist). +- `scripts/test_server.py` (9 groups) + `node scenegod/web/timeline_test.mjs`. + +## Repo docs + +`PLAN.md` — architecture, scene-JSON schema, module contracts, milestones. +`CLAUDE.md` — lane-agent protocol (3 parallel Opus lanes + orchestrator), +stack rules, inherited gotchas. `lanes/` + `logs/` — per-lane instructions +and session logs; the build history of this repo is readable there. diff --git a/lanes/A-stage.md b/lanes/A-stage.md index ab27a5f..220f0f5 100644 --- a/lanes/A-stage.md +++ b/lanes/A-stage.md @@ -158,3 +158,18 @@ M3-polish list (order): present, greyed otherwise. Add `stage.setMorph(id, name, weight)` for the timeline to drive. Rhubarb-JSON → keyframes lands on Lane B's side; your deliverable is detection + the setMorph hook + the badge. + +### 2026-07-18 — session 4 ACCEPTED with one matcher hotfix. **v1 COMPLETE — no open A work.** +applyState source:'none' fix verified live (rig survives save/load). Viseme +lane accepted, but the loose matcher had a real bug your self-check dict +happened not to trip in its assertions: `L: /(^|_)l($|_)/` matched every +ARKit *left-side* morph — your own selftest output showed +`L: [v_tongue_up, eyeblink_l, browdown_l]`, so setMorph('L') would blink +the eye on a real ARKit head. Orchestrator hotfix in detectVisemes (absorb +it): NONMOUTH blocklist (eye/blink/brow/squint/look/nose/cheek) + patterns +match against the side-stripped name (`eyeblink_l→eyeblink`, +`mouthsmile_l→mouthsmile`→E). Self-check hardened: dict grew mouthSmile_L; +asserts L === [v_tongue_up] and E includes the stripped smile. Lesson worth +keeping: assert group CONTENTS, not just which groups exist. +Still owed by John, not you: a viseme-bearing test character for a live +badge check. Standing by. diff --git a/lanes/B-timeline.md b/lanes/B-timeline.md index 4cf765e..6005a0f 100644 --- a/lanes/B-timeline.md +++ b/lanes/B-timeline.md @@ -137,3 +137,14 @@ refresh + snap select seen live; tests green). M4-B scope (PLAN §5): `stage.setMorph(id, name, weight)` with lerp between keys; plus `importRhubarb(id, rhubarbJson, t0)` mapping mouthShape cues → morph keys. Guard: only offer when `stage.visemeTargets(id).length > 0`. + +### 2026-07-18 — session 4 ACCEPTED. **v1 COMPLETE — no required B work.** +Audio rows + Web Audio + morphs track all verified live (🎵 picker, decoded +8s block at 0.5s offset, gain input, param/cut/sun lanes, zero console +errors). Your ownership note is acted on: CLAUDE.md now mandates staging +own files explicitly — thanks for flagging it instead of letting it slide. +Optional session-5 backlog, in order of value: (1) "lip-sync from audio" +button — POST the audio path to Lane C's `/rhubarb`, feed the JSON to +importRhubarb (503 → toast the install hint); (2) audio block trim; (3) UX +nit found live: mouse wheel over the track canvas zooms, so users may not +discover the panel scrolls — consider wheel = scroll, Cmd/Ctrl+wheel = zoom. diff --git a/scenegod/web/stage.js b/scenegod/web/stage.js index de7bf82..648a072 100644 --- a/scenegod/web/stage.js +++ b/scenegod/web/stage.js @@ -26,8 +26,12 @@ function detectVisemes(root){ for(const [name, idx] of Object.entries(o.morphTargetDictionary)){ const k = name.toLowerCase(); (morphs.get(k) || morphs.set(k, []).get(k)).push({mesh:o, idx}); } }); const visemes = {}; + // SYNC5: ARKit side suffixes (_L/_R) made /(^|_)l($|_)/ swallow eyeblink_l & co — + // never match non-mouth regions, and match against the side-stripped name. + const NONMOUTH = /eye|blink|brow|squint|look|nose|cheek/; for(const [canon, pats] of Object.entries(VISEMES)){ - const hits = [...morphs.keys()].filter(k => pats.some(p => p.test(k))); + const hits = [...morphs.keys()].filter(k => !NONMOUTH.test(k) + && pats.some(p => p.test(k.replace(/(.)[_-][lr]$/, '$1')))); if(hits.length) visemes[canon] = hits; } return { morphs, visemes }; @@ -35,12 +39,16 @@ function detectVisemes(root){ // runnable check for the matcher (assert-based). Call visemeSelfCheck() from the console. export function visemeSelfCheck(){ const dict = {'V_Open':0,'V_Explosive':1,'jawOpen':2,'mouthPucker':3,'mouthFunnel':4, - 'mouthClose':5,'V_Dental_Lip':6,'V_Tongue_up':7,'EyeBlink_L':8,'browDown_L':9}; - const mesh = { isMesh:true, morphTargetDictionary:dict, morphTargetInfluences:new Array(10).fill(0) }; + 'mouthClose':5,'V_Dental_Lip':6,'V_Tongue_up':7,'EyeBlink_L':8,'browDown_L':9,'mouthSmile_L':10}; + const mesh = { isMesh:true, morphTargetDictionary:dict, morphTargetInfluences:new Array(11).fill(0) }; const { morphs, visemes } = detectVisemes({ traverse(cb){ cb(mesh); } }); const has = v => v in visemes; const detectOk = ['A','U','MBP','FV','L'].every(has) // open/jaw, pucker/funnel, explos/close, dental, tongue - && !has('E') && !has('I') && !has('O'); // blink/brow must NOT register as visemes + && !has('I') && !has('O') + // SYNC5 regression guard: side-suffixed ARKit names must land on the RIGHT viseme — + // L is tongue only (no blink/brow/smile bleed), E gets the side-stripped smile. + && visemes.L.length === 1 && visemes.L[0] === 'v_tongue_up' + && has('E') && visemes.E.includes('mouthsmile_l'); // canonical 'A' drives every mesh morph mapped to it (jawOpen idx2 + V_Open idx0) for(const r of visemes.A) for(const {idx} of morphs.get(r)) mesh.morphTargetInfluences[idx] = 0.7; const driveOk = mesh.morphTargetInfluences[0] === 0.7 && mesh.morphTargetInfluences[2] === 0.7