ffmpeg-skill 0.8.5 → 0.10.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 (44) hide show
  1. package/README.md +296 -118
  2. package/SKILL.md +39 -7
  3. package/bin/install.js +10 -2
  4. package/mcp/server.py +40 -44
  5. package/package.json +19 -5
  6. package/scripts/_common.py +55 -7
  7. package/scripts/_contract.py +757 -0
  8. package/scripts/audio.py +100 -7
  9. package/scripts/caption.py +10 -3
  10. package/scripts/check.py +21 -7
  11. package/scripts/color.py +68 -4
  12. package/scripts/cut.py +83 -9
  13. package/scripts/export.py +15 -6
  14. package/scripts/fit.py +17 -3
  15. package/scripts/join.py +74 -3
  16. package/scripts/multicam.py +10 -0
  17. package/scripts/overlay.py +5 -0
  18. package/scripts/render.py +13 -2
  19. package/scripts/report.py +6 -3
  20. package/scripts/scenes.py +15 -3
  21. package/scripts/sync.py +8 -0
  22. package/mcp/__pycache__/server.cpython-311.pyc +0 -0
  23. package/scripts/__pycache__/_common.cpython-311.pyc +0 -0
  24. package/scripts/__pycache__/audio.cpython-311.pyc +0 -0
  25. package/scripts/__pycache__/batch.cpython-311.pyc +0 -0
  26. package/scripts/__pycache__/caption.cpython-311.pyc +0 -0
  27. package/scripts/__pycache__/check.cpython-311.pyc +0 -0
  28. package/scripts/__pycache__/color.cpython-311.pyc +0 -0
  29. package/scripts/__pycache__/cut.cpython-311.pyc +0 -0
  30. package/scripts/__pycache__/export.cpython-311.pyc +0 -0
  31. package/scripts/__pycache__/fit.cpython-311.pyc +0 -0
  32. package/scripts/__pycache__/graphics.cpython-311.pyc +0 -0
  33. package/scripts/__pycache__/join.cpython-311.pyc +0 -0
  34. package/scripts/__pycache__/look.cpython-311.pyc +0 -0
  35. package/scripts/__pycache__/loudness.cpython-311.pyc +0 -0
  36. package/scripts/__pycache__/multicam.cpython-311.pyc +0 -0
  37. package/scripts/__pycache__/overlay.cpython-311.pyc +0 -0
  38. package/scripts/__pycache__/probe.cpython-311.pyc +0 -0
  39. package/scripts/__pycache__/render.cpython-311.pyc +0 -0
  40. package/scripts/__pycache__/report.cpython-311.pyc +0 -0
  41. package/scripts/__pycache__/scenes.cpython-311.pyc +0 -0
  42. package/scripts/__pycache__/silence.cpython-311.pyc +0 -0
  43. package/scripts/__pycache__/sync.cpython-311.pyc +0 -0
  44. package/scripts/__pycache__/verify.cpython-311.pyc +0 -0
package/README.md CHANGED
@@ -1,168 +1,346 @@
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.
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 **21 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
+ Then talk to your agent:
79
+
80
+ > "Take `interview.mp4`, keep 0:45–3:10 and 5:00–6:30, and make it exactly 60 seconds for Reels."
81
+
82
+ 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.
83
+
84
+ The tools also work on their own, from any shell:
85
+
86
+ ```bash
87
+ S=~/.claude/skills/ffmpeg-skill/scripts
88
+ python3 $S/probe.py input.mp4 --compact
89
+ python3 $S/fit.py input.mp4 --duration 60 --aspect 9:16 --dry-run # print the plan, run nothing
90
+ python3 $S/export.py input.mp4 --preset reels --json # structured result with a probe of the output
62
91
  ```
63
92
 
