What does remotion-ffmpeg-skill do?
ffmpeg-skill measures → Remotion draws → ffmpeg-skill checks and delivers. Scripts live in scripts/ next to this file; run them with python3 <skill-dir>/scripts/<name>.py. --help on a script is the full flag list; references/timeline.md is the timeline format and the options every composition takes.
This skill depends on ffmpeg-skill (found next to this skill, in ~/.claude/skills/ffmpeg-skill, or via FFMPEG_SKILL_DIR). It never runs a raw ffmpeg/ffprobe command itself: measuring, checking and delivering go through ffmpeg-skill's scripts, so their guarantees carry over.
Shared flags (every script): --json (one JSON object on stdout; prefer it), --dry-run (plan, write nothing), --overwrite (the one way to replace an existing output), --timeout SECONDS.
Workflow (in this order)
- Project once. No Remotion project yet:
init.py ./motion --install(copies the template, runsnpm install; Remotion's headless browser downloads on the first render). An existing project from this skill: reuse it. - Measure.
analyze.py SOURCE [--captions FILE.srt] [--beats] -o SOURCE.timeline.json. Read the summary and thewarnings: a VFR or HDR source must be fixed with ffmpeg-skill before drawing (color.py --to-sdr, a constant-frame-rate export) — Remotion renders SDR on a fixed frame grid. Never write or edit timeline numbers by hand; re-measure instead (--threshold,--min-silencefor a quiet room). - Preview stills.
still.py --project ./motion --comp NAME --timeline TL --at 1.5 --at 6and view the PNGs: caption placement, punch-in, colours. Cheap; do it before a full render. - Render.
render.py --project ./motion --comp NAME --timeline TL [--options look.json] -o OUT.mp4(--fast= half-size preview,--frames 0-89= a range,--codec prores -o master.mov= a master). It verifies the output against the timeline (frame count, fps, size, audio) and exits non-zero on a mismatch.- Shape: draw in the shape it will be delivered in.
--aspect 9:16for Reels / TikTok / Shorts (1:1,4:5,16:9too);--fit blur(default when the shape changes: contained over a blurred copy),contain(bars) orcover(crop — check the subject in the still first). Never render 16:9 and let a 9:16 template crop it: captions lose their edges.pipeline.py --platformpicks the aspect from ffmpeg-skill's template for you. - Fonts: Japanese / Chinese / Korean captions get the CJK font ffmpeg-skill finds on the machine automatically (staged into the project, so no tofu);
--font-file F.ttfuses a specific font; the report linefont:says which was used, or that none was found. - Editing-app layer:
--overlay --keep-graphics(Captioned / PeakPulse, source shape) draws the graphics alone as a transparent ProRes 4444.graphics.movand composites it over the untouched source with ffmpeg-skilloverlay.py; hand the.movto someone finishing in Premiere / Resolve / Final Cut. It is not faster than a normal render.
- Shape: draw in the shape it will be delivered in.
- Deliver.
deliver.py OUT.mp4 --platform youtube|reels|tiktok|shorts|x|...runs ffmpeg-skill's delivery template and itscheck.pytable. Format rows are safe to fix; judgement rows (duration, aspect) change content — ask. Mention WARNs, don't chase them. No destination named: skip this step and say so. - Look. Not finished until the reply names the contact sheet render.py wrote (
Look: PATH) and says what it shows. Mechanical (check and report): text is where it was asked, cues appear at their times, nothing is cut off by the frame. Judgement (say what you see, the user decides): text over a face, a jump that lands badly. No image view:Look: PATH (pixels not inspected)— never claim you looked. No contact sheet (look_errorin the result, e.g. an FFmpeg withoutdrawtext): look atstill.pyframes instead and pass on the fix the error names.
Options files are checked before rendering: an unknown key or a wrong type is refused (kind: input) instead of silently doing nothing.
MCP: mcp/server.py exposes the same tools (schemas from scripts/_contract.py, contract --json prints them).
Everything in one go: pipeline.py SOURCE --project ./motion --comp NAME [--captions F] [--platform P] runs 2→4→5 with the same checks and stops at the first failed step.
Request → script / composition
| Request | Run |
|---|---|
| "animated captions / subtitles on this video" | analyze.py --captions → render.py --comp Captioned |
| "cut the silences / jump cut / tighten this talk" (with motion) | analyze.py → render.py --comp Jumpcut (captions re-timed if given) |
| "graphics that react to the audio / hit on the beat / on cuts" | analyze.py [--beats] → render.py --comp PeakPulse (pulses on beats when the grid was measured) |
| "karaoke captions / words light up as they're spoken" | analyze.py --words WORDS.json [--captions F.srt] → render.py --comp Karaoke (word timings from a local speech engine, whisper JSON; without an SRT the cues are grouped from the words) |
| "lower thirds / name titles / chapter titles" | render.py --comp Titled --options with lowerThirds / sceneTitles — the text comes from the user; ask for it, never invent names |
| "highlight reel / best moments" | analyze.py --highlights N [--target S] → render.py --comp Highlights (audio-energy ranking: candidates, say so) |
| "cut to the beat / music montage" | analyze.py --beats [--highlights N] → render.py --comp BeatMontage (audio unbroken, picture cuts every beatsPerCut measured beats; scene cuts if no confident grid) |
| "for YouTube / Reels / TikTok ..." | pipeline.py --platform (renders in the template's aspect, then delivers and checks), or render.py --aspect + deliver.py --platform |
| "vertical / 9:16 / square" | render.py --aspect 9:16 (or 1:1, 4:5) |
| "Japanese captions" / a specific font | nothing extra for CJK (auto font); --font-file for a chosen font |
| "give me the graphics as a separate layer" | render.py --overlay --keep-graphics |
| "change colour / font / caption size / zoom" | an --options JSON (references/timeline.md), re-render |
| several clips ("intro + talk + outro") | analyze.py A.mp4 B.mov C.mp4 (or pipeline.py with several inputs): ffmpeg-skill join.py normalises and joins them, then everything is measured on the joined file |
| "preview" / "show me a frame" | still.py |
| a new kind of graphic | add a composition to src/ of the project that reads Props from src/timeline.ts; render with --comp YourId (render.py then reports measured numbers without a built-in expectation) |
| plain cut / trim / resize / loudness with no graphics | not this skill: use ffmpeg-skill directly |
| something broke | doctor.py [--project DIR] |
What this skill decides and what it does not
- Decides (mechanical): composition length, fps and size (from the probe); where cuts fall (from measured silences); when cues show (from the caption file); when pulses hit (from measured peaks); whether a render matches its timeline.
- Does not decide: names, titles and chapter names (lower thirds and scene titles are the user's text — ask), which words a caption says — cue text is drawn exactly as written, never shortened or paraphrased, even to "make it fit" (offer a smaller
captionSizeinstead); what counts as a highlight; whether a look is good. Those belong to the user or the calling agent. - Never invents data. No captions without a timed caption file from the user or a local speech-to-text engine; no beat grid that
--beatsdid not measure.
Rules that keep jobs from failing
- Remotion licence: free for individuals and for-profit organisations with up to 3 employees; larger companies need a Remotion company licence (https://www.remotion.dev/license). If the user is plainly a larger company, say so once.
- Keep the user's originals: sources are linked into
public/media/of the project, never modified; outputs never overwrite an existing file without--overwrite. - CJK captions: the auto font comes from ffmpeg-skill's font detection; when the report says no font was found, install one (Noto Sans CJK) or pass
--font-file— tofu in the contact sheet is a failed job. Japanese lines break between phrases, not inside words. - Speed (measured on a 2-core Linux machine, all cores — render.py's default): about 2.6× the clip length for 1080p30 (30 s took 80 s). Preview with
still.pyand--fastbefore a full render of long footage; tell the user the expected time before starting a long render.--concurrencyabove the core count is refused by Remotion. - Remotion packages in a project must all be the same version; the template pins them. Don't mix in a different
remotionversion. - A render that fails verification is a failure: report the failed rows from
--json, don't ship it.