FEATURES (from a 5-lens audit: UX friction, feature gaps, bug hunt, visual
quality, discoverability):
- ACES filmic tone mapping + per-preset exposure. grammar.js runs physical sun
intensities of 1.2-3.4; with NoToneMapping every lit face clipped to white and
a warm key rendered neutral. Single biggest look win in the app.
- Real shadows: PCFSoft, a shadow box fitted to the scene's bounds, and plates
that no longer double as the shadow catcher (dedicated ShadowMaterial).
- A Render button in the scene bar. finalRender had ZERO callers - the app's
deliverable was console-only.
- Save actually saves the stage you built (gizmo moves, inspector edits), and
Save/Load stop lying about what happened.
- 2x supersample + lanczos downscale; correct bt709 tagging and faststart.
- Frame guide, camera-from-view, dock counts, delete confirmations.
DEFECTS FOUND BY ADVERSARIAL REVIEW AND FIXED (all reproduced first):
- BLOCKER: save wrote the playhead pose over the authored rest transform of
keyed entities, so every save produced a different file.
- renders were converted with the bt601 matrix while tagged bt709.
- deleting an unkeyed camera stranded its cut -> scene 422s forever.
- corner plates showed one photo at two exposures across the fold.
- exposure was advertised as keyable but nothing keyed it, so a second lighting
preset permanently poisoned the first.
- the audio pre-flight could never fire (FastAPI does not route HEAD -> 405).
- the two render buttons were not mutually exclusive.
- frame guide letterboxed a narrow viewport that the render does not crop.
ORCHESTRATOR (round 3): dangling camera cuts are pruned in Timeline.toJSON and
skipped by cutCameraAt, rather than trying to keep every undo history clean -
deleting a camera spans Stage (no undo) and Timeline (undo), so any older entry
can resurrect a cut for a dead camera. Verified against the real validator.
Also replaced a VACUOUS test that drained a stack whose only entry was a seeded
no-op; timeline_test.mjs now replays both real histories and discriminates.
Verified: 4 JS suites + 23 server groups + render client green; blocker repro
now preserves the authored rest at every playhead position; save round-trips
200 with zero dangling cuts; zero console errors.
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>
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>
GET /rhubarb?path= -> rhubarb -f json -> mouthCues for Lane A's viseme lane;
503 when the binary is absent (not installed on ultra yet). Path-guarded.
test_server.py: +503 gating assert (9 groups green).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>