# puppet-sprites Agent Guide

Base URL: `https://puppet-sprites.aisloppy.com`

The player’s FPS checkbox shows actual requestAnimationFrame cadence and a bounded 30-second history, with long-frame gaps retained in the history; it samples only visible playback and stops on pause, hiding, scene changes or leaving the page. It is local browser monitoring, without telemetry enrollment.

Live playback profiling is opt-in at `/composer?show=<id>&scene=<id>&profile=1`.
Play records a bounded 15-second sample, with measured frame rate, 95th-percentile
frame interval and a shared Telemetry nested timing tree. Pause or navigation
ends the sample as cancelled; the last result is retained on that browser by
scene. Timings aggregate actual calls and include browser/idle time at the root;
they are not a GPU trace or a claim about unmeasured devices. Record mode is excluded.

All live scene levels and auditions use Web Audio gain nodes, including score
ducking and bed envelopes, so iOS respects relative volume settings. Score upload
through `PUT /api/scenes/<id>/score` measures the source and applies one constant
gain toward -18 LUFS, constrained by true peak, matching the effect source scale.
Original source audio remains available as `source_path`; the stored music track
retains `normalization: {lufs, true_peak_db, gain_db, target_lufs}` separately from
its volume. Authored score dynamics and slider settings are preserved. Existing
scores need re-publication through that same endpoint to receive normalization.

`GET /api/health/embellishment` returns content-free daily counts of retained A/B capture failure reviews and completed comparisons from the last 30 days. Counts are saved Versions reviews, not every render attempt; recovery retains earlier failures. Each day also includes `direction_revision_requests`: recorded human feedback with direction concerns and explicit revision-request intent, by UTC recording date. These are feedback records, not unique defects or every operator complaint. Current day is partial. No source text, scene IDs or owner identities are returned.

## Purpose
Animated films of books: pose sprites on painted sets, one voiced take per scene. A show made from a book carries the book, its scene plan and the director's direction (the Composer workspace, `/composer?show=<id>`).

The composer’s Scene production view connects source/script, artwork, voices/timing,
direction/movement, effects/music and player/mix. It reads the active take and
canonical asset inventory. Planner models are separate from final choreography
authors; missing historical model/settings evidence is shown as not recorded.
Expand a module for saved decisions and motion parameters; motion entries seek
the existing player. Original prompts and version notes remain collapsed evidence.
`performance.director.production` may retain receipt-backed author/artwork/speech/
music/render metadata and `scope: "complete-production"` for a whole-production
benchmark. This is provenance, not a model selector. New director requests retain
the served model and task ID; new image jobs retain the submitted model/quality
and expose these as `parameters` in the existing scene provenance API.

Scene production also shows the production process and human feedback. Record the
actual director/builder sessions, their roles and models, internal review/correction
loop, and measured timing separately from the served planner models. A first-result
benchmark may still have an agentic build and internal corrections: explain what
was frozen rather than labelling it a single model call. Missing process history
is labelled as created through the primary system before methodology tracking; it is not assigned a fabricated hierarchy.

Since 2026-10-02, every successful scene version also saves an immutable methodology step:
profile `puppet-sprites-primary-v1`, server timestamp, app version/revision, actual writer session/model,
operation and optional supplied role/run/parent context. Root agents can omit parent_session_id. New scene creation is recorded separately from revisions
of legacy scenes. `GET /api/scenes/<id>/production-effort` includes `scene.methodology` with
`creation_recorded`, `created_at`, `tracking_since`, `profile_id` and `steps`. The existing evidence GET
also returns these automatic records with `kind: "methodology"`; POST remains for source-linked
process/feedback/context. Send `X-Puppet-Production` as compact JSON
`{"role":"builder","parent_session_id":"<12 hex>","run_id":"<wave>"}` (roles director/builder/reviewer/editor).
Malformed supplied context returns 422. Film/perform/review scripts accept `--production-context` or
`PUPPET_PRODUCTION_CONTEXT`; the builder launcher embeds its parent and wave in the generated brief.
Records capture successful writes and saved reviews; they do not claim unobserved agent completion.
Historical subsystem recovery is retained in process `details.recovered_modules`, with evidence scope
such as `Original build` or `Original take`, source links and matching artifact identity.

`GET /api/scenes/<id>/production-evidence` returns source-linked records for the
owned scene, oldest first. `POST` appends one immutable record:
`{client_mutation_id, kind: "feedback"|"context"|"process", source: {session_id,
event_seq?, speaker?, quote?, timestamp?}, summary?, concerns?: ["script"|"artwork"|
"voice"|"direction"|"sound"|"player"|"overall"|"process"], at_ms?, version_id?,
export_id?, details?: {…}}`. Feedback/context require the source event, speaker and
exact quote. Session and message links are derived from those identities. Capture
relevant operator complaints and comparison observations when working on a scene;
retain the original words separately from the agent's concise `summary`. Pin the
reviewed version/export when known. A comparison preference is qualitative evidence,
not a numeric rating or an invented Elo result. Concern tags are areas to investigate,
not proof of which model caused a flaw. Process `details` accepts `participants`
`[{role, model, session_id, work}]`, `review`, `constraints`, `result_label`,
`wall_seconds`, and `stages: [{name, seconds}]`; never add overlapping stage times
to infer total duration. Other structured evidence can be retained in `details`.
Exact retries return the existing record; different payloads reusing a mutation or
source event/kind return 409. Corrections are new source-linked records. Invalid
fields, missing source quotes or mismatched version/export return 422; unowned
scenes return 404. Maximum 32 KB per record. Recording evidence never changes the
scene, take, audio, versions or film. Use the shared authenticated HTTP client with
explicit deadlines; this synchronous endpoint starts no model work. For real human
feedback, source events come from the owned FairyStack conversation API, not an
invented recollection. The full session link complements the stored excerpts.

Scene production starts collapsed; its heading opens the overview without hiding Comparisons. Process and Human feedback disclosures also start closed. Quality & effort uses
`GET /api/scenes/<id>/production-effort?compare=<owned scene id>`: `{scene, show,
comparison, comparison_show, observed_at}`. Each scene reports `saved_versions`,
`version_kinds`, `version_models`, `writer_sessions`, `sessionless_versions`,
`review_seconds`, `confirmed_revision_requests`, `take_seconds`, `spoken_words`,
`travel_settings: [{at_ms, speed_mps}]` (saved landscape road-travel cues),
and its latest process evidence. Show history reports `stored_scenes` and
`version_models`. The optional comparison must be owned (otherwise 404); no new
model work or scene changes occur. Record a confirmed human request for a scene
change as feedback with `details.intent: "revision_request"`; do not count general
quality observations, UI requests, acknowledgments or repeated clarification as
extra scene revision requests. This is a lower bound on older human iteration,
not an automatic count of every version or model call. Player viewing time also
includes agent playback under the same account; it is not pure human effort.
Process `details` can
retain `comparison_scene_id`, `case_label`, `short_label`, `context_summary`,
`builder_elapsed_seconds`, `operator_revisions_before_freeze`, `comparison_limits`
and a source-linked `quality_observation: {summary, session_id, event_seq}`.
Pair human preference with measured effort. Existing productions are observational:
different scripts, assets, renderer versions and review histories prevent causal
model ranking. For later controlled comparisons, hold input/context/assets and
human/agent budgets constant, vary the chosen factor, and compare preferred cuts
at that budget. A quality-per-minute scalar requires a defined rating scale;
never manufacture one from complaints or divide Elo/ordinal preference by time.

Making or extending a film from a book: you are its director. Follow `scripts/film_director_brief.md` in the repository: write each plan scene's direction, then build scenes only through `scripts/build_scenes.sh`, one agent session per scene (`scripts/scene_builder_brief.md`). Never stage plan scenes from a batch script or one-shot model calls; the Composer scene plan shows which scenes lack direction and which model made each one.

Reviewer changes can be tested with `scripts/review_scene.py --compare-observation --show <id> --scene <id>
--token-file <path> --session <id> --model <agent-model> --evidence-source-file <verified-source.json>
--workdir <resume-directory>`. The source identifies the actual FairyStack request with
`{session_id,event_seq,speaker,quote}`. Direct judgment and description-before-judgment use identical saved
frames and the same served model; three paid calls total. The comparison does not edit the scene or change
the default reviewer. Its findings and receipts appear under Scene production → Human feedback → Relevant
conversation. `--max-cost` stops subsequent submissions once recorded spend reaches that amount; the last
call can overshoot. The session allowance remains enforced by BrightWrapper. Reusing the work directory
resumes paid receipts. Review is of sampled landscape stills: sound, continuous motion and unseen moments
remain unassessed. More findings do not prove a better reviewer; human adjudication and fresh scenes are needed.

