| name | burn-in-captions |
| description | Burns word-level captions into a video in one of several house styles. Transcribes via fal.ai Whisper, builds per-style cues, renders via libass (ffmpeg) or Remotion, and emits .ass/.srt sidecars next to the captioned video. Use when the user asks to caption a video, add subtitles, add karaoke captions, add burned-in text, or repurpose a clip for social. |
| argument-hint | <video-path> [style] [accent] [watermark] |
| allowed-tools | Bash, Read, Write, Edit, Glob |
| user-invocable | true |
/burn-in-captions
Turns a raw clip into a captioned clip in a deterministic house style. Six
presets, all driven from one word-level transcript.
Styles
| preset | look | renderer | status |
|---|
frosted-subtle | translucent dark pill, white active word + dim upcoming | ASS | ✅ shipped (default) |
karaoke-fill | bold all-caps, outline ghost fills word-by-word | ASS | ✅ shipped |
kinetic-pop | short 1-3 word chunks scale-pop in, accent color on the punch word | ASS | ✅ shipped |
word-flash | one big bold white word per beat, snaps in | ASS | ✅ shipped |
bold-accent | heavy white sans + lime bold-italic accent word, stacked two-tier | ASS | ✅ shipped |
serif-accent | elegant serif, white + gold italic accent word, stacked two-tier | ASS | ✅ shipped |
clean-minimal | minimal clean white caption, fade-in, with a faint serif ghost-echo word | ASS | ✅ shipped |
neon-glow | bold all-caps karaoke, white active / lavender upcoming, purple neon glow | ASS | ✅ shipped |
clean-bubble | light rounded pill, heavy text, per-word dark active / grey upcoming | ASS | ✅ shipped |
clean-white | plain static white text, no background — fallback / subtitle look | ASS | ✅ shipped |
energy-pop | light pill, heavy text, per-word dark active / grey upcoming, scale-pops in | ASS | ✅ shipped |
neon-pop | one big all-caps word per beat, accent on emphasis | ASS | ⏳ planned (template not built) |
keyword-pill | phrase line + brand-colored pill on the active word | Remotion | ⏳ planned (comp not built) |
impact-italic | italic condensed all-caps, stacked-echo gradient | Remotion | ⏳ planned (comp not built) |
Default: frosted-subtle.
The "karaoke / kinetic" family — kinetic-pop, word-flash, bold-accent,
serif-accent, clean-minimal, neon-glow, clean-bubble, energy-pop — are
dynamic, fast-paced caption styles recreated from real reference videos. Each has a
documented visual spec in references/caption-styles.md (font, color,
animation, layout, and which reference it was modelled on). Pick any of them
with --style.
Any planned style raises FileNotFoundError: missing template. Templates
live in templates/ (one <style>.ass.jinja per ASS style); a body builder
in scripts/ass_render.py and a preset in scripts/cue_builder.py complete
the wiring. See references/caption-styles.md for how to add a style.
Accent color
--accent "#RRGGBB" overrides a style's accent color. If omitted, each style
falls back to its signature color (declared in its cue_builder preset —
e.g. bold-accent → lime #CBFD23, serif-accent → gold #FFF300,
kinetic-pop → teal #20E0C8). --emphasis word1,word2 forces specific
punch words for the styles that color one word per cue.
When to use
- "caption this clip", "add subtitles to this video", "burn captions in"
- "make karaoke captions", "make TikTok-style captions"
- Any video deliverable that needs lower-third word-synced text
Pipeline (every invocation)
- Transcribe —
scripts/transcribe.py extracts mono 16 kHz audio
with ffmpeg, uploads to fal.ai (fal-ai/whisper with
chunk_level: "word"), and writes words.json (canonical) plus
.srt and .ass sidecars next to the source.
1b. Brand-name guard — Whisper transcribes phonetically and reliably
mis-spells unusual brand names as common-word homophones (Bristle
→ Bristol, Klarify → Clarify). Pass --word-fix "Bristol:Bristle,Bristol's:Bristle's" to apply exact-match
corrections to the transcript before cue-building; the fix is
persisted back to words.json so a later re-run (without --force)
keeps it. If you discover the error only after the burn, edit the
word in words.json by hand and re-run without --force so the
corrected transcript is reused, not overwritten by a re-transcribe.
- Build cues —
scripts/cue_builder.py groups words per the
style's words_per_cue and splits on long pauses; picks emphasis
words (heuristic: longest non-stopword in each cue, or user override).
- Render — based on the preset's
renderer:
- ASS path:
scripts/ass_render.py fills
templates/<style>.ass.jinja and burns with
ffmpeg -vf "ass=captions.ass:fontsdir=fonts/".
- Remotion path:
scripts/remotion_render.py writes
remotion/src/cues.json and runs npx remotion render against the
style's composition; the Remotion comp imports the source video and
overlays captions in React.
- Sidecars — every run leaves
words.json, captions.srt,
captions.ass next to the output. The user gets soft subs for free
even if they only asked for burn-in.
- Storyboard — if
--storyboard (default on for multi-style runs),
write storyboard.html with <video> tags for source + each
rendered style. Layout matches
gooseworks-ads/video-06-raise-a-goose/storyboard-clips-20s.html.
Usage
python3 .claude/skills/burn-in-captions/scripts/caption.py \
--video path/to/clip.mp4
python3 .claude/skills/burn-in-captions/scripts/caption.py \
--video path/to/clip.mp4 \
--style kinetic-pop --accent "#20E0C8" --emphasis "perplexity,nvidia"
python3 .claude/skills/burn-in-captions/scripts/caption.py \
--video path/to/clip.mp4 \
--style frosted-subtle,karaoke-fill,clean-bubble
python3 .claude/skills/burn-in-captions/scripts/caption.py \
--video path/to/clip.mp4 --style energy-pop \
--word-fix "Bristol:Bristle,Bristol's:Bristle's"
Output layout
<source-dir>/<clip>.captions/
├── words.json # canonical word-level transcript
├── captions.srt # soft-sub sidecar
├── captions.ass # generic plain-style ASS sidecar
├── <clip>__frosted-subtle.mp4 # burned-in video
├── <clip>__frosted-subtle.ass # the styled ASS used for the burn
└── storyboard.html # review surface
Requirements
ffmpeg, ffprobe on PATH (libass support is standard in Homebrew
builds).
- Python:
fal_client, jinja2. The script auto-installs both with
pip install --user if missing.
FAL_API_KEY (or FAL_KEY) in environment, or in the nearest
.env (auto-loaded). The script aborts with a clear message if
missing.
- For Remotion styles:
node ≥ 18 and npx. The skill bundles a
remotion/ workspace and runs npm install lazily on first use.
Conventions this skill respects
- 9:16 vertical assumed. Captions sit in the lower third (~y=72%)
by default. Override with
--position center or --y-pct 80.
- 88% safe area — the motion-graphics rule. ASS templates use
MarginL/MarginR=6% of frame width.
- Pill / box styles clamp to a safe width. Any style that draws a
background pill sized to the text (
clean-bubble, energy-pop) must
shrink-to-fit: if a cue's pill would exceed ~90% of frame width, the
font is reduced for that cue. Long words ("recommendations",
"opportunistic") are inevitable — never trust the transcript to stay
short. A new pill/box style without shrink-to-fit is a bug.
- Brand names get verified. Whisper mis-spells brand-name homophones;
use
--word-fix (see Pipeline step 1b) and eyeball the brand name in
the burned output before shipping.
- No placeholder anything. If transcription fails, the skill aborts
with the actual error from Fal; it does not invent words.
- Storyboard is the single review surface. Every multi-style run
writes a
storyboard.html so review happens visually, not in chat.