MOVES grammar + solveMove(): push in/pull out (one rung along the shot ladder,
along the aim ray), truck L/R (lateral, parallel, no re-aim), crane up/down
(vertical then re-aim, floor-clamped), orbit L/R (constant distance, +/-25deg),
zoom in/out (focal only - pos and rot returned identical). Amounts are
frame-relative so a move reads the same on a CU as a WS. Emits a new 'move' op
on scenegod:direct with explicit from/to states, pre-clamped duration and ease;
the START is applied to the live camera immediately.
'+ angle' creates a NEW camera from the current shot settings instead of
reframing the active one - the friction hit while building a 4-camera scene by
hand. 🎥 frame now prefers the live camera so it adjusts what you just made
rather than silently editing camera 1. fitAnchor exposed in the backdrop
inspector (it was inert from the UI).
Verified live: push_in 1.975->1.588m fov fixed; zoom_in travel 0.000 with
pos/rot identical, fov 37.85->26.99; orbit_L constant distance; truck_R
parallel. 4 suites green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A hand-placed cut is kept only if it genuinely changes the angle: the plan now
merges surviving cuts with the placed ones, walks the merged list tracking the
EFFECTIVE camera, and drops in-range user cuts whose camera equals it. Real
case: 4.50->cam2 became a no-op once auto-cut placed cam2 at 1.4161 - it
rendered as nothing but read as an edit in the saved file. Removed in the same
group() so it stays one undo; popover and toast report kept vs dropped; cuts
outside the placed range are still never touched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
autoCutPlan()/autoCut(): given a beat grid and 2+ cameras, place camera cuts
on grid downbeats at a chosen bar cadence, cycling cameras so the same angle
never lands twice running, with a phase offset so a from-the-playhead run
doesn't open on the camera already live. Cut times come straight out of
gridTimes('bar') so a placed t IS a downbeat rather than a rounded neighbour.
Applied inside group() = one undo; refusals throw rather than half-apply.
UI: a scissors popover with cadence, range, per-camera checkboxes and a live
count that is literally the plan (previewed count == applied count).
FILM panel gains 'align to beat': snaps every shot's in/out to the nearest
downbeat, never reorders, clamps inside the scene, returns collapsing shots
untouched with a skipped list, and always names which grid it used.
Verified on scenes/music-video: 4 cuts at exact downbeats 3.749s apart
(2 bars at 128.02 BPM), cam2->cam3->cam4->cam1, one undo restores exactly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Beat grid consumed from Lane C's /beats: one top-level scene.beatGrid (beats
kept in FILE seconds, effective offset re-derived from the audio clip so
dragging the audio drags the grid and head-trim leaves it put). beat and bar
join the snap selector, so keys, clip and audio blocks, both trim edges, cuts
and the playhead all became musical for free. Grid drawn faint on beats and
brighter on downbeats, suppressed below ~7px spacing. Low confidence still
snaps but says so. Tempo extrapolates past the analysed range.
FILM panel mounts inside #timeline: sequence CRUD, editable shot rows (scene
dropdown, in/out, cut|dissolve + seconds, reorder, delete), live runtime, size
picker, and Render film with per-shot/per-frame progress - gated behind
hasContent() + a confirm that says plainly every shot replaces the open scene,
and locked while running so it cannot be started twice. film.js imported
lazily so its absence cannot stop the timeline mounting.
Verified live: grid 128.02 BPM/35 beats off house128.wav, snapped positions
8-10ms from the true 0.468735s grid across the whole file, bar snap hits a
downbeat, grid survives toJSON->load; FILM lists both sequences with the
server's 11.50s runtime and 3 editable rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/beats?path= returns bpm, a phase-locked beat grid, downbeats (4/4) and an
honest confidence, using only stdlib + ffmpeg: decode to PCM, log-energy onset
flux, harmonic-enhanced autocorrelation with an octave check, comb phase
search, weighted LS grid refit. 4-min track analyses in ~0.5s, cached after.
Audio in/out now honoured at mux in both scene renders and film beds via one
resolve_audio() path. Found en route: on this ffmpeg build the obvious
atrim/asetpts + amix/apad chain yields an aac stream with ZERO samples, rc=0,
no warning - trim moved to the input side; and the test helper would have
passed on that empty stream, so it now refuses to answer on a window that
decodes nothing.
Tempo bias fixed after orchestrator ground-truth check: decoding at 22050
discarded the HF transient, and since the attack's HF share grew across a file
the surviving LF onset landed progressively later - a per-beat-varying
lateness that reads as tempo error. BEAT_SR now 44100/512 (same 86fps
envelope). Second bug found by sweeping: harmonics outside the lag table
counted as zero and the parabola was fitted on a slope, so 118 BPM read
114.79.
Verified vs independent ground truth: 120.030 vs 120.003 true, 128.020 vs
128.004; worst offset from the true grid 6.0/10.1ms over the whole file (was
45/52ms). Tests now assert whole-file DRIFT, not average BPM, incl. a 200s
track and hftick.wav (16kHz tick over a 60Hz body) which pins BEAT_SR.
18 groups green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sequences (ordered shots = scene + in/out + transition) with full validation,
CRUD, and a client-driven film render the server orchestrates: browser renders
each shot through the existing /render/* endpoints, server ffmpeg-concatenates
in order with cuts or xfade dissolves, muxes a film-level audio bed, reaps
after 48h. render.js refactored so there is one frame-post path in the repo
(captureFrames/withRenderSize/begin/end/poll); new web/film.js drives the loop.
Also fixes a pre-existing mux bug: no apad meant short audio truncated PICTURE.
Orchestrator: deploy compose/script now carry SCENEGOD_SEQUENCES + SCENEGOD_FILMS
volumes so shot lists survive a rebuild.
Verified live: demo-two-shot renders one 11.500s mp4 with a genuinely
compositing dissolve; a film built from the M7 templates (street wide ->
dissolve -> closer -> cut -> record store) renders 11.800s at 960x540.
15 test groups green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lane B: New... in the scene bar fetches the templates API and applies through
a new single apply path (applyScene = stage.applyState + load + preload, now
shared with Load), confirm-before-clobber via hasContent(), thumbs, toasts on
failure. ? overlay documents 24 real bindings read out of the code across
STAGE/TIMELINE/SCENE BAR, ignores ? while typing. Audio head-trim (left edge
eats into the source, one grouped undo). applyScene strips the template key so
saved scenes stay plain scene JSON.
Orchestrator SYNC-9 fix: help overlay dt column was auto-width + nowrap, so
long terms overflowed and the three columns overlapped visually.
Verified live: picker lists 3 (2 thumbs), no confirm when empty / confirms when
dirty, loads 7 entities + 2 cuts with bar refresh, no template key in toJSON,
overlay opens on ? and Escape closes, no hijack while typing. 5 suites green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
templates/ ships with the repo (two-hander-golden-hour, record-store,
empty-stage), GET /templates + /templates/{name} behind the same slug and
path-traversal guards as /scenes; presentation metadata rides in a template
key that validate_scene ignores and toJSON drops, so loading then saving
yields plain scene JSON.
Test group asserts the three list in order, round-trip, traversal/unknown
rejected, write verbs refused, and every template passes validate_scene with
every referenced asset present on disk - the check that stops a rotten
template shipping. 10 groups green.
Verified live: two-hander loads 7 entities, 8s, both performers animating,
cut fires cam1 to cam2 at 4.5s; other two load clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lane A: bone-alias layer so CC_Base_*/Unreal-HumanIK/Rigify/Rokoko/Biped rigs
fold onto the canonical mixamo names — lady.glb pairs 52/102 bones and
retargets (53 tracks) where she previously threw 'missing hips'; mixamo→mixamo
pairing asserted bit-identical. Backdrops gain frustum auto-fit, floor tint
sampled from the plate, and FogExp2 in the plate's colour via a keyable
param. Also caught: _buildLight ran _refreshDefaults before registration, so
scene lights never dimmed the built-in hemi (every preset was washed out).
Orchestrator SYNC-7 fixes (found only by rendering actual frames):
- editor chrome (gizmo/grid/helpers/cam proxies) was baked into output mp4s
- timeline's per-tick rest transform dragged fitted plates out of frame
- fit used the viewport aspect, output renders at its own → letterboxed plates
- plate offset put its top edge on the lens axis → void above centre-frame;
now params.fitAnchor (default 0.75) picks the row that sits at eye level
Verified: 5 suites green; 1280x720 render shows a CC character dancing a mixamo
clip against an edge-to-edge Flow plate with a horizon and matched ground.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- stage: detectVisemes() maps CC/ARKit/plain morph names -> canonical
A/E/I/O/U/MBP/FV/L at character load; visemeTargets(id) + setMorph(id,name,w)
for the timeline to drive lip sync; visemeSelfCheck() asserts the matcher
- dock: inspector 🗣 badge (greyed when a rig has no mouth morphs)
- stage: camera/light default source:{type:'none'} (PLAN §4.1); applyState
skips only upload mesh/backdrop bytes -> rig survives save/load (SYNC4)
Verified live at :8020: selfcheck ok=[A,FV,L,MBP,U]; save/load keeps cam+light.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PiP: sized in _resize (~24% w, 16:9, bottom-right) — CSS aspect-ratio is
unreliable on <canvas> and had ballooned it. Click-to-swap main<->active cam;
_render(cam,canvas) restores aspect after a pip blit so swap never distorts.
renderActiveCamera/pause/resume contract unchanged (M3 finalRender safe).
- room3d: SpecGlossPlugin on GLTFLoader — CC/Reallusion GLBs use
KHR_materials_pbrSpecularGlossiness (dropped in three r160) and rendered
flat white; map spec-gloss -> MeshStandard. lady.glb now withMap=26/26.
Verified live at :8020, zero console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- dock: /assets/tree {name,path,formats,thumb}; +camera/+key/+ambient toolbar;
clip apply dispatches scenegod:clipdrop when timeline present (else preview);
drag-from-dock places onto the viewport (screenToFloor / entityAt)
- stage: PiP overlay from active camera; renderActiveCamera hardened as M3 target
+ pause()/resume(); backdrop rebuild-on-param; fire onChange on add+remove
(fixes tlui empty-names + a gizmo mouseUp id-vs-entity bug); set-active-camera
- verified in the integrated app (:8020, Lane B timeline mounted), zero console
errors. Found + logged the entity-mirror integration gap (timeline model isn't
seeded from live stage adds) for the orchestrator.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>