Every agent write to a scene sends `X-FairyStack-Session: <your session>` and `X-Agent-Model: <your model id>` (the scripts' `--session` and `--model`). Each scene version records them (`GET /api/scenes/<id>/versions` returns `session_id`, `model`, `via`), so the film keeps which model created and edited every scene; operator edits in the browser record `via: browser`.

Two-cut embellishment: run `scripts/embellish_scenes.py --show <id> --session <session> --token-file <token-file> --workdir <checkpoint-directory>` as a bounded FairyStack background command (root for SOPS). It watches six story frames per performed scene, makes distinct A/B visual cuts with existing art and audio, lets B borrow useful A ideas, then compares both cuts at matching times including up to ten added-action frames. Review frames use the film export's virtual clock so brief poses and effects remain visible. Default batch ceiling is $40 within the session's existing allowance; the service budget remains authoritative. Resume the same directory to observe accepted paid tasks rather than paying again; `--review-scene <id>` re-examines a saved pair without regenerating its plans. Errors and comparison notes appear in the scene's Versions panel; this experiment never replaces the current film or purchases missing art automatically.

Saved comparisons appear in **Comparisons**, beneath Scene production. Each independent experiment is grouped by its pinned `pair_key`, retains its own A/B cuts, and may contain several review regions; no whole-scene A/B choice is placed in scene navigation. Each comparison offers Original beside A and B: Original plays that pair’s pinned starting version, which can differ from the current scene. All three share the exact review region and five-second lead-in, and their snapshots/assets are prepared together. Each cut’s button and action description share one clickable row. Rebuilt pairs with recorded inherited-cut lineage replace their parents in the active panel; those parents remain accessible under Earlier comparisons, including their original deep links. Similar wording alone never hides a comparison. A concise title names the comparison, shared action appears once, and plain-English A/B descriptions show the visible difference. Other region links expose differing actions; direction notes retain the broader concepts and borrowed ideas. The ordinary Versions list retains other history and promotion/restore controls.

The scene player starts with **Autoplay off** on every page load. Its toggle opts into advancing to the next scene; manual Next remains available. With Autoplay off, the final frame stays visible and Play restarts the scene. Recorded films keep their continuous clock.

A/B Versions responses include `review: {summary, label, pair_key:[run_id,base_version_id], inherited_version_id, focus_ms, points:[{at_ms,label,distinct,from_ms,to_ms,play_from_ms,context,actions}]}` derived from each cut’s saved director metadata and actual added cues against its pinned base. The highlighted region uses the pinned take: `from_ms` is the changed action, and `to_ms` ends within its shot, normally after four seconds or the spoken line. `play_from_ms` begins playback five seconds before the highlight for orientation; it does not enlarge the comparison segment. Shared action is described once in `context`; `actions` explains each cut's visible choice in plain English. Annotation-only changes never create a visual comparison. `/composer?show=<id>&scene=<id>&version=<id>&t=<seconds>&compare=<change seconds>` opens paused at an explicit scene take time and keeps the indicated comparison region selected while seeking or reloading. An Original link also includes `comparison=<A version id>` to pin the experiment context when several pairs share one starting version. The comparison moment must belong to that cut; older `t` links infer it from the lead-in or use the default focus. Clicking a regional comparison link starts playback only after its required assets are decoded. The timeline shades that region across its tracks; its Review time-range button returns to the highlighted start. The blue Scene clock shows elapsed/duration in that take, distinct from the overall film clock.


`POST /api/scenes/<id>/versions` accepts `{kind: baseline|review, note, client_mutation_id?, base_version_id?}` or `{kind: experiment, note, base_version_id, changes: {stage_config?, prop_config?, add_cues?}, experiment: {label: A|B, ...provenance}, client_mutation_id?}`. The pinned version must belong to the scene; owned assets, drawn poses and timed visual cues use the usual contracts. Experiments preserve the take, words, existing cuts/audio/look/travel cues. Exact mutation retries return the original version; conflicting reuse returns 409. Play any cut at `/composer?show=<show>&scene=<scene>&version=<version>` or select its region in Comparisons. Reviews can pin `base_version_id` to document the cut they examined.

Quick tweaks: the composer's Tweak box sends `POST /api/scenes/<id>/tweak {note, at_ms}` → 202 `{job_id, model}`. A `scene_tweak` job (poll `GET /api/jobs/<id>`; the composer shows BrightWrapper's progress bar) makes one structured call on the pinned model (`GET /api/scene-tweak` names it; no routing, since models are not interchangeable here) that edits only `stage_config`, `prop_config`, `audio_config` and the take's `cues` through the same validators as other edits, and records a `tweak` version naming the model. A completed job's `result_path` holds `{summary, edits, model, seconds, version_id, undo_version_id}`; a note it cannot honour (new art, words, a new take) fails with the model's reason and changes nothing. `POST /api/scenes/<id>/versions/<version_id>/restore` makes any earlier version current again as a new `restore` version (Undo, and the versions list's "Make this the current version"). Missing effects use licensed stock recordings through the existing sound-effects upload and sound-design APIs; tweaks never buy or synthesize audio. Library effects such as `camera_flash` are ordinary `scene_effect` cues.

## Standard Endpoints
- `GET /api/health` - Service health check.
- `GET /api/version` - App version.
- `GET /architecture` - Architecture page (typically login-gated).
- `GET /agent-guide.md` - Machine-readable integration guide.
- `GET /agent-guide` - Human-readable rendered guide.

## Authentication
- Endpoints marked as auth-protected require a valid JWT.
- Browser clients should use the app's own sign-in UI.
- API clients should send `Authorization: Bearer <jwt>` unless the app guide documents a different auth mechanism.
- If the app exposes local signup or login endpoints, document those app-local endpoints here instead of pointing agents at third-party auth vendors.

## Integration Rules
- Fail fast on errors; do not silently degrade.
- Read `PORT` from environment.
- Use network APIs between services; no cross-app imports.
- Long-running work must start async jobs and return `202` with a task ID quickly.
- Durable task status belongs on `GET /api/tasks/<task_id>`.
- SSE is optional live transport only; do not make it the sole source of task state.

## Notes For Coding Agents
- Start with `GET /agent-guide.md` for current contract.
- Confirm endpoint auth requirements before calling.
- Include explicit timeouts and propagate errors.