64
- Or without Node: clone this repo and copy `SKILL.md` and `scripts/` into your agent's skills directory.
93
+ 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`.
94
+
95
+ ## How it works
96
+
97
+ ```mermaid
98
+ flowchart TD
99
+ U[User request] --> A[AI agent<br/>Claude Code · Cursor · Codex]
100
+ A -->|reads| S[SKILL.md<br/>workflow, request → tool map, report format]
101
+ A -->|runs| T[Structured tool<br/>scripts/&lt;name&gt;.py, typed argparse flags]
102
+ T --> C[Contract<br/>input schema · role · capabilities · verification policy]
103
+ C --> D[Capability detection<br/>doctor: available / missing / unknown]
104
+ D --> F[FFmpeg execution<br/>no shell, stream copy when possible]
105
+ F --> V[Verification<br/>probe · check · look.py contact sheet]
106
+ V --> R[Structured result<br/>--json: status, output, commands, probe]
107
+ R --> A
108
+ ```
65
109
 
66
- You also need FFmpeg:
110
+ Over MCP the same tools are reached through a transport that holds no tool table of its own:
67
111
 
68
- | OS | Command |
69
- |----|---------|
70
- | macOS | `brew install ffmpeg` |
71
- | Ubuntu / Debian | `sudo apt install ffmpeg` |
72
- | Windows | `winget install Gyan.FFmpeg` |
112
+ ```mermaid
113
+ flowchart LR
114
+ M[MCP client<br/>Claude Desktop · Cursor · any client] --> P[mcp/server.py<br/>stdio JSON-RPC]
115
+ P -->|tools/list| C[Contract-derived ToolSpecs<br/>names · order · inputSchema]
116
+ P -->|tools/call| T[scripts/&lt;name&gt;.py]
117
+ C -.derived from.-> K[scripts/_contract.py]
118
+ T -.described by.-> K
119
+ ```
73
120
 
74
- ## Usage
121
+ 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.
75
122
 
76
- Once installed, just talk to your agent. Five things you can say to Claude Code:
123
+ ## Design principles
77
124
 
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`.
125
+ 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.
126
+
127
+ 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.
128
+ 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.
129
+ 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.
130
+ 4. **Machine-readable contract.** `contract --json` describes all 21 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 21 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`.
131
+ 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.
132
+ 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.
133
+ 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.
134
+ 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.
135
+ 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.
136
+
137
+ ## Tools
138
+
139
+ 21 public tools, all Python 3.9 standard library, all with `--help`, `--dry-run`, `--json`, non-zero exit and a reason on stderr on failure.
140
+
141
+ **Analysis and inspection**
142
+
143
+ | Tool | What it does |
144
+ |---|---|
145
+ | `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 |
146
+ | `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` |
147
+ | `look.py` | Contact sheet, single frames, side-by-side comparison as PNG so the agent can see what it made |
148
+
149
+ **Editing**
150
+
151
+ | Tool | What it does |
152
+ |---|---|
153
+ | `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` |
154
+ | `join.py` | Concatenate clips with xfade transitions, normalising size, fps and audio; audio-only inputs are joined as audio |
155
+ | `silence.py` | Detect and remove dead air (jump cuts) with a margin around speech; list or export the cut list |
156
+ | `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); force constant fps |
157
+
158
+ **Audio**
159
+
160
+ | Tool | What it does |
161
+ |---|---|
162
+ | `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` |
163
+ | `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) |
164
+ | `loudness.py` | Two-pass EBU R128 `loudnorm` to −14 LUFS / −1 dBTP or any target, video stream-copied; `--measure-only` |
165
+
166
+ **Picture**
167
+
168
+ | Tool | What it does |
169
+ |---|---|
170
+ | `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 |
171
+ | `overlay.py` | Logos, watermarks and titles with position, time range, opacity, fades |
172
+ | `graphics.py` | Lower-thirds, title cards, chapter chips, progress bars, countdowns, corner bugs drawn by FFmpeg from a brand kit |
173
+ | `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) |
174
+
175
+ **Delivery**
176
+
177
+ | Tool | What it does |
178
+ |---|---|
179
+ | `export.py` | Presets `youtube`, `youtube4k`, `reels`, `x`, `prores`, `h265`, `gif`, all tagged BT.709 |
180
+ | `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 |
181
+ | `report.py` | Single-file HTML delivery report: before/after sheets, media facts, loudness, compliance, the commands run |
88
182
 
89
- The scripts also work on their own:
183
+ **Orchestration**
184
+
185
+ | Tool | What it does |
186
+ |---|---|
187
+ | `render.py` | Render a whole edit from a declarative `project.json` (clips, transitions, captions, overlays, music, loudness, export, check); `--init`, `--dry-run`, `--stop-after` |
188
+ | `batch.py` | Apply a step recipe or a project to a folder with a content-hash cache; `--watch` |
189
+ | `multicam.py` | Align any number of cameras and recorders by audio (with drift correction) and cut between them from a switch list |
190
+ | `verify.py` | Run the toolchain on real device files and report PASS / FAIL per step |
191
+
192
+ 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).
193
+
194
+ ## Audio is a first-class input
195
+
196
+ 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.
197
+
198
+ - **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`.
199
+ - **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.
200
+ - **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.
201
+ - **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.
202
+ - **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.
203
+
204
+ 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.
205
+
206
+ ## Built for agents
207
+
208
+ ### Machine-readable contract
90
209
 
