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_tsin a1/sample_ratetime base, else a decode and count), cached at.reelwright/cache/probe/<sha256 of the file>.json. - Rhubarb: binary from
$RHUBARB, else PATH, elsetools/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.jsonnext to the take if a show commits it, else the TTS cache${REELWRIGHT_CACHE:-~/.cache/reelwright}/takes/<sha256>.words.json(written bytools/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 neitherdurationSecnoruntil, 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 (withparthead, 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 putsatMwhere the prop plan has it as it starts (in a hand, on a place, in the air, or on what carries it;fx.no_targetwhile it is nowhere) and the event carries the prop andpropAnchorM(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_aimwhen 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_beforewhen 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'slightingbefore 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.outadds its black hold to the scene's length and gives its last shotout; - 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 ofcontact(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 aninfodiagnostic; 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 anuntilcue, its rise (0.7 s), so the scene plays on under it (docs/design/m6-float.md).
- Pacing defaults when the episode sets none:
lineGapSec0.35,sceneHeadSec0.5,sceneTailSec0.75. (implementation) - Default
at. After the most recent cue that was not placedwithanother cue, pluspacing.lineGapSec. The first cue starts atpacing.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 aslineWordscounts 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 itsstrokeSec, 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, sowith+ 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.6titles) 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: framerounds to the nearest frame.snap: silencetakes 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'smarkers(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, plusoffsetSec, to the nearest frame inside the scene, and go on their character's actor asActor.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 pathviaMpoints (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 theirseat: 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_piecewarns, 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_bigwarns, 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.enterstarts 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 whosewalkOnis set (a ramp, a platform, steps, a dune) is walked on, not round: it is no obstacle, a mark or entrance over it with noheightMof 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
tonames 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 andblocking.crowdedwarns. 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_offstageis 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
toiselement:<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_unreachableis an error. The spot is not a mark, as above. - Starting at a piece of the set (EpisodeIR 1.64). A placement whose
markiselement:<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), isblocking.unplaced, said in words; validation calls the firstref.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
toisprop.<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_nowhereis 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 isblocking.inside_nowhere; standing or leaning inside one isposture.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
facingtarget, else the mark'sfacingDeg. A look cue withwith: bodyalso 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, thenyawDeg, then thetiltDegparam). Box, cylinder, ring and panel pieces are centered onpositionM; an extrusion's profile rises from it;mark:<id>: the mark at 1.5 m;prop.<name>(EpisodeIR 1.38; looks, gestures and linelookAt): 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'slookAt) 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
lookAtapplies 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 sameseat, 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.
Size → frame height
hat 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.
- 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,
Lens.
framing.lensMm, else stylelensMm.closefor ECU/CU/MCU,mediumfor MS/MLS,widefor LS/WS/EWS. Film back 36 mm horizontal:hfov = 2·atan(18/f),vfov = 2·atan(tan(hfov/2)/aspect).Distance
d = h / (2·tan(vfov/2)), measured horizontally from the anchor point.Direction. Let
Pbe the partner (the other axis character whenSfaces them, within 90°; else the actorSfaces, the one nearest their line of sight within 90° of it; else the other axis character; else the nearest other actor),uthe unit vector from subjectStoPon the floor plane, andnthe legal-side normal of the axis. (implementation) The first answer took the axis partner or the nearest actor whichever waySfaced, 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 whoeverupoints at. - Two-shot or group: direction =
n(turned byyawOffsetDeg), 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
nand 0.3 m up. (implementation) The camera sits at D = max(d, |S−O| + 0.6 m) from S, and when D > d and nolensMmwas 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.)
- Single: camera direction from S = u rotated by (35° + yawOffsetDeg) toward
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.
Legal side. The side of the set's
establishingsetup. (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,npoints 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.Moves. For push_in, pull_out and zoom, a second key at the end of the move with the
toSizeframing, orintensity× 30% closer (zoom: a lens 1/(1 − 0.3·intensity) longer). tilt/pan: rotate look-at bydegrees(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 bymeters. follow: keys that carry the camera with the followed actor. shake: a Shotshakeblock (8 Hz), seeded fromderiveSeed(sceneSeed, shotId). A move runs fromstartOffsetSecfordurationSec(default: to the end of the shot). (implementation, EpisodeIR 1.41) A move withtois keyframed between two framings: the shot's own (the start key) and its framing withto'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.toreplaces 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'scamera.keysare the general case, and a move withtothe two-key one (compile/framing.tskeyedCamera,throughKeys): each key is the shot's framing with its own size, subjects, yawOffsetDeg, height and lensMm, solved where the subjects are at itsatSec(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,atSecbelow 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'smovesays, 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_allowednames 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.tois 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'scamera.focusis on a character (the plane of their eyes along the view, wherever they and the camera go) or atdistanceM, atfStop(2.8 unless said); a key's ownfocuschanges it from there (a setup's or an insert'scamera.keyschange only the focus,keyStops: the camera stays the setup's, and a key that frames isshot.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) withfocusMandfStopon 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'scamera.handheldis the shot'shandheld, in place of the style's. (implementation, EpisodeIR 1.33, Timeline 1.16) A shake cue shakes the scene, not a shot:Scene.shakesholds one window per cue (itsintensity, 0.6 unless said, fordurationSec, 1.5 s unless said, at 8 Hz, seeded fromderiveSeed(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.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 (standingAtincompile/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). Aninfodiagnostic,framing.lens_adjusted, gives the old and new distance and lens, and what it stood outside or behind. (implementation)Roll (M4).
framing.rollDegrolls 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 ownrollDegcarries through the same way. Moves keep the roll. Positive turns the camera counterclockwise seen from behind it (the engine'sRwLens). The style decides whether any roll is allowed (grammar.dutch_not_allowedbelow).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, turnedyawOffsetDeg, on the size's lens (sizeMCU, 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
lookAtthat 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 reportsaxis_crossedfor 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
establishingor 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 oncecos(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
mouthspans play their shape over Rhubarb's, each key from one markeddrawn(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 whosewordsthe line no longer says there is not played, with the warningmouth.words_changedin plain words; re-direction drops it with the same warning (core/src/director/kept-mouths.ts), and a dub drops them all.edit_episodeset_mouth{scene, cue, shape | "rhubarb", fromSec, toSec}(seconds into the line) draws one over what was there, or gives the span back to Rhubarb.
- (implementation, EpisodeIR 1.62, Timeline 1.36) Mouths drawn by hand. A line's
- Expressions (implementation): the blocking emotion, then expression cues, and each
line's own
emotion/intensityfrom 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.messkeys):coverof it (0.6 unless said) in the kind's color or the cue's, overblendSec(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,damagesincore/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 overgrowSec(0.3 s, 0.5, 0.05 and 0.4) or going over 0.8 s (acrossM0). 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 fromstyle.motion.idle.blinkIntervalSecwith 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 = styleidle.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), whereepisodeSeed = 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 sourceprop.<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_sourcewarns while it is nowhere, and the clip plays from the middle. (M7) A cue withrepeat(EpisodeIR 1.5) plays again everyeverySec, else its own length and a quarter second, untiltimesplays, the start of the cueuntilnames, or the scene's end: one clip per play (<cue>.r2,.r3…), the last cut short at the stop. - Music:
startplaysaudio/music/<name>.flac(or its recipe) until itsstop; swell and duck warn. (implementation) A start withloop(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.
- 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
Mix: one ffmpeg filtergraph per ADR-0008:
aresample,atrimin samples,aformat=fltp,volume, fades,panto stereo (constant power for mono),adelay=<n>S:all=1per clip, thenapad/atrimof 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);amix=normalize=0;apadandatrimto the exact duration, into a 32-bit float pre-master;- two-pass
loudnormto the show target withlinear=true, then check that pass 2 reports linear. (implementation) When the linear gain would lift the true peak over the ceiling, a delay-compensatedalimiterfirst 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 fromshortName, 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 itscaption.textsays 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]
Validate, compile, and write
.reelwright/timelines/<timeline id>.json.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 isjob.+ the first 16 hex digits of the JCS hash of the job without its id. Jobs are schema-validated before they are written.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 runjob --job <path>per chunk in parallel, streaming progress. The core starts Godot itself (core/src/render/godot-launch.ts;tools/engine-cli.shisreelwright 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.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.
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,setparamstags, 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,
tvrange, the exact frame count, and audio as long as the picture.- H.264 with
Write
renders/<episode>/<episode>.mp4,.vtt,.srtandmix.wav(plusstems/), where<episode>is the id withoutep., plus arender.jsonsummary (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/).