Reelwright architecture
Status: accepted 2026-09-29. Milestones M0–M3 and M5 are done, and M4, M6, M7, M8 and
M10 are under way (roadmap). Toolchain: Godot 4.7.2-stable, ffmpeg 6.1 or later,
Node 24, Rhubarb Lip Sync 1.14.0 (built from source); tools/toolchain.json pins them.
Reelwright lets one person run an animated series by talking to an AI showrunner. The showrunner never draws. It writes specs: small, versioned, schema-validated JSON (and Fountain) documents that a person can read, edit and diff. Deterministic code turns specs into assets and assets into frames. Everything the AI proposes arrives as a reviewable change set, and nothing it made is ever silently replaced.
This document is the map. Decisions with real alternatives live in adr/; the
milestone plan and the changes proposed to it live in ROADMAP.md.
1. Principles
- Specs, not pixels. The AI (and every generator) emits documents. Final frames come only from deterministic builders and the renderer. Tier 2 generative output (M9) is source material: normalized, frozen by content hash, then used like any other asset.
- Decide upstream, draw downstream. Every choice (random or AI) is made in the core and recorded in a spec. The engine is a pure function from resolved specs to pixels: it makes no decisions, reads no clock, draws no unseeded random number.
- Reproducible by construction. Same specs + same seeds + same builder versions ⇒ same assets and the same frames (defined precisely in §6).
- Everything is a proposal. AI output becomes a change set with a diff and previews. The user approves, tweaks, locks or regenerates at any granularity, and can always undo.
- Canon is sacred. Locked documents and locked subtrees never change without an explicit approved change set, even when generators or builders improve.
- Film grammar is code. Rules like the 180-degree rule are validators and solver constraints over real geometry, not advice in a prompt.
- Offline first. Tier 1 (procedural, local) must make a watchable show with no network and no API key. Paid providers make it better, never make it work.
2. System overview
A diagram, as its Mermaid source.
flowchart LR
user([User]) -- chat --> panel[Chat panel<br/>Godot editor plugin]
mcp([Claude Code / any MCP client]) -- MCP --> tools
panel -- JSON-RPC --> sr[Showrunner agent]
sr --> tools[Tool registry]
tools --> cs[Change sets<br/>diff + previews]
cs -- approve --> store[(Show Pack<br/>specs in git)]
store --> val[Validators<br/>schema + semantic + film grammar]
store --> gen[Samplers<br/>pattern + seed → instance]
store --> comp[Compiler<br/>Episode IR → Timeline]
gen --> store
comp --> job[Render job]
job --> eng[Godot engine<br/>builders · player · frame server]
eng -- frames --> ff[ffmpeg<br/>mix · encode · deliver]
adapters[[Adapters: LLM · TTS · lip sync · Tier 2]] -.-> sr & comp & gen
Two programs, one contract (ADR-0003):
Core (core/, TypeScript on Node 24) |
Engine (engine/, Godot 4.7, typed GDScript) |
|
|---|---|---|
| Job | Decide: load, validate, sample, compile, orchestrate, talk to AI | Draw: build rigs and sets, play timelines, capture frames, host the editor UI |
| Owns | Show Pack I/O (the only writer of specs), change sets, undo, dependency graph, samplers, timing resolver, framing solver, film-grammar validators, audio mix plan, showrunner agent, MCP server, reelwright CLI, provider adapters |
Character/set/prop builders, style shaders and materials, timeline player (evaluate(frame)), frame server and Movie Maker capture, model sheets and previews, chat and timeline panels |
| Reads | Everything in the Show Pack | Resolved, validated engine inputs only: instances, style, Timeline |
| Never | Draws pixels | Writes specs, calls an LLM, draws unseeded randomness, reads the wall clock |
People use Reelwright through Reelwright Studio (a standalone Godot app), the Godot editor plugin, the CLI, or any MCP client. Studio and the plugin host the same UI components (ADR-0011).
They talk two ways:
- Jobs (batch). The core writes a job directory
(
.reelwright/jobs/<hash>/job.json+ inputs) and runsgodot --path engine -- rw <command> --job <dir>. The engine writes frames or images and aresult.json(frame hashes, timings, diagnostics). Used by the CLI, CI and render boxes. - Session (interactive). The editor plugin connects to the core daemon
(
reelwright serve) over WebSocket JSON-RPC 2.0. It streams chat, shows proposals, and asks the in-editor engine for previews. Without an editor, the core starts a headless- capable engine worker for previews instead.
Studio's languages. Studio says every word through RwLang (RwLang.t, RwLang.n for a
count), which looks it up in the chosen language's gettext catalog
(engine/translations/<code>.po, Godot's TranslationServer); Settings › Appearance › Language
chooses it (the computer's or the browser's by default), and it takes effect at Studio's next
start. node tools/i18n/extract.ts writes the template (studio.pot) from every call and brings
each catalog up to it; engine/translations/glossary.md keeps the words. The core stays in
English, for Claude, the CLI and the logs, and sends nothing new: its messages and hints are in
the template too, under the context core, and Studio shows what the core said through
RwLang.core, which finds the message a line was made from and fills in its translation. What
the core composes from pieces as it goes (the reading's staged lines, the timeline's business, a
scene's opening, the directions and shot lines it reads, a preview's caption) it says with
say() (core/src/said.ts): the English as before, and beside it (session protocol 1.8) the
template and what fills each slot, which RwLang.said translates, agreeing with a character's
gender from their pronouns (docs/design/translate-core.md). A show's own words (titles, names,
scripts) are never translated; a show has its own language (show.json).
3. The Show Pack (data model)
A Show Pack is a directory, meant to be its own git repository. Documents are found by
convention, <folder>/<slug>.<kind>.json, so tools, humans and editors agree on where
things are and VS Code can validate by glob.
my-show/
show.json ShowManifest: format, active theme+style, providers
theme/<slug>.theme.json ThemeSpec
styles/<slug>.style.json StyleSpec (several; show.json picks the active one)
patterns/{characters,sets,props,fx,graphics}/<slug>.<type>-pattern.json
cast/<slug>.character.json CharacterInstance
sets/<slug>.set.json SetInstance
props/<slug>.prop.json PropInstance
fx/<slug>.fx.json graphics/<slug>.graphic.json
bible/bible.json lore, relationships, running gags, tone rules
bible/continuity/<episode>.continuity.json facts established per episode (M10)
bible/memory.json ShowMemory: what the show remembers of every episode (M10)
episodes/<slug>/<slug>.fountain screenplay
episodes/<slug>/<slug>.episode.json EpisodeIR (the director's cut)
episodes/<slug>/writers-room/… premise, logline, beat outline (M5)
audio/takes/<sha256>.flac approved voice takes, content-addressed
audio/sfx/<slug>.flac or .sound.json a sound effect: recorded, or a SoundRecipe
assets/<sha256>.<ext> frozen Tier 2 assets (git LFS), content-addressed
assets/<sha256>.json each one's provenance: source, license, steps, QA
showrunner/NOTES.md the showrunner's durable notes on your preferences
comments/<slug>.thread.json CommentThread: people's comments on the work (M13)
.reelwright/ caches, jobs, renders, sessions (gitignored)
Document kinds (JSON Schemas in schemas/v1/):
| Kind | Written by | Read by | Status |
|---|---|---|---|
ShowManifest |
user, showrunner | everything | drafted |
ThemeSpec |
showrunner, user | writers' room, samplers, missing-asset pass | drafted |
StyleSpec |
showrunner, user | engine (shaders, timing), framing solver, palette solver | drafted |
CharacterPattern |
showrunner, user | character sampler | drafted |
CharacterInstance |
sampler, user | character builder, writers' room, director, TTS | drafted |
SetInstance |
sampler, user | set builder, director, framing solver | drafted |
EpisodeIR |
director, user, timeline editor | compiler | drafted |
EpisodeOutline |
writers' room, user | missing-asset pass, screenplay checks | drafted |
SoundRecipe |
showrunner, user | sound synthesis (director, compiler, render) | drafted |
SetPattern (kits, layout grammar) |
showrunner, user | set sampler | drafted |
PropPattern / PropInstance |
showrunner / sampler | prop builder | drafted |
FXPattern / FxInstance |
showrunner / sampler | FX builder | drafted |
GraphicsPattern / GraphicInstance |
showrunner / sampler | graphics builder | drafted |
Bible, Continuity |
showrunner, user | all AI stages | drafted |
ShowMemory |
remember passes, user | every writer, within a token budget | drafted |
ChangeSet |
core | UI, MCP clients, history | drafted |
CommentThread |
session daemon (comments.*), for the people in a show |
Studio; never a tool, the graph or a model | drafted |
Timeline (compiled, engine input) |
compiler only | engine | drafted |
RenderJob / RenderResult (ephemeral) |
core / engine | engine / core | drafted |
FrameExport (beside a render's frames) |
core (render --deliver png_frames) |
other programs (Cutwright) | drafted |
GestureLibrary |
Reelwright releases | engine, compiler | drafted |
RigTemplate, PartLibrary |
Reelwright releases | builders, compiler | M1 |
Two definition-only schemas hold shared vocabulary: recipe.schema.json (the parametric
geometry recipe used by kit pieces and props: flat nodes such as box, extrude, lathe, panel
with openings, decal, array and subtract, driven by params through a restricted expression
grammar) and pattern-rules.schema.json (pointer-addressed sampling rules shared by the
prop, FX and graphics patterns).
Sound ids map to files by convention: sfx.<name> → audio/sfx/<name>.flac and
music.<name> → audio/music/<name>.flac, both relative to the show root. Either may
instead be a recipe, audio/sfx/<name>.sound.json or audio/music/<name>.sound.json,
synthesized deterministically into .reelwright/cache/sfx/ (docs/design/m5-writers-room.md,
"Sound recipes").
Conventions (ADR-0005):
- JSON Schema 2020-12, and every schema is also valid as a Claude strict tool or
structured-output schema: closed objects, no recursion,
anyOf+constdiscriminators, no map-shaped objects. Hierarchies are flat lists linked byparentids. A CI lint enforces this. - Every document starts with
kind,schemaVersion,id, then a humanname/title, thenmeta(status, locks, provenance, derivation). - Ids are prefixed by kind (
char.otto,set.marigold_bridge,ep.lighthouse_cold_open), so a reference says what it points at and a regex can check it. Local ids inside a document are short slugs (helm,sh040). - Units live in field names:
heightCm,positionM,yawDeg,durationSec,weightPx1080(pixels at a 1080-line reference, scaled to the output resolution). - Addresses inside documents are JSON Pointers. Patterns, locks, variations
("vary only
/head/hair/style") and diffs all use the same addressing. - One writer. Only the core writes specs, in canonical form: schema key order, 2-space indent, shortest round-trip numbers, trailing newline, so diffs are minimal and stable. Content hashes use RFC 8785 (JCS) canonical bytes + SHA-256.
schemaVersionisMAJOR.MINOR. Minor versions only add optional fields. A major bump ships areelwright migratestep and golden tests for it.
4. World-building: Theme → Style → Pattern → Instance
- ThemeSpec: what exists. Premise, genres, tone (with a
neverlist of guardrails), setting, world rules (hardones are enforced by the writers'-room validators), in-world language, and a catalog of set types, costume/prop/FX/graphics families and archetypes. Catalog entries point at the pattern that can generate them; entries without one are what the missing-asset pass offers to create. - StyleSpec: how it looks and moves. Every field maps to a shader uniform, material
parameter, animation parameter or solver weight. Swapping the active style re-renders the
show without touching story data. Two parts of a style reach past rendering and are
treated as design changes:
designtransforms (e.g. bigger eyes for an anime restyle) andtiming.preferredFps. Applying those to locked characters produces a change set with before/after model sheets. - Patterns are seedable distributions. A
CharacterPatternis a list of rules addressed by JSON Pointer into the instance (range,choice,const), plus archetypes, conditional rules for correlated traits, costume layer rules, a palette plan (pick / ramp / derive-from-slot, solved in OKLab, then nudged to satisfy the style's color rules) and ensemble constraints for generating a cast together (minimum silhouette and palette distance, height spread, unique hair). - Instances are fully resolved: no ranges, no randomness. They record
meta.derivation(pattern id, pattern hash, seed, sampler version), so the core can reproduce them, tell when a pattern has drifted, and re-derive on approval.
Locking. meta.status: locked freezes the document; meta.locks[] freezes subtrees
("regenerate the costume, keep the face": lock /head). Samplers treat locked pointers as
fixed inputs.
Dependency graph. Nodes are documents and derived artifacts (built rigs, model
sheets, timelines, renders); edges come from references and derivations. Each derived
artifact stores the hashes of its inputs. reelwright status lists what is stale;
reelwright impact style.saturday_morning answers "what changes if I edit this?" before
you edit it.
5. Characters, sets and the stage
Stage space: 3D stage, 2D actors (ADR-0004)
Sets are built in 3D stage space (meters, +Y up). Characters are 2D cutout rigs drawn into camera-facing cards standing on floor marks. Consequences:
- Real cameras: lens in mm, dolly, crane, orbit and parallax come from actual geometry; cuts between set angles are just different camera positions.
- Real occlusion: the Chief crouching behind a console is hidden by the console's geometry. No hand-sorted layers.
- Film grammar is computable: the axis of action is a line on the floor, and "which side is this camera on" is a sign test.
- The look stays 2D: flat or cel shading, ink lines on silhouettes, painted textures and atmosphere all come from the StyleSpec. Distant scenery is backdrop cards at real depths.
- Path to 3D (M10): replace a card with a 3D character; nothing else in staging changes.
Done for rig template
biped_3d.v1: a figure of shaded primitives on the card's own skeleton and pose solver (docs/design/m10-3d-characters.md).
Characters
CharacterInstance → character builder (versioned, in the engine) → rig:
- Rig template (
biped_toon.v1): a fixed bone hierarchy with standard names (hips,spine,chest,neck,head,upper_arm_l,forearm_l,hand_l,thigh_l,shin_l,foot_l, …) and named part slots. Built asSkeleton2D+Bone2D, withPolygon2Dparts skinned to bones (bendylimbs) or rigidly parented (jointed). Because names are standard, gestures, walk cycles and posture changes retarget to every character on the template. - Part library (
toon.v1): parametric vector parts selected by the instance's enums (head shapes, eye styles, garment cuts…), tessellated to polygons with style-driven ink strokes (weight, taper, boil). Vector all the way down, so any resolution is crisp. - Turning. The head is a 2.5D rig: facial features sit at angles on an implicit head volume and are re-projected for any yaw and pitch, so heads turn smoothly. The body swaps between front, three-quarter, profile and back views, mirrored for the other side. The card shows the character at character facing minus camera direction, so the correct view falls out of the staging.
- Faces. Expressions are a small FACS-like parameter set (brow raise and angle, eye openness, squint, mouth curve and openness, cheek raise, head tilt…) blended continuously. Mouth shapes follow Rhubarb's A–H + X vocabulary as parameters (open, width, round, teeth), so lip sync blends with a smile instead of pasting over it.
- Model sheet. The engine renders turnaround, expression grid, mouth chart, color callouts and a height line-up against the cast. It is the approval artifact and the reference for canon-drift checks.
Sets
A SetInstance places kit pieces (prim.box, prim.extrude, prim.panel, …, or
kit.<kit>.<piece> recipes from a SetPattern) in stage space, and adds everything a
director needs: marks (position, facing, posture, seat), entrances with doors, walk
paths, named camera setups (the set's standard angles, including which are good
establishing shots) and lighting presets (key, fill, ambient, rim, practicals). Kit
recipes are data (small CSG-style node lists with parameter expressions), so "build an
alien marketplace" can mean new pieces without new engine code.
6. Determinism
Four levels, each with its own test (ADR-0002):
| Level | Promise | Scope | Test |
|---|---|---|---|
| L1 specs | Same inputs + seeds ⇒ byte-identical instances and Timelines | any platform | snapshot hashes in core tests |
| L2 geometry | Same resolved specs + builder major ⇒ same geometry | any platform, float tolerance | quantized geometry hashes in engine tests |
| L3 frames | Same Timeline + engine version + renderer + driver ⇒ identical frames | per renderer/driver | golden frames: exact on CI's canonical renderer; elsewhere perceptually equal (mean < 0.1 levels, edge pixels may differ more) |
| L4 files | Same frames + audio ⇒ identical deliverables | per ffmpeg build | -fflags +bitexact, no timestamps in metadata |
Rules that make it hold:
- Seeds by name, not by draw order.
seed(child) = u32(sha256(le32(parent) ‖ path))with a stable path such aschar.otto/head/hair/style. Adding a new attribute never reshuffles existing ones. Samplers draw from PCG32 streams (core only). Both languages implementderiveSeed, checked against shared test vectors. - The engine uses stateless randomness. Line boil, grain, flicker and starfields use
pcg_hash(a 32-bit integer hash, also usable in shaders) of(seed, frame, index), so any frame renders alone. - Playback is a pure function of the frame number.
evaluate(frame)sets every property: no accumulated state, no physics, no GPU particles without replay. Any stateful effect (springs for overshoot, secondary motion) is replayed deterministically from the shot start. That is what makes single-frame previews, timeline scrubbing, chunked and parallel rendering, and golden tests work. - Versioned builders (ADR-0007). Any change that alters a builder's output ships as a new major version, and golden tests fail otherwise. Instances pin the builder major. Locked instances keep their major until an approved migration shows before/after model sheets.
- No hidden inputs. Engine code may not call
randomize(),randi(),randf()orTime.*; resolvers may not callMath.random()orDate.now(). A CI grep enforces it.
7. The episode pipeline
premise → logline → beats → scenes → screenplay.fountain (writers' room, M5)
→ missing-asset pass: characters/sets/props the script needs (M5)
screenplay → EpisodeIR: performance + coverage (director, M4)
EpisodeIR + voices → Timeline (frame-exact) (compiler, M1)
Timeline → frames → mix + encode → deliverables (engine + ffmpeg, M1)
chat edits → IR patches → diff → apply → re-compile → re-render (re-direction, M7)
Episode IR: performance and coverage (ADR-0006)
Each scene has two independent parts, the way live action is shot and cut:
cues: one continuous performance. Lines (with emotion, intensity and delivery notes), moves, looks, gestures, expressions, posture changes, prop business, sound effects, FX, music cues and beats. Timing is relative: by default a cue follows the previous one;afterchains,withoverlaps (optionally anchored to a word of a line, e.g. point on the word "kid"). Audio lengths are unknown until voices exist, so absolute times are compiled, never authored.shots: coverage over that performance. Each shot has a cut point anchored to a cue (withsnap: silenceso cuts never land mid-word) and a camera: either one of the set's named setups or a framing intent (MCUonchar.otto, eye height, optional over-the-shoulder) that the framing solver turns into a camera on the legal side of the axis.
Re-cutting never changes the performance. Re-timing a line automatically moves the cuts anchored to it. J- and L-cuts come for free because picture and sound are separate.
Compiler: EpisodeIR → Timeline
Deterministic, in the core:
- Voices. Pick a take per line (approved recording → cached TTS → generate) and measure its exact length.
- Timing. Resolve the cue graph (topological; cycles are errors), in seconds, then to frames at the show's fps. Cut points snap to frames or to the nearest silence.
- Blocking. Walk paths between marks with the style's gait, look-at targets, posture transitions, and occlusion hints.
- Framing solver. Framing intents → camera position, orientation and lens from subject positions and body proportions. Closed-form and deterministic.
- Performance layers. Visemes from lip sync, expression curves, gesture clips, procedural idle (breathing phase, seeded blinks avoiding mid-line, eye darts).
- Film-grammar validation (errors and warnings, each with a JSON Pointer and a fix hint that goes back to the director for repair): 180-degree rule, 30-degree rule and jump cuts, screen direction on exits and entrances, eyeline match in shot/reverse-shot, establishing shot at a new location, shot length within the style's cutting bounds, speaker visibility, and cuts on silence.
- Emit the
Timeline(engine input) and the audio plan (every clip at an exact sample offset with gain, fades and ducking).
8. Performance: voices and lip sync
Provider chains, first available wins, with per-show order in show.json
(ADR-0009):
- TTS:
recorded(approved takes) → cloud (M6+: ElevenLabs, Cartesia, Azure, etc. behind one interface) →kokoro(local, Apache-2.0, word timestamps). Every take is cached byhash(text, voice binding, delivery, provider version)and stored as FLAC. - Lip sync:
rhubarb(MIT, v1.14.0, built from source for arm64; given the line text via-d, extended shapes G/H/X) → later a phoneme-alignment provider (TTS phoneme timings, or forced alignment) mapped to the same A–H/X shapes. Results are cached by audio hash and text. - Mix: the audio plan compiles to one ffmpeg filtergraph (
adelayin samples,amixwithnormalize=0, gains, fades, sidechain ducking under dialogue), then two-passloudnormto the show's target (−16 LUFS web, −23 LUFS broadcast). The mix is sample-exact and byte-reproducible.
9. Rendering (ADR-0008)
- Frame server (primary). The engine renders the stage into a
SubViewportat the exact output size. For each frame it evaluates the Timeline, then callsRenderingServer.force_draw()and reads the texture back. It runs with the render loop disabled and a fixed step. This one path serves full renders, single stills, storyboards, model sheets, golden tests and chunked parallel renders. It keeps working when the macOS window is minimized. - Movie Maker (supported alternative).
--write-moviewithstretch=viewportat the output size,physics_jitter_fix=0, and a pre-created, emptied output folder, which works around three confirmed 4.7.2 bugs. A CI parity test renders the same frames both ways. - Renderer. Forward+ is canonical (it has 2D MSAA, which Compatibility lacks):
Metal on macOS, Vulkan elsewhere. Linux boxes and CI have no GPU, so they use
xvfb-runwith Mesa's software Vulkan (lavapipe);--headlesscannot capture frames. Golden frames are exact for CI's canonical renderer; other renderers are compared for information (mean difference under 0.1 levels). - Encode. From the PNG frames, ffmpeg makes H.264 (BT.709, limited range, correct color tags, CRF 16), and optionally ProRes 422 HQ or 4444 masters, stems, WebVTT/SRT captions (free, because every line's timing is known), and an OpenTimelineIO export of the edit for Resolve, Premiere or Kdenlive.
10. The showrunner
The main interface. It is an agent with tools; every tool that changes anything returns a proposed change set, never a direct write (ADR-0003 explains why it lives in the core).
- Tools.
- Read:
get,list,search_bible,impact,estimate_cost,render_preview. - Propose:
create_theme,create_style,generate_characters(count, archetypes, ensemble constraints),vary(N options over chosen pointers),revise(id + pointer edits),build_set,generate_props,restyle_show,write_episode,direct_episode,revise_shot,render. - Decide (the user, via UI buttons or explicit confirmation):
approve,reject,lock,unlock,undo,redo.
- Read:
- Previews in chat. Model sheets, set thumbnails from each camera setup, style swatches
and storyboard frames, all rendered by the engine's frame server. "The second one, but
taller" works because variations are numbered proposals and "taller" is a pointer edit
on
/body/heightCm. - The repair loop. Tool inputs are strict JSON Schemas. Outputs pass schema validation, then semantic validation (references exist, marks are on the set, words exist for word-anchored cues), then film grammar. Diagnostics go back to the model with pointers and hints until the proposal is clean or a retry budget is spent.
- Context. The bible, theme, style summary and cast summaries form a stable, cached prompt prefix; scene-specific canon is added by id. Long context plus prompt caching replaces a vector database until a bible outgrows it.
- Memory.
showrunner/NOTES.md(committed) holds durable preferences ("keep cold opens under 40 seconds"); transcripts stay in.reelwright/sessions/. - Provider. One small interface, implemented first with the official Anthropic SDK.
Defaults:
claude-opus-5-5with adaptive thinking; effort and model are set per stage inshow.json; refusal fallbacks are on; forcedtool_choiceis not used (the current models reject it; tools are strict and the prompt names the tool). The key comes from the environment, then a key file only its owner can read, then the macOS Keychain, then the SDK's own credential chain. It is never written to disk or logs. - Cost and latency. Cached prefixes, per-stage models and effort, token estimates
before expensive stages, budgets in
show.jsonthat stop spending, the Batches API for bulk offline generation, and a--dry-runthat shows the plan and the estimate. - Tests without a key. The provider records request/response pairs keyed by the hash of the canonical request. CI only replays. Refreshing fixtures is an explicit command.
- MCP.
reelwright mcpexposes the same tool registry over MCP. From M3, Claude Code or Claude Desktop can act as the showrunner before the in-editor panel is finished.
11. Change sets, undo and history
A ChangeSet has a title, a rationale, its author (user or showrunner), per-file
operations (add, replace, remove) with base hashes (so a stale proposal cannot apply),
RFC 6902 patches for display, previews, and the impact list from the dependency graph.
- Approve: the core applies it atomically and, if the Show Pack is a git repo, commits it with the change set's id in a trailer.
- Undo: apply the inverse as a new change set. History is never rewritten.
- Conflict: an out-of-band edit (a person editing JSON in their editor) changes the base hash; the proposal is marked stale and regenerated against the new base.
12. Tier 2 generation and asset QA (M9)
Adapters for image, texture, 3D, voice, music and SFX sit behind narrow interfaces chosen at build time, not hard-coded. One API used for this project shut down in September 2026, which is the reason for the rule. Every generated asset runs one pipeline:
- Generate.
- Segment into rig parts on transparent backgrounds.
- Vectorize or keep raster.
- Snap to the palette, then line and shading passes to match the StyleSpec.
- Attach to the rig template.
- QA: rig validation, part naming, palette compliance (ΔE in OKLab), silhouette readability at small sizes, consistency against the approved model sheet.
- Freeze by content hash with full provenance: generator, model, prompt, seed, date, license.
A failure loops back with the QA report. Frozen assets make Tier 2 content reproducible even though the providers are not.
Art the person brings in runs the same pipeline from step 4, and is the first source built
(reelwright asset import, import_asset; design). Its first use is
painted backdrops: a set backdrop that draws a frozen equirectangular image instead of a
procedural generator. A frozen asset is stored before any document names it, as kept voice
takes are, since change sets carry JSON and text only; validation checks that each one a
document names is pinned by its hash, still holds those bytes, and has a licensed
provenance.
13. Quality gates
Tooling choices are in ADR-0010.
| Gate | Where | Runs in CI |
|---|---|---|
| Schema tests: every example validates; a negative corpus fails with the expected pointer | core | yes |
| Structured-output lint of every schema | core | yes |
Cross-language vectors: deriveSeed and pcg_hash, TS ≡ GDScript |
core + engine | yes |
| Unit tests: samplers, resolver, framing solver, validators, mixer plan | core (Vitest) | yes |
Engine tests: builders, rig validation, evaluate(frame) purity (seek ≡ play) |
engine (gdUnit4) | yes, under xvfb |
| Golden frames: key frames of the demo scene at 640×360 | engine | yes |
| AI stages against recorded fixtures | core | yes, replay only |
End-to-end: reelwright render of the demo, then ffprobe checks (duration, streams, loudness) |
CLI | yes, artifact uploaded |
| Hidden-input grep (random, time) | both | yes |
14. Repository layout
docs/ ARCHITECTURE.md, ROADMAP.md, adr/
schemas/v1/ JSON Schemas (source of truth; TS types and GDScript classes are generated)
core/ TypeScript: @reelwright/core, `reelwright` CLI, MCP server, showrunner
engine/ Godot 4.7 project: addons/reelwright/{runtime,editor}, cli/, tests/, goldens/
examples/shows/ demo Show Packs (original content, CC BY 4.0)
tools/ toolchain.json (pinned versions + checksums), install/build scripts
.github/workflows/ CI
15. Toolchain (pinned in tools/toolchain.json)
| Tool | Version | Notes |
|---|---|---|
| Godot | 4.7.2-stable | Homebrew cask on macOS; official zips + SHA512 on Linux (x86_64, arm64) |
| Node.js / pnpm | 24 LTS / 10 | Core runs TypeScript directly (type stripping); tsc --noEmit checks |
| ffmpeg | ≥ 6.1 (Ubuntu 24.04 apt), tested 6.1.1 and 9.0.2 | libx264 required; ProRes via prores_ks |
| Rhubarb Lip Sync | 1.14.0 | Built from source (the macOS release is Intel-only and crashes under Rosetta); tools/rhubarb/install.sh |
| Kokoro | 1.0 (82M) | Local TTS via uv, optional; Apache-2.0 |
| xvfb + Mesa (lavapipe) | Ubuntu 24.04 packages | Linux render boxes and CI only |
16. Risks and open questions
- Name. The project keeps "Reelwright"; its home is reelwright.diy. An unrelated early-access AI-video product uses reelwright.ai. There is no US trademark filing for it, but consider filing our own before a public launch.
- "Showrunner". Fable Studio's Showrunner is a well-known product in exactly this niche. Use "showrunner" only as a lowercase description of the agent, never as a product name.
- Patents. Google holds US 9,786,032 and US 10,089,715 on parametric, scalable vector 2D characters with animation templates (active to 2036). Get a freedom-to-operate review before any commercial use of the character system.
- Rhubarb is dormant (last release April 2025) and English-centric. It is behind an adapter with an alignment-based successor planned.
- Software rendering speed. Lavapipe renders 1080p slowly. CI renders goldens at 640×360 and the demo at 720p; real render boxes should have GPUs.
- Tier 2 provenance. Some permissive voice and image models were trained on unclear data. Provenance travels with every asset, so a show can audit and replace them.