[sync2+3] entity mirror seam, SYNC 2 demo + 1080p mp4 verified, M4/polish unlocked

- timeline.js: _mirror — Timeline subscribes stage.onChange, upserts/removes
  live stage entities into scene.entities (closes the dock-added-entity gap
  both lanes flagged; keyframing now works without a scene load)
- lane files: M2/M3 accepted for all lanes; A→PiP sizing + lady.glb material
  polish, B→absorb _mirror + test + setDuration mutator, C→M4 (audio mux,
  graft_limb, MB proxy)

SYNC 2: lady+man+street+2cams+sunset built via live dock path, all
frame-exact (probes in transcript). SYNC 3: finalRender → 240 frames
1920x1080 → ffmpeg h264 8.000s, cuts + sunset verified in extracted frames.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-18 20:34:32 +10:00
parent dfd4cebd2b
commit 41f2c9fa8d
4 changed files with 71 additions and 0 deletions

View File

@ -120,3 +120,20 @@ retargeted clip + timeline scrub, all through the real Stage). Answers + M2 orde
backdrop rebuild-on-param-change, drag-from-dock onto viewport. backdrop rebuild-on-param-change, drag-from-dock onto viewport.
Test assets now live in `assets/{characters,animations,backdrops}/test/` Test assets now live in `assets/{characters,animations,backdrops}/test/`
(man.fbx, hiphop.fbx, carrying.fbx, street.jpg — gitignored, on ultra). (man.fbx, hiphop.fbx, carrying.fbx, street.jpg — gitignored, on ultra).
### 2026-07-18 — M2 ACCEPTED, SYNC 2 PASSED. **M3-polish unlocked.**
Your entity-mirror seam report was exactly right and well-diagnosed. Resolution:
Lane B's Timeline now subscribes to `stage.onChange` and mirrors adds/removes
into its model (orchestrator implemented, `timeline.js _mirror`) — your
onChange-on-every-add fix is what made that possible; no dedicated
entityadd/remove events needed. Verified live: 6 dock-added entities all
keyframable, full sunset demo saved as scene `sync2-sunset`.
M3-polish list (order):
1. **PiP is enormous** — it covered ~70% of the viewport in the SYNC 2
screenshot. Cap it (~24% width, bottom-right corner, click to swap
director/active-cam views or a size toggle).
2. lady.glb (non-mixamo rig, `assets/characters/test/lady.glb`) renders
flat white — her textures didn't come through the GLB load path. Check
whether it's the asset or a loader/material issue (compare in rigroom).
3. Keep `renderActiveCamera`/`pause`/`resume` stable — the finalRender
client drives them now (SYNC 3).

View File

@ -105,3 +105,18 @@ clip retarget playback verified frame-exact via `step()`. Notes:
6. Injected `<style id=laneB-tl-css>` is fine permanently — keep it. 6. Injected `<style id=laneB-tl-css>` is fine permanently — keep it.
Then per plan: crossfade fade-IN polish, params + camera-cut lane UI, Then per plan: crossfade fade-IN polish, params + camera-cut lane UI,
snapping config, box-select multi-drag (undo already wired). snapping config, box-select multi-drag (undo already wired).
### 2026-07-18 — M2 ACCEPTED, SYNC 2 PASSED. **Session 3 (polish) granted.**
Your flagged dock-entity gap is closed: orchestrator added `_mirror` to
`timeline.js` (ctor subscribes `stage.onChange`; upserts an entity with empty
tracks, removes when gone from stage — removal detected via
`stage.getEntity(id) === null`). Verified live: 6 dock-added entities entered
the model, clipdrop landed a 4.47s block, sunset param lerp + camera cuts all
frame-exact. Session 3 items:
1. **Absorb `_mirror`** — review it, then add a timeline_test.mjs case
(stub add → mutate → remove; assert entity+tracks lifecycle). The stub
needs `getEntity` to return null after removal for this to test honestly.
2. Programmatic `scene.duration = x` doesn't refresh the scene bar (only
`load()` fires onLoad). Add a `setDuration(x)` mutator that clamps
existing keys' visibility and notifies the UI; wire the dur field to it.
3. Your listed polish: snapping increment config, keyable-param add UI.

View File

@ -106,3 +106,21 @@ solid), go straight into §C5 — implement `/render/begin|frame|end|status|out`
4. uvicorn for local dev: `.venv/bin/uvicorn scenegod.server:app --port 8020` 4. uvicorn for local dev: `.venv/bin/uvicorn scenegod.server:app --port 8020`
— there's also a launch entry in MESHGOD/.claude/launch.json ("scenegod") — there's also a launch entry in MESHGOD/.claude/launch.json ("scenegod")
used by the orchestrator's browser preview. used by the orchestrator's browser preview.
### 2026-07-18 — M2+M3 ACCEPTED, **SYNC 3 PASSED. M4 is UNLOCKED.**
Orchestrator ran your `finalRender` end-to-end in the integrated app against
Lane A's `renderActiveCamera`: 240 frames @1920×1080 → ffmpeg → out.mp4,
h264, exactly 8.000s, with camera cuts and sunset light lerp visible in
extracted frames. The browser client path you couldn't exercise is now
verified — no changes needed. Proceed with M4 in this order:
1. ffmpeg audio mux from scene `audio[]` (the sync2-sunset scene in
`scenes/` is your fixture; add a wav to `assets/audio/test/`).
2. `scripts/graft_limb.py` (spec in §C6 — blender binary is on ultra's PATH
as `blender`; verify with a mixamo body + any rigged hand GLB, and add
the refusal-path test).
3. `/mb/submit` MODELBEAST proxy last (needs `SCENEGOD_MB=1` gating like
riggermore; test only that auth/upload contract matches meshgod/ops.py —
don't burn farm jobs on repeat runs).
One render-quality note for your backlog, not urgent: draft webm via
MediaRecorder is untested in the embedded preview (rAF suspension) — verify
it once in a real Chrome window before calling it done.

View File

@ -99,6 +99,27 @@ export class Timeline {
if (e.detail && e.detail.id) this.addClipDrop(e.detail); if (e.detail && e.detail.id) this.addClipDrop(e.detail);
}); });
} }
// SYNC2 seam: mirror live stage adds/removes into scene.entities so
// dock-added entities can be keyframed without a scene load.
if (stage && stage.onChange) stage.onChange((en) => this._mirror(en));
}
_mirror(en) {
if (!en || !en.id) return;
const list = this.scene.entities;
const i = list.findIndex((x) => x.id === en.id);
if (!this.stage.getEntity(en.id)) { // gone from stage → drop entity + its tracks
if (i >= 0) list.splice(i, 1);
return;
}
if (i >= 0) return; // known — timeline already owns its tracks
list.push({
id: en.id, kind: en.kind, label: en.label,
source: en.source ? structuredClone(en.source) : { type: 'upload' },
params: en.params ? structuredClone(en.params) : {},
transform: this.stage.entityTransform(en.id),
tracks: { transform: [], params: [], clips: [] },
});
} }
get fps() { return this.scene.fps; } get fps() { return this.scene.fps; }