Making promo videos in code
Overview
A promo film made in code (Remotion: React components rendered frame by frame). It runs on four principles:
- One sync map drives everything. The picture, the camera, the music and the SFX all read named hits, so retiming one retimes all of them.
- Cast before cutting. The director hears and sees the options before the film changes.
- Previews before finals.
- Every claim is true to the product.
House rules first: read the video repo's own rules before anything else: its README, its CLAUDE.md, or a house-rules.md at the repo root. They override the defaults below. house-rules.example.md in this folder is a template for writing one.
Setting up a new film
- One film per branch or worktree, in a video-only repo, never inside the product app. Each film gets its own folder:
src/videos/<name>/. - Copy
templates/from this skill into that folder:sections.ts: the sync map,HITS,TRACK_BPMfilm-camera.tsx: the 2.5D camerashots.ts: the shot listFilm.tsx: the composition skeleton, with theRoot.tsxregistration in its header
- Copy
templates/render.shandscripts/*.pyinto the repo'sscripts/. - Dependencies, pinned to one exact version together:
remotion,@remotion/cli,@remotion/motion-blur. For the audio scripts:ffmpeg, plusnumpyandsoundfile. - One composition renders both formats from a
formatprop (horizontal 1920x1080, vertical 1080x1920). Each format is laid out on its own and never letterboxed.
Workflow
- Storyboard spec (use the superpowers:brainstorming skill if it is installed). Write it to
docs/.../YYYY-MM-DD-<name>-design.md. Check every on-screen claim against the product's code and live flags, and word it the way the product does. - Casting board before any film edit. Show voices (with the lines read aloud), typing sounds, several music options, and look or character drafts, each with a code (A1, B4, M1…). Render the visual drafts as small board compositions, publish the page privately, and wait for the director's picks. Never choose a voice, track or look silently.
- Build.
- Copy and timing live in config.
- Scenes, shots and sounds reference
HITSnames, never raw numbers. - Camera work: read
camera.md.
- 720p preview (
scripts/render.sh <Comp> <format> preview <name>-preview-vN-720p). Report what you verified, what you could not test, and anything you changed beyond the request. - Finals only after approval: full size, both formats. Long renders die if the machine sleeps, so check power first.
- Commit on the film's branch. Merge or push only when asked. Earlier films must keep rendering unchanged.
Audio rules
| Rule | Why |
|---|---|
Render the picture muted, then a separate --codec=wav, then scripts/mux.py (-15 LUFS plus a limiter). | Remotion's in-render AAC puts the whole soundtrack 43 ms late. |
Music is commercially licensed or generated, and every file is logged in a CREDITS.md. | Some "free" SFX packs are not licensed for commercial use. |
To fit a picked track: scripts/beatgrid.py track [bars] [arc] gives the BPM and downbeat. Set TRACK_BPM, then cut the track with music-edit.py (whole bars, crossfaded on the downbeat) or fit-track.py (a straight window). | The film retimes to the track, not the other way round. |
Voices come only from a TTS with a commercial licence (Kokoro-82M is Apache-2.0). Never use macOS say. | The say licence is personal and non-commercial. |
| SFX are generated deterministically. Add each new sound at the end with its own PRNG. | Otherwise every later file changes. |
Never call Math.random() in a render. Use hash() from film-camera.tsx. | A frame must render the same every time. |
QA before handing anything over
- Typecheck.
- Loudness about -15 LUFS and true peak under -1 dBTP (
ffmpeg -af ebur128). The raw WAV peak stays under 0.99, so nothing clips. - SFX land on their hit frames.
- Any QR code decodes at both 1080p and 720p.
- Render stills of every changed section in both formats (Remotion
renderStill) and look at them yourself. Reviewer agents miss cut-off edges, and a review by a small, fast model is not a review of record.
Common mistakes
| Mistake | Fix |
|---|---|
| Motion keyed to a raw frame, beat or second | Add a named hit in sections.ts and reference it. |
| A camera roll or tilt leaking into later shots | Keys carry forward, so reset the field (roll: 0). |
| Finals rendered before approval | Send a 720p preview first. |
| A voice, track or look the director never heard or saw | Put it on the casting board first. |
| A single-pass mp4 with audio | Muted picture, then WAV, then mux. |
Scratch work under /tmp for a multi-day film | The system cleans it up. Use a real worktree folder. |
Shell one-liners with $(...) or loops refused in auto mode | Put the logic in a script file. |