Add animated captions to any video. Word-by-word MFM/Hormozi style with keyword highlighting. Works for reels, YouTube, and any format.
Today: !date +%Y-%m-%d
Add MFM/Hormozi-style animated captions to any video. Word-by-word display with keyword highlighting, ALL CAPS, burned into the video. Auto-detects format and adjusts sizing/positioning.
Pipeline position: /video-edit -> /video-animate -> /video-finalize -> /video-subtitle
[active-business]/brand.md (Section 4: Brand Voice) for brand colors (used for highlight color).[active-business]/lessons.md for any subtitle/caption lessons.Ask for the video file path if not provided as argument.
ffprobe -v quiet -print_format json -show_streams "[input_video]"
Parse width and height:
Tell user: "Detected [format] video ([width]x[height]). I'll optimize captions for [format]."
Get exact FPS and duration. Check if a transcription JSON already exists in the same folder.
Dependencies:
which ffmpeg -- required for burning subtitlesReuse existing transcription if available ([video]-transcript.json or transcription.json in the same folder).
If not found:
1. Try: whisper --help (local openai-whisper)
2. If not available: check for ELEVENLABS_API_KEY
3. If neither: "Transcription needed but no tool found. Install whisper: pip install openai-whisper or set ELEVENLABS_API_KEY."
4. Run: whisper [input] --model base --output_format json --word_timestamps True
5. Save to [video]-transcript.json
If the video was processed by /video-animate and has a hook animation in the first 3 seconds, don't add captions there. Check for a broll-map.md or ask the user.
Rule: Captions start at 3.0 seconds (or whenever the first non-filler word after 3.0s begins) if a hook animation exists. Otherwise, captions start from the beginning.
Group words into display phrases of 2-3 words each (MFM/Hormozi style).
Strip these BEFORE grouping (don't show in captions):
Remove entirely. Adjust phrase timing to skip over filler timestamps.
Each phrase appears when its first word starts and disappears when its last word ends. No gaps between phrases (next phrase appears immediately when previous ends).
For each phrase, determine if any word should be color-highlighted.
[Script Info]
ScriptType: v4.00+
PlayResX: [WIDTH]
PlayResY: [HEIGHT]
WrapStyle: 0
| Setting | Landscape | Vertical | Square |
|---|---|---|---|
| PlayResX | 1920 | 1080 | 1080 |
| PlayResY | 1080 | 1920 | 1080 |
| Font | Arial Black | Arial Black | Arial Black |
| Font size | 60 | 90 | 75 |
| MarginV | 80 | 500 | 120 |
| Alignment | 2 (center-bottom) | 2 (center-bottom) | 2 (center-bottom) |
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial Black,[SIZE],&H00FFFFFF,&H000000FF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,5,2,2,40,40,[MARGINV],1
Style: Highlight,Arial Black,[SIZE],&H0080DEFF,&H000000FF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,5,2,2,40,40,[MARGINV],1
Key parameters:
&H00FFFFFF)&H0080DEFF = #FFDE80 in ASS BGR format). If the brand has a primary color in brand.md, use that instead (convert to ASS BGR format).Use ASS override tags for highlighted words:
Dialogue: 0,0:00:01.20,0:00:02.10,Default,,0,0,0,,YOU DON'T {\rHighlight}NEED{\rDefault} CODE
ffmpeg -y -i "[INPUT_VIDEO]" \
-vf "ass=subtitles.ass" \
-c:v libx264 -crf 18 -preset medium \
-c:a copy \
"[OUTPUT_VIDEO]"
Output naming: {input-name}-captioned.mp4
Extract 4 frames at different timestamps:
ffmpeg -ss 0.5 -i "OUTPUT" -vframes 1 check_hook.jpg
ffmpeg -ss 5 -i "OUTPUT" -vframes 1 check_early.jpg
ffmpeg -ss [mid] -i "OUTPUT" -vframes 1 check_middle.jpg
ffmpeg -ss [late] -i "OUTPUT" -vframes 1 check_late.jpg
Read each frame and check:
ffprobe -v quiet -print_format json -show_streams "OUTPUT"
Captioned: [filename]
Duration: [X]s
Format: [landscape/vertical/square] ([WxH])
Total phrases: [N]
Highlighted words: [M] ([M/N]% of phrases)
Filler words removed: [K]
Caption start: [X]s (after hook: yes/no)
Output: [path]
Save to [active-business]/output/video/:
[date]-[slug]-captioned.mp4[date]-[slug]-subtitles.ass| Setting | Landscape | Vertical | Square |
|---|---|---|---|
| Font | Arial Black | Arial Black | Arial Black |
| Size | 60 | 90 | 75 |
| Color | White | White | White |
| Highlight | Orange/Yellow or brand color | Same | Same |
| Outline | 5px black | 5px black | 5px black |
| Shadow | 2px | 2px | 2px |
| MarginV | 80 | 500 | 120 |
| Case | ALL CAPS | ALL CAPS | ALL CAPS |
| Words/phrase | 2-3 | 2-3 | 2-3 |
| Highlight % | 20-30% | 20-30% | 20-30% |
| CRF | 18 | 18 | 18 |
| Audio | Copy (no re-encode) | Copy | Copy |
Want to change anything? If you give feedback, I'll apply it and add a lesson to your lessons.md so I remember next time.