ffmpeg-skill 0.9.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +315 -122
  2. package/SKILL.md +115 -18
  3. package/bin/install.js +16 -2
  4. package/mcp/server.py +2 -0
  5. package/package.json +15 -3
  6. package/references/ci-platform-pitfalls.md +111 -0
  7. package/references/process-pitfalls.md +85 -0
  8. package/references/scripts.md +122 -11
  9. package/scripts/_common.py +247 -15
  10. package/scripts/_contract.py +420 -48
  11. package/scripts/audio.py +101 -8
  12. package/scripts/background.py +73 -0
  13. package/scripts/caption.py +97 -18
  14. package/scripts/check.py +21 -7
  15. package/scripts/color.py +104 -13
  16. package/scripts/crop.py +79 -0
  17. package/scripts/cut.py +85 -11
  18. package/scripts/export.py +16 -7
  19. package/scripts/fit.py +76 -12
  20. package/scripts/graphics.py +12 -3
  21. package/scripts/insert.py +128 -0
  22. package/scripts/join.py +88 -8
  23. package/scripts/loudness.py +3 -3
  24. package/scripts/multicam.py +11 -1
  25. package/scripts/overlay.py +64 -5
  26. package/scripts/proxy.py +82 -0
  27. package/scripts/render.py +13 -2
  28. package/scripts/reverse.py +56 -0
  29. package/scripts/scenes.py +15 -3
  30. package/scripts/sequence.py +124 -0
  31. package/scripts/silence.py +2 -2
  32. package/scripts/stabilize.py +83 -0
  33. package/scripts/sync.py +9 -1
  34. package/mcp/__pycache__/server.cpython-311.pyc +0 -0
  35. package/scripts/__pycache__/_common.cpython-311.pyc +0 -0
  36. package/scripts/__pycache__/_contract.cpython-311.pyc +0 -0
  37. package/scripts/__pycache__/audio.cpython-311.pyc +0 -0
  38. package/scripts/__pycache__/batch.cpython-311.pyc +0 -0
  39. package/scripts/__pycache__/caption.cpython-311.pyc +0 -0
  40. package/scripts/__pycache__/check.cpython-311.pyc +0 -0
  41. package/scripts/__pycache__/color.cpython-311.pyc +0 -0
  42. package/scripts/__pycache__/cut.cpython-311.pyc +0 -0
  43. package/scripts/__pycache__/export.cpython-311.pyc +0 -0
  44. package/scripts/__pycache__/fit.cpython-311.pyc +0 -0
  45. package/scripts/__pycache__/graphics.cpython-311.pyc +0 -0
  46. package/scripts/__pycache__/join.cpython-311.pyc +0 -0
  47. package/scripts/__pycache__/look.cpython-311.pyc +0 -0
  48. package/scripts/__pycache__/loudness.cpython-311.pyc +0 -0
  49. package/scripts/__pycache__/multicam.cpython-311.pyc +0 -0
  50. package/scripts/__pycache__/overlay.cpython-311.pyc +0 -0
  51. package/scripts/__pycache__/probe.cpython-311.pyc +0 -0
  52. package/scripts/__pycache__/render.cpython-311.pyc +0 -0
  53. package/scripts/__pycache__/report.cpython-311.pyc +0 -0
  54. package/scripts/__pycache__/scenes.cpython-311.pyc +0 -0
  55. package/scripts/__pycache__/silence.cpython-311.pyc +0 -0
  56. package/scripts/__pycache__/sync.cpython-311.pyc +0 -0
  57. package/scripts/__pycache__/verify.cpython-311.pyc +0 -0
package/README.md CHANGED
@@ -1,177 +1,370 @@
1
- # ffmpeg-skill
1
+ <p align="center">
2
+ <img src="assets/logo.png" alt="FFmpeg Skill: media processing for AI agents" width="760">
3
+ </p>
2
4
 
3
- **Give your coding agent a video editor.** Local FFmpeg, Python standard library, nothing else.
5
+ <h1 align="center">ffmpeg-skill</h1>
4
6
 
5
- ![before / after demo](assets/demo.gif)
7
+ <p align="center"><strong>Give your coding agent a video editor.</strong></p>
8
+
9
+ <p align="center">
10
+ Local FFmpeg · No cloud · No API keys · Python standard library<br>
11
+ Claude Code · Cursor · Codex · MCP
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="https://github.com/kajisho5/ffmpeg-skill/actions/workflows/ci.yml"><img src="https://github.com/kajisho5/ffmpeg-skill/actions/workflows/ci.yml/badge.svg" alt="tests"></a>
16
+ <a href="https://www.npmjs.com/package/ffmpeg-skill"><img src="https://img.shields.io/npm/v/ffmpeg-skill" alt="npm"></a>
17
+ <img src="https://img.shields.io/badge/python-3.9%2B-blue" alt="Python 3.9+">
18
+ <img src="https://img.shields.io/badge/ffmpeg-5.0%2B-orange" alt="FFmpeg 5.0+">
19
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="MIT"></a>
20
+ </p>
6
21
 
7
22
  ```bash
8
23
  npx ffmpeg-skill
9
24
  ```
10
25
 
