A Remotion starter with epilot's branded look-and-feel and a pre-wired audio pipeline (ElevenLabs TTS + Speech-to-Text, OpenAI as fallback).
What you get out of the box:
- 1920×1080 / 30fps composition with three example scenes (Intro, Compare, Outro).
- Brand backdrop (drifting gradient blobs + faint grid), Inter font, bottom-left epilot logo, frosted-pill captions, scene-progress chip.
- Timeline that auto-sizes to your narration: just write text, run one command, get audio + word-by-word captions in sync with gentle scene transitions.
- Single source of truth for scenes (
src/scenes/scenes.ts) shared by both the TTS scripts and the runtime.
Node and npm versions are pinned via Volta (see
voltafield inpackage.json). If you have Volta installed, the right toolchain is selected automatically the moment youcdinto the project. Without Volta, any reasonably recent Node 20+ will work.
# 1. Install
npm install
# 2. Add your API keys
cp .env.example .env
$EDITOR .env
# 3. Generate audio + captions for the example scenes
npm run narrate
# 4. Open the studio (live preview while you edit)
npm run dev
# 5. Render the final MP4
npm run renderElevenLabs is the recommended provider for both TTS and STT — same vendor, same key. Get a key at https://elevenlabs.io and grant it both text_to_speech and speech_to_text permissions.
ELEVENLABS_API_KEY=sk_…
ELEVENLABS_VOICE_ID=jdKpAe6rxAe99tFGbsAc # Daniel — Corporate Narration (German)
ELEVENLABS_MODEL=eleven_multilingual_v2 # optionalIf you don't have ElevenLabs, set OPENAI_KEY and the scripts will fall back to OpenAI's TTS + Whisper. You can also force a provider per run:
PROVIDER=openai npm run tts # use OpenAI even if ElevenLabs is configured
STT=openai npm run transcribe # use Whisper even if ElevenLabs is configured.
├── public/
│ ├── brand/ # epilot logo (used via staticFile)
│ ├── voiceover/ # generated narration MP3s — gitignored
│ └── captions/ # generated caption JSONs — gitignored
├── scripts/
│ ├── generate-voiceover.ts # text → MP3 (ElevenLabs / OpenAI TTS)
│ └── transcribe.ts # MP3 → captions (ElevenLabs Scribe / Whisper)
├── src/
│ ├── components/
│ │ ├── Brand.tsx # backdrop, eyebrow, title, subtitle, brand mark, scene chip
│ │ ├── Captions.tsx # word-by-word frosted-pill captions
│ │ └── SceneShell.tsx # the chrome that wraps every scene
│ ├── scenes/
│ │ ├── scenes.ts # ⭐ scene registry — id, label, narration text
│ │ ├── Scene01Intro.tsx
│ │ ├── Scene02Compare.tsx
│ │ └── Scene03Outro.tsx
│ ├── theme.ts # colour tokens + gradients
│ ├── font.ts # Inter via @remotion/google-fonts
│ ├── get-audio-duration.ts # mediabunny helper for calculateMetadata
│ ├── Video.tsx # composition + scene registry → React → frames
│ ├── Root.tsx # Remotion Composition definition
│ └── index.ts / index.css
└── .env.example
┌────────────────────────────────┐
│ src/scenes/scenes.ts │
│ one entry per scene: │
│ { id, label, text } │
└──┬─────────────────────────┬───┘
│ (narration) │ (id, label)
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ generate-voice… │ │ <Video> │
│ writes mp3 │ │ reads audio length │
│ public/voiceover │ │ builds timeline │
└────────┬─────────┘ │ routes scene id → │
│ │ React component │
▼ └─────────┬────────────┘
┌──────────────────┐ │
│ transcribe │ │
│ writes captions │ ───────────────►│ used by <SceneCaptions>
│ public/captions │ │
└──────────────────┘ ▼
final MP4
The composition's duration is computed from actual MP3 durations via calculateMetadata (using mediabunny), so the visual timeline always matches the narration to the frame.
-
Add the entry to
src/scenes/scenes.ts:{ id: "04-deep-dive", label: "Deep dive", text: "Hier ist ein neues Kapitel..." }
-
Create the component in
src/scenes/Scene04DeepDive.tsx(copyScene02Compare.tsxas a starting point). -
Register it in
src/Video.tsx:import { Scene04DeepDive } from "./scenes/Scene04DeepDive"; const SCENE_COMPONENTS = { ..., "04-deep-dive": Scene04DeepDive, };
-
Generate audio + captions:
npm run narrate
That's it — Remotion auto-detects the new scene length and extends the composition.
Edit the text: fields in src/scenes/scenes.ts, then:
FORCE=1 npm run narrate # regenerate everythingFORCE overwrites existing audio/caption files. Without it, generation is incremental — it only fills in scenes that don't have an MP3 / JSON yet, which is what you want when adding new scenes.
ElevenLabs reads what you give it. If a brand or term is mispronounced (e.g. epilot → "eh-pee-lot" instead of "ehpilot"), the lowest-effort fix is a phonetic spelling in the source text:
text: "Wir nutzen e-pilot…", // pronounced "eh-pilot"Then add a normalization rule in scripts/transcribe.ts so the on-screen caption still shows the brand spelling:
[/\be-pilot\b/g, "epilot"],The template ships with this exact rule for epilot — keep it or replace it with rules for your brand.
Colour tokens live in src/theme.ts. The brand palette and gradients are applied in src/components/Brand.tsx (backdrop blobs, eyebrow chip, gradient text). To rebrand, change the tokens in theme.ts and the styling in Brand.tsx.
# Default render → out/video.mp4
npm run render
# Faster on a high-core-count machine
npx remotion render Video out/video.mp4 --concurrency=12 --quality=90
# Quick low-res preview
npx remotion render Video out/preview.mp4 --scale=0.5out/ is gitignored.