Contents

M1 compiler and render orchestration

How the core turns an EpisodeIR into a Timeline, a RenderJob, and finally an MP4. Scope: the Lighthouse cold open. Read with ARCHITECTURE §7–§9 and ADRs 0006, 0008 and 0009. Code lives in core/src/compile/, core/src/audio/, core/src/render/ and core/src/adapters/. Places where the implementation settled a question this design left open, or changed its first answer, are marked (implementation).

Pipeline

validate show
  → takes: resolve a voice take per LineCue (take field → cache → TTS chain)
  → measure: exact duration of every take and sound file (ffprobe, cached by hash)
  → lip sync: Rhubarb per take (cached by take hash + transcript + rhubarb version)
  → timing: resolve cue graph in seconds, quantize starts to frames
  → blocking: positions, facing, posture, look targets
  → framing: framing intents → camera keys
  → performance: gestures, expressions, visemes, blinks, breathing, speaking spans
  → validate film grammar → diagnostics (errors stop, warnings recorded)
  → emit Timeline (+ audio plan, captions)
  → RenderJob(s) → engine → RenderResult (verify hashes)
  → mix audio → encode → deliverables

Every stage is a pure function of its inputs plus cached, content-addressed adapter outputs. The Timeline's inputsHash covers all of them.

In M1 a line must carry a take; the TTS chain is not wired into compile yet (take.missing says how to voice it). (implementation)