11
- `ffmpeg-skill` is an [Agent Skill](https://docs.anthropic.com/en/docs/agents-and-tools/agent-skills) for Claude Code, Cursor, Codex and any other agent that reads `SKILL.md`. It teaches the agent a fixed editing workflow (probe → edit losslessly where possible → verify) and ships eight small CLI scripts that do the actual work with `ffmpeg`/`ffprobe`. Think of it as the fully local FFmpeg counterpart to cloud video-agent tools such as browser-use/video-use.
12
-
13
- **No API keys. No cloud. No dependencies.** If `ffmpeg` and `python3` are on your PATH, it works — offline, on any footage you'd rather not upload.
14
-
15
- ## Features
16
-
17
- - **Probe first, verify last** — the skill forces the agent to read real duration/fps/resolution before editing and to check the result after, so you get "final.mp4: 59.98 s, 1080×1920, 30 fps" instead of guesses.
18
- - **Lossless when possible** — cuts and joins use stream copy by default; re-encoding only happens when it must (frame-accurate cuts, filters, format changes).
19
- - **Cut & join** segments with `mm:ss` / `hh:mm:ss.ms` times.
20
- - **Declarative edits** — describe the whole edit in a `project.json` (clips, transitions, captions, overlays, music, loudness, export, check) and re-render after every tweak.
21
- - **MCP server** — `mcp/server.py` exposes every script as an MCP tool over stdio (stdlib only) for Claude Desktop, Cursor or any MCP client; tool names, order and `inputSchema` are derived from the contract, so the MCP surface follows the scripts.
22
- - **Batch / watch folder** — one recipe over a whole shoot with a content-hash cache; re-runs only touch what changed.
23
- - **Optional local transcription** — `caption.py --transcribe` uses whisper.cpp / faster-whisper / openai-whisper when present; never required.
24
- - **Brand kit** — one `brand.json` (fonts, colours, logo, safe margins, caption style) applied by captions, overlays, graphics and projects.
25
- - **Motion graphics without assets** — lower-thirds, title cards, chapter chips, progress bars, countdowns and corner bugs drawn by FFmpeg.
26
- - **HTML delivery report** — before/after contact sheets, media facts, loudness, compliance and the commands run, in one file.
27
- - **Scene detection and highlight picks** — find cuts and loud moments, get a 60-second digest proposal as a cut list.
28
- - **Delivery checks** — PASS/FAIL against YouTube, Shorts, Reels, TikTok, X, LinkedIn, broadcast and podcast specs, with the fix for each failure.
29
- - **Multicam** — align any number of cameras and recorders by audio (with drift correction) and cut between them from a switch list.
30
- - **Real-footage verification kit** — run the whole toolchain on your own device files and get a PASS/FAIL report.
31
- - **Silence removal / jump cuts** — detect dead air, keep a margin around speech, render frame-accurate in one pass; export the cut list for hand editing.
32
- - **Join with transitions** — crossfade, wipes, fade-to-black between mismatched clips (any size, fps, audio layout).
33
- - **Agent eyes** — contact sheets, single frames and before/after comparisons as PNG so the agent verifies caption placement, crops and colour visually.
34
- - **Plan before render** — every script has `--dry-run` (print the ffmpeg commands), `--json` (structured result with a probe of the output), `--fast` (preview quality) and `--progress` (percent / ETA).
35
- - **Captions** — burn SRT/ASS with font, size, colour, outline and position control; generate SRT from a plain timed-text file; animated (fade/pop/slide) and word-by-word karaoke highlight timed to the speech energy in the audio.
36
- - **Fit** to an exact duration (pitch-preserving speed change or trim) and to 16:9 / 9:16 / 1:1 / 4:5 by padding or cropping; motion-interpolated or blended slow motion.
37
- - **Real-world footage handling** — variable-frame-rate phone clips are conformed to constant fps automatically, rotation metadata is honoured, 10-bit HEVC and 5.1 sources are handled.
38
- - **Multicam / external-audio sync** — offset detection by cross-correlation implemented in pure Python (no numpy), 1 ms resolution, plus clock-drift correction for long takes.
39
- - **Colour management** — HDR10 / HLG / Dolby Vision (iPhone) → SDR BT.709 tone mapping, Dolby Vision layer stripping, 3D LUT (.cube) for Log footage and looks, Log-footage detection, metadata-only retagging.
40
- - **Audio post** — voice clean-up chain (highpass, de-esser, FFT denoise, compressor), background music with sidechain ducking, fades, 5.1 → stereo downmix, track replacement.
41
- - **Loudness** — two-pass EBU R128 normalisation to −14 LUFS (or any target) with true-peak ceiling.
42
- - **Overlays** — logos, watermarks and titles with position, time range, opacity and fades.
43
- - **Export presets** — YouTube, Instagram Reels/Shorts/TikTok, X, ProRes 422 HQ master, H.265, GIF — all tagged BT.709.
44
- - **Agent-friendly CLI** — every script has `--help`, prints the output path on stdout, exits non-zero with a reason on stderr, and names outputs `<input>_<operation>.<ext>` by default.
26
+ ![before / after demo](assets/demo.gif)
45
27
 
46
- ## Install
28
+ `ffmpeg-skill` is an [Agent Skill](https://docs.anthropic.com/en/docs/agents-and-tools/agent-skills) for Claude Code, Cursor, Codex and any agent that reads `SKILL.md`. It teaches the agent a fixed workflow (probe → edit losslessly where possible → check → verify) and ships **28 tools** that do the actual work with `ffmpeg` / `ffprobe`: cut, join, silence removal, fit to duration and aspect, captions and karaoke, overlays and motion graphics, HDR → SDR and LUTs, audio clean-up and typed dynamics, sync with drift correction, multicam, loudness, delivery checks, whole-edit project rendering, batch folders. Every tool is also an MCP tool, and the whole set is described by a machine-readable contract.
29
+
30
+ If `ffmpeg` and `python3` are on your PATH, it works: offline, on footage you would rather not upload.
31
+
32
+ ---
33
+
34
+ ## Standalone, and in an ecosystem
35
+
36
+ **Standalone**, this is a local FFmpeg engine: probe → edit → verify, `npx ffmpeg-skill` and nothing else. No API key, no account, no other repo required. Everything above and below this section describes that standalone tool, and none of it changes if you never read the rest of this one.
37
+
38
+ **In [kajisho5](https://github.com/kajisho5)'s wider video-production ecosystem**, this repo is the *hands*: it cuts, measures and exports files, and reports back in structured JSON. It does not decide what to cut, whether a deliverable is approvable, or what makes a highlight interesting — those are a *brain*'s job, sitting in front of this engine, not inside it.
39
+
40
+ | You want to... | Use |
41
+ |---|---|
42
+ | Cut / join / measure / export a file right now | **this repo** (`ffmpeg-skill`), standalone |
43
+ | Decide cut points, approve a deliverable, plan a whole edit | [`video-production-agent`](https://github.com/kajisho5/video-production-agent) / [`AI-video-production-OS`](https://github.com/kajisho5/AI-video-production-OS) |
44
+ | Build a typed editing graph across a workspace, without writing raw `ffmpeg` | [`video-editing-skill`](https://github.com/kajisho5/video-editing-skill) / [`audio-production-skill`](https://github.com/kajisho5/audio-production-skill) |
45
+
46
+ Other repos in the ecosystem — [`media-analysis-skill`](https://github.com/kajisho5/media-analysis-skill), [`transcription-skill`](https://github.com/kajisho5/transcription-skill), [`subtitle-skill`](https://github.com/kajisho5/subtitle-skill), [`thumbnail-skill`](https://github.com/kajisho5/thumbnail-skill), [`color-grading-skill`](https://github.com/kajisho5/color-grading-skill), [`motion-graphics-skill`](https://github.com/kajisho5/motion-graphics-skill), [`qc-skill`](https://github.com/kajisho5/qc-skill) — read this repo's `contract --json`, its tools' `--json` output and `doctor`, the same way any agent framework would; this repo does not call into any of them. The dependency runs one way.
47
+
48
+ ---
49
+
50
+ **Contents**
51
+ [Standalone, and in an ecosystem](#standalone-and-in-an-ecosystem) · [Why](#why) · [Quick start](#quick-start) · [How it works](#how-it-works) · [Design principles](#design-principles) · [Tools](#tools) · [Audio](#audio-is-a-first-class-input) · [Built for agents](#built-for-agents) · [FFmpeg compatibility](#ffmpeg-compatibility) · [Tested on real footage](#tested-on-real-footage) · [Install](#install) · [Requirements](#requirements) · [Development](#development) · [Docs](#docs)
52
+
53
+ ---
54
+
55
+ ## Why
56
+
57
+ An agent that "knows FFmpeg" still guesses: it assumes a frame rate, picks a codec the container cannot hold, re-encodes a file that only needed a stream copy, and reports "done" without opening the result. ffmpeg-skill exists to take the guessing out:
58
+
59
+ - **Real files first.** Every job starts with `probe.py`; the agent decides from the measured duration, fps, resolution, colour and audio layout, not from the file name.
60
+ - **Structured tools, not shell strings.** Each operation is a script with typed arguments. Nothing runs through a shell; no filter graph is accepted from the caller.
61
+ - **A contract the agent can read.** `contract --json` states, for every tool, what it takes, what it writes, which FFmpeg components it needs and how the result is verified. The MCP surface is derived from it.
62
+ - **Verification after execution.** The result is probed, checked against the destination's spec and, when the picture changed, looked at as a contact sheet.
63
+ - **Local first.** No cloud, no API keys, no Python dependencies. Optional local transcription is used when a whisper is installed, never required.
64
+
65
+ ## Quick start
47
66
 
48
67
  ```bash
49
- # Claude Code (default) ~/.claude/skills/ffmpeg-skill
68
+ # 1. install the skill for Claude Code (Cursor: --cursor, Codex: --codex, all three: --all)
50
69
  npx ffmpeg-skill
51
70
 
52
- # Cursor ~/.cursor/skills/ffmpeg-skill
53
- npx ffmpeg-skill --cursor
71
+ # 2. check the machine: ffmpeg, ffprobe and every FFmpeg component the tools need
72
+ npx ffmpeg-skill doctor
54
73
 
55
- # Codex ~/.codex/skills/ffmpeg-skill
56
- npx ffmpeg-skill --codex
74
+ # 3. (for agent frameworks) read the machine-readable contract
75
+ npx ffmpeg-skill contract --json | head -40
76
+ ```
57
77
 
58
- # everything, or a project-local copy, or a custom directory
59
- npx ffmpeg-skill --all
60
- npx ffmpeg-skill --project
61
- npx ffmpeg-skill --dir ./my-skills
78
+ Already installed? re-run `npx ffmpeg-skill` to refresh `~/.claude/skills/ffmpeg-skill`. Copies are not updated automatically.
79
+
80
+ `doctor`'s overall `ok` and a single tool's `usable: no` are different signals: `ok` means nothing *required by every tool* is missing, but a plain Homebrew `ffmpeg` on macOS can still be `ok` while `caption.py` specifically can't run (no `subtitles` filter) — check `doctor --json`'s `tools` field for the per-tool answer, not just `ok`.
81
+
82
+ Then talk to your agent:
83
+
84
+ > "Take `interview.mp4`, keep 0:45–3:10 and 5:00–6:30, and make it exactly 60 seconds for Reels."
85
+
86
+ The agent runs `probe.py`, `cut.py --segments 0:45-3:10,5:00-6:30`, `fit.py --duration 60 --aspect 9:16 --fit crop`, `export.py --preset reels`, `check.py --platform reels` and `look.py`, then reports "final.mp4: 59.98 s, 1080×1920, 30 fps, AAC stereo" with the contact sheet it inspected.
87
+
88
+ The tools also work on their own, from any shell:
89
+
90
+ ```bash
91
+ S=~/.claude/skills/ffmpeg-skill/scripts
92
+ python3 $S/probe.py input.mp4 --compact
93
+ python3 $S/fit.py input.mp4 --duration 60 --aspect 9:16 --dry-run # print the plan, run nothing
94
+ python3 $S/export.py input.mp4 --preset reels --json # structured result with a probe of the output
62
95
  ```
63
96
 
64
- Or without Node: clone this repo and copy `SKILL.md` and `scripts/` into your agent's skills directory.
97
+ More requests and the commands behind them: [examples/README.md](examples/README.md). To see everything run end-to-end on generated footage: `npm run demo`.
98
+
99
+ ## How it works
100
+
101
+ ```mermaid
102
+ flowchart TD
103
+ U[User request] --> A[AI agent<br/>Claude Code · Cursor · Codex]
104
+ A -->|reads| S[SKILL.md<br/>workflow, request → tool map, report format]
105
+ A -->|runs| T[Structured tool<br/>scripts/&lt;name&gt;.py, typed argparse flags]
106
+ T --> C[Contract<br/>input schema · role · capabilities · verification policy]
107
+ C --> D[Capability detection<br/>doctor: available / missing / unknown]
108
+ D --> F[FFmpeg execution<br/>no shell, stream copy when possible]
109
+ F --> V[Verification<br/>probe · check · look.py contact sheet]
110
+ V --> R[Structured result<br/>--json: status, output, commands, probe]
111
+ R --> A
112
+ ```
65
113
 
66
- You also need FFmpeg:
114
+ Over MCP the same tools are reached through a transport that holds no tool table of its own:
67
115
 
68
- | OS | Command |
69
- |----|---------|
70
- | macOS | `brew install ffmpeg` |
71
- | Ubuntu / Debian | `sudo apt install ffmpeg` |
72
- | Windows | `winget install Gyan.FFmpeg` |
116
+ ```mermaid
117
+ flowchart LR
118
+ M[MCP client<br/>Claude Desktop · Cursor · any client] --> P[mcp/server.py<br/>stdio JSON-RPC]
119
+ P -->|tools/list| C[Contract-derived ToolSpecs<br/>names · order · inputSchema]
120
+ P -->|tools/call| T[scripts/&lt;name&gt;.py]
121
+ C -.derived from.-> K[scripts/_contract.py]
122
+ T -.described by.-> K
123
+ ```
124
+
125
+ Names, order and `inputSchema` in `tools/list` are translated from each tool's argparse parser at start-up, so a new flag or a new script appears in MCP with no edit to `mcp/`. A test copies the skill, adds, removes and edits a script, and reads `tools/list` again to prove it.
126
+
127
+ ## Design principles
128
+
129
+ These are the rules the skill file gives the agent and the code enforces. Together they are what separates this from a list of FFmpeg one-liners.
130
+
131
+ 1. **Probe first.** No tool decides from the file name. `probe.py` measures duration, fps (with variable-frame-rate detection), resolution, rotation, bit depth, HDR format including Dolby Vision, colour tags and every audio stream before anything is cut.
132
+ 2. **Lossless when possible.** `cut.py`, `join.py` and `loudness.py` stream-copy what they do not need to touch. Re-encoding happens only when it must: frame-accurate cuts, filters, format changes, or a keyframe farther than the tolerance.
133
+ 3. **Plan before render.** Every tool takes `--dry-run` (print the ffmpeg command lines, write nothing), `--json` (structured result with a probe of the output), `--fast` (preview quality) and `--progress` (percent and ETA). A test runs every tool under `--dry-run` behind a fake ffmpeg and asserts that no ffmpeg call happened and no file appeared.
134
+ 4. **Machine-readable contract.** `contract --json` describes all 28 tools: input schema generated from the parser, output schema, role, required and conditional FFmpeg capabilities, dry-run support, the verification tools to run afterwards, whether a visual check is required, `mutates_input: false`. `provides` lists all 28 by a cross-repository Capability id (`ffmpeg-skill.cut`, `ffmpeg-skill.loudness`, ...) for [`kajisho5/AI-video-production-OS`](https://github.com/kajisho5/AI-video-production-OS)'s `CapabilityContract.provides` — see `docs/contract.md`.
135
+ 5. **Contract-derived MCP.** `mcp/server.py` builds its `tools/list` from the contract. Tool names, order and `inputSchema` cannot drift from the scripts; a test keeps the two byte-identical.
136
+ 6. **Capability detection.** `doctor` reads `ffmpeg -encoders / -filters / -bsfs` and reports which of the components the tools need are present on this build (libx264, libass, zscale, loudnorm, xfade, …), before a job fails inside ffmpeg.
137
+ 7. **Unknown is not missing.** When a listing cannot be read (a layout the parser does not know, ffmpeg exiting non-zero) the affected capabilities are `unknown`: never `missing`, never silently `available`. An installed filter is not reported absent; a failed detection is not a pass.
138
+ 8. **Verify the result.** The output is probed, and when the picture changed (captions, overlays, crops, colour, transitions) the agent runs `look.py` and inspects the PNG. The report is not finished until its `Look:` line names that image; audio-only jobs say `Look: not needed`. **"Inspects" means the calling agent's own vision, not a feature of this skill:** `look.py` only renders a PNG; nothing in this repository detects faces, products, subjects, or "the interesting part" of a frame or a scene. When a crop or reframe needs to keep a specific part of the frame (`fit.py --fit crop --crop-x/-y`, see [Tools](#tools)), it is the multimodal agent looking at that PNG and choosing the anchor — a non-visual caller (a script, a CLI user without eyes on the sheet) has to supply that decision itself, and the default is a plain centre crop. Likewise `scenes.py --highlights` ranks candidate scenes by a measured proxy (`--rank-by audio` or `--rank-by duration`), never by content; it is the agent that turns a look at the sheet into a judgement.
139
+ 9. **Keep originals.** No tool overwrites its input. Outputs are new files named `<input>_<operation>.<ext>` unless told otherwise, and a test hashes every input after the run.
73
140
 
74
- ## Usage
141
+ ## Tools
75
142
 
76
- Once installed, just talk to your agent. Five things you can say to Claude Code:
143
+ 28 public tools, all Python 3.9 standard library, all with `--help`, `--dry-run`, `--json`, non-zero exit and a reason on stderr on failure.
77
144
 
78
- 1. **"Take `interview.mp4`, keep 0:45–3:10 and 5:00–6:30, and make it exactly 60 seconds for Reels."**
79
- → `probe.py` → `cut.py --segments 0:45-3:10,5:00-6:30` → `fit.py --duration 60 --aspect 9:16 --fit crop` → `export.py --preset reels` → `probe.py` to confirm 60.0 s at 1080×1920.
80
- 2. **"Burn these captions in TikTok style, words popping in with a yellow highlight, in Japanese."**
81
- → `caption.py --text cues.txt --font "Noto Sans CJK JP" --animate pop --karaoke --highlight-color FFD200`.
82
- 3. **"The lav mic recording is out of sync with the camera and drifts over the hour fix it, clean up the hiss and normalise to −14 LUFS."**
83
- `sync.py camera.mp4 lav.wav --fix-drift --replace-audio` `audio.py --voice` `loudness.py` report the detected offset, drift ppm and final LUFS.
84
- 4. **"Put our logo in the top-right corner for the whole video at 80% opacity, and a title card for the first 4 seconds."**
85
- → `overlay.py --image logo.png --position top-right --scale 220 --opacity 0.8` → `overlay.py --text "…" --start 0 --end 4 --fade 0.4`.
86
- 5. **"This iPhone HDR clip looks washed out on YouTube — fix it and give me a ProRes master too."**
87
- → `probe.py` (shows `hdr: true`) → `color.py --to-sdr` → `export.py --preset youtube` and `export.py --preset prores`.
145
+ **Analysis and inspection**
146
+
147
+ | Tool | What it does |
148
+ |---|---|
149
+ | `probe.py` | Duration, fps (+ VFR detection), resolution, codecs, bit depth, HDR format incl. Dolby Vision, colour space, rotation, every audio stream; `--analyze` flags Log footage |
150
+ | `scenes.py` | Scene changes, audio peaks, highlight proposals (`--rank-by audio` loudest, or `--rank-by duration` longest both proxies, not "best") and a per-scene sheet; cut list for `cut.py --segments` |
151
+ | `look.py` | Contact sheet, single frames, side-by-side comparison as PNG so the agent can see what it made |
152
+
153
+ **Editing**
154
+
155
+ | Tool | What it does |
156
+ |---|---|
157
+ | `cut.py` | In/out or multi-segment cuts, lossless `-c copy` first, re-encode fallback, `--accurate` for frame-exact video and sample-exact audio; reports `precision` |
158
+ | `join.py` | Concatenate clips with xfade transitions, normalising size, fps and audio; audio-only inputs are joined as audio |
159
+ | `silence.py` | Detect and remove dead air (jump cuts) with a margin around speech; list or export the cut list |
160
+ | `fit.py` | Fit to a duration (pitch-preserving speed change or trim, smooth slow-mo) and/or aspect ratio (pad or crop, with `--crop-x`/`--crop-y` to keep an off-centre subject) and/or exact `--width`/`--height`; rotate 90/180/270, flip h/v; force constant fps |
161
+ | `crop.py` | Crop to an exact pixel rectangle (`--x --y --width --height`) — distinct from `fit.py --fit crop`, which crops to an aspect ratio it computes itself |
162
+ | `insert.py` | Turn a still image into a silent, fixed-duration video clip (title card, end slate) at an exact frame size / fps, with an optional Ken Burns zoom/pan |
163
+ | `background.py` | Generate a solid-colour or two-colour gradient clip at an exact size/duration — no input file |
164
+ | `reverse.py` | Reverse playback (video and, unless `--no-audio`, audio) |
165
+ | `stabilize.py` | Two-pass motion stabilisation (`vidstabdetect`/`vidstabtransform`) |
166
+ | `sequence.py` | Numbered (`frame_%04d.png`) or glob-matched still images into a video |
167
+
168
+ **Audio**
169
+
170
+ | Tool | What it does |
171
+ |---|---|
172
+ | `audio.py` | Voice clean-up chain, FFT denoise, typed compressor / limiter / gate, music bed with sidechain ducking, fades, 5.1 → stereo, track replacement, extraction (`-o out.wav`), `--audio-stream N` |
173
+ | `sync.py` | Offset between two recordings by audio cross-correlation (1 ms, pure Python), clock-drift correction; aligned video or audio out (audio-to-audio only — no lip-sync/face detection) |
174
+ | `loudness.py` | Two-pass EBU R128 `loudnorm` to −14 LUFS / −1 dBTP or any target, video stream-copied; `--measure-only` |
175
+
176
+ **Picture**
177
+
178
+ | Tool | What it does |
179
+ |---|---|
180
+ | `caption.py` | Burn SRT/ASS with font, size, colour, outline, position; build SRT from timed plain text; animated and word-by-word karaoke timed to the speech energy; optional local transcription |
181
+ | `overlay.py` | Logos, watermarks and titles with position, time range, opacity, fades; `--video` for picture-in-picture, `--chromakey` for green-screen compositing |
182
+ | `graphics.py` | Lower-thirds, title cards, chapter chips, progress bars, countdowns, corner bugs drawn by FFmpeg from a brand kit |
183
+ | `color.py` | HDR10 / HLG / Dolby Vision → SDR BT.709 tone mapping, DV layer stripping, 3D LUT (.cube), colour-tag rewriting, typed primary correction (exposure/contrast/saturation/white balance) |
184
+
185
+ **Delivery**
186
+
187
+ | Tool | What it does |
188
+ |---|---|
189
+ | `export.py` | Presets `youtube`, `youtube4k`, `reels`, `x`, `prores`, `h265`, `gif`, all tagged BT.709 |
190
+ | `proxy.py` | Small, low-bitrate proxy for downstream AI analysis/preview/editing decisions — resize by `--width`/`--scale`, proxy-grade `--crf`, `--fps`, `--no-audio`; not a delivery preset |
191
+ | `check.py` | PASS / WARN / FAIL against YouTube, Shorts, Reels, TikTok, X, LinkedIn, broadcast and podcast specs, with the fix for each failure and a `format` / `judgement` kind per row |
192
+ | `report.py` | Single-file HTML delivery report: before/after sheets, media facts, loudness, compliance, the commands run |
88
193
 
89
- The scripts also work on their own:
194
+ **Orchestration**
195
+
196
+ | Tool | What it does |
197
+ |---|---|
198
+ | `render.py` | Render a whole edit from a declarative `project.json` (clips, transitions, captions, overlays, music, loudness, export, check); `--init`, `--dry-run`, `--stop-after` |
199
+ | `batch.py` | Apply a step recipe or a project to a folder with a content-hash cache; `--watch` |
200
+ | `multicam.py` | Align any number of cameras and recorders by audio (with drift correction) and cut between them from a switch list |
201
+ | `verify.py` | Run the toolchain on real device files and report PASS / FAIL per step |
202
+
203
+ Not tools, but part of the surface: `mcp/server.py` (the MCP transport) and `scripts/_contract.py` (`contract --json`, `doctor`). Per-flag reference for every tool: [references/scripts.md](references/scripts.md).
204
+
205
+ ## Audio is a first-class input
206
+
207
+ WAV, FLAC, MP3, M4A/AAC, OGG and Opus go through `probe`, `cut`, `join`, `silence`, `loudness`, `audio`, `sync` and `check --platform podcast` with the same commands as video. The output extension picks the codec: `-o out.wav` writes PCM, `-o out.flac` FLAC, `-o out.mp3` MP3, `-o out.m4a` AAC.
208
+
209
+ - **Extraction.** An audio extension on a video input drops the picture: `audio.py talk.mp4 -o talk.wav`, or `--voice -o talk.m4a` to clean it on the way. `--audio-stream N` picks a track; `probe` lists them under `audio_streams`.
210
+ - **Join.** `join.py intro.wav episode.m4a outro.wav -o full.flac` resamples every clip to one rate and channel layout and crossfades them (`--transition none` for a butt join). Audio and video inputs cannot be mixed in one join.
211
+ - **Sample-accurate trims.** `cut.py talk.wav --start 1.2345 --end 2.3456 --accurate` trims at the sample; the JSON reports `precision` (`packet` for a stream copy, `sample` for PCM / FLAC, `codec_frame` when a lossy encoder frames the audio again, `frame` for video) and the measured `duration_error_ms`. A `.wav` never receives compressed packets.
212
+ - **Typed dynamics.** `audio.py --compress --comp-threshold -20 --comp-ratio 4`, `--limit --limit-ceiling -1`, `--gate --gate-threshold -45`. Each flag is one documented option of FFmpeg's `acompressor`, `alimiter` or `agate`, range-checked before ffmpeg runs; no filter string is accepted from the caller.
213
+ - **Loudness.** `loudness.py talk.wav -I -16 --tp -1.5 -o talk.m4a` for podcast levels; `check.py talk.m4a --platform podcast` measures LUFS and true peak.
214
+
215
+ Picture tools (`fit`, `caption`, `overlay`, `graphics`, `color`, `export`, `scenes`, `look`) refuse an audio file with "input has no video stream" instead of inventing a picture.
216
+
217
+ ## Built for agents
218
+
219
+ ### Machine-readable contract
90
220
 
91
221
  ```bash
92
- python3 ~/.claude/skills/ffmpeg-skill/scripts/probe.py input.mp4 --compact
93
- python3 ~/.claude/skills/ffmpeg-skill/scripts/fit.py input.mp4 --duration 60 --aspect 9:16
222
+ npx ffmpeg-skill contract --json # or: python3 scripts/_contract.py --json
223
+ npx ffmpeg-skill contract --json --static # without environment detection
94
224
  ```
95
225
 
96
- More examples: [examples/README.md](examples/README.md). To see everything run end-to-end on generated footage: `bash examples/make_demo.sh`.
97
-
98
- ## Scripts
99
-
100
- | Script | What it does |
101
- |--------|--------------|
102
- | `probe.py` | Duration, fps (+ VFR detection), resolution, codecs, bit depth, HDR format incl. Dolby Vision, colour space, rotation, audio channels as JSON; `--analyze` flags Log footage |
103
- | `cut.py` | In/out or multi-segment cuts, lossless `-c copy` first, re-encode fallback, `--accurate` for frame-exact |
104
- | `render.py` | Render a whole edit from `project.json`; `--init`, `--dry-run`, `--stop-after` |
105
- | `batch.py` | Apply a step recipe or render project to a folder, cached, optional watch |
106
- | `mcp/server.py` | MCP server exposing all scripts as tools (stdio JSON-RPC) |
107
- | `graphics.py` | Lower-third, title, chapter, progress, countdown, bug templates (brand colours) |
108
- | `report.py` | Single-file HTML delivery report with sheets, facts, loudness, compliance, commands |
109
- | `scenes.py` | Scene changes, audio peaks, highlight proposals and per-scene sheet |
110
- | `check.py` | Pre-delivery compliance per platform (duration, aspect, codec, colour, loudness, size) |
111
- | `multicam.py` | Align cameras/recorders by audio and switch between them from a time list |
112
- | `verify.py` | Run the toolchain on real device files and report PASS/FAIL per step |
113
- | `silence.py` | Detect and remove silences (jump cuts), list or export the cut list |
114
- | `join.py` | Concatenate clips with xfade transitions, normalising size, fps and audio |
115
- | `look.py` | Contact sheet, single frames, side-by-side comparison as PNG for visual checks |
116
- | `caption.py` | Burn SRT/ASS (font, size, colour, outline, position); build SRT from timed plain text; animated + karaoke ASS |
117
- | `fit.py` | Fit to a duration (speed or trim, smooth slow-mo) and/or aspect ratio (pad or crop), force constant fps |
118
- | `sync.py` | Detect offset between two recordings by audio cross-correlation (1 ms), correct clock drift; output aligned video/audio |
119
- | `color.py` | HDR10/HLG/Dolby Vision SDR tone mapping, DV layer stripping, 3D LUT application, colour-tag rewriting |
120
- | `audio.py` | Denoise / voice chain, music bed with auto-ducking, fades, downmix, replace track |
121
- | `loudness.py` | Two-pass EBU R128 `loudnorm` to −14 LUFS / −1 dBTP (or custom), video stream-copied |
122
- | `overlay.py` | Composite image/logo or drawtext title with position, time range, opacity, fade |
123
- | `export.py` | Presets: `youtube`, `youtube4k`, `reels`, `x`, `prores`, `h265`, `gif` |
124
-
125
- All scripts: Python 3.9+, standard library only, `--help`, non-zero exit + stderr message on failure.
126
-
127
- ## Measured, not assumed
128
-
129
- `tests/corpus.py` downloads public real-device footage (GoPro, DJI, iPhone incl. Dolby Vision, Android screen recordings, HDR10, 24p, Tears of Steel) and runs the toolchain on it; `tests/bench_sync.py`, `bench_silence.py` and `bench_scenes.py` score the algorithms against known ground truth.
130
-
131
- | What | Result (0.8.0, local ffmpeg 6.1) |
226
+ The contract is generated from the code that runs, not maintained beside it. For each of the 28 tools (`ffmpeg-skill/<name>`) it states:
227
+
228
+ | Field | Meaning |
229
+ |---|---|
230
+ | `input_schema` | generated from the tool's argparse parser: properties, types, enums, defaults, required, positional order, mutually exclusive groups |
231
+ | `output_schema` | what `--json` prints: `status`, `output`, `commands`, `probe`, plus tool-specific fields (`precision`, `checks`, `offset_seconds`, …) |
232
+ | `role` | `analysis`, `analysis_and_execution`, `execution` or `verification` |
233
+ | `capabilities` | the FFmpeg encoders, filters and bitstream filters the tool always needs, and the ones needed only for a flag or input |
234
+ | `supports_dry_run`, `supports_json` | measured by the tests, not declared |
235
+ | `verification` | which tools to run on the output afterwards (`probe`, `check`, `look`) |
236
+ | `requires_visual_verification` | the picture changed; inspect the contact sheet |
237
+ | `audio_only`, `video_required` | whether an audio-only input is accepted or refused |
238
+ | `mutates_input` | always `false` |
239
+ | `idempotency_hint` | `bit_exact`, `content_equivalent`, `cached` or `environment_dependent` |
240
+
241
+ `contract_version` (1.0) is separate from the skill version, so a consumer can pin the shape and read the version for provenance. The document also states the invocation mapping (structured arguments → argv), the JSON shapes for success and failure (`{"status": "failed", "error": {"kind": "input | ffmpeg | missing_tool", "message": …}}`), and that no tool runs a shell or executes anything other than the named script, `ffmpeg` and `ffprobe`. Field-by-field reference: [docs/contract.md](docs/contract.md).
242
+
243
+ ### MCP
244
+
245
+ ```json
246
+ {"mcpServers": {"ffmpeg-skill": {"command": "python3", "args": ["/Users/you/.claude/skills/ffmpeg-skill/mcp/server.py"]}}}
247
+ ```
248
+
249
+ On Windows, `python3` is only on PATH if Python was installed from the Microsoft Store; a python.org install exposes `python` (or the `py` launcher) instead if your MCP client reports the server failed to start, change `"command"` above to `"python"` (or the full path from `where python`).
250
+
251
+ `mcp/server.py` is a stdio JSON-RPC transport with no tool table of its own. `tools/list` is derived from the contract at start-up: the same 28 names, the same order, and `inputSchema` translated from each tool's `input_schema`. `tools/call` maps structured arguments to argv and runs the named script; a raw `argv` form is accepted for compatibility and marked non-canonical. `python3 mcp/server.py --list` prints the tools; `--call probe '{"inputs": ["a.mp4"]}'` runs one from the shell.
252
+
253
+ ### Capability detection
254
+
255
+ ```bash
256
+ npx ffmpeg-skill doctor # human-readable
257
+ npx ffmpeg-skill doctor --json # available / missing / missing_optional / unknown / detection / errors / tools / gpu_encoders
258
+ ```
259
+
260
+ `doctor` reads `ffmpeg -encoders`, `-filters` and `-bsfs` and resolves every capability the contract declares against this machine's build. Three states per capability: `available`, `missing`, `unknown`. Exit 0 when everything required is available, 1 when something required is missing, 2 when nothing is proven missing but a required capability is unknown. With detection on (the default), `contract --json` carries the same lists under `capabilities`. `doctor --json`'s `tools` field folds that down to one answer per tool — `{"caption": {"usable": "no", "missing": ["filter:subtitles"], "fix": "..."}, ...}` — so "is `doctor` overall `ok`" and "can I run `caption.py` on this machine" are answered separately: a plain Homebrew `ffmpeg` is `ok` for tools that don't need `subtitles`/`drawtext`/`zscale`, while `caption`'s own `usable` is `"no"`.
261
+
262
+ `doctor --json`'s `gpu_encoders` reports which GPU-backed encoders (`nvenc`, `videotoolbox`, `qsv`, `vaapi`, `amf`) this ffmpeg *build* was compiled with — read from `-encoders` alone, so it proves the capability shipped, not that the GPU/driver on this machine will actually accept a job (that needs a real encode, which `doctor`'s introspection never runs). No tool here uses one yet — every tool still assumes CPU x264/x265 — so this is purely informational and never affects `ok` or any tool's `usable`. GPU-accelerated encoding stays deliberately off the roadmap until there's a real-hardware-verified design for it (build-presence alone is not proof a job will succeed) — not a promised feature, just an honest "not yet, and not without proof it actually works."
263
+
264
+ ## FFmpeg compatibility
265
+
266
+ The tools need FFmpeg 5.0 or later. The capability parser has been run against the listings of these builds:
267
+
268
+ | FFmpeg | `-filters` row layout | Source |
269
+ |---|---|---|
270
+ | 6.1.1 | three flag characters: `..C acompressor A->A` | Ubuntu 24.04 apt, captured |
271
+ | 7.x | same as 6.x | constructed fixture (no capture at hand) |
272
+ | 8.1.2 | two flag characters: `TS aap AA->A`, three-character legend, `------` separator | Homebrew on the macOS CI runner, captured |
273
+ | 9.0.1 | same as 8.x, CRLF | gyan.dev build on the Windows CI runner, captured |
274
+
275
+ FFmpeg 8 shortened the flag column of `ffmpeg -filters`. A parser anchored on the old width matches nothing on FFmpeg 8 and, if "nothing matched" is read as "nothing installed", reports every filter missing; that is what 0.9.0 did on macOS and Windows. Since 0.9.1 rows are recognised by their io-spec token (`A->A`, `AA->A`, `|->V`, `N->N`), so the flag width, the legend and the separator do not matter, and a listing that still cannot be read yields `unknown` rather than `missing`. The captured listings live in [tests/fixtures/](tests/fixtures/README.md) with their provenance; CI uploads each runner's listing and `doctor --json` as an artifact so a new layout is visible before it bites.
276
+
277
+ ## Tested on real footage
278
+
279
+ | Result | Measurement |
132
280
  |---|---|
133
- | Real-device corpus, 10 files | 92 verify steps, all pass after fixes |
134
- | sync.py, ±30 s offsets, gain/noise/EQ, real dialogue+music | 120 s windows (the documented rule): 40/40 within 10 ms, max 1.1 ms. 60 s stress windows: 95 % within 10 ms, 4 of 5 misses flagged by confidence |
135
- | silence.py, 20 cases, known gaps | 0 missed gaps, ≤ 1 ms leftover silence |
136
- | scenes.py, 53 hard cuts between single takes (GoPro/DJI/iPhone/…) | precision 0.95, recall 1.00, F1 0.97 at the default threshold |
281
+ | **92 / 92** | verification steps on a 10-file real-device corpus (GoPro, DJI, iPhone incl. Dolby Vision, Android screen recordings, HDR10, 24p, Tears of Steel), 0.8.0, local ffmpeg 6.1 |
282
+ | **40 / 40 within 10 ms** | `sync.py` offset detection, ±30 s offsets with gain, noise and EQ changes on real dialogue and music, 120 s windows (max error 1.1 ms); 60 s stress windows 95 % within 10 ms, 4 of 5 misses flagged by confidence |
283
+ | **0 missed gaps** | `silence.py`, 20 cases with known gaps, ≤ 1 ms leftover silence |
284
+ | **F1 0.97** | `scenes.py`, 53 hard cuts between single takes, precision 0.95, recall 1.00 at the default threshold |
285
+ | **exact to the sample** | `cut.py --accurate` on WAV, FLAC (44.1 kHz) and AAC → WAV; WAV stream copy within 2 ms; AAC output +21 ms of encoder priming, reported as `codec_frame` (0.9.1) |
286
+ | **72 / 72** | agent runs of 24 prompts (12 English edits, 8 Japanese, 4 that must be declined), three repeats, graded by an independent model: routing, honest refusals and user's language 72/72, report format 71/72, visual check whenever the picture changed 24/24 (0.8.4) |
287
+ | **6 / 6** | 0.9.1 audio evals (audio join, extraction, track selection, sample-accurate trim, typed dynamics; 2 in Japanese): routing, report format and audio-as-audio handling 6/6 |
137
288
 
138
289
  ```bash
139
290
  python3 tests/corpus.py --fetch --verify # ~1.4 GB download, then verify (slow on 4K)
140
291
  python3 tests/bench_sync.py --cases 100
292
+ python3 tests/bench_silence.py
293
+ python3 tests/bench_scenes.py
141
294
  ```
142
295
 
143
- ## Machine-readable contract
296
+ Benchmarks live in `tests/bench_*.py`, agent evals in [evals/](evals/), results by iteration in `evals/results/`.
297
+
298
+ ## Install
144
299
 
145
300
  ```bash
146
- npx ffmpeg-skill contract --json # every tool as a ToolSpec: schema from argparse, role, capabilities, verification, dry-run
147
- npx ffmpeg-skill doctor # which required ffmpeg components this machine has
301
+ npx ffmpeg-skill # Claude Code → ~/.claude/skills/ffmpeg-skill
302
+ npx ffmpeg-skill --cursor # Cursor → ~/.cursor/skills/ffmpeg-skill
303
+ npx ffmpeg-skill --codex # Codex → ~/.codex/skills/ffmpeg-skill
304
+ npx ffmpeg-skill --all # all three
305
+ npx ffmpeg-skill --project # this project → ./.claude/skills/ffmpeg-skill
306
+ npx ffmpeg-skill --dir ./my-skills
307
+ npx ffmpeg-skill --uninstall # remove from the selected targets
148
308
  ```
149
309
 
150
- For agent frameworks that treat ffmpeg-skill as an execution skill: `contract --json` lists the 21 tools as `ffmpeg-skill/<name>` with input/output schemas, `analysis` / `execution` / `verification` roles, the ffmpeg encoders and filters each one needs, whether the result must be probed, checked or looked at, and that no tool modifies its input or runs a shell. `contract_version` (1.0) is separate from the skill version. Details in [docs/contract.md](docs/contract.md).
310
+ Without Node: clone this repository and copy `SKILL.md`, `scripts/`, `references/` and `mcp/` into your agent's skills directory.
151
311
 
152
- ## MCP
312
+ After installing:
153
313
 
154
- ```json
155
- {"mcpServers": {"ffmpeg-skill": {"command": "python3", "args": ["/Users/you/.claude/skills/ffmpeg-skill/mcp/server.py"]}}}
314
+ ```bash
315
+ npx ffmpeg-skill doctor # every required FFmpeg component present?
316
+ npx ffmpeg-skill contract --json # what the agent framework will see
156
317
  ```
157
318
 
158
- `python3 mcp/server.py --list` prints the tools (the same set, order and schemas as `contract --json`: the contract is the source of truth, MCP is the transport); `--call probe '{"inputs": ["a.mp4"]}'` runs one from the shell.
319
+ FFmpeg itself:
320
+
321
+ | OS | Command |
322
+ |----|---------|
323
+ | macOS | `brew install ffmpeg-full` (the plain `ffmpeg` formula lacks the subtitles, drawtext and zscale filters) |
324
+ | Ubuntu / Debian | `sudo apt install ffmpeg` |
325
+ | Windows | `winget install Gyan.FFmpeg` |
159
326
 
160
327
  ## Requirements
161
328
 
162
- - FFmpeg 5.0+ with `libx264`, `libx265`, `libass`, `prores_ks` and `libzimg` (for `color.py --to-sdr`); the default builds from Homebrew, apt and gyan.dev include all of them
163
- - Python 3.9+
329
+ - FFmpeg 5.0+. Always required: `libx264`, `aac`, and the `drawtext`, `subtitles` (libass), `loudnorm`, `xfade`, `acrossfade`, `scdet`, `silencedetect` and `tile` filters. Needed only by the flags that use them: `libx265`, `prores_ks`, `libzimg` / `zscale`, `libmp3lame`, `libopus`, `libvorbis`, the `ass` filter. `doctor` tells you which are present. The apt and gyan.dev builds carry all of them; some Homebrew bottles lack `libass` / `libfreetype` / `libzimg`, which `doctor` reports as missing.
330
+ - Python 3.9+, standard library only
164
331
  - Node 16+ only for the `npx` installer
165
332
 
333
+ `doctor`'s own introspection calls (`ffmpeg -filters`/`-encoders`/`-bsfs`/`-version`) time out after 10s and report `failed` rather than hanging forever — those are meant to be fast. Every tool's actual media-processing `ffmpeg` invocation (cut, fit, caption, ...) has no timeout: a legitimate `--accurate` re-encode of a long file can genuinely take a long time, so bounding it would risk killing real work. `-nostdin` is always passed, so a hung ffmpeg process waiting on stdin cannot happen; a caller that needs a hard ceiling on a specific job should apply its own external timeout/kill around that one invocation.
334
+
166
335
  ## Development
167
336
 
168
337
  ```bash
169
- bash examples/make_demo.sh # generates footage, runs every script, rebuilds assets/demo.gif
170
- python3 tests/test_all.py # end-to-end tests incl. VFR, rotated, 5.1, 10-bit HDR10 and drifting sources (needs ffmpeg)
171
- python3 evals/run.py --list # routing eval prompts (see evals/)
338
+ npm test # tests/test_all.py (end-to-end incl. VFR, rotated, 5.1, HDR10, drifting sources) + tests/test_contract.py
339
+ npm run release-check # pack, install, contract from the installed copy, MCP == contract, doctor, tests, contract evals
340
+ npm run demo # generate footage, run every tool, rebuild assets/demo.gif
341
+ python3 evals/run.py --list # agent eval prompts (see evals/)
172
342
  node bin/install.js --dir /tmp/skills # try the installer without touching ~/.claude
173
343
  ```
174
344
 
345
+ CI (`.github/workflows/ci.yml`) runs on every pull request and on pushes to `main`, on Ubuntu (FFmpeg 6.1), macOS (Homebrew FFmpeg 8.x) and Windows (gyan.dev FFmpeg 9.x), and uploads each runner's FFmpeg listings as an artifact.
346
+
347
+ `tests/test_contract.py` runs on all three OSes, but a handful of its tests build a fake `ffmpeg` as a `#!/bin/sh` script on a PATH shim to force specific FFmpeg 6/7/8/9 fixture layouts through `doctor`'s parser — that technique isn't portable to Windows, so `test_dry_run_never_runs_ffmpeg_and_writes_nothing` and the whole `DoctorDetectionTests` class (fixture-driven layout parsing) are individually `skipIf`'d there and show as `skipped`, not silently absent, in the Windows job's log. Everything else — contract schema, `reencodes_*`, `doctor.tools`, MCP derivation, and every tool exercised through the contract, including `cut.py`'s provenance fields — runs against the real Windows `ffmpeg` on every PR. See [references/ci-platform-pitfalls.md](references/ci-platform-pitfalls.md) for this and other per-OS behaviour differences already diagnosed, before spending a CI cycle re-diagnosing a platform-only failure.
348
+
349
+ **Releasing**: bump `version` in `package.json`, merge to `main`, then tag that commit (`git tag vX.Y.Z && git push origin vX.Y.Z`). `.github/workflows/release.yml` picks up from there: it verifies the tag matches `package.json`'s version, extracts that version's `CHANGELOG.md` section, and publishes the GitHub Release automatically — tagging stays a deliberate, manual act; only the release-notes step is automated. A repo that depends on this one (an editing skill, an agent) should pin an `ffmpeg-skill` version by tag or npm version, not by tracking `main` — `main` can be ahead of the last published npm version.
350
+
351
+ Contributing a change: see [CONTRIBUTING.md](CONTRIBUTING.md).
352
+
353
+ ## Docs
354
+
355
+ | | |
356
+ |---|---|
357
+ | [CONTRIBUTING.md](CONTRIBUTING.md) | scope, dev setup, tests, PR expectations |
358
+ | [SKILL.md](SKILL.md) | what the agent reads: workflow, request → tool map, audio-only rules, report format, pitfalls |
359
+ | [references/scripts.md](references/scripts.md) | per-flag reference for every tool |
360
+ | [references/devices.md](references/devices.md) | real-device notes (iPhone HDR, GoPro, DJI, screen recordings) |
361
+ | [references/ci-platform-pitfalls.md](references/ci-platform-pitfalls.md) | per-OS ffmpeg/CI behaviour differences already diagnosed once — read before re-diagnosing a Windows/macOS-only test failure |
362
+ | [references/process-pitfalls.md](references/process-pitfalls.md) | process mistakes already made once (breaking a pinned test by narrowing a capability list, retrying a git/GitHub operation this environment can't do, re-designing a fixture instead of recognising a real platform difference) — a living record, add to it whenever one recurs |
363
+ | [docs/contract.md](docs/contract.md) | the execution contract field by field, MCP relationship, how a planner consumes it |
364
+ | [examples/README.md](examples/README.md) | natural-language requests and the commands behind them, `brand.json`, `project.json`, batch recipes |
365
+ | [tests/fixtures/README.md](tests/fixtures/README.md) | captured and constructed FFmpeg listings, which is which |
366
+ | [CHANGELOG.md](CHANGELOG.md) | what changed in each release |
367
+
175
368
  ## Support
176
369
 
177
370
  If this skill saves you time, you can help keep it maintained through [GitHub Sponsors](https://github.com/sponsors/kajisho5). Issues and pull requests are just as welcome.