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.pyextracts mono 16 kHz audio with ffmpeg, uploads to fal.ai (fal-ai/whisperwithchunk_level: "word"), and writeswords.json(canonical) plus.srtand.asssidecars 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 towords.jsonso a later re-run (without--force) keeps it. If you discover the error only after the burn, edit the word inwords.jsonby hand and re-run without--forceso the corrected transcript is reused, not overwritten by a re-transcribe. - Build cues —
scripts/cue_builder.pygroups words per the style'swords_per_cueand 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.pyfillstemplates/<style>.ass.jinjaand burns withffmpeg -vf "ass=captions.ass:fontsdir=fonts/". - Remotion path:
scripts/remotion_render.pywritesremotion/src/cues.jsonand runsnpx remotion renderagainst the style's composition; the Remotion comp imports the source video and overlays captions in React.
- ASS path:
- Sidecars — every run leaves
words.json,captions.srt,captions.assnext 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), writestoryboard.htmlwith<video>tags for source + each rendered style. Layout matchesgooseworks-ads/video-06-raise-a-goose/storyboard-clips-20s.html.
Usage
# Default (frosted-subtle, lowest-risk style)
python3 .claude/skills/burn-in-captions/scripts/caption.py \
--video path/to/clip.mp4
# Kinetic creator-native captions with a brand accent color
python3 .claude/skills/burn-in-captions/scripts/caption.py \
--video path/to/clip.mp4 \
--style kinetic-pop --accent "#20E0C8" --emphasis "perplexity,nvidia"
# Render multiple styles in one pass (drives the storyboard preview)
python3 .claude/skills/burn-in-captions/scripts/caption.py \
--video path/to/clip.mp4 \
--style frosted-subtle,karaoke-fill,clean-bubble
# Correct a brand name Whisper mis-hears (homophone)
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,ffprobeon PATH (libass support is standard in Homebrew builds).- Python:
fal_client,jinja2. The script auto-installs both withpip install --userif missing. FAL_API_KEY(orFAL_KEY) in environment, or in the nearest.env(auto-loaded). The script aborts with a clear message if missing.- For Remotion styles:
node≥ 18 andnpx. The skill bundles aremotion/workspace and runsnpm installlazily on first use.
Conventions this skill respects
- 9:16 vertical assumed. Captions sit in the lower third (~y=72%)
by default. Override with
--position centeror--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.htmlso review happens visually, not in chat.