Explainer
A faceless explainer is a voiceover over one continuous, code-built animated world (a cut-through, a magnified mechanism, a hand holding an object), with captions at the top and no music. The output is a 1080×1920 mp4.
Read these before writing:
script-craft.md: how the script is shaped (hook, twist, imagine, mechanism, flip, CTA) and paced.craft-rules.md: what makes the visuals feel alive and dimensional. A competent first pass breaks most of these.
The hard part is the visual metaphor for each beat. The second hardest is keeping every frame moving and lit with depth. Code is the easy part.
Modes
- One-shot (default): go from concept to final video without stopping. Make the judgment calls yourself and report them at the end.
- Review: if the user asks to review or approve, stop twice: after the script, and after the storyboard. Revise from their notes and continue.
Setup check (once)
ffmpeg on PATH, remotion/node_modules present (otherwise run npm install in remotion/), and FISH_API_KEY in
.env (copy .env.example). With no key, build everything on voice --placeholder and tell the user that the final
step is one voice re-run plus a render.
Layout
projects/<concept>/ (the concept is a slug, like how-fridges-work):
script.txt,storyboard.mdcomposition.tsx(layer stack),timeline.ts(TL),captions.ts,words.ts(generated),scenes/S01.tsx …public/vo.wav,vo/(raw take and alignment),out/(renders, stills)
The core library is remotion/src/core (scenes import it as ../../../remotion/src/core):
makeTimeline→TL.at("phrase"),TL.end("phrase")Captions,Drift,Bokeh,Vignette,handheld,Backdrop,Dust,FootnoteCamera(WorldSvg,camAt,toScreen),Label,Sparks,Snow/Rain- easings,
ramp,keys,rand, fontsSANS/MONO
Tools: python3 bin/explainer.py … (run -h)
| Command | Does |
|---|---|
new <c> | Scaffolds the project |
voice <c> [--voice-id ID] [--speed 1.0] | Fish Audio take with word timestamps → words.ts, plus public/vo.wav at −14 LUFS. Prints the wpm |
voice <c> --placeholder [--wpm 200] | Silent audio and synthetic timings, with no API call |
stills <c> <sec>… | Frames at those seconds → one contact sheet in out/. Look at it |
studio <c> | Live preview in Remotion Studio (for the user) |
render <c> [--blur] | 60 fps mp4 in out/. --blur renders at 240 fps and averages to 60 for motion blur (~4× slower) |
qa <c> | Liveness gate: frozen frames < 10%, no frozen run ≥ 0.3 s. Exits 1 on FAIL and names each frozen stretch |
Workflow
-
Script. Run
new <c>.- If the user gave a script, use it word for word (fix only numerals and symbols into words).
- Otherwise write
script.txtfromscript-craft.md: everyday-object format by default (270–320 words), system format for products and networks. Run its checklist. - Review mode: stop for approval.
-
Voice. Run
voice <c>, then check the printed wpm (aim for 190–215; re-run with the suggested--speedif it's off).- The voice is
--voice-id, elseFISH_VOICE_ID, else Fish's default. A cloned voice's id works the same way. - The model is
--model, elseFISH_MODEL, elses2.1-pro. - Author against
words.ts, notscript.txt. The voice spells out numbers, andTL.at()and caption phrases must match the spoken words.
- The voice is
-
Storyboard (
storyboard.md). Fill the World section first, then one row per beat. To invent a beat's metaphor, ask what the viewer would see if they could:- Magnify: a cut-through or exploded view.
- X-ray: inside the object.
- Slow time: a wave travelling, a part switching.
- Measure: a readout climbing or dipping.
The "imagine" scenario becomes the excursion world, and every mechanism beat returns to the home diagram. Run the storyboard checks.
- Review mode: stop for approval.
-
Build. Write one scene file per storyboard shot in
scenes/and add each tocomposition.tsx.- Each scene is a pure function of
tthat returnsnulloutside its window. - Timing: every event is keyed to
TL.at("exact words"). Never write raw seconds. - Continuity: beats that share a diagram live in ONE scene with ONE camera path (A4).
- Moves: each scene enters by moving, overlapping the previous scene's exit (A2).
- Prefix SVG ids per scene.
- Each scene is a pure function of
-
Iterate. Run
stillsat each shot's key words and look at them against craft rules A and B. Then runrenderandqa. Fix every FROZEN stretch in its own shot by giving it a carrier of motion (A1b). Writecaptions.tsphrases by hand: one highlighted (amber, pops when spoken) key noun per phrase. -
Deliver. The final
rendermust passqa(use--blurif the user wants maximum polish). Give the user:- the mp4 path
- the script
- a one-line-per-beat summary of the storyboard
- any placeholder voice that still needs a real take
Red flags: stop and fix
| Thought | Rule |
|---|---|
| "A new setting for this beat keeps it fresh" | C1: one home diagram plus 1–2 excursion worlds. Count them |
| "Hold dead still on the payoff for suspense" | A1c: stop the subject, keep the camera and ambient moving |
| "Add a music bed, grain or a drop on the payoff" | D, B8: voice only, no grain; depth comes from lighting |
| "Fade it in" / "crossfade to the next scene" | A2: enter, exit and transition by moving |
| "The global drift will keep it alive" | A1b: each shot needs a mid-size mover at 10–60 px/s |
| "Cut to a closer camera for this beat" (same diagram) | A4: one world, one camera; hand off keyframes |
| "Even lighting is fine, it's a diagram" | B1: falloff into the dark, rim light, vignette |
| "Tint the whole thing when it activates" | B4: propagate from the cause, with a front and flow |
| "A rectangle is fine for the object" | B2: one anatomy pass; build what a pull-out will reveal |
"Looks fine to me, skip qa" | E: qa must PASS; eyes miss freezes |