91
210
  ```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
211
+ npx ffmpeg-skill contract --json # or: python3 scripts/_contract.py --json
212
+ npx ffmpeg-skill contract --json --static # without environment detection
94
213
  ```
95
214
 
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) |
215
+ The contract is generated from the code that runs, not maintained beside it. For each of the 21 tools (`ffmpeg-skill/<name>`) it states:
216
+
217
+ | Field | Meaning |
218
+ |---|---|
219
+ | `input_schema` | generated from the tool's argparse parser: properties, types, enums, defaults, required, positional order, mutually exclusive groups |
220
+ | `output_schema` | what `--json` prints: `status`, `output`, `commands`, `probe`, plus tool-specific fields (`precision`, `checks`, `offset_seconds`, …) |
221
+ | `role` | `analysis`, `analysis_and_execution`, `execution` or `verification` |
222
+ | `capabilities` | the FFmpeg encoders, filters and bitstream filters the tool always needs, and the ones needed only for a flag or input |
223
+ | `supports_dry_run`, `supports_json` | measured by the tests, not declared |
224
+ | `verification` | which tools to run on the output afterwards (`probe`, `check`, `look`) |
225
+ | `requires_visual_verification` | the picture changed; inspect the contact sheet |
226
+ | `audio_only`, `video_required` | whether an audio-only input is accepted or refused |
227
+ | `mutates_input` | always `false` |
228
+ | `idempotency_hint` | `bit_exact`, `content_equivalent`, `cached` or `environment_dependent` |
229
+
230
+ `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).
231
+
232
+ ### MCP
233
+
234
+ ```json
235
+ {"mcpServers": {"ffmpeg-skill": {"command": "python3", "args": ["/Users/you/.claude/skills/ffmpeg-skill/mcp/server.py"]}}}
236
+ ```
237
+
238
+ `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 21 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.
239
+
240
+ ### Capability detection
241
+
242
+ ```bash
243
+ npx ffmpeg-skill doctor # human-readable
244
+ npx ffmpeg-skill doctor --json # available / missing / missing_optional / unknown / detection / errors / tools
245
+ ```
246
+
247
+ `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"`.
248
+
249
+ ## FFmpeg compatibility
250
+
251
+ The tools need FFmpeg 5.0 or later. The capability parser has been run against the listings of these builds:
252
+
253
+ | FFmpeg | `-filters` row layout | Source |
254
+ |---|---|---|
255
+ | 6.1.1 | three flag characters: `..C acompressor A->A` | Ubuntu 24.04 apt, captured |
256
+ | 7.x | same as 6.x | constructed fixture (no capture at hand) |
257
+ | 8.1.2 | two flag characters: `TS aap AA->A`, three-character legend, `------` separator | Homebrew on the macOS CI runner, captured |
258
+ | 9.0.1 | same as 8.x, CRLF | gyan.dev build on the Windows CI runner, captured |
259
+
260
+ 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.
261
+
262
+ ## Tested on real footage
263
+
264
+ | Result | Measurement |
132
265
  |---|---|
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 |
266
+ | **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 |
267
+ | **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 |
268
+ | **0 missed gaps** | `silence.py`, 20 cases with known gaps, ≤ 1 ms leftover silence |
269
+ | **F1 0.97** | `scenes.py`, 53 hard cuts between single takes, precision 0.95, recall 1.00 at the default threshold |
270
+ | **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) |
271
+ | **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) |
272
+ | **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
273
 
138
274
  ```bash
139
275
  python3 tests/corpus.py --fetch --verify # ~1.4 GB download, then verify (slow on 4K)
140
276
  python3 tests/bench_sync.py --cases 100
277
+ python3 tests/bench_silence.py
278
+ python3 tests/bench_scenes.py
141
279
  ```
