Two outputs, one transcription engine (mlx-whisper, same as transcribe):
- Sidecar file (
.srt/.vtt) — for YouTube/Vimeo upload, re-editing, or other tools. - Burned-in — pixels baked into the video (social clips where the player won't show a
subtitle track). Rendered via PIL → ffmpeg overlay (mise ffmpeg lacks
drawtext).
CRITICAL: use --clean for captions
This is the opposite of filler-removal. Whisper is verbatim by default here (keeps
um/uh) because filler-removal needs them. Subtitles do not — viewers don't want "um, uh"
on screen. Always pass --clean when transcribing for captions:
uv run video-agent transcribe video.mp4 --clean --srt -o subs.srt
Export a subtitle file (no burn-in)
uv run video-agent transcribe video.mp4 --clean --srt -o subs.srt # SubRip
uv run video-agent transcribe video.mp4 --clean --vtt -o subs.vtt # WebVTT
Upload subs.srt alongside the video, or hand-edit it (fix names/typos, retime) and burn
that edited file — see below.
Burn captions into the video
# Auto-transcribe and burn in one step (uses --clean transcription internally? NO — pass it)
uv run video-agent captions video.mp4 -o captioned.mp4 --clean
# Best workflow for accuracy: transcribe → hand-edit the .srt → burn the edited file
uv run video-agent transcribe video.mp4 --clean --srt -o subs.srt
# ...fix typos, proper nouns, retime any off lines in subs.srt...
uv run video-agent captions video.mp4 --srt subs.srt -o captioned.mp4
--srt accepts both .srt and .vtt. When --srt is given, no transcription runs — it
just burns the file you pass, so this is also how you re-burn after editing.
Styling
uv run video-agent captions video.mp4 -o out.mp4 --srt subs.srt \
--size 48 --color yellow --position bottom --no-box
--sizepx (default ~5% of frame height) ·--colorwhite|yellow|red|black--positionbottom (default) | top | center--no-boxremoves the semi-transparent legibility box (keep it for busy backgrounds)- Text auto-wraps to ~90% of frame width, centered.
Gotchas
- Caption count = encode passes? No — one pass. Every segment becomes a full-frame RGBA
PNG overlaid with an
enable='between(t,…)'gate, all in a single ffmpeg encode. Fine for clips and typical talks. For a very long video (many hundreds of segments) the command line gets large; split the video, caption each part, then concat. - Timing comes from whisper segment boundaries, which read fine but can be ~0.3s loose.
For tight sync, edit the times in the
.srtand re-burn with--srt. - Word-level "karaoke" captions (one or two words popping per beat) aren't built in here.
Approximate by transcribing
--words, grouping into short segments, and writing your own.srtwith tight start/end per group, thencaptions --srt. For animated per-word pops (eased, scaled), use the optionalremotion-graphicsskill'sKineticCaption— fed the same--wordstimestamps. Either way the timing is grepped from the--wordstranscript, never scrubbed. - Burn on the final-resolution video. If you're also reframing for social
(
reframeskill), reframe first, then caption — so font size and wrapping match the output frame, not the original.