Code generation and render
You are turning a brief into plan.json and rendering it. The reference
project already implements the launch-video grammar — you are handing it data,
not writing a video from scratch.
The polish level of the reference project is the polish level of every video
this tool will ever produce. So the strong default is: write plan.json,
change nothing else. Every line of bespoke animation you add to one run is
polish that never reaches the next video, and a new way for the render to break.
The file map
pitchframe/
├── plan.json ← the scene plan. This is the video.
├── palette.json ← extracted colours. Data, never prose.
├── public/assets/*.png ← captured screenshots.
├── scripts/
│ ├── extract-palette.mjs ← tailwind → CSS vars → live URL
│ ├── apply-palette.mjs ← writes src/theme.ts from palette.json
│ └── capture.mjs ← dev server + Playwright capture
├── src/
│ ├── Root.tsx ← registers the LaunchVideo composition
│ ├── LaunchVideo.tsx ← maps beats to Sequences. Owns the cross-dissolve.
│ ├── theme.ts ← GENERATED. Every colour resolves here.
│ ├── lib/
│ │ ├── types.ts ← the plan contract
│ │ └── motion.ts ← easing, fades, click pulse, held drift
│ ├── components/ ← Background, Cursor, FocusOverlay, Zoom, Panel, …
│ └── scenes/
│ ├── TypographyBeat.tsx
│ ├── InteractionBeat.tsx ← the hero moment
│ ├── UIBeat.tsx
│ └── CTABeat.tsx
└── output.mp4
Writing the plan
Load pitchframe-launch-video for the structure and schema. The short version:
- Four parts — setup, hero, payoff, CTA. In that order. Always.
- Exactly one
interactionbeat, and it gets at least 30% of the frames. - Duration comes from
thesis_weightin the brief, not from a default. - Copy
video_thesisandhero_momentinto the plan so the critique pass can check the video against the claim.
Get the arithmetic right before you render
A failed render costs two minutes; a plan whose beats drift costs a re-render plus the time to notice. Check by hand, every time:
- Beats contiguous — each
frames[0]equals the previousframes[1]. - First beat starts at 0; last beat ends exactly at
total_frames. total_frames === duration_seconds * fps.- No typography beat under 45 frames.
- Interaction beat ≥ 30% of
total_frames.
Reason about coordinates, don't guess them
target_element.coordinates and zoom_target are percentages of the
screenshot. They are claims about where the interesting pixels are, and a
focal point 10% off centres the hero shot on empty chrome.
Open the screenshot, find the element, convert to percentages, and sanity-check
the result against what the beat is supposed to show. When the consequence of
an action happens somewhere other than the target — a drag's destination, a
field that updates elsewhere — point zoom_target at the consequence, not
the thing the cursor touched.
Colours: never in the plan, never in a component
theme.ts is generated by apply-palette.mjs from palette.json. It is the
only place a colour is written down.
node pitchframe/scripts/extract-palette.mjs # → palette.json
node pitchframe/scripts/apply-palette.mjs # → src/theme.ts
Rules that follow from that:
- Never put a colour in
plan.json. The plan says which word is accented; the theme says what the accent is. - Never put a colour literal in a component. If you need a shade the theme doesn't expose, add a derived token to the generator so every future video gets it too.
- Never hand-edit
theme.ts. It is overwritten on the next generate.
If two products' videos look identically coloured, theme.ts was not
regenerated. That is the first thing to check, before touching anything else.
The render
cd pitchframe && npx remotion render LaunchVideo output.mp4
Three checks first, costing a second and saving a two-minute failed render:
plan.jsonparses; the arithmetic above holds.- Every screenshot named in the plan exists in
public/assets/. An interaction beat needs bothbefore_screenshotandafter_screenshot. node_modulesexists inpitchframe/. If not,npm installthere first.
Expect 20–60 seconds for a 20-second video.
When the render fails
Read the actual error before changing anything.
| Symptom | Cause | Fix |
|---|---|---|
Cannot find module '../plan.json' | Scaffold incomplete | Re-run npx pitchframe install |
Cannot find name 'ACCENT' etc. | theme.ts not generated | Run apply-palette.mjs |
| Delay render timeout on fonts | Offline, or a font not on Google Fonts | Set fonts.sans to Inter in palette.json and regenerate |
| Image decode error | Corrupt or partial screenshot | Re-capture that shot |
| Composition duration mismatch | total_frames disagrees with the beats | Fix the arithmetic |
| OOM / Chrome crash | Huge screenshots | Downscale anything over ~4000px wide; --concurrency=2 |
Never respond to a render failure by rewriting the composition. Fix the data.
If the render fails twice, stop and preserve the evidence. Leave
plan.json, palette.json and the assets in place and say which file to look
at. A failed render with an inspectable plan is recoverable; one that has been
"fixed" by a rewrite is not.
When you do have to touch the components
Only when a plan needs something the beat components genuinely cannot express. Three rules if you get there:
- Add, never rewrite. A new camera move is an entry in
SHOTSinlib/camera.ts, not a restructured component — and it has to be listed inpitchframe-motion-languageor nothing will ever choose it. - Use
lib/motion.tsandlib/camera.ts.EASE,EASE_OUT,clickPulse,heldDrift,entranceBlurare the genre. Hand-rolled easing is how one beat ends up looking like it came from a different video. Springs, bounces and elastic curves are banned. There is exactly one camera system. Do not addperspectiveor a 3D transform outsideStage.tsx; this repo had two once, they drifted, and the shots stopped looking like they were filmed by the same person. - Keep it data-driven. Anything you add must be reachable from a field in
plan.json. If the only way to trigger it is editing the component, it is a one-off and it does not belong in the reference.
Always wrong here: a colour literal; the product's name or copy hardcoded into a component; a new top-level composition; changing the 1920×1080 / 30fps contract; importing an animation library.
Adding a beat type is almost always the wrong answer. The schema has four because a launch video needs four. A new beat type is usually a feature beat wearing a disguise, and the moment the schema can express a feature list, plans start containing one.
After a successful render
Report the path and the edit affordance:
Done. Video ready at ./pitchframe/output.mp4
To edit, just ask:
"hold the hero moment longer"
"zoom on the field that changed instead"
Editing is handled by pitchframe-editing, not by re-running the pipeline.