142
280
 
143
- ## MCP
281
+ Benchmarks live in `tests/bench_*.py`, agent evals in [evals/](evals/), results by iteration in `evals/results/`.
144
282
 
145
- ```json
146
- {"mcpServers": {"ffmpeg-skill": {"command": "python3", "args": ["/Users/you/.claude/skills/ffmpeg-skill/mcp/server.py"]}}}
283
+ ## Install
284
+
285
+ ```bash
286
+ npx ffmpeg-skill # Claude Code → ~/.claude/skills/ffmpeg-skill
287
+ npx ffmpeg-skill --cursor # Cursor → ~/.cursor/skills/ffmpeg-skill
288
+ npx ffmpeg-skill --codex # Codex → ~/.codex/skills/ffmpeg-skill
289
+ npx ffmpeg-skill --all # all three
290
+ npx ffmpeg-skill --project # this project → ./.claude/skills/ffmpeg-skill
291
+ npx ffmpeg-skill --dir ./my-skills
292
+ npx ffmpeg-skill --uninstall # remove from the selected targets
147
293
  ```
148
294
 
149
- `python3 mcp/server.py --list` prints the tools; `--call probe '{"inputs": ["a.mp4"]}'` runs one from the shell.
295
+ Without Node: clone this repository and copy `SKILL.md`, `scripts/`, `references/` and `mcp/` into your agent's skills directory.
296
+
297
+ After installing:
298
+
299
+ ```bash
300
+ npx ffmpeg-skill doctor # every required FFmpeg component present?
301
+ npx ffmpeg-skill contract --json # what the agent framework will see
302
+ ```
303
+
304
+ FFmpeg itself:
305
+
306
+ | OS | Command |
307
+ |----|---------|
308
+ | macOS | `brew install ffmpeg-full` (the plain `ffmpeg` formula lacks the subtitles, drawtext and zscale filters) |
309
+ | Ubuntu / Debian | `sudo apt install ffmpeg` |
310
+ | Windows | `winget install Gyan.FFmpeg` |
150
311
 
151
312
  ## Requirements
152
313
 
153
- - 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
154
- - Python 3.9+
314
+ - 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.
315
+ - Python 3.9+, standard library only
155
316
  - Node 16+ only for the `npx` installer
156
317
 
157
318
  ## Development
158
319
 
159
320
  ```bash
160
- bash examples/make_demo.sh # generates footage, runs every script, rebuilds assets/demo.gif
161
- python3 tests/test_all.py # end-to-end tests incl. VFR, rotated, 5.1, 10-bit HDR10 and drifting sources (needs ffmpeg)
162
- python3 evals/run.py --list # routing eval prompts (see evals/)
321
+ npm test # tests/test_all.py (end-to-end incl. VFR, rotated, 5.1, HDR10, drifting sources) + tests/test_contract.py
322
+ npm run release-check # pack, install, contract from the installed copy, MCP == contract, doctor, tests, contract evals
323
+ npm run demo # generate footage, run every tool, rebuild assets/demo.gif
324
+ python3 evals/run.py --list # agent eval prompts (see evals/)
163
325
  node bin/install.js --dir /tmp/skills # try the installer without touching ~/.claude