## Characters, pose sheets and scenes
- Jobs: `GET /api/jobs/<job_id>` (202 while pending or running, 200 when terminal).
- Test shows: create trials, experiments and probes with `POST /api/shows {title, …, is_test: true}` (or `PUT /api/shows/<id> {is_test: true}`). My Shows hides them unless the viewer turns on its Test shows toggle; real productions leave `is_test` false.
- Show cards: give every show an `emoji` (`PUT /api/shows/<id> {emoji}`). The My Shows cover is the show's opening set plate with its most-staged character on it; pin others with `cover_background_id` / `cover_character_id`. `GET /api/shows` returns the resolved `cover {plate_path, sprite_path}`.
- Film: `GET /api/shows/<id>/film` (title, plan with each scene's number, direction and staged scene, standing direction, page count); `GET /api/shows/<id>/film/pages?first=&last=` (at most 60 pages); `GET /api/shows/<id>/film/search?q=<regex>&limit=` finds passages across the whole book (`scripts/film.py search --q`) — ask the book ("where does the goo come off?") instead of rereading it; `PUT /api/shows/<id>/film/source {title: "<title> by <author>", pages: [text]}`; `PUT /api/shows/<id>/film/plan {plan: {logline, acts: [{title, summary, sequences: [{title, summary, scenes: [{id, ...}]}]}]}}`; `PUT /api/shows/<id>/film/direction {direction, scene_id?}`; `GET /api/film-spend`. A scene realizes a plan scene with `plan_scene_id` on `PUT /api/scenes/<id>`. `scripts/film.py` (source, plan, direct, pages, reference, voice) wraps these for agents. The composer's independent Book passage disclosure displays the exact extracted pages for that scene's plan range. `/composer?show=<id>&book=<first>-<last>#book-source` opens those pages in the full book reader, with earlier/later navigation through every retained PDF page. Scene plan page labels link there too. The shared BookPassage component uses the canonical page API; this is source text, not the screenplay or an agent summary.
- `POST /api/characters/<id>/generate-reference` with `{brief?}` designs the character's reference sheet from its description, the brief and the show's style; the job's `result_path` joins the base look's references.
- `POST /api/characters/<id>/reference-images` with `{image_base64, mime_type}` attaches an approved design image (at most four) to the character's base look. Pose sheets for a look with references are generated from them with GPT Image 2, so identity survives restyling.
- `POST /api/characters/<id>/generate-pose-sheet` plans six poses (`neutral`, `speaking`, four story gestures) from the scenes that include the character, generates one sheet and cuts each pose out (ISNet). Poses face screen-right; stage `flip: true` faces a character left. A scene director can instead send **1–6** poses: body `{poses: [{tag, label, description, holds?}], force?: false}` (same on `/api/look-variants/<id>/generate-pose-sheet`); `holds` is drawn into that cell ("a tiny black dart gun"), and the new poses join the look's existing ones, so a sheet of action poses may leave out neutral and speaking once the look has them. A single malformed cell needs only a one-pose repair; omitted tags and prior candidates remain intact. Directed grids are 1×1, 2×1, 3×1, 2×2, 5×1 and 3×2 (read left-to-right then top-to-bottom); responses report the actual `pose_budget` and `pose_count`. Omitting `poses` retains six-pose automatic planning.
- Image model: every paid image (sprites, pose sheets, references, props, sets) is gpt-image-2 at medium quality through BrightWrapper (`IMAGE_SETTINGS` in `backend/server.py`). `/image-models` shows the evidence: saved production requests replayed on other models (cost, time and pictures beside the high-quality baseline). Inspect that evidence before changing the model.
- Pose generation reuses an identical active job, or a completed candidate only while its requested tags still point to those same assets. The identity includes the prompt, pose intent/held items, approved reference content, look, style, model and quality. `force: true` intentionally generates a new candidate after matching active work finishes. Job metadata persists inputs and provider task ids; a restart or transient polling error resumes observation without sending another generation. Interrupted acceptance before recording a remote id fails visibly and requires checking provider history before a deliberate redo. The owner has a 25-minute overall deadline, bounded provider polling and terminal cancellation/timeout states. Paid sheets remain saved even if extraction or description fails.
- Pass `X-FairyStack-Session: <your session id>` on all image generation requests to preserve attribution and the session budget across background execution. New sprite, reference, background, prop and pose image calls, plus pose planning, description and prop wheel-reading calls use `puppet-sprites.show.<show-id>` project names; `/api/project-costs` continues rolling up every Puppet Sprites show. General image jobs have a 17-minute overall deadline including submission; pose jobs retain their 25-minute owner deadline. Existing historic totals remain unattributed to individual shows.
- Repaired pose artwork: `PUT /api/look-variants/<id>/pose-artwork` with `{pose, image_base64, note}` replaces only an existing pose's PNG. Supply a nonempty RGBA PNG (up to 10 MiB, dimensions 32–4096 px) and a short repair note. The content-addressed original remains available in `artwork_history`; the base look synchronizes its character. Design changes clear an obsolete animation sheet; background-only repairs retain it, preserves pose metadata and records provenance. For a background-only repair, also send `background_removal_mode` naming the canonical method; this records the method and retains the original opaque `source_path` for future comparisons. Optional `expected_path` rejects concurrent artwork changes with 409; `source_base64` stores a recovered opaque source on the same registered canvas. Returns `{pose, path, variant_id}`; unknown pose or invalid PNG returns 422, unauthorized look returns 404.
- Approved animation cycles: `scripts/motion_cycles.py --character <id> [--look <id>] --pose neutral --session <session>` gives any character a walk + idle cycle (Kling v3 on fal through BrightWrapper, ~$0.84, resumable, never buys twice); it stops for review of the sheets and `--attach` publishes them. `PUT /api/look-variants/<id>/pose-animation` with `{pose, note, sheet_base64, frame_count, frame_duration_ms, cell_size: [width,height], foot_anchor: [x,y], figure_height_px}` stores the horizontal sheet (equally sized transparent cells) and sets only `poses.<tag>.animation`, keeping the pose's still `path` and every other pose; a missing pose is created from `{still_base64, drawn, height_fraction}`. The base look updates its character in the same transaction. The composer selects frames on its playback clock, resets at a performance pose cue, and registers the feet and physical height; pause, seek and film exports share that clock.
- Scenes: `POST /api/shows/<id>/scenes`, `PUT /api/scenes/<id>` with `background_id`, `stage_config[{character_id, flip, expression, position_m}]` and `timeline[{type: dialogue|narration, speaker, text, cues}]`. Speakers must match a character name with a cast voice. Pose cues `{type: character_pose, character_id, pose}` must name an existing pose. A stage entry's `variant_id` is the look a character starts in; a costume change mid-scene is a beat cue `{type: character_look, character_id, variant_id?}` (no `variant_id` = the base look), which `perform_scene.py` keeps at its line like a cut — put the new look's opening `character_pose` (`at_cut: true`) right after it, since later poses are drawn on the look worn at that moment. Set `development_status: active`, then poll `GET /api/scenes/<id>/build-manifest` until `ready`. Play at `/composer?show=<show>&scene=<scene>`.
- Reusable pose sizing: `PUT /api/characters/<id>/pose-size` or `/api/look-variants/<id>/pose-size` with `{pose, size_scale: .05..8, expected_size_scale: .05..8, expected_path}` stores only a source pose’s `size_scale`; absent means 1. It multiplies inferred `height_fraction` in the shared player, including plate/vehicle registration, animations and exports. Base character/base look sizing is synchronized when they use the same artwork. Named looks remain independent. Artwork or calibration changes return 409; unknown/unowned owners return 404; invalid values/poses return 422. Returns `{character_id,variant_id,pose,path,size_scale}`. The editor’s **Save in scene** retains local placement; **Save size everywhere** publishes a size-only gesture to the source pose, for every scene using that pose/look. It preserves source art, inferred anatomy, other poses and scene adjustments. Reloading another scene reads the saved correction. New rendered downloads require an export refresh.
- Pose sprites carry `drawn`, a description of what each cut-out actually shows (helmet on/off, what is held), written from the image after the sheet is cut; the plan's `description` can disagree with the drawing. `POST /api/characters/<id>/describe-poses` (`?tags=a,b` for just those poses) rewrites it, together with `height_fraction`: how tall the drawing is next to the character standing (crouch ≈ 0.6, arms overhead ≈ 1.2). Sheets draw every figure to fill its cell; the composer sizes each pose by that fraction. Scene performances require it.
- Voices: `GET /api/tts-voices` lists the 13 speech presets with their gender, age and timbre. Cast from those traits (`PUT /api/characters/<id>` with `voice`, `voice_direction`); direction shapes accent and mood but can't change the preset's apparent gender or build.
- Performances (preferred for dialogue): `scripts/perform_scene.py --show --scene --token-file --session --model` (run as root for SOPS) uses each character's designed voice (`performance_voice`, designed by `scripts/film.py voice`; `PUT /api/characters/<id>` with `{provider: elevenlabs, voice_id, source, brief}`), has a director model tag the lines for acting, voices the whole exchange in one take on fal, times every word with speech-to-text (Scribe v2; every fal call goes through BrightWrapper and counts against `--session`), then shortens the gaps and anchors poses and prop moves to words. It stores the result with `PUT /api/scenes/<id>/performance` (`{audio_base64, duration_ms, lines[{beat_index,start_ms,end_ms}], cues[{t_ms, type, …}], cast, director}`); the composer then plays that take on one clock with subtitles on the picture. A voice-provider failure after successful direction can reuse the retained `{lines: [{beat_index, text}]}` receipt with `--tagged-lines <file>`: the same exact-spoken-word check runs before voice submission and the paid tags request is skipped. Tagged lines are saved before the voice call. For an edited recording, include the matching `timeline` and `if_match_updated_at` in that same performance PUT: text and take save together without clearing the take or enqueuing duplicate speech; stale writes return 409. Changing the timeline's words clears it (changed cues or acting notes keep the take; re-stage with `--reblock` to pick up new cuts); `DELETE` returns to per-line playback.
- Narration and movement: narration beats are spoken by an unseen character named `Narrator` (give it a designed `performance_voice`, `film.py voice --unseen`; don't stage it). `character_move` cues `{character_id, to_position_m, duration_ms, flip?, arc_height_m?, easing?}` move a character (held props travel with it); `to_position_m.y_m` lifts them off the floor (a rope, a roof), `arc_height_m` adds a leap's sine arc, and `easing` is `in_out` (default, a walk), `linear` (a leap or run) or `in` (a fall that accelerates); `from_edge`/`to_edge` (`left`|`right`) walk someone into or out of shot from wholly outside that frame edge at the move's depth (the player measures the picture's width; to_position_m still gives y and z). After the authored movement ends, exits continue at the movement speed until the edited picture (including saved size/position corrections) and an extra half-picture-width gap have cleared the edge; the take clock then retires the object. The original on-screen movement and its timing stay intact. Never guess an off-frame x: the visible width grows with depth and a figure's picture is wide, so a guessed x leaves them half in shot; a `prop_move` to a `to_position_m` drives a prop such as a car.
- Cuts inside a scene: a timeline beat's `background` (`background_id`), `character_enter` and `character_exit` (`edge` left|right) cues are the screenplay's cuts (a flashback set, the destination). `perform_scene.py` places them just before that line and tells the blocking director which set and cast each line plays on; mark any other beat cue `at_cut: true` (e.g. a car already parked on the new set) to keep it with the cut. Camera travel shows only on the scene's own set; a cut elsewhere shows that set still.
- Openings, transitions and effects: the composer opens the show's first scene on a title card, fades every scene up from black and out to black before the next. Performance cues `{type: scene_effect, effect: speed_lines|impact|camera_flash|spam_flicker|snow_crash|stutter, duration_ms}` add animated effects on the take's clock (`snow_crash`: a flash of full-frame flickering TV static, keep it under a second; `spam_flicker`: a hot-pink/orange ad leaking through a junkbuster proxy; `stutter`: about 1.6 s of the whole picture freezing dim and jerking in fuzzy stop-action, a computer swamped by data). Give a `data_pulse` or `slashdot` cue a `character_id` to pin it to that character's head (a cyan blink or pixel burst at his glasses) instead of the whole frame. `{type: sound, sound_id}` plays one of the scene's `audio_config.events` tracks (each with an `id`, e.g. a phone ringing or a splash) at that moment of the take, in the composer and in rendered films; put it on a timeline beat with `at_cut: true` to keep it through re-staging, or place it exactly in the stored performance.
- Entrances and exits: a `prop_move` frame may give `from_edge`/`to_edge` (`left`|`right`) instead of an x: the player puts the prop wholly outside that frame edge at its depth (its picture's width included), so a vehicle drives into and out of shot. Show a prop only where it is out of frame or when its move starts from an edge; never make a moving vehicle appear or vanish inside the picture.
- Dazed reaction: `{type: "scene_effect", effect: "dazed", character_id, t_ms, duration_ms, head_anchor?: [x,y]}` makes the seated or standing character lurch forward at impact, settle, and briefly see three stars circling their head. Duration must be 1–20000 ms; head_anchor uses source-image fractions, default `[0.5,0.08]`. Playback, pause, seek and export share the take clock. The body stays clipped by vehicle glazing; stars render above it.
- Sound audition: `scripts/review_audio.py` evaluates the actual normalized recordings with `fal-ai/audio-understanding` through the receipt-backed shared BrightWrapper owner. Blind descriptions (no titles/briefs) are cached by audio and request hashes; direction chooses from what was heard and records physical interactions and selection reasons. Every proposed mix is heard with narration/music before publication. `POST /api/scenes/<id>/sound-design/preview` accepts the same direction with `if_match_updated_at` and returns its unpersisted scene snapshot; it changes no saved scene/version. `record_film.mjs --audio-only --sound-preview <direction.json>` uses the production composer layout and mixer. A rejected mix leaves the current scene unchanged. This is model-assisted listening, not human approval.
- Sound direction: `scripts/direct_sound.py` selects licensed stock recordings from `config/stock-sounds.json`, anchors them to performed words and publishes through the shared sound APIs. AI sound generation and locally synthesized substitutes are disabled. Missing stock clips are named in saved direction notes for sourcing, never replaced with generated audio. The catalog retains title, source/license links, recording hash and excerpt processing. `--prepare-only` prepares the saved selection; `--apply-plan` resumes publication using its durable provider receipt. `--published-cut` selects only chapters in the newest full film; `--augment` returns missing additions while retaining approved cues, tracks and envelopes. `PUT /api/shows/<id>/sound-effects` accepts `{effect_id, audio_base64, brief, provenance}` for a 0.1–30 s MP3; its shared owner normalizes to −18 LUFS and retains provenance. `PUT /api/scenes/<id>/sound-design` accepts `{take, notes, events:[{effect,t_ms,volume,anchor}], beds?:[{effect,start_ms,end_ms,volume,fade_ms,prop_id?}], augment?:boolean, if_match_updated_at?:string}` and preserves narration, music and visual staging. `augment:true` appends atomically without rebuilding existing sound tracks; retries of the last addition are deduplicated. A stale `if_match_updated_at` returns 409. Driving recordings play only through actual movement; a parked engine needs a separate idle recording. Beds loop within their bounded take-clock intervals and use the same gain envelopes in playback, seeking, timeline and exported audio. A crowd can cut off abruptly; operating machinery stays audible until departure. The director covers motivated voices/reactions, contact/impacts and electrical/mechanical sources rather than limiting each scene to a few isolated hits. New/re-staged performances use this same stock-only path.
- Score: music written for a scene's current take. `PUT /api/scenes/<id>/score` `{audio_base64, plan, brief, volume?}` stores it as the scene's `audio_config.music` track with `follows_take`; the player runs it on the take's clock, dips it under every line and fades it out with the picture (exports too). `scripts/film.py score --show <id> --scene <id>` composes one: a model reads the scene (book, direction, lines, set cuts, travel, neighbouring scores) and writes an instrumental composition plan whose sections change where the scene does, then ElevenLabs Music renders it. A new take needs a new score.
- Travel: a `prop_config` entry with `passing: {every_m, z_m, y_m?, offset_m?, from_ms?, until_ms?}` is scenery that rushes past while the camera travels (every_m up to 5000, so a one-off landmark or a rare truck passes once a scene rather than every few seconds; traffic adds its own `speed_mps` along the road — slower same-way cars get overtaken, negative is oncoming — and `flip: true` to face the other way — flip mirrors the whole picture, lettering included, so lettered traffic gets its own left-facing prop (`view: "side profile facing left"` with the right-facing image as `reference_image_base64`); vehicles with a wheel rig roll their wheels). Optional integer take times `from_ms` (inclusive, default 0) and `until_ms` (exclusive) limit a landmark's appearance or swap its artwork at an impact without restarting its road position. Times are 0–3600000 ms and until must be after from. Performance cues `{type: travel, speed_mps}` set the camera speed (eased; negative travels leftward, for a journey that runs right to left), which scrolls a seamless version of the set plate slowly, races lane dashes along the road and slides passing scenery at its depth.
- Floating and bouncing: a `prop_config` entry's `bob: {amplitude_m, period_s?, phase?}` lifts the prop up to `amplitude_m` above where it is put and lets it settle again on a slow loop (default 3 s) on the take's clock: a hovering HUD vision, a moored boat rocking, a rubberized building bouncing (a cut-out of the painted building placed exactly over itself: `POST /api/shows/<id>/props` with `cutout_png_base64`, a PNG with alpha you matted from the plate, registers it as drawn and returns `{id, image_path}` at once). Held props do not bob.
- Agent-made plates: `PUT /api/backgrounds/<id>` with `image_base64` (a landscape PNG/JPEG/WebP, e.g. the painted plate re-framed closer so people stand bigger on its floor) and an optional `image_note` replaces the set's picture.
- Set repaints from a plate: `POST /api/backgrounds/<id>/generate` takes optional `reference_image_base64` (the same place as painted before); the new plate keeps its composition, viewpoint and hand and changes only what the set's description says has changed.
- Ground trails: a performance cue `{t_ms, type: character_trail, character_id, style?: scorch|blood, until_ms?, clear_ms?: number|null, width_m?, fade_ms?}` draws a mark on the floor behind that character as they actually move on the take's clock (a scorched, smoking drag furrow with cooling embers, or a skipping blood smear), growing from `t_ms` until `until_ms`. Omit `clear_ms` to clear the mark at the next `background` cut; a numeric `clear_ms` clears it at that exact time, including the held final frame if it equals `duration_ms`. Set `clear_ms: null` to keep it through subsequent cuts and the final frame, without an out-of-range timestamp. `to_position_m {x_m, z_m}` (with `until_ms`) runs the mark on along the floor to that point when the figure leaves the floor (lifted into a machine); it is in the character's stage coordinates, so its sprite-editor placement applies. The mark stays where it was laid. `width_m` is across the floor (defaults 1.6 scorch, 1.3 blood); `fade_ms` is how long embers glow and smoke rises (defaults 2600/1800). Pause, seek and rendered films draw it identically. Use it instead of static stripe or streak props for anything left behind by movement.
- Cables: a `prop_config` entry's `tether: {character_id, kind?, up?, reach?, anchor?: {x, y}, poses?: {<pose>: {up?, reach?}}, color?, width?, slack?, reel_mps?, max_slack_m?}` draws a hanging rope from that character's hands (`up` of the drawn figure's height, default 0.5; `reach` of it toward where they face, default 0.1; per-pose overrides in `poses`) to a point on the prop's picture (`anchor`, fractions from its top-left, default the centre), whenever both are on screen and the prop is out of their hands. It draws just in front of the prop, so the cable lies over the magnet. A poon: the magnet prop is held, flies to the car with a `prop_move` (the cable pays out as it flies), and stays stuck there; the cable follows the rider at any distance. The rope is simulated on the take's clock: it is straight while pulled, pays out as the ends separate, and when the rider moves toward the prop the extra length sags, swings and lies on the road while a reel winds it back at `reel_mps` (default 0.6 m/s, at most `max_slack_m` hanging, default 1.5) down to a resting `slack` (fraction of the span, default 0.012). Keep props that are released, thrown or planted as independent scene objects after the handoff; attach them through `sprite_registration` to their receiving object so pose changes cannot erase them. Existing pose art may depict an object before release; reveal the separate prop at that pose's handoff without drawing it twice. Draw the rider's poses without a cable. For a laser, set `kind: "beam"`: the same anchors define a straight light beam with no gravity, slack, floor collision or remembered motion. The default `kind: "cable"` retains rope physics.
- Attached artwork: a `prop_config` entry's `sprite_registration: {prop_id or character_id, center:[x,y], height_fraction, aspect, anchor?:[x,y]}` places the prop's child anchor (default bottom centre) on fractions of its staged parent's image (top-left origin). `height_fraction` (0–32) sizes it relative to that image's height; `aspect` is the parent's image width/height. Keep a world `position_m` at the parent's depth for editing. Stickers and laser dots then follow the parent's motion, facing, size adjustments and visibility in playback and films. A registered prop remains in its holder's hand and follows a `prop_move` from that character until arrival, then attaches to the receiving surface. Parents must be staged and cannot themselves use `sprite_registration`.
- Spinning blades: `rig.rotors` (`PATCH /api/props/<id>` with `{rig:{rotors:[{center:[x,y],polygons:[[[x,y],...]],axis:"horizontal"|"screen",turns_per_second,phase?:0}]}}`) registers only the source artwork blades. Main rotors foreshorten about a fixed mast; tail rotors rotate about a fixed hub. Coordinates are image fractions, rate is 0–30 turns/s, phase is 0–1. The shared sprite surface masks the static blades and samples the take clock for player, ingredients and films; pause and seek retain exact motion.
- Rolling wheels: a vehicle prop's `rig.wheels` (`PATCH /api/props/<id>` with `{rig: {wheels: [{x, y, r}]}}`, fractions of the image; or `POST /api/props/<id>/find-wheels` to have a vision model find them) makes the player spin each wheel by distance travelled (camera travel plus its own movement) over its radius.
- Paths over a plate: `scripts/plate_paths.py --show <id> --scene <id> [--grid]` draws every ambient route and prop/character move on the set it plays on (landscape, through `frontend/scene-space.js`), and `--to-world 'px,py ...'` turns pixels picked on that 1280×720 drawing into ground positions (`SceneSpace.groundAt`). Route moving things over the painted ground and end them at a frame edge, a door, or behind a depth layer.
- Set depth: painted sets are flat, so `PUT /api/backgrounds/<id>` with `set_config.depth_layers: [{top_pct, z_m, left_pct?, right_pct?}]` redraws the plate below `top_pct` (percent from the top of the frame; optionally only between `left_pct` and `right_pct` from the left) at depth `z_m`, occluding anything further back — e.g. a freeway barrier hiding the poles of passing signs, or a bar counter in the left third hiding the barman's legs while the back of the room stays open. A see-through band (a gate's ironwork, railings, bare branches) adds `mask_png_base64`: a PNG the size of the plate whose alpha is opaque only on the occluding parts; it is stored and becomes `mask_path`, and the band then hides things only there, so someone behind the gate is seen through the bars (static sets only).
- Manual sprite drawing order follows the take: `stage_config` and `prop_config` entries may have `layer_order: [{background_id, t_ms, layer}]`, with integer layer 1–19999 or `null` to return to physical depth. The editor's Forward/Backward operation starts at the needle and persists across movement and pose cues until the next set change or take end; it does not spread through pose size/position groups. The Debug values checkbox displays live z metres, drawing order and whether it comes from depth or a manual interval. Hovering or focusing a debug panel raises it above overlapping panels. Order is CSS painting priority (higher draws in front); automatic order maps physical depth using a 10,000 baseline, and negative z is valid closer-to-camera staging. Existing fixed picture layers are retained until an explicit interval supersedes them.
- Manual foreground editing: the foreground lightbox exports a full-canvas transparent PNG, with its original clipping and mask applied. Download or drag it out, edit it, then drop it back or choose the PNG. Keep canvas proportions; uniform resizing is supported. `PUT /api/backgrounds/<id>/depth-layers/<index>/artwork` accepts `image_base64` (RGBA PNG, up to 4096 pixels and 10 MB), `expected_layer` (the current depth-layer descriptor), `expected_background_path` and optional `note`. It rejects stale replacements with 409 and retains previous descriptors in `artwork_history`; the original background plate stays intact. Returned `layer.image_path` supplies the replacement alpha texture for static and scrolling playback, using the source plate's registration and seam position.
- Source registration: a character or prop stage entry may include `plate_registration: {background_id, source_size:[width,height], center:[x,baseline], standing_height_px, poses?: {pose: {center?, standing_height_px?}}}`. On that background the canonical projection cover-fits the source pixel registration, with character pose height fractions retained. It follows `set_config.framing: {landscape?:[x,y], portrait?:[x,y]}` alignment fractions (default `[0.5,0.5]`) shared by the background and depth mask. Use this for seated figures and built-in architecture; ordinary world positions remain the fallback on other sets. Foreground layer z_m can be nearer than zero (greater than -7.75) to cover a close-up figure.
- Moving scenery through cabin windows: declare `set_config.scenery_windows: [{id, scene_id?, source_size:[width,height], polygon:[[x,y],...], background_id, scale?:1, offset?:[x,y], speed_factor?:1, z_m?:0}]`. Each source-pixel polygon clips a seamless version of the referenced approved background while the cabin plate, source-registered actors and foreground mask remain still. Use `scale` and `offset` to frame an enlarged exterior through close-up windows; motion follows existing `travel` cues continuously across cuts, with `speed_factor` for a slower distant view. Apertures follow the plate's cover alignment and camera inserts in both formats. Assets use the scene's bounded decode owner; pause, seek and scene-film exports sample the same clock.
- Saved scene recovery: the composer’s Change history remains available after autosave and reload. Clicking an older entry restores its full saved scene through the existing optimistic version-restore API; the displaced scene is retained in the same history. Unsaved edits keep Undo draft.
- Sliding windows: a set can declare `set_config.sliding_windows: [{id, source_size:[width,height], rect:[x,y,width,height], corners?:[[TLx,TLy],[TRx,TRy],[BRx,BRy],[BLx,BLy]], z_m, initially_open?}]`. The aperture is in the source plate's pixels, cover-fitted exactly like its depth mask in either format. Optional clockwise corners map the moving shutter to an angled aperture; a rect retains a straight-on aperture. `scene_effect` cues `window_open` / `window_close` (optional `window_id`, `duration_ms`) move an opaque shutter on the take's clock; pair it with a source-sized foreground depth mask to keep a character inside the building. Pause, seek and export share the same opening amount.
- For paired glass sliding doors use that same panel with `material: "glass"`. The two leaves slide outward by half the aperture width. `interior_offset:[dx,dy]` samples an unobstructed interior from the retained plate; `interior_z_m` places that interior behind the actors independently of the doors' `z_m`, so people pass through the open aperture and remain visible inside. No handles or replacement artwork are needed.
- Live set displays: `set_config.countdowns: [{id, source_size:[width,height], rect:[x,y,width,height], initial_ms, start_ms, label, age_label?}]` draws a clock over a painted display. `initial_ms` counts down from `start_ms` on the performance clock and stops at zero; a static `age_label` can retain narrative context such as a pizza already twenty minutes old. The live timer replaces baked digits in player and exports.
- Interlocked dashboard buttons: `set_config.interlocked_buttons: [{id, source_size:[width,height], rect:[x,y,width,height], origin:[x,y], axes:[a,b,c,d], buttons:[{id,label,rect:[x,y,width,height]}], initial_pressed, press_button, press_at_ms, duration_ms?, travel_px?}]` covers painted buttons with source-registered pushbuttons. Pressing one depresses it and illuminates it while the previously pressed button rises and goes dark. `press_at_ms` is the performance clock, so pause, seek, speed and export preserve the same mechanical state. The source axes align the panel to a slanted dashboard; framing follows the plate and foreground matte.
- Set lighting: `set_config.figure_filter` (CSS filter functions only: brightness, contrast, saturate, sepia, grayscale, hue-rotate) grades every character and prop drawn on that set, never light props — the dark Clink lit only by a green lightstick darkens and greens the figures on it, so a lit sprite does not sit pasted on a dark plate.
- A figure's own grade: a `stage_config` or `prop_config` entry's `appearance_filter` takes the same colour functions plus `opacity()` for that one figure — a black-and-white public-terminal avatar (`grayscale(1) contrast(1.15)`) or the ghostly, translucent avatars of a crowded Street that others walk through (`opacity(0.45)`).
- Acting: a dialogue beat may carry `direction` (≤400 chars, e.g. "flat and dry, barely looking up"), which is added to the speaker's voice direction for that line, and `pause_ms` (0–4000), the silence before the line. Without it lines follow each other after 180 ms.
- Props: `POST /api/shows/<id>/props` with `{name, description, physical_height_m, default_mount_point}` generates one cut-out object in the show's style (job `result_path` is its image). `physical_height_m` is the height of the whole image, trimmed to the object (up to 100 m: a giant billboard on its pillars far off down the road). Optional `view` ("side profile facing right" suits the stage) and `reference_image_base64` (an approved design to match). `PATCH /api/props/<id>` changes the fields; `visual_treatment: "light"` draws a prop as light, screen-blended onto whatever is under it (a spotlight beam, a laser fan, a glow; generate it as a saturated flat shape, since white would be keyed out). Put a held prop in `prop_config[{prop_id, relation: {type: held_by, character_id, mount_point: hands|torso|lap|head}}]`. Hand it over with a `prop_move` cue whose `frames.landscape` and `frames.portrait` each name `from_character_id`/`from_mount_point` and `to_character_id`/`to_mount_point` (plus `duration_ms`, `arc_height_m`); after the move the receiving character holds it.

- Rendered films: agents render on request — `scripts/film.py export --show <id> --kind trailer|scene|show [--scene <id>] [--clips cut.json]` (runs as ubuntu, no model calls; use fairystack-background for long renders). It renders the film exactly as the composer plays it — picture, voices, sound beds, title card and fades — frame by frame on a virtual clock (`/composer?…&record=1`): smooth 30 fps H.264/AAC 1280×720 at any machine load; performed scenes only. For a trailer, read `film.py export --moments` (every performed line and visual event with times), choose the clips yourself as `{"clips": [{scene_id, from_ms, to_ms, why}], "tagline"}` (edges are snapped out of lines; ends on the title card), then render. Failed exports retain their completed MP4 and export checkpoint; `--resume-export <directory>` with the same show/kind/scene retries only publication. The finished MP4 is uploaded with `PUT /api/shows/<id>/video-exports?kind=…` (streaming `multipart/form-data`: `video` MP4 file and `result` JSON text containing `{segments,end_card}`; metadata is in the body rather than a header) through the stable public HTTPS origin; the Composer workspace (`/composer?show=<id>`) plays saved film/trailer videos, offers the current film/trailer/scene downloads, and shows MP4 availability beside every scene. It also contains the book reader, scene plan/direction and cast/sets. `/film?show=<id>` and `/show?id=<id>` redirect there, preserving book/version/time queries. `GET /api/shows/<id>/video-exports` lists them, `GET /api/video-exports/<id>.mp4` downloads, `DELETE /api/video-exports/<id>` retires one.

Whole-film export (`--kind show`) renders and publishes each scene as its own film, then stitches those completed films in show order, preserving their mixed sound. Finished scenes remain available if a later render fails. To replace one scene, run `--kind scene --scene <id>`, then `--kind show --assemble-only`: assembly uses the newest published film of every scene, records source export IDs and exact frame ranges, and fails before assembly if any scene film is missing. Assembly-only intentionally uses the published footage; it does not compare it with later scene or asset edits. Omit `--assemble-only` to render every scene from current inputs. The first scene keeps the show's opening; other scenes retain their directed lead-in and fade.

Scene loading resolves the scene’s used poses, look changes, props and active frame format through one batched `/api/assets-manifest` request (up to 200 paths per batch), then downloads immutable files in parallel. Scenes with saved comparisons prepare their displayed immutable cuts and complete shared asset inventory once, under the existing bounded scene-load owner. Later A/B replays and switches use the cached snapshots and decoded assets without network requests or a loading overlay. Current-scene reloads still fetch fresh edits. Every image is decoded before it becomes available to the player; decoded artwork, sound blobs and image dimensions stay retained across saved-cut switches for the current scene. Readiness covers the entire selected comparison region or the requested take position plus a 12-second lookahead, whichever is longer, including assets first used earlier; it never assumes a deep link starts at the opening. A missing required comparison asset fails visibly before autoplay. Leaving the scene retires its blob URLs and decoded cache. Pose changes replace the sprite node together with its image and geometry, preventing a previous pose from flashing with the next pose’s facing or size. Failed loading keeps playback stopped and offers Retry loading; changing scene cancels the preceding load. Unused show assets do not block a scene.

The scene player supports paused frame review at the films’ canonical 30 fps: use Previous frame / Next frame or comma / period (outside text fields). Stepping pauses the mix; the scene time reads minutes:seconds:frames, and the scrubber also uses one-frame intervals.

The composer shows Scene composition below the player: the current shot’s source background, foreground masks, one Sprites stack containing both characters and props, and clock-driven overlays. All sprite thumbnails are clickable: characters open their originating pose sheet; props open the exact displayed cutout and, when retained, its original source. These previews reuse the already loaded assets and the same player state.

Scene composition → Sounds has a persistent volume slider for each music/ambient/effect track and the shared narration/dialogue take. Dragging previews the gain in the player and audition audio; releasing saves that one level against fresh scene configuration with a conditional update. A concurrent edit to the same level fails visibly. Saved levels are included in scene history and future film renders; existing MP4s retain their recorded mix. `audio_config.speech_volume` is the take's gain from 0 to 1 (default 1); each track retains its own `volume`. Historical scene versions are read-only. Pooled playback elements reset gain and mute state on every claim, and released sound callbacks cannot change their next owner.

The 2× button beside Play toggles double-speed review, preserving speech pitch and keeping sound cues on the scene clock. The selection persists between scenes and reloads. Film exports always retain their directed timing at normal speed.

The composer sidebar shows each scene's accumulated seconds viewed and a bar normalized to the most viewed scene in that show. Time counts only during successful playback while the loaded viewer is visible and focused, including historical versions; pausing, loading, hidden/unfocused tabs, sleeping devices and record mode do not count. Every pause ends that playback visit; a new visit begins after playback succeeds. Previously accumulated totals are retained. Totals persist across visits and devices. Unsaved visits are retained on this device and retry automatically after a visible save error. Existing reviewed checkmarks contain no duration and do not contribute invented seconds.

`GET /api/shows/<id>/scenes` and `GET /api/scenes/<id>` include `review_seconds` for the authenticated owner. `PUT /api/scenes/<id>/review-time {visit_id, viewed_ms}` records a visit's cumulative milliseconds (integer 0–86400000; visit_id 16–80 URL-safe characters), returning `{id, visit_id, viewed_ms, review_seconds}`. Reusing a visit ID only advances its recorded clock: duplicate, older and retried requests cannot add time twice. Unknown/unowned scenes return 404; invalid inputs return 422. Viewing time never updates creative notes, staging, versions or `updated_at`.

Motivated lighting: `background.set_config.lighting = {ambient: 0.9, lights: [{id, label, source_prop_id, cast_z_m: 2.2, cast_y_m: 0, radius_m: 1.25, intensity: 0.5, color: "#ffc078"}]}`. A passing lamp uses its resolved physical travel position; its pool and localized light footprints share the composer take clock, including pause, seek and recording. Footprints move across illuminated copies clipped by sprite alpha; ambient brightness stays constant, so a passing lamp does not brighten the entire vehicle at once. Static window/lamplight instead uses `position_m: {x_m, y_m, z_m}`. Keep darkness between pools, use the existing set lamps as motivation, and opt in only where the shot needs light. Preserve `figure_filter` for the set's base treatment. The Scene composition inspector shows those resolved light overlays. Combined directed sequences may retain unlinked source scenes and original take paths in director provenance; the linked plan scenes alone play in the film.

## Direct sprite editing

The scene viewer has a multitrack review timeline: speech beats, sound effects, music, ambience, picture cues, sprites and props share the performed clock. Tracks have fixed compact rows and text-free bars, with tiny approved sprite thumbnails at pose/look/entrance transitions. Hover or focus a bar to preview its label below; click to pin the event and seek. Drag the film-time ruler to scrub, or use 4× zoom for close timing. Thumbnails reuse the player’s decoded artwork and pose resolver. Drag an event or pose thumbnail to move its switch time, or select it and edit its Start in seconds. Cue dragging stays available with sprite editing enabled unless a sprite change is unsaved. Drag a speech block to edit its preceding pause and shift later speech/events together; select a speech block for Pause before and Close gap. Insert at playhead adds silence between speech lines. Recorded voices are retained and every save can be restored from Tweak history. “How this scene was made” shows saved source/direction, sound decisions, artwork prompts, voice timing and score plans. New performances and sound passes retain explicit director requests and structured outputs; missing older requests stay visibly unavailable. Versions play their own timeline and take.

In the current viewer, **Edit sprites** pauses playback. Select a character or staged prop on the canvas or use **Select sprite** for overlapping objects. Clicking empty space on the canvas deselects; clicking outside the canvas and editor controls saves pending edits and exits editing. A failed save keeps the editor open with its error. **Forward** / **Backward** reorder the selected sprite without changing perspective. Drag to move, and drag a corner to resize with its feet anchored and aspect ratio preserved. Arrow keys move (Shift moves faster); +/− resize. **Save** commits, **Undo** reverses the latest gesture (Save again to persist an undo), **Reset** removes this appearance's viewer adjustment, and **Discard & close** abandons unsaved previews.

Adjustments belong only to the current scene / background / pose / look / facing (props use current state). They leave canonical heights, approved artwork, narration and motion intact. The shared projection applies them to live playback, seeking and record mode; source-registered figures retain their plate alignment. Passing cars and timeline-only props use the same editor and authenticated scene save. Earlier scene versions remain read-only.

`stage_config` and `prop_config` entries accept `visual_adjustments`, a map whose key is JSON `{background_id,pose,variant_id,flip}` for characters or `{background_id,state}` for props. Each value is `{scale: .05..8, offset:{x,y}, layer?: 1..19999, flip?: true, flip_y?: true, scale_mode?: "absolute"}`. Horizontal `flip` and vertical `flip_y` are independent picture adjustments; absent means unflipped. The editor offers Flip X and Flip Y, and the shared surface carries both into ingredient previews and recorded films. New size and position saves belong to explicit pose-group member keys; flips and drawing order stay specific to the selected appearance. Legacy `{background_id}` corrections are preserved as a baseline and never rewritten by the editor. Legacy relative sizes multiply, while `scale_mode: "absolute"` sets this picture’s exact size; offsets are world x/y metres for world-staged objects and source x/y pixels for plate-registered objects. World depth remains unchanged; optional integer `layer` overrides only the draw order for this appearance. Read fresh, merge only owned keys, and send `if_match_updated_at` on `PUT /api/scenes/<id>`; a conflict returns 409. Editor requests have 8-second timeouts under one 30-second operation deadline; failed saves visibly discard previews.
Depth layers may declare `scene_id` to scope a source-aligned foreground mask to a single shot sequence while reusing its approved plate elsewhere. New edits use `scale_mode: "absolute"` on full pose/look/facing (or prop state) keys, preserving legacy placement offsets; a group edit applies the same size ratio and position delta to every member. Legacy picture scales without this mode still multiply the shared size. Independent poses preserve all other poses when moved or resized; linked groups share size and position changes. The Group selector is saved immediately on the source pose across scenes. Missing membership and null both mean Independent. Independent edits affect only the current pose and facing; joining a group is explicit. The Link edits control shows independence or names every linked pose, and follows the selected character across pose switches. `PUT /api/{characters|look-variants}/<id>/pose-group` accepts `{pose, editor_group: "main"|"group-N"|null, expected_group, expected_path}`, returns `{character_id, variant_id, pose, path, editor_group}`, and returns 409 for stale membership or artwork. `pose-size` optionally accepts `expected_group`; when present it applies the requested ratio to all group members atomically and returns their `poses` metadata. Scene positions remain local. Editing controls sit in the compact sidebar beside the viewer, using its available width and height without moving the picture or timeline when opened. Narrow screens place the controls below the player. Scene composition character thumbnails open the actual originating pose sheet; repaired sprites and scoped aliases resolve through their recorded artwork history. Its View character link opens `/composer?show=<id>&character=<id>&scene=<return scene>` to browse every saved sheet across that character’s looks. Missing source provenance is shown explicitly.

Pose-local sizing can reuse an approved pose path under a scoped pose name and `height_fraction`, leaving the canonical character height and prior poses intact.

Native production treatments: the first-scene title and trailer end card share a deterministic 3-second clock-driven title over the continuously moving opening scene, whose entrance each film decides on its first scene: `audio_config.title_opening.title: {entrance: strike|dawn|type|fade, lines?: [..], layout?: "row", position?: {x,y}}` (row keeps independently timed words side by side; strike slams each line in, Snow Crash; dawn rises into warm light; type sets letters one by one; fade, the default when undecided, just appears); `audio_config.title_intro` carries the synchronized reusable impact track; `audio_config.title_opening: {prop_id}` optionally moves that approved vehicle in from the left before narration begins. Title motion freezes on pause; record mode samples the same owner. `position` is the title’s top-center anchor as fractions of picture width/height, each 0–1. Drag the visible title directly, or select Title in Edit sprites & props, then Save in scene; draft Undo/Reset and arrow-key nudges reuse the sprite editor. Saves merge only title position into fresh audio_config, preserve timing/audio and reject a concurrent title move. Background cues can add `transition: {direction: "left"|"right", duration_ms}` to slide preloaded outgoing/incoming plates on the take clock, or `transition: {kind: "dissolve", duration_ms}` to cross-fade the whole frame (plates, depth bands, figures and props; anyone unchanged through it stays solid). A dissolve to the same `background_id` is a time-passing dissolve on one set: someone leaves, someone else arrives. Prop entries can use `visual_plane: {yaw_deg: 0..85, skew_y_deg: -30..30}` for a source-local near-profile plane. Prop `rig.glazing: {polygon: [[normalized_x, normalized_y], ...], opacity: 0..1}` changes only registered window opacity, preserving the body, wheel rig and source artwork. Reusable effect ingestion preserves stereo. Creating an already performed scene as active reuses its validated whole take instead of synthesizing separate duplicate per-line audio.

Looping ambient tracks may use `envelope: [[take_time_ms, gain_multiplier], ...]` with strictly increasing times. Playback samples this curve on the take clock; record audio samples the same curve, including negative title-opening times before narration. Engine beds reuse one approved effect across shots, with lower idle/speech levels and motivated rev curves.

Audio-only scene export corrections: `scripts/film.py export --kind scene --scene <id> --reuse-video <canonical-mp4>` retains the picture and remixes current narration, score and effects through the real composer audio layout. Verify that visual inputs have not changed before using it. Incorrect dimensions, fps or exact frame count fail before publication.

For a short visual repair inside an existing chapter, the canonical recorder accepts `{scene_id, from_ms, to_ms, seamless: true}` segments without trailer fades. Preserve the original 30 fps frame phase including scene lead, splice with `film_export.edit_segments`, then use `reuse_video` to remix the whole current soundtrack.

Scene composition previews open the current artwork in the shared inspector, including masked foregrounds, backgrounds and overlays. Move selects that object in Edit sprites & props; staged, passing and ambient set props share scene-local movement, resize, flip and layer edits. Ambient corrections leave the shared set route unchanged.

Background Lab (`/background-lab?show=<id>&character=<id>`) keeps a large original-resolution sprite inspector among the selected look’s thumbnails. Backdrops (checkerboard, white, black, gray, blue, or three tones) change only the display. Enlarge → Actual pixels inspects the original image dimensions. `GET /api/background-lab/shows/<id>` lists assets with `look_id`, `look_name`, `scene_uses`, plus `usage_available` and current `image_settings`. Usage delegates to the player’s `sceneAssetPlan` for the latest published full film’s scene IDs (current scene documents), or performed scenes if no full film exists; it is not a claim about every historical cut. Look is a saved character design version; Pose filter is its visible subset (all, film-used, or saved problems). All subsets intersect the selected look. The show/character sidebar hides is_test shows using the existing My Shows test-visibility preference; explicit links to a test show reveal it. Type a character name in the sidebar filter to narrow the list; matching ignores case and surrounding spaces, and clearing it restores the list without changing the selected sprite. Each character shows its stored sprite count. Browsing defaults to all base-look poses. When a carried film-used or problem-corpus scope would exclude a newly selected character entirely, the view switches to All stored poses; empty explicit scopes are not mistaken for missing art. All looks retains access to other designs. The selected sprite’s originals are resolved with canonical asset versions and predecoded through the shared image-decode owner. Switching methods retains the image node and previous picture until the requested version is ready; failures offer Retry preview loading. A compact Compare & rank rail sits directly beneath the large image so both remain visible. Clicking a card selects that method in the large inspector, highlights the card and brings the inspector into view; it does not open a modal. Enlarge opens the selected image in the lightbox, with autosaving Better/Worse controls that keep that image open. The View menu offers reference images: Current cutout is the working production sprite, Source before removal is the input image, and Saved problem source/cutout are the first saved case paths. Identical reference paths appear once. Methods are selected on the comparison cards; only the active method is displayed in View, without duplicating the whole method list. Browsing starts no background-removal job; Run comparison suite uses the existing persisted job owner for exactly that selected sprite.

Automated audit → `/background-lab?show=<id>&view=audit` shows agent-run experiment evidence separately from human rankings. `GET /api/background-lab/shows/<id>/audits` returns `{audits:[report]}` newest first (up to ten), owner-scoped; an abandoned running report becomes visibly timed_out after its declared deadline. `PUT /api/background-lab/shows/<id>/audits/<audit_id>` saves `{audit_id, metric_version:"semantic-probes-v1", status:running|completed|partial|failed|timed_out, deadline_at, cases:[{key,label,results:{mode:{score,foreground_loss_pct,background_retained_pct,worst_probe_error_pct,...}},labels?,...}],...}` and returns `{audit_id,status}`; invalid reports return 422, unowned shows 404. The server recomputes method aggregates and character/prop cohorts. Agent-reviewed reports retain their original run via `parent_audit_id`, changed reference labels, and review notes; a development-set correction is not held-out validation. `POST /api/background-lab/audit-images {show_id,image_base64}` stores an immutable content-addressed PNG and returns `{path}`; nonempty PNG up to 10 MiB, 32–4096 px, including blank candidate mattes, validation 422, ownership 404. Reports retain reference labels/model/task and source hash. This evidence never changes production artwork, human rankings or corpus membership.

`audit_backgrounds.py --show <id> --session <session> --workdir <persistent experiment directory> --limit 10 --minutes 45 --publish` runs one bounded serial CPU experiment through the canonical remover and durable shared paid-task client. Resume the same directory to reuse vision labels and finished cutouts; accepted provider tasks are not resubmitted. The agent’s background command owns process cancellation. Source-only vision labels define small definite keep/remove regions; `100 − (foreground alpha loss + retained background alpha)/2` scores their opacity, averaging regions within each class and images equally. Confidence below .85, ambiguous boundaries and unlabelled pixels are excluded. These are provisional sparse scores, not whole-image accuracy or a calibrated guarantee. Experimental ISNet/BiRefNet model-matte candidates skip shared hole clearing/restoration; they do not change the production default.

Background Lab’s emphasized primary action → Run comparison suite explicitly tests the selected sprite with all five local strategies, reusing fresh cached results. `POST /api/background-lab/previews {character_id, variant_id?, pose_tag, modes?}` → 202 `{job_id}`; invalid modes → 422. `GET /api/jobs/<id>` includes persisted `progress {completed,total,step,methods:[{mode,status,seconds?,error?}]}`; each method is pending, running, cached, completed or failed. The overall bar measures completed methods; local model inference does not emit internal percentages, so a method remains Computing cutout until its result completes. This granularity is independent of first-run initialization or duration history. The 10-minute job deadline and reload recovery belong to the existing generation owner. Successful suites dismiss the progress widget after comparisons load; failures remain visible. Partial results survive failures; successful cached methods are not rerun.

The comparison cards expose automatic transparency, soft-alpha, pale-halo and enclosed-hole diagnostics. These are not ground-truth scores. Every Rank better/worse click immediately saves through the existing `PUT /api/background-lab/assets/<type>/<id>/ranking {ranking:[all five modes best first]}`; human points are 5 through 1 and averaged only across confirmed rankings. Saving/Saved status reflects the server acknowledgement; failures show Retry saving and retain the draft. Unsaved rankings are journaled by signed-in account/show/asset and replayed when that sprite is opened after refresh. No separate Save ranking click is required.

Problem corpus → Save problem sprite uses `PUT /api/background-lab/assets/<type>/<id>/case {problem:true, note?}`; `problem:false` removes only corpus membership, leaving art and rankings intact. Notes are bounded to 1000 characters. The owner-scoped corpus stores the observed cutout and opaque-source paths at first save, preserving them when notes change. Show manifests and asset details return `problem_case` or null; Pose filter → Problem sprites filters the selected character across its looks. Saving or browsing a case does not start removal jobs or change production artwork.

Production background removal uses `birefnet_production`: BiRefNet Lite with edge defringing, preserving opaque source cores and small pale details enclosed by source ink with a confident foreground rim, including eye whites, and skipping destructive flat-region hole clearing. Props and character generation share the canonical remover. `GET /api/props/<id>/artwork` returns `{image_path,source_path,source_url,background_removal_mode,task_id,history}`. `PUT` accepts `{image_base64,source_base64?,background_removal_mode,note,expected_path?}`; registered replacement retains dimensions, physical size, rig, state variants and prior art, with 409 for a changed expected path. Newly generated props retain their opaque source in artwork_metadata. Existing provider task IDs permit retrieval of already paid source art, never regeneration.

`PUT /api/characters/<id>/pose-artwork` uses the same repair contract as look variants and synchronizes an existing base look.

Recorded pause API: `POST /api/scenes/<id>/timing` with `{if_match_updated_at, gaps: [{after_beat_index, gap_ms}]}` sets pauses between consecutive lines; `{after_beat_index, close: true}` removes the safe measured silence. Alternatively send `{if_match_updated_at, insert_at_ms, insert_ms}` to insert silence outside speech. Pauses/inserts are bounded to 10 seconds each. Returns the saved scene with `undo_version_id` and `timing_edit`; 409 means a newer scene exists, 422 identifies invalid timing or speech that cannot safely be removed, and 504 reports the 80-second audio deadline. Audio and all active take clocks save together; no new voice generation.

Movement paths: **Edit paths**, select a sprite, then drag its Start/From/To points; arrow keys nudge (Shift for larger steps). Ground-plane drags change horizontal position and depth; Alt-drag changes height at fixed depth. Undo and Save in scene use the same bounded sprite editor. Saved movement uses the canonical player and future exports. Registered, held and generated passing objects have no independent authored path. Historical versions are read-only. `POST /api/scenes/<id>/paths {take, changes}` saves up to 200 points atomically: staging points `{field:stage_config|prop_config,object_id,before,position}` or movement endpoints `{index,cue,endpoint:from_position_m|to_position_m,format:null|landscape|portrait,position}`. Positions are finite `{x_m,y_m,z_m}` with -7.75 < depth <= 200. Original cue/position and take must match (409 otherwise), and the response is the saved scene plus `undo_version_id`.

Scene prop deletion: scene PUT accepts `prop_removals: [{prop_id, passing?, background_id?, ambient_instance?}]` with `if_match_updated_at`. It removes scene staging and active timeline/take cues while retaining speech, library art and restorable versions. Passing deletion removes its repeating scene placement; ambient instance deletion suppresses only that instance on the named background in this scene. Delete in Edit sprites & props, or Delete prop, previews removal; Undo restores the draft and Save in scene commits it. Keyboard deletion ignores text inputs.

Timeline gap inputs accept millisecond precision, including 0.25 seconds. The inserted region is highlighted and centered at 4× zoom; Undo timing restores the preceding scene version with concurrency protection. The scene start stays fixed, later clips ripple forward, and clips spanning the needle retain their start and duration. Existing speech lines cannot be split; Remove quiet pause removes measured silence only.

Timeline sound removal: select a sound effect and press Delete, or use Delete sound effect in its event controls. `DELETE /api/scenes/<id>/cues/<index>` accepts `{cue, if_match_updated_at, label?}`; `cue` must exactly equal the selected stored sound cue. It removes only that occurrence, retains shared audio/art, narration and other sound uses, records a restorable tweak, and returns the saved scene with `undo_version_id` and `deleted_cue`. Missing/invalid input returns 422; stale scene/cue returns 409. Undo timing restores the prior scene with concurrency protection. Keyboard deletion is scoped to the timeline and ignores text fields.

Timeline box selection and Shift-click group active motion, picture and sound cues. Drag a selected cue to move the group by one delta; POST `/api/scenes/<id>/cues/time` accepts `{moves:[{index,cue,t_ms}],if_match_updated_at}` and commits one undoable edit. Speech stays on the recorded take and uses the pause controls.

Scene-local overlay clocks: the timeline Overlays row exposes registered device readouts, countdowns, color grades, interlocked-button transitions and animated source layers. Drag or edit their start time through the existing cue owner; a first edit uses `POST /api/scenes/<id>/cues/time` with `{moves:[{index:null,cue:{type:"overlay_timing",background_id,overlay_kind,overlay_id,t_ms:<original start>},t_ms:<new start>}],if_match_updated_at}`. Later edits use the stored cue index. Playback and export shift the source clock (including animation keyframes/end bounds) by the cue delta without changing the shared background. Stale/unknown registrations fail visibly; the edit retains normal Undo timing.

Pose-bound occupant articulation: a staged character can have `source_parts:{pose,parts:[{id,polygon:[[x,y],...],pivot:[x,y],keyframes:[{at_ms?,cue_anchor?:{type,sound_id?|effect?,window_id?},offset_ms?,rotation_deg?,scale_x?,translate:[x,y]?}]}]}`. Geometry uses original canvas fractions; translation uses canvas fractions; cue anchors follow sound/window retiming; take-clock joints replace the same pixels in the base sprite and retain vehicle glazing clips. Loading-arm window extensions accept the same `parts` registration. Prop movement frames accept `easing:"out"` for gradual deceleration, retaining linear motion by default.

Registered loading reaches: a window `extension` can carry `target_prop_id`, `target_point:[x,y]` source fractions, and paired arm/parcel `parts` with `reach:"arm"|"parcel"`, `endpoint:[x,y]`, and take-clock `reach_progress` / `opacity` keyframes. The stage projects the current target sprite back into the source plate; the source joint reaches that point in both formats, while the parcel keeps its orientation.

Highway maneuvers: `speed_factor` scales forward road motion as well as side-window scenery (default 1); it leaves the narration and cabin fixed. Scenery window `mode: "forward"` supports `driving_path: [{at_ms,lane_x_m,yaw_deg}]` and rear-view `traffic: [{id,kind:"sedan"|"minivan",color,lane_x_m,offset_m,speed_mps,every_m,width_m,height_m,from_ms?,until_ms?,keyframes?:[{at_ms,x_m,z_m}]}]`. Optional `destination: {label,x_m,distance_m,width_m,height_m}` places a frontage in the same road coordinates. The take clock, travel distance and physical projection are shared with playback and film export. Stage/prop entries may carry `road_motion: {background_ids:[id],keyframes:[{at_ms,x_m?,y_m?,z_m?}]}` to offset authored blocking during highway shots without affecting other sets. Optional `free_motion: {until_ms, lag_ms?:0..2000, wander_m?:{x_m:0..2,z_m:0..2}}` gives an independent rider delayed lane reactions and smooth deterministic drift until a tow/handoff catches; the take clock owns the transition and traffic replanning retains these per-rider settings. `audio_config.title_opening.entrance_sound` is an existing normalized sound descriptor with volume, mixed alongside the retained title impacts during the opening.

Sound direction also accepts `opening_effect: {effect, volume}` for a scene with an existing title opening. Preview and publication use the same owner; the entrance sound layers alongside existing title impacts in playback and exports.

Vehicle cabin illumination: a staged prop can declare `cabin_light: {color:"#ff3020", polygon?:[[x,y],...], keyframes:[{at_ms?,cue_anchor?,offset_ms?,intensity:0..1}]}`. The polygon defaults to the prop rig glazing. It emits a source-local window wash and soft halo on the take clock, so placement, size, flips, pause, seek and exports stay registered. Cue anchors use the source-joint owner; an impact anchor can switch power off at the crash. Interior `color_grades` optionally declares `tint:"#rrggbb"` with `from/to.tint_opacity` for an emissive cabin wash that excludes the windshield.


Articulated loading extensions retain a fixed installed base; `extension.base_clip:[[x,y],...]` clips it to the source-registered service opening. A `scene_effect` cue `{effect:"speed_lines",direction:"forward",background_id?,t_ms,duration_ms}` draws outward motion lines inside forward scenery windows on the same take clock, while the cockpit remains fixed.

Recorder scene index: `GET /api/shows/<id>/scenes?index=1` returns only `id`, `show_id`, `seq`, `title`, `plan_scene_id`, `background_id`, `updated_at` and account-scoped `review_seconds`, in scene order. The recorder uses this catalogue and loads its chosen complete scene through `GET /api/scenes/<id>`, preserving canonical staging without downloading every take. Omit `index` for the existing full scene list.

Forward traffic keeps each overtaken vehicle through its complete windshield exit. Repeating trajectories wrap behind the camera, and projected body/wheel/shadow bounds determine clearance; authored traffic paths and clocks are preserved.

## Rendering performance
`GET /api/rendering-performance` returns the authenticated account's latest 100 browser captures. `PUT /api/rendering-performance/<capture UUID>` records one immutable measurement (128 KiB maximum); context.scene_id must belong to the account. Captures distinguish advancing scene frames, pauses over 100 ms, browser long tasks when exposed, viewport, device pixel ratio, source revision, and start/resume. `/rendering-performance` is linked from the header. These are received samples, not proof that unsampled devices are smooth.
