ffmpeg-skill integration contract
September 8, 2026 · View on GitHub
motion-graphics-skill never talks to ffmpeg/ffprobe directly. It locates one ffmpeg-skill checkout
(adapter.FfmpegSkill.locate) and calls exactly three of its tools, always as
[sys.executable, <dir>/scripts/<tool>.py, <typed argv...>, --json] (never a shell, never a request-supplied
string as a flag):
| Tool | Used for |
|---|---|
probe | Source video facts (resolution, duration, presence of a video stream) and output validation after every render. |
graphics | TITLE (--template title) and LOWER_THIRD (--template lower-third) — built-in fade / slide + fade animation, brand-free (colours passed explicitly via --text-color/--primary, never through a brand.json). |
overlay | TEXT_OVERLAY (--text), IMAGE_OVERLAY (--image), and VIDEO_OVERLAY (--video, with optional --chromakey/--chromakey-similarity/--chromakey-blend green-screen removal) — position, margin, static scale, opacity, and (text/image only) a configurable --fade. |
Every graphics/overlay invocation may also carry --audio-stream (document.options.audio_stream) to select
which audio track of the source survives.
Version window
adapter.SUPPORTED_MIN = 0.12.1, adapter.SUPPORTED_MAX_EXCLUSIVE = 1.0.0, adapter.SUPPORTED_CONTRACT_VERSION = "1.0". FfmpegSkill.info() reads scripts/_contract.py --json --static, checks the version window and contract
version, and that graphics/overlay/probe still declare the flags in adapter.FLAGS_USED. Any mismatch is a
TOOL_ERROR (retryable) surfaced by doctor and refused by run/plan before anything renders. 0.12.1 is the
floor (raised from 0.9.1) because it is the oldest release carrying every flag/field this adapter now relies on:
--video/--chromakey* on overlay (0.11.0), --audio-stream on graphics/overlay (0.12.0), and
dropped_non_av_streams in both tools' --json response (0.12.1) — the last of which is not itself a CLI flag,
so FLAGS_USED's contract check alone would not have caught a stale checkout missing it.
Discovery order (FfmpegSkill.candidates)
--ffmpeg-skill <dir>(explicit; never silently replaced by a fallback)MOTION_GRAPHICS_FFMPEG_SKILL_DIRenvironment variableVIDEO_AGENT_FFMPEG_SKILL_DIRenvironment variable (shared with sibling Skills in this ecosystem)~/.claude/skills/ffmpeg-skill./vendor/ffmpeg-skill../ffmpeg-skill
Flags used, by tool (adapter.FLAGS_USED)
probe:inputs(positional)graphics:input,output,template,start,end,crf,preset,audio_stream,title,subtitle,name,position,count_from,text_color,primary,jsonoverlay:input,output,position,margin,start,end,opacity,crf,preset,audio_stream,fade,text,font_size,font_color,border,border_color,box,box_color,font,font_file,image,scale,scale_percent,video,chromakey,chromakey_similarity,chromakey_blend,json
dropped_non_av_streams (ffmpeg-skill 0.12.1)
graphics/overlay's --json response carries dropped_non_av_streams: bool — true only when that tool's own
attempt to preserve the source's subtitle/data stream(s) failed and it fell back to a video+audio-only re-encode
for that one invocation. adapter.ToolRun reads it off every tool response; executor.StageResult.to_dict()
includes it per operation only when the field was actually present and truthy-or-falsy as a real bool (an older
ffmpeg-skill response that never carries the field at all leaves it out entirely, never guessed as false), and
Executor.response() adds one warnings[] entry per stage where it was true. Matches this Skill's own stated
philosophy (docs/decisions.md ADR-12/ADR-17): never claim support — or non-loss — that isn't backed by what the
real renderer actually reported.
Known gaps (observed, not assumed)
ffmpeg-skill/overlay --fadealways fades both in and out when--start/--endare both given: there is no in-only or out-only mode over a bounded window (ADR-2 indecisions.md).ffmpeg-skill/overlay's--videobranch never readsargs.fadeat all (only--image/--textdo): avideo_overlayelement cannot have a configurablefadeanimation for the same reasontitle/lower_thirdcannot (ADR-15 indecisions.md).ffmpeg-skill/graphics --template title|lower-thirdbakes in a fixed, non-configurable 0.3s fade (title) or slide+fade (lower-third): no flag exists to change the timing (ADR-3).- Filter-capability detection through
ffmpeg-skill doctoris unreliable on FFmpeg builds whose-filtersoutput format changed (observed on FFmpeg 8.0+, seeffmpeg-skill's owndocs/contract.md);doctor.pyreports those capabilities asunknown, neverunsupported, whenffmpeg-skillitself reports zero filters detected. - On some Windows FFmpeg builds,
ffmpeg-skill/overlay --font-file <absolute Windows path>fails to parse (No option name near ...,Invalid argument) regardless of slash style or escaping, because ffmpeg-skill's own filter-path escaping round-trips the value throughpathlib.Pathand always re-normalises it to the same form before escaping the drive letter's colon. Worked around entirely on this skill's side (ADR-9 indecisions.md): that one invocation runs withcwdset to the font file's directory and--font-filegets just the bare file name, which needs no escaping.