164
326
  ```
165
327
 
328
+ 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.
329
+
330
+ **Releasing**: bump `version` in `package.json`, merge to `main`, then tag that commit (`git tag vX.Y.Z && git push origin vX.Y.Z`) and cut a GitHub Release from the tag, with the matching `CHANGELOG.md` section as its body. 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.
331
+
332
+ ## Docs
333
+
334
+ | | |
335
+ |---|---|
336
+ | [SKILL.md](SKILL.md) | what the agent reads: workflow, request → tool map, audio-only rules, report format, pitfalls |
337
+ | [references/scripts.md](references/scripts.md) | per-flag reference for every tool |
338
+ | [references/devices.md](references/devices.md) | real-device notes (iPhone HDR, GoPro, DJI, screen recordings) |
339
+ | [docs/contract.md](docs/contract.md) | the execution contract field by field, MCP relationship, how a planner consumes it |
340
+ | [examples/README.md](examples/README.md) | natural-language requests and the commands behind them, `brand.json`, `project.json`, batch recipes |
341
+ | [tests/fixtures/README.md](tests/fixtures/README.md) | captured and constructed FFmpeg listings, which is which |
342
+ | [CHANGELOG.md](CHANGELOG.md) | what changed in each release |
343
+
166
344
  ## Support
167
345
 
168
346
  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.
package/SKILL.md CHANGED
@@ -62,10 +62,22 @@ Ask one short question only when the answer changes the output materially and th
62
62
  - **Captions** without a text source: use `--transcribe` if a local whisper exists, otherwise ask for the text or a timed file; never invent dialogue.
63
63
  - **Fonts and brand**: if the user mentions a brand, colours or "our font", ask for or create `brand.json` once and reuse it.
64
64
  - **CJK / non-Latin text**: check that a font exists before rendering (`fc-list :lang=ja file` / `:lang=ko` / `:lang=zh`); pass it with `--font "Name"` or `--font-file /path.ttf`. Tofu boxes are a failed job, not a style.
65
- - Anything else (crop position, transition type, caption style): pick the conventional default, say what you picked, and offer the alternative in one line.
65
+ - **Crop position** for `--fit crop`: default to centre, but if the request or the source names an off-centre subject ("keep the product on the right", "don't cut off my hands", a logo/person visibly off-centre in `look.py`'s sheet) use `--crop-x`/`--crop-y` (0=left/top, 1=right/bottom) instead of the silent centre guess. Ask which edge to keep when the sheet shows the subject near an edge and the request doesn't say.
66
+ - Anything else (transition type, caption style): pick the conventional default, say what you picked, and offer the alternative in one line.
66
67
 
67
68
  Do not ask for things `probe.py` can tell you.
68
69
 
70
+ ## What this skill does and does not decide
71
+
72
+ This skill cuts, joins, measures, syncs, exports and checks files — it executes an edit, it does not decide one. Some things that sound like part of the job but belong to the human, the calling agent, or another skill instead:
73
+
74
+ - **Which cut is the right one, or whether a deliverable is approvable for release** — this skill measures and reports (`check.py`'s PASS/WARN/FAIL, `cut.py`'s measured duration error); a production agent or the user decides whether that's good enough to ship.
75
+ - **What makes a highlight interesting** — `scenes.py --highlights` ranks by a measured proxy (audio energy or scene duration, see its own docs), never by understanding the content; treat its output as candidates, not a verdict.
76
+ - **Thumbnail or cover-image composition** — that's a design decision, not a measurement; a thumbnail-generation skill or the user makes it.
77
+ - **Understanding what a video is *about*** — this skill has no transcription or vision beyond `look.py`'s contact sheets, which exist for the calling agent's own eyes, not for this skill to interpret on its own.
78
+
79
+ If a request needs an FFmpeg feature none of the 21 scripts expose, say so and name the closest built-in option (`--dry-run` to show what would run, or a documented limitation) — never fall back to guessing a raw `ffmpeg`/`ffprobe` invocation or a hand-built filter graph outside `scripts/*.py`. A raw command bypasses every guarantee this skill makes (no shell, typed arguments, verification afterwards); it is exactly the failure mode this skill exists to prevent, so it is never the fallback when a script's flag doesn't cover something.
80
+
69
81
  ## Request → script
70
82
 
71
83
  | User says | Do |
@@ -103,10 +115,13 @@ Do not ask for things `probe.py` can tell you.
103
115
  | "the colours look washed out / it's an iPhone HDR video" | `color.py input.mov --to-sdr` (probe shows `hdr: true`) |
104
116
  | "apply this LUT", "convert the S-Log / V-Log footage" | `color.py input.mp4 --lut grade.cube [--lut-strength 0.7]` |
105
117
  | "the colours are tagged wrong" | `color.py input.mp4 --retag bt709` (no re-encode) |
118
+ | "brighten it a touch / punch up the contrast and saturation / fix the white balance" | `color.py input.mp4 --correct --exposure 0.3 --contrast 1.1 --saturation 1.05 --temperature 5600 --tint -0.05` (typed, no filter string) |
106
119
  | "clean up the audio", "remove the hiss / room noise" | `audio.py input.mp4 --voice` (speech) or `--denoise` |
107
120
  | "add background music under the talking" | `audio.py input.mp4 --music bed.mp3 --duck --fade-out 3` |
108
121
  | "convert the 5.1 to stereo" | `audio.py input.mov --downmix` |
109
122
  | "swap in the narration track" | `audio.py input.mp4 --replace narration.wav` |
123
+ | "pull the audio out of this video", "give me the sound as WAV" | `audio.py input.mp4 -o input.wav` (any audio extension drops the picture; `--audio-stream 1` picks another track) |
124
+ | "compress the voice", "limit the peaks to -1 dB", "gate the room noise" | `audio.py input.mp4 --compress --comp-threshold -20 --comp-ratio 4` / `--limit --limit-ceiling -1` / `--gate --gate-threshold -45` (typed acompressor / alimiter / agate options, range-checked) |
110
125
  | "the audio drifts out of sync over the hour" | `sync.py camera.mp4 recorder.wav --fix-drift --replace-audio` |
111
126
  | "smooth slow motion", "half speed but fluid" | `fit.py input.mp4 --duration 2x --smooth interpolate` (slow) or `--smooth blend` |
112
127
  | "TikTok-style captions with the words popping / highlighted" | `caption.py input.mp4 --text cues.txt --animate pop --karaoke` |
@@ -124,12 +139,22 @@ commands work with `talk.wav` in place of `talk.mp4`. What changes:
124
139
  - The output extension picks the format: `-o out.mp3` converts, `-o out.wav`
125
140
  keeps PCM, `-o out.m4a` writes AAC. `audio.py in.wav -o out.mp3` with no
126
141
  other flag is a plain conversion.
127
- - `cut.py` stream-copies audio too, so trims are lossless unless the format
128
- cannot be cut on a packet boundary.
142
+ - `cut.py` stream-copies audio too, so trims land on a packet boundary
143
+ (`precision: packet`, a few ms; the JSON reports `duration_error_ms`). Pass
144
+ `--accurate` for a sample-exact trim: `precision: sample` when the output is
145
+ PCM or FLAC, `codec_frame` when a lossy codec (AAC, MP3, Opus) frames it
146
+ again. A `.wav` output is always PCM, never AAC packets inside a WAV.
147
+ - `join.py` joins audio-only clips as audio (`acrossfade` or a butt join) at
148
+ one sample rate and channel layout; the output must have an audio extension.
149
+ Video and audio clips cannot be mixed in one join.
150
+ - An audio extension on a video input (`audio.py talk.mp4 -o talk.wav`,
151
+ `cut.py talk.mp4 --start 1:00 --end 2:00 -o part.wav`) extracts the audio; the
152
+ output has no video stream. `audio.py --audio-stream N` picks a track when
153
+ `probe` lists several under `audio_streams`.
129
154
  - `Look: not needed` in the report; `Check:` still applies for loudness
130
155
  (`check.py file.wav --platform podcast` measures LUFS and true peak).
131
156
  - Scripts that need a picture (`fit`, `caption`, `overlay`, `graphics`,
132
- `color`, `export`, `join`, `scenes`, `look`) refuse an audio file with
157
+ `color`, `export`, `scenes`, `look`) refuse an audio file with
133
158
  "input has no video stream". Say so instead of forcing a video wrapper.
134
159
 
135
160
  | User says (audio file) | Do |
@@ -138,11 +163,16 @@ commands work with `talk.wav` in place of `talk.mp4`. What changes:
138
163
  | "remove the silence from this recording" | `silence.py talk.wav -o talk_tight.wav` |
139
164
  | "clean up the noise in this M4A" | `audio.py talk.m4a --voice -o talk_clean.m4a` (speech) or `--denoise` |
140
165
  | "convert this WAV to MP3" | `audio.py talk.wav -o talk.mp3` |
141
- | "trim this audio from 00:30 to 02:00" | `cut.py talk.wav --start 0:30 --end 2:00 -o talk_cut.wav` |
166
+ | "trim this audio from 00:30 to 02:00" | `cut.py talk.wav --start 0:30 --end 2:00 -o talk_cut.wav` (`--accurate` for sample-exact) |
167
+ | "join these recordings", "intro + episode + outro" | `join.py intro.wav episode.m4a outro.wav -o full.flac` (`--transition none` for a butt join) |
168
+ | "extract the audio from the video", "mp4 to wav" | `audio.py talk.mp4 -o talk.wav` (`--voice -o talk.m4a` to clean it on the way) |
169
+ | "compress / limit / gate the voice" | `audio.py talk.wav --compress --comp-threshold -20 --comp-ratio 4 --limit --limit-ceiling -1 -o talk_dyn.wav` |
142
170
  | "is this loud enough for Apple Podcasts?" | `check.py talk.m4a --platform podcast` |
143
171
 
144
172
  ## Report format
145
173
 
174
+ Reply in the language the user wrote their request in — a Japanese request gets a Japanese report, English gets English, Chinese gets Chinese, and so on for any other language. Keep the shape below and the field labels (`Done:`, `Steps:`, `Check:`, `Look:`, `Notes:`) in English (they read like log fields, not prose, and stay recognisable across languages); the sentences around them, any question asked, and any explanation of a judgement call are in the user's language. Never default to English because the tool names and flags happen to be English. A mid-conversation language switch follows the user's latest message, not the first one.
175
+
146
176
  Finish every job with this shape (numbers from `probe.py`/`check.py`, not memory):
147
177
 
148
178
  ```
@@ -159,16 +189,18 @@ Keep it to those five lines plus anything the user must decide. Attach the conta
159
189
 
160
190
  - Re-encoding an HDR (iPhone, HDR10) source through the SDR path: colours go flat. The scripts keep HDR; if you hand-write ffmpeg, do not tag BT.709 on BT.2020 pixels.
161
191
  - Lossless `-c copy` cuts on VFR or non-keyframe boundaries: the file "works" but starts on a frozen or wrong frame. `cut.py` re-encodes automatically when the snap exceeds 0.5 s; respect that.
162
- - A sync with `confidence` under 0.3, or an offset larger than 60 % of the analysis window: probably wrong; enlarge `--analyze-seconds` or find a clap.
192
+ - A sync or multicam alignment with `confidence` under 0.3, or an offset larger than 60 % of the analysis window: probably wrong; enlarge `--analyze-seconds` or find a clap. `multicam.py` reports one `confidence` per camera — check all of them, not just that the command succeeded, before trusting the cut.
193
+ - `sync.py`/`multicam.py` align audio tracks to each other, never lip sync (mouth movement vs. audio) — there is no face or mouth detection anywhere in this skill. A high confidence means the audio matched well, not that the picture looks right; if the user asks whether lip sync is correct, that needs a look at the actual video, not just the reported offset.
163
194
  - "Normalised" audio that still clips: check true peak, not just LUFS (`check.py` does both).
164
195
  - Normalising ambience or near-silence to a speech target: a clip measured at
165
196
  -40 LUFS or below is room tone, wind or nothing; raising it 25 dB raises the
166
197
  noise, not the content. Leave the level, say so, and offer music or narration.
167
198
  - Captions burned before a crop/resize: text lands off-frame. Frame changes first, then text.
168
199
  - Anything chained by hand through three re-encodes: use `render.py` so the plan is one file and the user can change one number.
169
- - `--fit crop` to reach 9:16 from 16:9 throws away 70 % of the width: a wide shot loses people at the edges. Check the sheet; pad (bars) or a reframe is often the honest answer.
200
+ - `--fit crop` to reach 9:16 from 16:9 throws away 70 % of the width: a wide shot loses people at the edges. Check the sheet; pad (bars), `--crop-x`/`--crop-y` toward the subject, or a reframe is often the honest answer — a silent centre crop is a guess, not a decision.
170
201
  - Conforming 60 fps to 30 halves the motion samples: fine for a talking head, visibly choppy for sports, gaming, drone pans. Keep 60 when the platform allows it.
171
202
  - "Make it 60 seconds" on a 3-minute talk by speed change is unwatchable (3×); by trim it drops two thirds of the words. Ask which, or propose a highlight cut with `scenes.py`.
203
+ - `scenes.py --highlights` defaults to the loudest scenes (`--rank-by audio`): a quiet but important moment (a confession, a punchline landing in silence) is skipped, and pure crowd noise or a mic bump can outrank it. `--rank-by duration` picks the longest unbroken scenes instead. Neither is "the best parts" — check the contact sheet (`--sheet`) before treating the picks as final.
172
204
 
173
205
  ## Gotchas
174
206