Adapters (implementation)

  • ffprobe: exact length in samples (duration_ts in a 1/sample_rate time base, else a decode and count), cached at .reelwright/cache/probe/<sha256 of the file>.json.
  • Rhubarb: binary from $RHUBARB, else PATH, else tools/rhubarb/install.sh. The take becomes 16-bit WAV in a temp dir; -d <line text> --extendedShapes GHX -f json. Cached at .reelwright/cache/lipsync/<hash of (take hash, text, "rhubarb <version>")>.json, holding only the mouth cues (never Rhubarb's temp paths). Rhubarb is asked its version once a binary as it is on disk (its path, size and modification time), in memory and in .reelwright/cache/lipsync/rhubarb-versions.json: a compile's takes share one question, a later compile asks none, and a replaced binary is asked again.
  • Word timings: audio/takes/<sha256>.words.json next to the take if a show commits it, else the TTS cache ${REELWRIGHT_CACHE:-~/.cache/reelwright}/takes/<sha256>.words.json (written by tools/tts-kokoro/takes.py). A file counts only if it names the same take hash and text.
  • A missing tool is one clear error naming the tool and how to install it.
  • Tools run beside the process, never in its way (runTool), each with a time limit. A run timed out only when the limit's kill ended it: a timer comes due only once the event loop is free, so a busy process can find it due after the tool had answered and exited, and that answer counts. The engine's runs (runWithTimeout) and Claude Code's commands (claude/local.ts) are judged the same way.

Timing

  • Durations.
    • line: the take's duration;
    • beat: durationSec;
    • posture: durationSec (default 0.6 s);
    • sfx: the file's duration; (implementation) a scene waits for one that plays once no more than 2 s past its start, and the rest of it is cut at the scene's end, fading out over its last second (the thirty-first watch-it's 20 s rain held its scene open for 8 silent seconds);
    • fx: durationSec, else a show's effect's own (fx.<id>, unless it loops), else 1 s. (implementation) A show's effect that loops, told neither durationSec nor until, plays on to its scene's end (the thirty-first watch-it's rain fell for its four seconds in a storm), and still holds the scene open for only that 1 s. Any effect holds its scene no more than 3 s past its start, and stops with the scene (the thirty-third watch-it's 15 s snowfall stretched its last shot to 6 s). It emits from the fx anchor of the element it is on (else its top), a character's chest (with part head, EpisodeIR 1.31, the top of their head, where a drip lands) or a target's point. (implementation) On a prop (EpisodeIR 1.21, prop.<name>) it emits from the prop's fx anchor (else the top of its middle): the compile puts atM where the prop plan has it as it starts (in a hand, on a place, in the air, or on what carries it; fx.no_target while it is nowhere) and the event carries the prop and propAnchorM (Timeline 1.11), and the engine emits from that point of the prop as it is drawn each frame (RwSceneStage.fx_point), so the confetti follows the cannon in the Chief's arms; toward (EpisodeIR 1.17) aims it (toM, Timeline 1.8) at a character's eyes, an element's look point or a mark's look height, fx.no_aim when there is nothing there, and what it is aimed at stops it (implementation): a particle that comes as far as the aim within 0.3 m of the line to it is gone from then on, and one wide of it flies on (RwFxInstance._hit; the twenty-fifth watch-it's jet of water went in at Otto's face and out of the back of her head); until (EpisodeIR 1.27) plays it until a cue of the scene starts, instead of for its length (the event ends on that cue's frame; fx.until_before when it starts first, and the effect plays its own length): the twenty-fifth watch-it's rain fell for a second while its sound rang on until the CLANG;
    • lighting: none. From its frame the set is lit by another of its presets (EpisodeIR 1.18 LightingCue; Timeline 1.9 Scene.lightingChanges, frame and preset in frame order, the scene's lighting before the first). The engine puts the preset on every cel surface of the set and the props, turns the key, and swaps the practicals, from the frame alone (RwSetBuilder.relight);
    • title (implementation, EpisodeIR 1.44): durationSec, else 2.5 s for a card and 3 s for words over the picture. Words over the picture hold nothing up: the cue after them starts as they do, as after a sound that goes on; a card holds the scene like a beat. Each is a title card of the Timeline over its frames (Timeline 1.21, docs/design/m7-titles.md); Scene.out adds its black hold to the scene's length and gives its last shot out;
    • look: snap 0.12 s, normal 0.3 s, slow 0.6 s;
    • gesture: from the GestureLibrary, durationSec / intensityTempo (tempo 1 for now). (implementation) A gesture of contact (touch, press) aimed at a character targets their shoulder, not their eyes. Without a library, 1.0 s with the stroke at 0.4 s and an info diagnostic; a name the library lacks gets the same default and a warning;
    • expression: blendSec (default 0.2);
    • move: path length / (gait speed × speed). Gait speeds: walk 1.2 m/s, run 3.5, sneak 0.6, shuffle 0.5, stride 1.5, hop 1.0, slide 2.0, float 0.8. Where a mover starts follows from cue order, so a move's length is known before its time;
    • prop: 0.5 s (timed, not rendered in M1: info); music: fadeSec;
    • float (M6): its whole span (durationSec, default 2.5 s), or, landing on an until cue, its rise (0.7 s), so the scene plays on under it (docs/design/m6-float.md).
  • Pacing defaults when the episode sets none: lineGapSec 0.35, sceneHeadSec 0.5, sceneTailSec 0.75. (implementation)
  • Default at. After the most recent cue that was not placed with another cue, plus pacing.lineGapSec. The first cue starts at pacing.sceneHeadSec. (implementation) A sound that repeats (repeat) holds nothing up: what follows it starts as it starts, not after its first play (the twenty-sixth watch-it's looping waltz held the next line for 16 s).
  • with + word. Anchor on the onset of word n (words as lineWords counts them). Word timings come from TTS word timestamps when they align word for word with the line, otherwise from the Rhubarb cues (the words spread over the non-X speech by letter count, a word after a pause starting after it), otherwise proportional to letter count over the whole take. A gesture anchored to a word is shifted back by its strokeSec, so the stroke lands on the word (the GestureLibrary's own contract).
  • Quantizing. Cue starts snap to frames (round(sec × fps)), so audio starts on frame boundaries: at 48 kHz and 24 fps each frame is exactly 2000 samples. A cue's start is computed from its anchor's quantized start, so with + 0 lands on the anchor's frame and gaps are measured from where the anchor's audio really sits. (implementation)
  • Scene end = max cue end + sceneTailSec, rounded up to a frame so no audio is cut. Scenes are laid end to end. (M7) An opening title card (EpisodeIR 1.6 titles) comes first, so the first scene starts at its end; the credits follow the last scene (docs/design/m7-titles.md).
  • Cuts. A shot starts at its anchor cue's edge (or word) + offsetSec; an end edge is the frame after the cue's (or word's) last, so a cut on a line's end never opens on its last frame (implementation). snap: frame rounds to the nearest frame. snap: silence takes the nearest frame within 0.3 s whose time is not strictly inside speech (TTS words, else Rhubarb's non-X cues, else the whole take); none found keeps the nearest frame and warns. The first shot always starts at the scene start; shots must cut in order.
  • Markers (M7, EpisodeIR 1.42): a scene's markers are placed as a cut is (their cue's edge or word + offsetSec, to the nearest frame, kept inside the scene) and listed in the Timeline's markers (1.19) in time order, only when there are any. Nothing plays them.
  • Poses keyed by hand (EpisodeIR 1.63, core/src/compile/poses.ts): a scene's poses are placed on their anchor, a line's word (its onset) or a line's or a beat's start, plus offsetSec, to the nearest frame inside the scene, and go on their character's actor as Actor.poses (Timeline 1.37) in frame order, with the frames they come in over, are held and go out over, and the hand a one-handed one is made with. One whose scene no longer says what it was keyed by there (at.words: the word, the line's words or the beat's action) is not played: pose.words_changed, a warning. So is one whose character's rig has no pose of that name (pose.unknown) or who is not in the scene (pose.not_in_scene).
  • Errors: cycles, unknown references, negative resolved starts, shots out of order.

Intercut places (implementation)

An intercut scene (EpisodeIR 1.45 Scene.places) is compiled as one scene of the Timeline per place, all over the scene's frames (placeViews, compile/places.ts): the scene's own set, then each place's, each with its set, lighting, blocking and props, the cues of whoever its blocking puts there (every cue that is no one's plays on the scene's own set) and the shots whose set is its (Timeline 1.22: the place's scene is <scene>_p2, _p3, with of naming the scene; shots name the Timeline scene they show). Walks are planned on each place's own floor and the scene is timed as one, so a line in one place follows the line before it in the other. Each place is blocked, framed, mixed and captioned as a scene is, a line once, where it is said. Film grammar judges a line by the shot that plays as it is said, whichever place it shows, and a cut from another place's shot is no jump cut and matches no eyeline (ShotFacts.fromElsewhere). The validator checks each place against its own set: marks, setups, elements and lighting.

Blocking

  • Positions. The start position comes from the blocking mark (positionM, heightM); a move walks the set's path viaM points (either direction), else the shortest chain of paths through other marks and entrances (the walkways keep walks off the furniture). A path that runs through furniture neither of its ends stands in is no walkway, and is left out (implementation): the twenty-fifth watch-it's galley had one from the hatch straight across the table to a bench seat. Where no walkway joins them, the walk is planned as the set generator plans walkways (Stage.planWalk, generate/set/walkways.ts). It is straight when nothing is in the way; otherwise it runs a corridor round the footprints padded by 0.2 m, a body's width, so even its last step misses them by that much. An end inside padded furniture (a mark in its seat) steps out of it first, by its nearest side, or out of a seat by the most forward of the sides within half a meter of the nearest (the front of a bench sat in the middle of, the back of a seat at a table it faces), and comes in that way at the other end. (implementation) Let it leave by any side, the twenty-fourth watch-it's Chief walked from the bench straight out through its back to the window. Not out of a piece both marks name as their seat: from a tent's bedroll to its dry corner the walk stays inside the tent (the thirty-first watch-it's Chief went out through its side, round its back and in again, 18 s for 2.6 m). With no way round, it is a straight line. (implementation) blocking.head_in_piece warns, once per character, piece and spot, where someone settled inside a piece made from a recipe has their head in its own parts: the line from their chest to 0.15 m under the top of their head passes into them (a tent's low eave over a bedroll, the top bunk over someone sitting up in the bottom one), and still is a moment (12 frames) on, turned to where they settled. The thirty-first watch-it's Chief sat with his head through the tent's roof. (implementation) blocking.too_big warns, once per character and spot, where a body with a length (a plan's: a whale, a horse) settles in an interior set reaching more than 0.25 m into its side or back walls, its length along its facing and its width across it centred on its floor point, with how long it is and the room it needs: a humpback whale floated by a living room's door with her tail through the wall. A planned way round the furniture under 0.6 of the walkways' length is taken instead of them: the nineteenth watch-it's cargo hold had every walkway meet in the middle, and the Captain crossed the hold and back, 6 m, to sit on a crate 1.5 m away. So is one shorter than the walkways when they run more than 0.5 m on past where the walk goes, to come back to it: the hiccups watch-it's engine room had walkways from the workbench and the core to the ladder, none between them, and every walk from the bench to the core went out by the ladder and back. enter starts off stage at the entrance; moving to an entrance exits. A walk starts where the body is when it starts, which is where the plan has them unless a drift carried them off (docs/design/m6-float.md, "Drifting"). (implementation) A kit piece whose walkOn is set (a ramp, a platform, steps, a dune) is walked on, not round: it is no obstacle, a mark or entrance over it with no heightM of its own stands on its top, and a walk that crosses it is sampled every 0.2 m at its surface, so it climbs the ramp and comes down it (Stage.groundAt, Stage.drape). The top is found by climbing the piece's parts from the floor 0.35 m at a time, so a sloped extrusion counts by its profile and the hatch frame over the ramp does not. The thirty-second watch-it's showrunner laid the Marigold's ramp almost flush with the sand, as nobody could walk up a slope.
  • Approaches (EpisodeIR 1.8). A move whose to names a character walks up beside them (compile/approach.ts): along the walk to wherever they stand, stopping where it first comes within 0.9 m of them (a hand-off's reach, or talking close). That spot must be inside the walls and clear of furniture by 0.25 m and of everyone else by 0.6 m. Otherwise the next clear spot round them, 35° at a time either way up to 105°, is reached by a step off the walk 1.5 m out, planned like the set's walkways round the furniture and the people. With nowhere clear, they stop where the walk came within reach and blocking.crowded warns. Of the clear spots, the first that is beside them as the set's establishing camera sees it (the line between the two runs at least 53° across its view) and hides no one from it (nobody within 0.45 m across its view, one behind the other) is taken, then the first that hides no one, then the first beside them, then the first clear one; square beside them each way, and 20° off that, are tried after the turns. A stage director keeps a newcomer from upstaging someone: walking up to someone in a corner from the room ends at their side, not in front of them with a back to every camera that has room. Someone already within 1.1 m only turns to them, over a look's 0.3 s. They face whoever they approached on arrival. blocking.approach_offstage is an error when that character has left or not come on. The spot is not a mark: a walk on from it, or to someone standing on it, is planned round the furniture and round everyone standing on stage, each a 0.7 m square, rather than through them.
  • Up to a piece of the set (EpisodeIR 1.16). A move whose to is element:<id> walks up to that element (approachElement, compile/approach.ts) and stops 0.35 m out from one of its sides, then faces its look point. Spots are tried at the middle of each side and at half and 85% of the way to either end of it, and must be inside the walls and clear of furniture by 0.25 m and of everyone else by 0.6 m. The front (the element's own +z, where a console or a bench is worked) wins unless another side's spot is more than a metre nearer. Someone already within 0.55 m of it only turns to it. With no clear spot, blocking.element_unreachable is an error. The spot is not a mark, as above.
  • Starting at a piece of the set (EpisodeIR 1.64). A placement whose mark is element:<id> starts them on that piece's spot (elementSpot, the same function a walk up to it uses): with nowhere to come from, the front's middle first, then along it and round the other sides, clear of those placed before them, facing the piece. A walk from there starts from the spot. A piece the set does not have, or one with no clear side (a screen on its console), is blocking.unplaced, said in words; validation calls the first ref.unknown_element. Episodes before 1.64 compile byte for byte as they did (core/test/element-placement.test.ts).
  • Up to a prop (EpisodeIR 1.38). A move whose to is prop.<name> goes where the prop is as the cues before it leave it, read from the scene's own word since nothing is timed yet (propWhere): its place in the scene's props, then each prop cue's place, the hand that took it, the recipient it went to; a prop riding another is where that one is. In someone's hands, the walk goes up to them; on a piece of the set, up to the piece; on the floor at a mark, up close (it stops 0.55 m out, or only turns to it within 0.7 m) and faces it, not onto the mark. Flown off, up on the ceiling or nowhere said, blocking.prop_nowhere is an error.
  • Inside a prop (EpisodeIR 1.39). A posture cue with inside: prop.<name> slides them in to where the prop is (the prop plan's, so the scene is blocked twice) as they get down, keys them out of sight once down, and back in sight at their next move or posture, which brings them out. A prop nowhere then is blocking.inside_nowhere; standing or leaning inside one is posture.inside_standing.
  • Keying a walk. Each corner of a walk is keyed at its share of the walk's length, the arrival on the walk's last frame. A corner that rounds onto that frame or past it is dropped, never the arrival: a short last leg once left the walker that far short.
  • Facing starts from the blocking facing target, else the mark's facingDeg. A look cue with with: body also turns the facing toward the target over the look's duration, but not of someone lying down, whose head turns instead (implementation). Walking turns onto each leg and, on arrival, to the mark's facing (to the character, for an approach).
  • Look targets resolve to world points:
    • a character: that character's eye point at that time (see Metrics). While a look at a character holds, extra keys follow the character's eyes through posture changes and walks (implementation);
    • element:<id>: the element's world-space top-center, through its parent chain (position, then yawDeg, then the tiltDeg param). Box, cylinder, ring and panel pieces are centered on positionM; an extrusion's profile rises from it;
    • mark:<id>: the mark at 1.5 m;
    • prop.<name> (EpisodeIR 1.38; looks, gestures and line lookAt): the prop's middle wherever the prop plan has it then, in a hand, set down, in flight or riding another prop. While a look at it holds, keys follow it every 4 frames it moves 3 cm or more; a body turn toward it blocks the scene twice, once to plan the props (implementation);
    • camera: the active shot's camera; offscreen_left, offscreen_right: off past the lens on that side of the screen, as someone looks at a person off screen in a shot and its reverse (toward the camera, turned 35 degrees to that side, 4 m off; the face still shows); up, down: up and ahead, or the floor ahead. These are the looker's own: as they walk or turn while the look holds, keys take it with them (Night Watch's Chief, looking down as he set off, turned his head back to the floor where he had stood) (implementation).
    • Film grammar and the framing score read a look as the engine draws it (drawnLook): the point of the look's last key, named by the look held then. (implementation)
    • A shot opens on a look already made (clearOfCuts): a turn of the head (a look cue, a line's lookAt) that would still be moving in a new shot's first 8 frames is made in the shot going out, done by the cut; with no room for it there after the look before, it waits until those 8 frames are past. Only a new shot that shows whoever turns (the top of their head, their eyes or their chin in its picture at the cut, headShows) moves their turn: anyone else's, moved into the shot going out, would change the look that shot ends on, and a single's eyeline at the cut with it. The Salvage Claim's close-up on the Chief for l05, cut on the line's start, opened on his head whipping round to Otto (implementation).
    • A look at the lens or off past it turns again at each cut to the new shot's camera (LENS_TARGETS, TargetWorld.cuts): it was held where the first shot's camera put it, so a second shot from the other side ran it the other way (implementation).
  • Line lookAt applies from the line's start (a head turn, 0.3 s), then holds. It adds nothing when the actor already looks there. (implementation)
  • Posture keys come from blocking and posture cues (blend = durationSec). A walk stands up first and settles into the destination mark's posture, unless a posture cue starts the frame they arrive: that is how they arrive (a seat too low to sit up in is crouched on, and sitting first put a head through the top bunk). (implementation) Not inside a piece made from a recipe that is too low to stand up in (both ends inside it, a standing head going into it at an end or between): it crouches and shuffles there, as the thirty-first watch-it's Chief should have crossed the tent. Not along a seat: a move between two marks of the same seat, both sat or crouched on, at most 1.2 m apart, slides straight over as they are, at 0.6 m/s, with no steps and no turn to go, and turns to the new mark's facing. The thirty-first watch-it's Chief stood up, shuffled and sat back down to scoot 45 cm along his bedroll. A key blends from the one before's value, so a blend the next key cuts into ends where that key starts (settleBlends, for posture and expression keys alike), and the pose or face never jumps.
  • Visibility is true from the scene start unless blocking says visible: false.

Metrics (shared with the engine's character builder; both implement these formulas)

From a CharacterInstance, in meters:

H = heightCm/100
headH = H/headsTall
torsoLen = (H − headH) × (1 − legLength)
L = (H − headH) × legLength                   # leg length
Point Standing height
top H
eye H − headH × eyes.height
mouth H − headH × (e + 0.6 × (1 − e)), e = eyes.height (the head builder's mouth line)
chin H − headH
chest chin − 0.35 × torsoLen
waist L + 0.25 × torsoLen
hip L
knee 0.5 × L

Postures move the hip and keep the upper body rigid above it (pitch positive forward, as the engine's RwCharacterMetrics):

Posture Hip height Upper-body pitch
stand L 0°
sit 0.46 m −5° (back)
crouch 0.42 × L +15° (forward)
lean L +8°
lie 0.2 m −90° (M1 needs no framing solve for it)

Every other point = hip + (standing point − L), rotated by the pitch. The engine test checks rig bounds against these within 5%.

(implementation) The sit hip is always 0.46 m. "The seat element's top" cannot reach the engine (the Timeline carries no seat), and the bridge's pilot_seat is a cylinder centered on the floor, so its top is 0.25 m.

Framing solver

Inputs: the shot's framing, subject metrics at the shot's start frame, the scene axis, the style lens defaults, and the format aspect. Without scene.axis, the axis is the first exchange between two characters the scene shows at some point: a voice that is never seen has no screen direction.

  1. Size → frame height h at the subject, with a vertical anchor:

    • ECU: 0.55·headH, centered on the face itself: halfway between the eyes and the mouth, 0.43·headH before the eye line Metrics names (half the depth the engine gives a head, RwJointLayout.head_d, RwPoseSolver.face_point), the way they face. (implementation) Centred on the middle of the head, an ECU seen three-quarters on had an ear and the hair in the middle of the picture and the eyes and mouth off to one side.
    • CU: 1.5·headH, eye line at 1/3 from the top.
    • MCU: (top − chest) × 1.35, eye line at 1/3.
    • MS: (top − waist) × 1.25, eye line at 1/3.
    • MLS: (top − knee) × 1.15, eye line at 1/3.
    • LS: H × 1.25, body centered.
    • WS: H × 2.5, centered, or fit all subjects plus 20% margin.
    • EWS: H × 6.

    Heights are taken in the subject's posture at the shot's start. "Eye line at 1/3" puts the look-at h/6 below the eye. (implementation) Lying down, the head is no higher than the waist, so MCU, MS and MLS are measured along the body as it stands, and the look-at is on the eye line (blended while they lie down or get up); a medium shot of the Chief under the galley table had put the camera on its own target.

  2. Lens. framing.lensMm, else style lensMm.close for ECU/CU/MCU, medium for MS/MLS, wide for LS/WS/EWS. Film back 36 mm horizontal: hfov = 2·atan(18/f), vfov = 2·atan(tan(hfov/2)/aspect).

  3. Distance d = h / (2·tan(vfov/2)), measured horizontally from the anchor point.

  4. Direction. Let P be the partner (the other axis character when S faces them, within 90°; else the actor S faces, the one nearest their line of sight within 90° of it; else the other axis character; else the nearest other actor), u the unit vector from subject S to P on the floor plane, and n the legal-side normal of the axis. (implementation) The first answer took the axis partner or the nearest actor whichever way S faced, which put the camera behind someone who turned to a third character: a toast to the Chief with the axis between the Captain and Otto, or a line to Otto while the Chief walked past closer.

    • Single: camera direction from S = u rotated by (35° + yawOffsetDeg) toward n. With no partner, u is the subject's facing. (implementation) The subject's top is the top of what they wear on their head when they wear something (the props are planned before the cameras), and the frame clears it. yawOffsetDeg runs to ±90°, and (EpisodeIR 1.24) to ±180° for a framed shot, for someone who faces away from whoever u points at.
    • Two-shot or group: direction = n (turned by yawOffsetDeg), and the distance fits the larger of the vertical size (the union of each subject's frame) and the horizontal span (shoulder to shoulder) / aspect + 20%.
    • Over-the-shoulder of O: behind O on the extension of S→O, offset 0.35 m toward n and 0.3 m up. (implementation) The camera sits at D = max(d, |S−O| + 0.6 m) from S, and when D > d and no lensMm was asked for, the lens lengthens by D/d so S keeps its size. (The first answer, "d grown by |S−O|", shrank the subject by that ratio.)
  5. Height. eye: the subject's eye height; low: eye − 0.35·h; high: eye + 0.35·h; overhead and ground: ±1.2·h, kept above the floor and under an interior's ceiling. Look-at = the vertical anchor point.

  6. Legal side. The side of the set's establishing setup. (implementation) A set with none takes the side of the scene's first shot that shows both axis characters (a setup whose frame holds both eye points), else of the first shot. The first answer put the first shot that shows both before the establishing setup, which the heuristic director never could know while it planned: the thirteenth watch-it's Captain began outside the wide, so the compiler judged against a later camera on the other side and found four axis errors in a cut the director had kept legal. Without an axis, n points from the actors toward that camera. The side is read where the pair first stands on stage, at least 0.5 m apart and still (else where both are first on stage), and the axis holds from that frame on. Two people walking on through one door stand on one spot at the scene's start and single file after it; neither says which side the scene plays to. A float scatters them, and its wide shows it: the axis lapses while either of the pair floats, or is out of sight (in a barrel, under a tarp: with both of the hide-and-seek pair hidden, a pan with the Captain crossed a line nobody stood on), and where the float lands them or they come out the line is drawn again, the legal side the reference camera's (unless it stands within 0.1, the sine of its angle, of the line). A drift round each other that turned the old side to a wall leaves the camera in the room. A walk on the deck draws it again the same way where the walk ends (the side holds while they walk): the nineteenth watch-it's search sent Otto to the rack and the Chief to the crates, swapping them as the room's wide saw them, and the old side put every camera in the hold, the wide too, across the line. A cut that snapped to silence reads the side at the time it asked for, where the director planned it.

  7. Moves. For push_in, pull_out and zoom, a second key at the end of the move with the toSize framing, or intensity × 30% closer (zoom: a lens 1/(1 − 0.3·intensity) longer). tilt/pan: rotate look-at by degrees (tilt up and pan right are positive; whip_pan defaults to 90° in 0.25 s). orbit: rotate the camera about the look-at. truck/dolly/crane: translate by meters. follow: keys that carry the camera with the followed actor. shake: a Shot shake block (8 Hz), seeded from deriveSeed(sceneSeed, shotId). A move runs from startOffsetSec for durationSec (default: to the end of the shot). (implementation, EpisodeIR 1.41) A move with to is keyframed between two framings: the shot's own (the start key) and its framing with to's size, subjects, yawOffsetDeg and height (the end key), solved by the framing solver where the subjects are when the move ends. pan, tilt, whip_pan and zoom turn where the camera stands: two keys, the second aimed at the end framing's look-at with the lens that gives its frame height from there (8 to 400 mm), eased by the move's easing. The other moves travel: a key every frame (fewer past 900 frames), each eased already and joined straight, the look-at sliding between the two and the camera swinging round it, its bearing, its distance across the floor and its height each going from one to the other, so a turn arcs round the subjects rather than cutting the chord. to replaces toSize, degrees, meters and intensity (a warning names one given anyway); on a static, shake or follow move, or a shot that is not framed, the camera holds and says why (camera.move_ignored). (implementation, EpisodeIR 1.50, Timeline 1.27) A framed shot's camera.keys are the general case, and a move with to the two-key one (compile/framing.ts keyedCamera, throughKeys): each key is the shot's framing with its own size, subjects, yawOffsetDeg, height and lensMm, solved where the subjects are at its atSec (of two on one frame the later wins; one that cannot be solved is left out with a warning). A key keeps its time outside the shot too, atSec below 0 or past its end: the camera comes from it or is on its way to it, and the shot plays its part of the way (withinShot: a span across the shot's start or end is keyed every frame of its part in the shot, where the whole span puts the camera then, so nothing outside the shot is written; spans wholly in it keep their keys). Before the first key the camera holds it, after the last the last. From each key to the next it comes as the later key's move says, else as what changes says (keyMove: nothing, static; only the lens, zoom; only the subjects, pan; only the size, push_in or pull_out; only the turn, orbit; only the height, crane; more, dolly), a move the style must allow (grammar.move_not_allowed names the one a change makes). static holds the camera where the key before left it (a rack focus with no move); pan, tilt, whip_pan and zoom turn it where it stands as above, to the key's own lens when it names one; the rest travel a key a frame. to is keys [0 s, the move's start, its end], the first being the shot's framing as it starts, so it compiles to the very keys it did. Lenses go in equal steps of magnification (zoomBetween: f₀·(f₁/f₀)^u), here and in the engine, so a zoom runs evenly and a dolly zoom (two keys on one size, a long lens against a wide one, move pull_out) holds its subject while the room swells behind. (implementation, EpisodeIR 1.50, Timeline 1.27) Focus (compile/focus.ts): a shot's camera.focus is on a character (the plane of their eyes along the view, wherever they and the camera go) or at distanceM, at fStop (2.8 unless said); a key's own focus changes it from there (a setup's or an insert's camera.keys change only the focus, keyStops: the camera stays the setup's, and a key that frames is shot.key_framing), and from one key to the next the focus racks over the span, eased as the later key eases, its distance's inverse and its aperture's inverse each going evenly from one to the other (from none, all sharp, the aperture opens on the other's distance). A focused shot is keyed a frame at a time (fewer past 900) with focusM and fStop on each, the camera as the engine would draw its keys there, and then every key a straight line between its neighbours draws within a fifth of a millimetre, a hundredth of a percent of lens and a thousandth of a diopter is left out, so a held focus is two keys and a rack a key a frame where its ease bends. A shot's camera.handheld is the shot's handheld, in place of the style's. (implementation, EpisodeIR 1.33, Timeline 1.16) A shake cue shakes the scene, not a shot: Scene.shakes holds one window per cue (its intensity, 0.6 unless said, for durationSec, 1.5 s unless said, at 8 Hz, seeded from deriveSeed(sceneSeed, cueId)), and the engine adds each to whichever of the scene's shots plays, on top of the shot's own. One shake carries across a cut as it was, and two jolts keep the calm between them.

  8. Stay inside the set. A solved camera stands inside stage.boundsM: 0.25 m in from the walls of an interior set and at most 1.0 m past its open downstage edge (+Z); 1.0 m past any edge of an exterior set. When the ideal distance lands outside, the camera slides in along its own floor direction to the edge of that region and the lens shortens by the same ratio (f′ = f·D′/D, min 8 mm), so the frame height at the subject is unchanged. Height and look-at stay as solved. Nor does it stand inside furniture as tall as the lens (what film grammar cannot see through, compile/sight.ts, unless the piece stands round the look-at too, like a dome): it comes on in to 0.1 m in front of it (the twenty-ninth watch-it's entrance, pulled in from the wall, stood in the lockers). The props standing in the scene as the shot starts (not in a hand) count as its furniture does, each by its box (standingAt in compile/props.ts): the thirty-sixth watch-it's close shot of Otto stood the lens in the oven on the counter. Over a lower piece it stays put. A group's camera brought in so keeps the frame's size at the group's middle, not at whoever stands nearer it, so its lens then widens further until every subject's body is in, side to side (Hat Day's Captain under the vent was half out of every three-shot). An info diagnostic, framing.lens_adjusted, gives the old and new distance and lens, and what it stood outside or behind. (implementation)

  9. Roll (M4). framing.rollDeg rolls the solved camera about its view axis (a dutch angle), so the look-at stays at the frame's center and nothing else moves. A setup's own rollDeg carries through the same way. Moves keep the roll. Positive turns the camera counterclockwise seen from behind it (the engine's RwLens). The style decides whether any roll is allowed (grammar.dutch_not_allowed below).

  10. Inserts (EpisodeIR 1.10). A shot's camera { "type": "insert", "element": … } frames a set element close, for a thing that matters where the set has no close camera on it (the tenth watch-it's kettle whistled a few percent tall in the bridge's wide). The element's box is its footprint and height band (Stage.box), or, while the scene shows a prop that stands in for it, the prop's own box on it (standingBox: the kettle, handle and all, not the shelf's stand-in). The camera looks at the box's center from the legal side of the axis, turned yawOffsetDeg, on the size's lens (size MCU, CU, the default, or ECU), back far enough that the box fills half, two thirds or nine tenths of the frame's height (its width counting for the frame's shape; at least 0.2 m of the world), and 0.15 of the frame above its center, looking down a little. It is kept inside the set (rule 8). Film grammar sizes it at its own size; the people in it are only who it happens to show. { "type": "insert", "prop": … } (EpisodeIR 1.36) frames a prop where it is as the shot starts: on its place, riding another prop, or in a hand, boxed as the engine draws it there (heldScale times its size about its grip, at the carry point, turned with whoever holds it), not at its true size.

A named setup is the set's own and is not moved; one behind an interior wall or above the ceiling is a warning (framing.camera_behind_wall).

A framed camera standing out past an interior's open front that sees the floor's front edge, or what lies past the end of a side wall, shows where the set stops: a warning (framing.past_the_set, seesPastSet in core/src/compile/framing.ts). The night watch's wide group shots, turned 30 degrees, showed the stars past the bridge's deck; turned 20 they do not. Each corner of its picture must come in through the open front, over the floor, under the ceiling and between the side walls: Hat Day's long shot of the Captain, from just past the bridge's front looking along it, had the deck's edge under the frame and the space under the deck in a corner.

Film-grammar validation (diagnostic codes)

Every finding carries a pointer and a hint. The director's repair loop routes it by pointer (/scenes/i/shots/… goes to its coverage stage), so shot findings point into the shot.

Code Rule Severity Pointer
grammar.axis_crossed Camera on the illegal side without crossesAxis (inserts and cutaways that show neither axis character are exempt) error shot /camera
grammar.jump_cut Consecutive shots share a subject, the camera direction changes < 30°, and the size differs by < 2 steps warning shot
grammar.shot_too_short / grammar.shot_too_long Outside the style's cutting bounds warning shot
grammar.no_establishing First shot of a new set is tighter than LS and is not an establishing setup warning shot
grammar.speaker_unseen An on-screen line whose speaker is framed for < 50% of it info line cue
grammar.cut_mid_word A cut with snap: silence found no silence within 0.3 s (reports the offset used) warning shot /start
grammar.eyeline_mismatch (M4) Shot/reverse-shot (consecutive singles on X then Y, X looking at Y and Y at X) whose looks run the same way across the screen. Names both looks by the cue that set them error; warning when either shot has crossesAxis incoming shot /camera
grammar.screen_direction (M4) A character walks out of frame-left and into the next shot that shows them from frame-left (or right and right), still travelling the same way error entering shot /camera
grammar.pace_too_slow / grammar.pace_too_fast (M4) A scene's mean shot length is over 1.5× or under 0.5× the style's cutting.averageShotSec. The hint gives how many cuts to add or remove and names the longest shots to split or the shortest to merge warning /scenes/i/shots
grammar.framing_bias (M4) The episode's shot sizes against the style's framingBias, by shot count: total variation distance over 0.3 (only with 6+ sized shots). The message gives both distributions; the hint names the over-used shots warning "" (the episode)
grammar.move_not_allowed (M4) A shot's move is not in the style's moves.allowed error shot /camera/move/type
grammar.transition_not_allowed (M4) A shot's transitionIn is not in the style's transitions.allowed error shot /transitionIn/type
grammar.dutch_not_allowed (M4) A shot rolls the camera (framing.rollDeg, or a setup's rollDeg) and the style's dutchAngles is off or absent error shot /camera/framing/rollDeg or /camera/setup
grammar.face_hidden (M4) While a character says an on-screen line in a shot that frames them, their face points more than 105° from the camera for most of the frames they are in it. Framed: a framing's subjects (never an over-the-shoulder's foreground shoulder), or a setup's most central character and anyone else it shows at LS or closer error for a single (MLS or closer), anything MCU or closer, or a setup centered on them at MLS or closer; warning for wider groups shot (implementation) Lying on their back, the face is to the ceiling: any camera not below it sees it.
grammar.subject_hidden (M7) Someone a shot frames (a framing's subject, or whoever a setup centers at MLS or closer) has their face hidden from the camera for most of the frames they are in it: the sight lines to the middle of the face and to one side of it (0.35 head heights) both pass through the solid middle of a set element (an ellipse 0.8 of its footprint's half-extents, within its height; windows, doors and wild walls hide nothing, nor does an element they stand within 0.15 m of, (implementation) except one they are inside that is made from a recipe, a tent, a bunk or a booth, which hides them where the line passes through its own parts: its boxes, its extrusions by their profile, its cylinders and rings round and its lathes by their profiles, not rods under 0.1 m) or through someone else's head (0.42 head heights wide), torso (0.75 of half the shoulder width) or legs (0.5), or (implementation) through what anyone carries, their own face's holder too (the prop's box as built, heldScale times its size, about the carry point and turned with them; not while a use lifts it to the mouth or the eyes). An over-the-shoulder's foreground shoulder hides nothing, nor what it carries warning shot
grammar.subject_buried (implementation) Someone a shot frames (as grammar.subject_hidden picks them), whose face the camera sees, has a picture that is the set rather than them for most of the frames they are in it: the camera stands inside a piece's own parts (inside its box, for a piece with no recipe; a tent's air is not a part); or a piece hides half or more of their upper body (top of the head, eye, chin, shoulder, chest, waist, of those in the picture), the line to each point read through the piece's parts, a piece they stand within 0.15 m of hiding nothing; or the pieces before them cover more than a third of the picture (a 9 by 5 grid of sight lines as deep as the nearest of them, each counted when it first enters a piece in the near half of the way, or one that hides part of them; a piece they are inside is the room they are in, not before them). The props standing then count as pieces too, by their own parts, except for standing in. A group's picture wider than MLS may show a head over a counter; a picture of one person may not. What someone hides behind on purpose, down (not standing or leaning) on a mark the piece occludes, buries no one, and an insert buries no one behind the thing it shows. Cylinders and rings are read round and lathes by their profiles warning shot
grammar.leap_hidden (implementation) Someone getting up onto something or down off it (a move with onto, and the move after it that takes them down: the frames the height they stand at changes, leapFrames) is not seen in a shot that plays it for more than half of those frames: they are out of the picture, or their legs (hip and knee) are hidden by a piece of the set, read through its parts (what they climb and what they hide behind on purpose hide nothing), or under the picture's edge. Fewer than 3 frames of it in a shot are not judged warning shot
grammar.cut_mid_leap (implementation) A shot starts while someone is in the air: strictly after a leap leaves the ground and before it lands warning shot
grammar.cut_mid_flight (implementation) A shot starts while a thing thrown (a throw prop cue) is in the air: strictly inside its flight as the prop's plan draws it, from the hand to where it lands warning shot
grammar.subject_out_of_frame (M7) Someone a framing names as its subject is out of the picture (their eye point outside the frame) for most of the frames they are on stage in the shot. A group's camera brought in from a wall can end beside whoever stands in front of the others warning shot
grammar.face_cut (M7) The left or right edge of the picture cuts through a face (its middle within 0.6 of its half-width of the edge, the face 0.9 head heights wide) for most of a shot. A framing's own subjects and an over-the-shoulder's foreground shoulder are exempt; a set's camera exempts nobody; faces narrower than 4% of half the frame do not count warning shot
grammar.mouth_hidden (0.12) The framing score (compile/framing-score.ts): a speaker in the picture while they say an on-screen line has their mouth unseen for most of the frames they are in it: out of the picture, turned more than 90° from the lens (a speaking face is seen from its front half), behind a piece of the set (read through its parts, a piece they stand at too) or behind someone else or what someone else carries. (implementation) The mouth is where the face is drawn: on its front (half the head's depth ahead), carried ahead by the posture's lean, turned toward the look (at most 60°) and tipped toward it (at most 25°) about the head's pivot 0.18 heads over the chin, as RwPoseSolver poses the head. A line written (O.S.), (V.O.), or with a parenthetical that has them unseen ("from behind", "turned away", "muffled", "hidden") is not judged, nor a mouth behind the cover they hide behind on purpose. Not said where grammar.face_hidden already says it warning shot
grammar.subject_crossed (0.12) The framing score: someone or something across whom a shot is of, for most of it: another body before them covering a quarter of their face (half, for anyone else in the picture), or half their body and a quarter of their face; a piece over their face or half their upper body; people before them filling more than 0.3 of the picture, or pieces more than a third. Short of that, a piece before them costs part of the weight, by how much of them (against half) or of the picture (against a third) it covers, once it covers 0.3 of either and holds for a third of the shot; not a piece within 0.8 m of their feet, which is where they stand (the Chief at his console). Bodies are boxes on the picture as the engine draws them: the head at its width ratio, its hair out at the sides (spiky hair 0.7 of its thickness there) and over its top, a body posture's lean ahead of the feet (hunched 0.13 of the trunk), the face's front 0.1 heads past the head's side (0.45 under a brim); the torso half the shoulders and an arm wide, narrower side on. What someone hides behind on purpose is listed in the score as meant and not said. Not said where grammar.subject_hidden or subject_buried already says it warning shot
grammar.body_cut (0.12) The framing score: the left or right edge of the picture cuts a head (between 10% and 97% of its width out, or within 0.04 of the edge), the bottom cuts a head, an edge cuts a body in half (25% to 85% of its width out), or the bottom of the picture sits on a joint of whom the shot is of (the neck, the knees or the ankles, within 0.2 of a head; not where a piece hides the joint), for most of a shot. An over-the-shoulder's shoulder is exempt; heads narrower than 3% of the frame do not count. Not said where grammar.face_cut already says it warning shot
grammar.headroom (0.12) The framing score, against the top of the head as it is drawn: its hair's tips (spiky hair's crown its thickness over the head, as the lighthouse's renders draw Otto's), a cap's or a brimmed hat's crown, a hat worn as a prop. From MLS to MCU, the top of the picture cuts the head, its hat or its hair (by more than 0.05 of a head), leaves less than 0.02 of the picture over it, or leaves more than 0.3 of the picture over a lone subject's head; in a close-up, the top cuts more than a quarter of a head into the head under its hair, or more than half of a hat. A top that cuts or touches is said when it holds for a third of the frames judged (the street's Maya walked up into the bench camera's top halfway through her shot); too much room, over half warning shot
grammar.tangent (0.12) The framing score, from MLS to CU, for whom a shot is of: a piece of the set or a prop standing or hung whose bottom edge on the picture lies along the top of their head as it is drawn (its hair and hat), within 0.2 of a head either way, across 0.4 of its width or more, rising over it (Kitchen Science's cupboards on Dad's head in his medium shot); or, standing still and holding nothing, a piece behind them (not one within 0.8 m of their feet) whose top or bottom edge runs within 0.35 of a hand through a hand at rest. For most of a shot; not for a piece they hide behind on purpose. The reframe tries a step aside, a longer or wider lens and the camera higher or lower for it, then the rest (compile/tangents.ts) warning shot
grammar.setup_repeated (0.12) The framing score's variety rule: a shot from the same camera as the shot before (the same setup or framing, or a camera within 0.3 m, 10° and a fifth of its lens), or as the one before that, when the shot between is no insert and no one in either spoke anew (a shot and its reverse is a reason to come back) warning shot

Style validation keeps the camera language self-consistent: style.transition_default_not_allowed (error) when transitions.default is not in transitions.allowed, since a shot without a transition takes the default for transitions.durationSec (default 0.5 s; cuts take none), and style.framing_bias_unknown (warning) for a bias value that is neither a size nor a class.

A setup's subjects are the characters whose eyes are in its frame; their size is the size whose frame height at their distance is nearest. A shot's size, for framing bias, is its framing's size, else its most central subject's. (M4)

(M4, implementation) How the new checks read the picture:

  • Eyeline. A single is a shot with exactly one subject. A character's look is the last look cue or line lookAt that took hold (a line looking where they already look changes nothing). The outgoing look is read on the last frame before the cut, the incoming one 0.5 s after it (a look that lands just after the cut counts). The screen direction is the sign of the look's sideways motion across the image, taken in each shot's own camera at that frame; a look within 10° of the lens axis has none, and a looker a move has carried out of frame is not on screen: neither is judged. A shot that already reports axis_crossed for the same pair is not reported again. With two characters and a scene axis on them, the axis rule implies this one; it earns its keep on third characters, scenes without an inferable axis, and deliberate crossings.
  • Screen direction. An exit is the last frame the character's eyes are in frame, the next frame outside a side edge, while walking. The next shot that shows them is the entrance: through a side edge, or already walking at the cut (moving screen-left means they came from the right). Walking within 30° of the lens axis (a neutral shot) or standing has no side and starts afresh, as does an establishing shot (a setup marked establishing or a shot of that purpose) that leaves them out, or walking back the way they came. Checked within a scene.
  • Faces. The face points along the body's facing turned by the head toward the look in effect, at most 60° (the engine's LOOK_HEAD_MAX_YAW; a look with the eyes alone leaves it), and is compared with the floor direction from the eyes to the camera. 105°: the head builder hides a feature once cos(azimuth + yaw) drops below −0.2, so the nose and mouth go behind the cheek at 101.5°; the rest absorbs a look still settling and the head's pitch. A line is judged in a shot only after 6 frames of it are framed there. The hint offers both fixes: turn the character (a body look, a swivel when seated) or move the camera to the side they face.
  • Framing bias. Bias values are sizes (EWS … ECU) or classes (wide = EWS/WS/LS, medium = MLS/MS, close = MCU/CU/ECU, as the lenses). A shot counts toward the value that holds its size, else the nearest by size steps (the first listed on a tie).

Performance channels

  • Visemes. Rhubarb mouth cues are offset by the line start. Sample each lipSyncStep-aligned frame at its time; the shape active at that time wins. Use X when nobody is speaking. Keys are emitted only on change.
    • (implementation, EpisodeIR 1.62, Timeline 1.36) Mouths drawn by hand. A line's mouth spans play their shape over Rhubarb's, each key from one marked drawn (Studio's Mouths row shows it in the accent). A span is kept as places among the line's words (fromWord, toWord: 2.5 is halfway from the third word's start to the fourth's; -1 to 0 runs from the take's start to the first word, n to n + 1 from the last word's end to the take's end), placed on whatever take plays by the line's word timings (core/src/compile/mouths.ts), so a new take of the same words plays it over the same words. One whose words the line no longer says there is not played, with the warning mouth.words_changed in plain words; re-direction drops it with the same warning (core/src/director/kept-mouths.ts), and a dub drops them all. edit_episode set_mouth {scene, cue, shape | "rhubarb", fromSec, toSec} (seconds into the line) draws one over what was there, or gives the span back to Rhubarb.
  • Expressions (implementation): the blocking emotion, then expression cues, and each line's own emotion/intensity from its start (0.2 s blend) when it differs from the emotion already showing. An expression cue on the same frame wins.
  • On the face (implementation, EpisodeIR 1.32, Timeline 1.15): a mess cue puts soot, dust, sand, mud, paint, snow or cream on someone's face (Actor.mess keys): cover of it (0.6 unless said) in the kind's color or the cue's, over blendSec (0.1 s coming on). It stays until another mess cue for them or the scene's end. Cover 0 wipes it off over 0.6 s, the key keeping the kind and color of what is there; a wipe with nothing on the face is left out. The thirty-second watch-it's Chief got a faceful of sand that left no trace.
  • On the set (implementation, EpisodeIR 1.40, Timeline 1.17): a damage cue leaves a crack, a scorch, a dent or a stain on a piece of the set (Scene.damage, damages in core/src/compile/performance.ts): one mark per piece and kind, keyed where each cue sets its size (acrossM: a crack 0.6 m unless said, a scorch 0.5, a dent 0.2, a stain 0.4), spreading to it over growSec (0.3 s, 0.5, 0.05 and 0.4) or going over 0.8 s (acrossM 0). A later cue's color, face or spot replaces the earlier's, and taking away a mark that is not there is left out. Its seed comes from the scene's, by its first cue's id. The twenty-fourth watch-it's crack in the dome was a prop lying on the floor.
  • Blinks. Pcg32(deriveSeed(actor.seed, "blinks")). Intervals are drawn from style.motion.idle.blinkIntervalSec with its bias; a blink lasts 3 frames at 24 fps. Add an extra blink at the start of every body or head turn longer than 0.25 s. Never blink inside the first 4 frames of a shot: such a blink moves to the cut + 4 frames.
  • Breathing. Period 3.8 s × (1.4 − energy) with energy from the character's voice, phase from deriveSeed(actor.seed, "breath"), amplitude = style idle.breathing.
  • Speaking spans cover each line's take; intensity comes from the line; off-screen and voice-over lines are marked offscreen.
  • Actor seed = deriveSeed(episodeSeed, sceneId + "/" + characterId), where episodeSeed = deriveSeed(0, episodeId). FX seeds: deriveSeed(sceneSeed, "fx/" + cueId).

Audio

  • Clips.

    • Dialogue: the take at its frame-aligned start, pan from the speaker's screen x in the active shot (−0.3..0.3; center when offscreen). (implementation) Gain follows delivery.volume: whisper −8 dB, soft −3, normal 0, loud +2, shout +4. Takes are normalized when they are made, so this is where soft and loud differ.
    • SFX: sfx.<name> → audio/sfx/<name>.flac, gain from the cue, pan from its source target's screen x. (implementation) A source prop.<name> (EpisodeIR 1.23) pans from where the prop plan has the prop as the sound starts, in a hand, on a head, set down, in the air or on what carries it; sfx.no_source warns while it is nowhere, and the clip plays from the middle. (M7) A cue with repeat (EpisodeIR 1.5) plays again every everySec, else its own length and a quarter second, until times plays, the start of the cue until names, or the scene's end: one clip per play (<cue>.r2, .r3…), the last cut short at the stop.
    • Music: start plays audio/music/<name>.flac (or its recipe) until its stop; swell and duck warn. (implementation) A start with loop (EpisodeIR 1.28) plays the track again from its start, back to back, until its stop or the scene's end: one clip per play (<cue>.r2, .r3…), the first fading in and the last fading out; a stop fades out the play under way and drops any after it.
    • (implementation) A cue whose sound or track the show does not have plays nothing and says so (audio.no_sound, with where to add it).
    • A 10 ms fade in and out on every clip.
  • Mix: one ffmpeg filtergraph per ADR-0008:

    1. aresample, atrim in samples, aformat=fltp, volume, fades, pan to stereo (constant power for mono), adelay=<n>S:all=1 per clip, then apad/atrim of every clip to the full length (implementation: ffmpeg 6.1.1's amix silently dropped the end of an input when inputs ended at different times);
    2. amix=normalize=0;
    3. apad and atrim to the exact duration, into a 32-bit float pre-master;
    4. two-pass loudnorm to the show target with linear=true, then check that pass 2 reports linear. (implementation) When the linear gain would lift the true peak over the ceiling, a delay-compensated alimiter first holds the peaks down by the difference (the demo needs it), and pass 2's LRA target is raised to the measured range, because loudnorm refuses linear mode otherwise.

    Write mix.wav (48 kHz, 24-bit) and stems. (implementation) Stems (dialogue, sfx, music) are each their own graph of the same form, 32-bit float at pre-master level, and sum exactly to the pre-master. The mix is byte-identical on ffmpeg 6.1.1 and 9.0.2.

  • Captions. WebVTT (<v SPEAKER>) and SRT (SPEAKER: ) from line spans, with speaker names from shortName, wrapped at 42 characters. (implementation) A line that is a sound the character makes (vocal, EpisodeIR 1.29) is captioned as the sound in brackets, [sneezes], unless its caption.text says otherwise.

Render orchestration (reelwright render)

reelwright render <show-dir> --episode ep.lighthouse_cold_open
  [--out <dir>] [--size 1920x1080 | --preview (1280x720)] [--frames a:b]
  [--capture frame_server|movie_maker] [--chunks N] [--timeout-sec N]
  1. Validate, compile, and write .reelwright/timelines/<timeline id>.json.

  2. Build RenderJobs (split into N chunks by frame range, each with its own output directory under .reelwright/frames/) under .reelwright/jobs/<job id>/job.json. A job embeds the style, every character acting in the Timeline and every set; its id is job. + the first 16 hex digits of the JCS hash of the job without its id. Jobs are schema-validated before they are written.

  3. Check the engine starts (version, after importing the project when a script or scene changed; the import runs beside the process, so a daemon keeps answering meanwhile, and launches that ask while one is under way wait for that one), then run job --job <path> per chunk in parallel, streaming progress. The core starts Godot itself (core/src/render/godot-launch.ts; tools/engine-cli.sh is reelwright engine), so rendering needs no bash. Every run has a timeout (default 120 s + 5 s per frame) that kills its whole process group, because a script error makes Godot hang instead of exiting.

  4. Read each RenderResult. It must be schema-valid, name the job, report no errors, and list every frame exactly once as a file that exists with a matching sha256, with no gaps.

  5. Mix the whole Timeline, then encode the range:

    • H.264 with scale=out_color_matrix=bt709:out_range=tv:flags=lanczos+accurate_rnd+full_chroma_int, yuv420p, setparams tags, CRF 16, -preset slow, +faststart, 8 x264 threads (a fixed count keeps the output byte-identical between machines on one build);
    • AAC 192k;
    • -fflags +bitexact, -map_metadata -1.

    ffprobe then checks BT.709 primaries, transfer and matrix, tv range, the exact frame count, and audio as long as the picture.

  6. Write renders/<episode>/<episode>.mp4, .vtt, .srt and mix.wav (plus stems/), where <episode> is the id without ep., plus a render.json summary (hashes, timings, loudness, engine info, diagnostics).

--size must keep the show's aspect (cameras are solved for it) and be even.

Also reelwright compile (emit the Timeline only) and reelwright stills --frames … (PNG storyboards, copied to renders/<episode>/stills/).