ffmpeg-skill 1.26.0 → 2.0.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.
- package/README.md +5 -5
- package/SKILL.md +2 -2
- package/docs/contract.md +54 -46
- package/package.json +1 -1
- package/references/gotchas.md +5 -5
- package/references/scripts.md +6 -6
- package/scripts/_common/__init__.py +6 -6
- package/scripts/_common/decision.py +2 -2
- package/scripts/_common/emit.py +2 -37
- package/scripts/_common/probe.py +10 -6
- package/scripts/_common/runner.py +13 -45
- package/scripts/_contract.py +34 -31
- package/scripts/background.py +1 -1
- package/scripts/broll.py +2 -2
- package/scripts/caption.py +1 -1
- package/scripts/check.py +3 -3
- package/scripts/color.py +4 -4
- package/scripts/crop.py +1 -1
- package/scripts/cut.py +1 -1
- package/scripts/deinterlace.py +1 -1
- package/scripts/denoise.py +1 -1
- package/scripts/export.py +2 -2
- package/scripts/fit.py +2 -2
- package/scripts/freeze.py +1 -1
- package/scripts/graphics.py +1 -1
- package/scripts/grid.py +2 -2
- package/scripts/insert.py +1 -1
- package/scripts/join.py +2 -2
- package/scripts/look.py +1 -1
- package/scripts/loop.py +1 -1
- package/scripts/multicam.py +2 -2
- package/scripts/overlay.py +1 -1
- package/scripts/pad.py +1 -1
- package/scripts/probe.py +1 -1
- package/scripts/proxy.py +2 -2
- package/scripts/redact.py +1 -1
- package/scripts/render.py +4 -1
- package/scripts/report.py +1 -1
- package/scripts/reverse.py +1 -1
- package/scripts/sequence.py +1 -1
- package/scripts/silence.py +1 -1
- package/scripts/speedramp.py +1 -1
- package/scripts/sphere.py +1 -1
- package/scripts/stabilize.py +1 -1
- package/scripts/straighten.py +1 -1
- package/scripts/sync.py +1 -1
- package/scripts/verify.py +2 -2
- package/scripts/waveform.py +1 -1
package/README.md
CHANGED
|
@@ -194,7 +194,7 @@ These are the rules the skill file gives the agent and the code enforces.
|
|
|
194
194
|
|
|
195
195
|
| Tool | What it does |
|
|
196
196
|
|---|---|
|
|
197
|
-
| `probe.py` | Duration, fps (+ VFR detection), resolution, codecs, bit depth, HDR format incl. Dolby Vision (`hdr` for
|
|
197
|
+
| `probe.py` | Duration, fps (+ VFR detection), resolution, codecs, bit depth, HDR format incl. Dolby Vision (`hdr` for a real PQ/HLG/DV transfer only, `bt2020_or_hdr` also for BT.2020 SDR), colour space, rotation, every audio stream; `--analyze` flags Log footage |
|
|
198
198
|
| `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`; `--beats` measures the music's beat grid (tempo, beat times, confidence); `--shots` classifies each shot static/pan/motion by measured optical flow; `--audio-peaks` lists per-second dBFS; `--speech` reports a speech-vs-music ratio, not a classification |
|
|
199
199
|
| `look.py` | Contact sheet, single frames, side-by-side comparison as PNG so the agent can see what it made; `--safe NAME` shades the zones a platform's own UI covers |
|
|
200
200
|
|
|
@@ -249,7 +249,7 @@ These are the rules the skill file gives the agent and the code enforces.
|
|
|
249
249
|
| Tool | What it does |
|
|
250
250
|
|---|---|
|
|
251
251
|
| `export.py` | Presets `youtube`, `youtube4k`, `reels`, `tiktok`, `shorts`, `linkedin`, `facebook`, `x`, `youtube-hdr` (HEVC Main10, source HDR tags kept), `youtube-av1`, `prores`, `h265`, `gif`, `copy`, all tagged BT.709 unless they carry HDR; `--normalize` meets the platform's loudness in the same call (`render.py` turns it on by default for platform presets) |
|
|
252
|
-
| `proxy.py` | Small, low-bitrate proxy for downstream AI analysis/preview/editing decisions — resize by `--width`/`--scale`, proxy-grade `--
|
|
252
|
+
| `proxy.py` | Small, low-bitrate proxy for downstream AI analysis/preview/editing decisions — resize by `--width`/`--scale`, proxy-grade `--quality` (default 30), `--fps`, `--no-audio`; not a delivery preset |
|
|
253
253
|
| `check.py` | PASS / WARN / FAIL against YouTube, Shorts, Reels, TikTok, X, LinkedIn, Facebook, broadcast and podcast specs, from the same delivery table the export presets and templates read (podcast also reports chapter markers and channel count), with the fix for each failure and a `format` / `judgement` kind per row |
|
|
254
254
|
| `report.py` | Single-file HTML delivery report: before/after sheets, media facts, loudness, compliance, the commands run; `--pack` renders a social pack table |
|
|
255
255
|
|
|
@@ -343,7 +343,7 @@ On Windows, `python3` is only on PATH if Python was installed from the Microsoft
|
|
|
343
343
|
|
|
344
344
|
`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, in contract order, with `inputSchema` translated from each tool's `input_schema`. By default it lists only the core 12 (`render`, `look`, `caption`, `export`, `check`, `fit`, `cut`, `audio`, `loudness`, `graphics`, `silence`, `probe`) so a client doesn't pay context for 30 schemas it rarely calls directly; set `FFMPEG_SKILL_MCP_FULL=1` to list all 42. Every tool, listed or not, is callable through `tools/call`, which 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.
|
|
345
345
|
|
|
346
|
-
`
|
|
346
|
+
No `inputSchema` lists `json` or `progress`: they are transport flags the server sets itself, not tool arguments (2.0; 1.x listed them unless `FFMPEG_SKILL_MCP_LEAN=1`). A client that still sends them is not refused.
|
|
347
347
|
|
|
348
348
|
The server also advertises a `prompts` capability: five canned workflow recipes an agent can ask for by name instead of composing the individual tool calls itself — `reel`, `podcast`, `multicam`, `delivery_check`, `hdr`. `prompts/list` returns each one's name, description and arguments; `prompts/get {name, arguments}` fills the matching template and returns one text message naming the actual command lines to run and what to check afterward. These are recipes, not new tool calls — every command line a prompt names is one `tools/call` (or the CLI) can already run.
|
|
349
349
|
|
|
@@ -379,7 +379,7 @@ The short list for humans. The agent-facing version, with the reasoning, is the
|
|
|
379
379
|
- **Non-Latin text picks a font by script (1.12).** Japanese, Chinese, Korean, Arabic, Hebrew, Devanagari, Thai, Cyrillic and Greek cues, titles and overlays resolve a font file that covers them automatically, and a machine with no such font fails the job (`kind: input`) instead of rendering boxes. `doctor --json`'s `fonts.scripts` says which languages this machine can render; `--lang ja|ko` disambiguates Han-only text; an explicit `--font`/`--font-file` is always kept.
|
|
380
380
|
- **Silence detection finds nothing?** The default threshold is −35 dBFS. The tool prints a hint with the track's measured level; raise the threshold (`silence.py --threshold -25`) or shorten `--min-silence`.
|
|
381
381
|
- **Sync results carry a confidence.** Below 0.3, or an offset near the edge of the analysis window, is probably wrong: enlarge `--analyze-seconds` or find a clap. Recordings over ten minutes from separate devices need `sync.py --fix-drift`.
|
|
382
|
-
- **Outputs are never overwritten silently.** An existing output path is
|
|
382
|
+
- **Outputs are never overwritten silently.** An existing output path is refused (`kind: input`, before anything runs); pass `--overwrite` where a replacement is intended.
|
|
383
383
|
- **Long chains belong in a plan.** Three hand-chained re-encodes lose quality and are hard to change; `render.py` runs the whole edit from one JSON file, and `--dry-run` shows every ffmpeg command before anything is written.
|
|
384
384
|
|
|
385
385
|
## FFmpeg compatibility
|
|
@@ -494,7 +494,7 @@ FFmpeg itself:
|
|
|
494
494
|
|
|
495
495
|
## Stability
|
|
496
496
|
|
|
497
|
-
|
|
497
|
+
2.x keeps every tool name, CLI argument, JSON output key and exit code working: nothing is removed or renamed, and nothing optional becomes required, until 3.0. The full list of what is promised and what is not, and the three-step deprecation policy, is in [docs/contract.md](docs/contract.md#stability-guarantee-2x). It is enforced by a test that pins every tool's argument names against a snapshot, so a breaking change fails CI instead of slipping into a patch. 2.0.0 made the five changes announced since 1.10.0 -- `--crf` removed in favour of `--quality`, an existing output refused without `--overwrite`, `probe`'s `hdr` narrowed to a real HDR signal, `json`/`progress` out of the MCP schema, the `result_v2` preview withdrawn -- listed with what to change in [docs/contract.md](docs/contract.md#what-20-changed) and under `removed` in `contract --json`.
|
|
498
498
|
|
|
499
499
|
## Development
|
|
500
500
|
|
package/SKILL.md
CHANGED
|
@@ -20,7 +20,7 @@ Writing tools run nothing under `--dry-run`; the measuring tools (`probe`, `chec
|
|
|
20
20
|
4. **Chain in a sensible order.** A delivery request with no other editing is one template run (`render.py --template NAME INPUT`), not a hand-built chain. Otherwise: colour (HDR→SDR / LUT) → cut → join → silence → fit → caption/overlay → sync → audio → loudness → export. Frame changes before captions, so text is sized for the final frame. Re-encode as few times as possible: intermediates at CRF 18, `export.py` last. **Three or more steps: `render.py` with a project.json.**
|
|
21
21
|
5. **Check the deliverable.** Before reporting, run `check.py OUTPUT --platform X` for the destination named (a template run already does). Format rows (codec, pixel format, size, true peak, colour tags, VFR) are safe to fix. Judgement rows change content: duration (cut loses material), aspect (crop loses edges), fps (drops motion), loudness (ambience must not be boosted) — fix only when the request implies the answer, else state the choice and its cost. Mention WARNs; do not chase them.
|
|
22
22
|
6. **Verify the output.** Confirm duration, resolution, fps and audio match the request, from the writing tool's `--json`/`--json-brief` probe or `probe.py`, and report those numbers ("final.mp4: 59.98 s, 1080x1920, 30 fps, AAC stereo"). A step is done only when the script exited 0 and the output probes as expected: a non-zero exit, a missing or empty file, or a probe that contradicts the request is a failure reported with the script's error message.
|
|
23
|
-
7. **Keep the user's originals.** Never overwrite the source; write new files next to input or where asked.
|
|
23
|
+
7. **Keep the user's originals.** Never overwrite the source; write new files next to input or where asked. An existing output path is refused (`kind: input`); `--overwrite` is the one way to say "yes, replace it".
|
|
24
24
|
8. **Look at the picture.** Whenever the picture changed (captions, overlays, graphics, crop/pad, resize, colour, transitions, a `join.py` scaling a clip to the first clip's frame, `color.py --to-sdr`) run `look.py OUTPUT --tiles 3x2` (or `--at T` for one frame) and view the PNG; the full 4x3 sheet is for whole-clip layout jobs. Not finished until `Look:` names that PNG — a probe cannot see a caption on someone's face. Audio-only jobs write `Look: not needed`. What to look for splits like `check.py`'s rows in step 5:
|
|
25
25
|
- **Mechanical (this skill's own job to verify and report):** the specified text/logo is at the specified position, subtitles appear at the specified timestamps, dimensions are even. Letterboxing from `fit.py --fit pad` is the *correct* result of that mode, never a defect to flag.
|
|
26
26
|
- **Judgement (report it, don't silently pass or fail):** whether a subject or face is cut off, text sits over a face, colours look washed out, a transition lands. These need deciding what the subject *is*, which belongs to the calling agent — say what you see in one line and let them judge it.
|
|
@@ -203,7 +203,7 @@ Every script prints `{"status": "failed", "error": {"kind": input | ffmpeg | out
|
|
|
203
203
|
|
|
204
204
|
One line each; open the linked `references/gotchas.md` section when the job is in that area.
|
|
205
205
|
|
|
206
|
-
- HDR (iPhone, HDR10) re-encoded through an SDR path goes flat; the scripts keep HDR
|
|
206
|
+
- HDR (iPhone, HDR10) re-encoded through an SDR path goes flat; the scripts keep HDR; `hdr: true` is a real PQ/HLG/DV signal, `bt2020_or_hdr: true` also BT.2020 SDR. -> [#hdr-and-colour](references/gotchas.md#hdr-and-colour)
|
|
207
207
|
- Log footage (S-Log/V-Log/C-Log) is tagged SDR and looks grey: `probe.py --analyze`, then `color.py --lut` first. -> [#log-footage](references/gotchas.md#log-footage)
|
|
208
208
|
- A `-c copy` cut can start on a wrong or frozen frame; `cut.py` re-encodes past a 0.5 s snap, respect it. -> [#keyframe-cuts](references/gotchas.md#keyframe-cuts)
|
|
209
209
|
- VFR phone/screen recordings: re-encodes conform to CFR, `cut.py` switches to `--accurate`; pick the rate with `fit.py --fps` when odd. -> [#variable-frame-rate](references/gotchas.md#variable-frame-rate)
|
package/docs/contract.md
CHANGED
|
@@ -21,7 +21,7 @@ The contract is derived from the code that runs, not maintained beside it:
|
|
|
21
21
|
| Field | Meaning | Changes when |
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| `contract_version` | shape of this document (`1.0`) | a key is renamed, removed or changes meaning |
|
|
24
|
-
| `skill.version` | the npm / package.json version (`
|
|
24
|
+
| `skill.version` | the npm / package.json version (`2.0.0`) | any release |
|
|
25
25
|
|
|
26
26
|
A release that adds a tool or a flag keeps `contract_version`; a breaking change to the
|
|
27
27
|
ToolSpec shape bumps it. Consumers pin on `contract_version` and read `skill.version`
|
|
@@ -35,11 +35,12 @@ from `scripts/` on every CI run instead — a stale count fails a test rather th
|
|
|
35
35
|
silently. (`SKILL.md` was added to that check after its "the 28 scripts" sat stale through
|
|
36
36
|
twelve tool additions while the other three files were correct.)
|
|
37
37
|
|
|
38
|
-
## Stability guarantee (
|
|
38
|
+
## Stability guarantee (2.x)
|
|
39
39
|
|
|
40
40
|
1.0.0 was published on 2026-09-11 (by accident: see CHANGELOG.md's 1.0.0 entry; the number
|
|
41
|
-
is kept rather than burned). From 1.0.3 on, the number is treated as the promise it implies
|
|
42
|
-
|
|
41
|
+
is kept rather than burned). From 1.0.3 on, the number is treated as the promise it implies;
|
|
42
|
+
2.0.0 (2026-09-22) is the first deliberate break, listed under "What 2.0 changed" below.
|
|
43
|
+
The same promise holds for the whole of 2.x:
|
|
43
44
|
|
|
44
45
|
| Surface | Promise |
|
|
45
46
|
|---|---|
|
|
@@ -71,36 +72,54 @@ out to be misleading) is retired in three steps, never in one:
|
|
|
71
72
|
naming the replacement is printed to stderr when it is used, the CHANGELOG entry says
|
|
72
73
|
"deprecated", and `--help` marks it `(deprecated: use ...)`.
|
|
73
74
|
2. **Keep** it for at least two further minor releases or 90 days, whichever is longer.
|
|
74
|
-
3. **Remove** it only in the next major
|
|
75
|
+
3. **Remove** it only in the next major, listed in that release's CHANGELOG under
|
|
75
76
|
"Removed", together with the version that first deprecated it.
|
|
76
77
|
|
|
77
78
|
A defect fix that changes behaviour is not a deprecation: it ships in a patch with a
|
|
78
79
|
CHANGELOG line, and if the old behaviour was something a caller could reasonably have relied
|
|
79
80
|
on, the line says so.
|
|
80
81
|
|
|
81
|
-
## What 2.0 changes
|
|
82
|
-
|
|
83
82
|
`contract --json` carries a top-level `deprecated` list, next to `contract_version`: one entry per
|
|
84
|
-
thing
|
|
85
|
-
the surface (`cli`, `json`, `mcp`, `behaviour`). It is
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
thing the next major removes, `{"what", "since", "replacement", "removed_in", "where"}` with `where`
|
|
84
|
+
naming the surface (`cli`, `json`, `mcp`, `behaviour`). It is empty in 2.0.0. `tests/test_contract.py`
|
|
85
|
+
refuses a package.json major bump that still lists an entry that major should remove, or that
|
|
86
|
+
comes before step 2's window has passed -- unless the waiver is written here, as 2.0.0's is.
|
|
87
|
+
|
|
88
|
+
**2.0.0 waived the 90-day half of step 2.** Its deprecations date from 1.10.0 (2026-09-13), so
|
|
89
|
+
the policy's earliest 2.0.0 was 2026-12-12; on 2026-09-22 the maintainer decided not to wait.
|
|
90
|
+
The other half held with room to spare: sixteen minor releases (1.11-1.26) carried every one
|
|
91
|
+
of them, each already had a 1.x opt-in to the 2.0 behaviour (`--quality`,
|
|
92
|
+
`FFMPEG_SKILL_MCP_LEAN=1`, `hdr_signal`, `FFMPEG_SKILL_NO_OVERWRITE=1`), and the one change a
|
|
93
|
+
caller could not have prepared for mechanically -- promoting `result_v2` -- was withdrawn
|
|
94
|
+
rather than shipped early (below).
|
|
95
|
+
|
|
96
|
+
## What 2.0 changed
|
|
88
97
|
|
|
89
|
-
|
|
98
|
+
`contract --json`'s `removed` list is the machine-readable form of this table, one entry per
|
|
99
|
+
change with the version that deprecated it.
|
|
100
|
+
|
|
101
|
+
| What changed in 2.0.0 | Deprecated in | 2.0 behaviour | Migrating from 1.x |
|
|
90
102
|
|---|---|---|---|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
| `
|
|
94
|
-
|
|
|
95
|
-
|
|
|
103
|
+
| `--crf` as an alias of `--quality` on every re-encoding tool that takes `--quality` (`export.py` keeps `--crf`: its preset chooses the encoder) | 1.10.0 | removed; `--quality N` defaults to what `--crf` did (18, `proxy.py` 30) | Pass `--quality` where you passed `--crf`; `--crf` is now an unknown argument |
|
|
104
|
+
| `json` and `progress` in the MCP `inputSchema` | 1.10.0 | left out of every schema (as `FFMPEG_SKILL_MCP_LEAN=1` did); the transport sets them itself | Nothing: a client that still sends them is not refused |
|
|
105
|
+
| `hdr` in `probe` meaning "BT.2020 primaries *or* a PQ/HLG transfer" | 1.10.0 | `hdr` is true only for PQ / HLG / Dolby Vision, equal to `hdr_signal` (kept); `bt2020_or_hdr` carries the 1.x meaning, and every editing tool still routes on it | Key on `bt2020_or_hdr` where you meant the old `hdr` (e.g. "needs the 10-bit path"); `hdr_format` still names `BT.2020 SDR` |
|
|
106
|
+
| Overwriting an existing output with only a warning | 1.10.0 | refused (`kind: input`, before anything runs, dry runs included) unless `--overwrite` | Pass `--overwrite` where a replacement is intended; `FFMPEG_SKILL_NO_OVERWRITE` no longer does anything |
|
|
107
|
+
| The `result_v2` preview key and `FFMPEG_SKILL_RESULT_V2` | 1.10.0 (the v1 keys it was to replace) | removed; the top-level keys are the 2.0 shape, unchanged | Read the top-level keys; `--json-brief` for a short document |
|
|
108
|
+
|
|
109
|
+
The v1 success keys were deprecated in 1.10.0 in favour of `result_v2`. 2.0 withdrew that
|
|
110
|
+
deprecation instead: `result_v2` decided whether a key landed in `metrics` or in `details` by its
|
|
111
|
+
value (a number went to `metrics`, the same key holding `null` went to `details`), so a caller
|
|
112
|
+
could not know where to look without knowing the value first, and every consumer of every tool's
|
|
113
|
+
JSON would have been rewritten for that. The flat keys, typed per tool in `output_schema`, stay.
|
|
96
114
|
|
|
97
115
|
## Skill
|
|
98
116
|
|
|
99
117
|
```json
|
|
100
118
|
{
|
|
101
119
|
"contract_version": "1.0",
|
|
102
|
-
"deprecated": [
|
|
103
|
-
"
|
|
120
|
+
"deprecated": [],
|
|
121
|
+
"removed": [{"what": "...", "since": "1.10.0", "replacement": "...", "removed_in": "2.0.0", "where": "cli | json | mcp | behaviour"}],
|
|
122
|
+
"skill": {"id": "ffmpeg-skill", "version": "2.0.0", "execution_mode": "local", "kind": "execution",
|
|
104
123
|
"entrypoints": {"cli": "...", "mcp": "...", "contract": "...", "doctor": "..."},
|
|
105
124
|
"not_provided": ["AI reasoning", "decisions", "production plans", "project IR", "approvals", "network access", "transcription engine"]},
|
|
106
125
|
"requirements": {"python": ">=3.9 (standard library only)", "ffmpeg": ">=5.0", "ffprobe": ">=5.0"},
|
|
@@ -120,7 +139,7 @@ One entry per tool under `tools`, sorted by id. Tool ids are stable:
|
|
|
120
139
|
| Field | Meaning |
|
|
121
140
|
|---|---|
|
|
122
141
|
| `id`, `name`, `version`, `executable` | `ffmpeg-skill/cut`, `cut`, skill version, `scripts/cut.py` |
|
|
123
|
-
| `examples[]` | `{prompts[], command}` (1.
|
|
142
|
+
| `examples[]` | `{prompts[], command}` (1.21.0): SKILL.md's own "User says" / "Do" table rows for this tool, machine-readable — `prompts` is the row's phrasing(s), `command` the row's `Do` text with the markdown backticks stripped. Every tool has at least one; parsed from SKILL.md at read time, so it never drifts from the table a person reads |
|
|
124
143
|
| `role` | `analysis`, `analysis_and_execution`, `execution` or `verification` (see below) |
|
|
125
144
|
| `capabilities.required` | ffmpeg components the tool always needs |
|
|
126
145
|
| `capabilities.optional[]` | `{capability, when}`: needed only for that flag or input |
|
|
@@ -129,7 +148,7 @@ One entry per tool under `tools`, sorted by id. Tool ids are stable:
|
|
|
129
148
|
| `output_schema` | what `--json` prints on stdout |
|
|
130
149
|
| `supports_dry_run`, `dry_run` | whether `--dry-run` plans without running ffmpeg or writing files |
|
|
131
150
|
| `supports_json` | whether `--json` exists |
|
|
132
|
-
| `supports_json_brief` | whether `--json-brief` exists (1.
|
|
151
|
+
| `supports_json_brief` | whether `--json-brief` exists (1.11.0): the same success document with `probe` replaced by a compact `summary` (`duration_s`, `width`, `height`, `fps`, `vcodec`, `acodec`, `channels`, and `lufs` when the tool measured one), `commands` replaced by the number of commands run, and the per-step `verification` list dropped (its verdict stays in `verified`). Tool-specific keys are unchanged, `--json`'s own output is unchanged, and a failure prints the same failure document either way |
|
|
133
152
|
| `mutates_input` | always `false`: no tool overwrites its input |
|
|
134
153
|
| `produces_artifact` | writes a file (media, PNG, HTML, EDL) |
|
|
135
154
|
| `verification` | `{required, tools}`: which tools to run on the output afterwards |
|
|
@@ -181,12 +200,13 @@ tool's job; only the artifact is skipped, including side files such as `--edl`,
|
|
|
181
200
|
generated `.ass`), and `verify` does not support dry-run (its steps run). `SKILL.md` and
|
|
182
201
|
`references/scripts.md` repeat the same list; the contract is the authority.
|
|
183
202
|
|
|
184
|
-
`--codec h264|hevc|av1|prores` and `--quality N` (1.8) are on every tool
|
|
185
|
-
|
|
203
|
+
`--codec h264|hevc|av1|prores` and `--quality N` (1.8) are on every tool that re-encodes,
|
|
204
|
+
marked `common`. They are resolved in one place
|
|
186
205
|
(`_common.encoder_args()`): hevc keeps an HDR source Main10 with its tags and writes 8-bit
|
|
187
206
|
BT.709 for SDR; av1 is SVT-AV1 with libaom as the fallback; prores is 422 HQ and needs a
|
|
188
|
-
`.mov`/`.mkv` output; h264 refuses an HDR source (`kind: input`). `--quality` is the CRF scale
|
|
189
|
-
|
|
207
|
+
`.mov`/`.mkv` output; h264 refuses an HDR source (`kind: input`). `--quality` is the CRF scale;
|
|
208
|
+
its default is the tool's own (18, `proxy.py` 30). 1.x also accepted `--crf` as an alias; 2.0
|
|
209
|
+
removed it (`export.py` keeps its own `--crf`). Without `--codec` the encoder is what it always was (x264 for SDR, x265
|
|
190
210
|
Main10 for HDR), so the flags add no behaviour to a caller that does not pass them. The
|
|
191
211
|
encoder each value needs is listed under the tool's optional capabilities (`--codec hevc` and
|
|
192
212
|
so on). `export.py` refuses `--codec`: its presets decide the codec.
|
|
@@ -435,7 +455,7 @@ given a different type):
|
|
|
435
455
|
|
|
436
456
|
| key | tool | what it holds |
|
|
437
457
|
|---|---|---|
|
|
438
|
-
| `caption` | `caption.py` | the cue-layout counts the run only printed before (`shifted`, `wrapped`, `rebalanced`, `split`, `extended`, `dropped`) plus `wrap` (`"phrase"` or `"measured"`) and `phrase_breaks`, the number of cues a phrase rule broke somewhere the 1.15 width rule would not. `broken_inside_word` (1.
|
|
458
|
+
| `caption` | `caption.py` | the cue-layout counts the run only printed before (`shifted`, `wrapped`, `rebalanced`, `split`, `extended`, `dropped`) plus `wrap` (`"phrase"` or `"measured"`) and `phrase_breaks`, the number of cues a phrase rule broke somewhere the 1.15 width rule would not. `broken_inside_word` (1.18.4) counts atoms hard-sliced at the live column's edge because they did not fit alone even at the size floor — never a rewrite, the sliced pieces are the exact original characters. `overlong` is now residual: it fires only when even a single character is wider than the column |
|
|
439
459
|
| `tracks`, `subtitle_tracks` | `caption.py --mode mux` | one entry per subtitle stream in the output — `{index, file, language, title, codec, default, cues, kept_from_input}`; a stream the input already carried has `file: null` and `kept_from_input: true`. `subtitle_tracks` is the total. Every field describes the file as written, not as asked for: an MPEG-4 output reports `title: null` (the muxer stores none) and `default: true` on its first track (the muxer always enables it), each with a `notes` line |
|
|
440
460
|
| `auto_chapters` | `metadata.py --auto-chapters` | `{source, min_chapter, max_chapters, proposed, kept, titles, chapters, description_block, files}`. `titles` is always `"placeholder"`: the machine-readable form of "the skill did not name these". Each chapter carries its `evidence` (`start`, `silence`, `scene`, or `silence+scene` with the span, its length and the cut time) |
|
|
441
461
|
| `audiogram` | `waveform.py` (every run) | `{style, background, image, position, vis_height, platform, captions, title, stages, verified}`. `background` is `"image"` or `"color"`; `verified` is true when the render probes at the asked-for frame size, frame rate and within 0.05 s of the source audio, and is `false` under `--dry-run`, where nothing was rendered to verify |
|
|
@@ -451,7 +471,7 @@ Per-tool keys added in 1.17, all additive:
|
|
|
451
471
|
| `jobs`, `jobs_requested`, `wall_seconds`, `item_seconds_total`, `timed_out` | `batch.py` | the parallelism actually applied and the number asked for, the batch's wall clock, the sum of the per-item times (so the speed-up can be quoted), and whether the shared timeout budget ran out. A timed-out item carries `"skipped": "timeout"` in its result row |
|
|
452
472
|
| `cache` | `render.py --cache` | `{dir, ffmpeg, hits, misses, saved_seconds, entries}`, plus `would_hit` under `--dry-run`. The ffmpeg build banner, the skill version, the contract version, the forwarded flags (`--fast`, `--codec`, …) and the output's extension are all part of every key, so a cache is never reused across any of them — a `--fast` draft is never served to a run that did not ask for one |
|
|
453
473
|
|
|
454
|
-
Per-tool keys added in 1.
|
|
474
|
+
Per-tool keys added in 1.17.1, all additive:
|
|
455
475
|
|
|
456
476
|
| key | tool | what it holds |
|
|
457
477
|
|---|---|---|
|
|
@@ -459,7 +479,7 @@ Per-tool keys added in 1.26.0, all additive:
|
|
|
459
479
|
| `text_unchanged` | `caption.py` | a sibling inside the `caption` block, **burn mode only** (`--mode mux` never touches the text and omits the key): `true` when the drawn text equals the cues that were handed in — nothing transcribed, no cue dropped, no cue **split** across two consecutive cues and no glyph stripped (`--emoji none`). Wrapping, line breaks and timing do not count: the words are the same. This tool never rewrites, shortens or translates a cue, so the key is a statement of what happened, not a judgement of the text |
|
|
460
480
|
|
|
461
481
|
|
|
462
|
-
Per-tool keys added in 1.
|
|
482
|
+
Per-tool keys added in 1.18.0, all additive:
|
|
463
483
|
|
|
464
484
|
| key | tool | what it holds |
|
|
465
485
|
|---|---|---|
|
|
@@ -494,15 +514,6 @@ listed step met its target. A `--dry-run` document has `verified: false` and an
|
|
|
494
514
|
`export.py` whose written file misses the platform's loudness spec stays `completed` (the
|
|
495
515
|
file is valid) with `verified: false` and the fix in `notes`, so a caller keys on one field.
|
|
496
516
|
|
|
497
|
-
With `FFMPEG_SKILL_RESULT_V2=1` in the environment, every writing tool's success document
|
|
498
|
-
also carries `result_v2`: a preview of the one shape 2.0 will use for every tool
|
|
499
|
-
(issue #189). `{"schema": 2, "output", "probe", "commands", "metrics", "notes", "dropped":
|
|
500
|
-
{"non_av_streams"}, "verified", "verification", "details"}` -- `metrics` holds the numbers a caller keys on (loudness's
|
|
501
|
-
measurement dicts flattened, plus any numeric top-level key such as `expected_duration` or
|
|
502
|
-
`offset_seconds`), `notes` the free text, `details` the tool's remaining keys unchanged. The
|
|
503
|
-
1.x keys are not moved; the environment variable only adds the key, and its absence is the
|
|
504
|
-
default until 2.0.
|
|
505
|
-
|
|
506
517
|
Success is decided by `verify_output` in `_common` (`scripts/_common/probe.py`), not by the
|
|
507
518
|
ffmpeg exit code alone:
|
|
508
519
|
the file must exist, be non-empty and give ffprobe at least one stream. A tool that ran
|
|
@@ -574,14 +585,11 @@ already applied when the ToolSpec is built.
|
|
|
574
585
|
The `tools/list` document is deterministic (byte-identical across processes and
|
|
575
586
|
identical to the translation of `contract --json`), which the tests check.
|
|
576
587
|
|
|
577
|
-
`
|
|
578
|
-
`
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
`tools/list` carries the tool names, argument names and `required` lists the frozen 1.x snapshot
|
|
583
|
-
pins -- descriptions may change between releases (the `--crf` deprecation mark did) -- so a lean
|
|
584
|
-
client and a default client see the same tools with the same names.
|
|
588
|
+
No `inputSchema` lists `json` or `progress`: they are transport flags `mcp/server.py` sets
|
|
589
|
+
itself -- it appends `--json` for every tool but `look` and `probe` -- rather than arguments a
|
|
590
|
+
caller chooses. 1.10.0-1.26 listed them unless `FFMPEG_SKILL_MCP_LEAN=1`; 2.0 leaves them out for
|
|
591
|
+
good (see "What 2.0 changed"). A client that still sends either is not refused: the server maps
|
|
592
|
+
it like any other key.
|
|
585
593
|
|
|
586
594
|
### Default `tools/list` surface: core 12, opt-in for all 42
|
|
587
595
|
|
|
@@ -593,7 +601,7 @@ most often across the corpus in `evals/agent_prompts*.json`. Every tool -- inclu
|
|
|
593
601
|
-- is still callable by name through `tools/call` regardless of what `tools/list` advertised; the
|
|
594
602
|
contract (`ffmpeg-skill contract --json`) still describes all 42 unconditionally. Set
|
|
595
603
|
`FFMPEG_SKILL_MCP_FULL=1` (anything but "" or `0`) in the server's environment to make `tools/list`
|
|
596
|
-
return all 42, as every version before 1.
|
|
604
|
+
return all 42, as every version before 1.18.3 did.
|
|
597
605
|
|
|
598
606
|
### `prompts` capability: five workflow recipes
|
|
599
607
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ffmpeg-skill",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Agent Skill that gives coding agents (Claude Code, Cursor, Codex) a local video editor: 42 FFmpeg tools with a machine-readable contract, contract-derived MCP server, FFmpeg capability detection, probe-first / verify-last workflow. Cut, join, silence removal, fit, captions and karaoke, overlays, motion graphics, HDR to SDR, LUTs, audio clean-up and typed dynamics, sync with drift correction, multicam, loudness, delivery checks, project rendering, batch. No API keys, no cloud, no dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ffmpeg",
|
package/references/gotchas.md
CHANGED
|
@@ -9,13 +9,13 @@ detail, not an aside.
|
|
|
9
9
|
|
|
10
10
|
### HDR and colour
|
|
11
11
|
SDR outputs are H.264 tagged BT.709 `yuv420p`. When `probe.py` reports
|
|
12
|
-
`
|
|
13
|
-
the output
|
|
12
|
+
`bt2020_or_hdr: true` (HDR10/PQ, HLG, Dolby Vision, or BT.2020 SDR), every editing script keeps
|
|
13
|
+
the output on the 10-bit path (HEVC Main10, source colour tags) so nothing is silently
|
|
14
14
|
flattened. Decide with the user: keep HDR (fine for YouTube/phones) or run
|
|
15
15
|
`color.py --to-sdr` first for SDR-only destinations, LUT work or H.264
|
|
16
|
-
deliverables. `hdr: true`
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
deliverables. Since 2.0 `hdr: true` (and its 1.9 twin `hdr_signal`) is the narrow
|
|
17
|
+
fact — a real PQ / HLG / Dolby Vision transfer; `bt2020_or_hdr` also counts a
|
|
18
|
+
wide-gamut SDR file (1.x's `hdr`), and `hdr_format` names that in-between case
|
|
19
19
|
(`BT.2020 SDR`). `export.py` platform presets are SDR and warn on HDR input.
|
|
20
20
|
iPhone `.mov` files also carry timecode/metadata tracks; scripts map only the
|
|
21
21
|
first audio track, so extra tracks are dropped on re-encode. Keep ProRes masters
|
package/references/scripts.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Script reference
|
|
2
2
|
|
|
3
|
-
Every script prints the same information with `--help`; this file exists so the agent can read several at once. All scripts accept `--dry-run`, `--json`, `--json-brief` (since 1.11.0: the same JSON result trimmed to `status`, `output`, `dry_run`, `verified`, a compact `summary` of the output probe -- duration_s, width, height, fps, vcodec, acodec, channels, and lufs when measured -- the tool's own keys, and the count of commands instead of the command lines; it implies `--json`, leaves `--json`'s own output untouched, and failures print the usual full failure document), `--fast`, `--progress`, `--timeout SECONDS`, `--overwrite`, `--plan FILE` (the dry run written as a plan document that `render.py FILE` executes later; see render.py), `-o OUT`; every editing tool that re-encodes (not `export.py`, whose preset decides the codec) also takes `--codec h264|hevc|av1|prores` (the encoder for the re-encode; default x264 for SDR, x265 Main10 for HDR, unchanged) and `--quality N` (CRF scale,
|
|
3
|
+
Every script prints the same information with `--help`; this file exists so the agent can read several at once. All scripts accept `--dry-run`, `--json`, `--json-brief` (since 1.11.0: the same JSON result trimmed to `status`, `output`, `dry_run`, `verified`, a compact `summary` of the output probe -- duration_s, width, height, fps, vcodec, acodec, channels, and lufs when measured -- the tool's own keys, and the count of commands instead of the command lines; it implies `--json`, leaves `--json`'s own output untouched, and failures print the usual full failure document), `--fast`, `--progress`, `--timeout SECONDS`, `--overwrite`, `--plan FILE` (the dry run written as a plan document that `render.py FILE` executes later; see render.py), `-o OUT`; every editing tool that re-encodes (not `export.py`, whose preset decides the codec) also takes `--codec h264|hevc|av1|prores` (the encoder for the re-encode; default x264 for SDR, x265 Main10 for HDR, unchanged) and `--quality N` (CRF scale, default 18 -- 30 for `proxy.py`; up to 63 for av1; ignored by prores). 2.0 removed `--crf` from these tools (it was `--quality`'s alias); `export.py` keeps its own `--crf`, which is not an alias. Every tool refuses (`kind: input`, before anything runs) to replace an existing output unless `--overwrite` is given. `--codec hevc` on SDR writes 8-bit BT.709 HEVC (`hvc1`), `av1` uses SVT-AV1 (libaom fallback), `prores` is 422 HQ and needs an explicit `-o NAME.mov` (or `.mkv`), `h264` refuses an HDR source (`kind: input`, run `color.py --to-sdr` first). `export.py` keeps choosing the codec from its preset and has neither flag; a `render.py` project cannot choose a codec either -- but `--dry-run` only guarantees nothing is written for writing tools: `probe` (read-only, `--dry-run` changes nothing) still runs ffprobe, `check`/`sync`/`multicam`/`scenes`/`cropdetect`/`report`/`silence`/`loudness`/`stabilize` still run their ffmpeg/ffprobe measurements (a dry-run plan rests on real numbers; they just don't write the final artifact), and `verify` accepts the flag but ignores it entirely. Exact per-tool semantics: `contract --json`'s `dry_run` field (or `docs/contract.md`).
|
|
4
4
|
|
|
5
5
|
## Time grammar (every time-taking flag, 1.9)
|
|
6
6
|
|
|
@@ -56,9 +56,9 @@ between tools.
|
|
|
56
56
|
```
|
|
57
57
|
probe.py INPUT... [--compact] [--field duration|video.fps|...]
|
|
58
58
|
```
|
|
59
|
-
JSON with `duration`, `video{codec,width,height,fps,pix_fmt,color_space,rotation,variable_frame_rate_suspected,hdr,hdr_signal,hdr_format}`
|
|
60
|
-
(`hdr_signal` is true only for a PQ / HLG transfer or Dolby Vision
|
|
61
|
-
BT.2020 primaries on an SDR transfer, which `hdr_format` names "BT.2020 SDR"
|
|
59
|
+
JSON with `duration`, `video{codec,width,height,fps,pix_fmt,color_space,rotation,variable_frame_rate_suspected,hdr,hdr_signal,bt2020_or_hdr,hdr_format}`
|
|
60
|
+
(`hdr`, and `hdr_signal` equal to it, is true only for a PQ / HLG transfer or Dolby Vision -- since 2.0;
|
|
61
|
+
`bt2020_or_hdr` also counts BT.2020 primaries on an SDR transfer, which `hdr_format` names "BT.2020 SDR"),
|
|
62
62
|
`audio{codec,channels,sample_rate}`. `--compact` gives one line per file.
|
|
63
63
|
|
|
64
64
|
### cut.py — cut / join segments
|
|
@@ -1408,10 +1408,10 @@ neither.
|
|
|
1408
1408
|
|
|
1409
1409
|
### proxy.py — low-bitrate proxy for analysis/preview
|
|
1410
1410
|
```
|
|
1411
|
-
proxy.py INPUT [--width W | --scale F] [--
|
|
1411
|
+
proxy.py INPUT [--width W | --scale F] [--quality N] [--fps N] [--no-audio] [-o OUT]
|
|
1412
1412
|
```
|
|
1413
1413
|
Not a delivery preset: resizes to `--width` (default 640) or by `--scale`
|
|
1414
|
-
factor, re-encodes at a proxy-grade `--
|
|
1414
|
+
factor, re-encodes at a proxy-grade `--quality` (default 30) with the fastest
|
|
1415
1415
|
x264/x265 preset, keeps the source's own dynamic range (HDR stays HDR;
|
|
1416
1416
|
run `color.py --to-sdr` first if SDR is wanted) and keeps a subtitle/data
|
|
1417
1417
|
track when the container can hold it (`dropped_non_av_streams`). Only executes the spec
|
|
@@ -53,7 +53,7 @@ for _stream in (sys.stdout, sys.stderr):
|
|
|
53
53
|
|
|
54
54
|
from _common.runner import (
|
|
55
55
|
add_common, apply_common, _check_existing_output, _check_no_overwrite_input, _check_output_path, child_args,
|
|
56
|
-
child_limit, _CHILDREN, _cleanup_partial_output, _cmdline, CODECS, Context,
|
|
56
|
+
child_limit, _CHILDREN, _cleanup_partial_output, _cmdline, CODECS, Context, DEFAULT_TIMEOUT,
|
|
57
57
|
_DRAWTEXT_PENDING, _DRAWTEXT_TMPDIR, _drawtext_tmpdir, dry_run_input_pending, _ENCODERS, _env_timeout,
|
|
58
58
|
ERROR_CODE, ERROR_RETRYABLE, EVEN_SCALE, _execute, _fail, ffmpeg_base, ffmpeg_encoders, _FFMPEG_VERSION,
|
|
59
59
|
ffmpeg_version, flush_drawtext_textfiles, INSTALL_HINTS, install_signal_handlers, _is_ffmpeg, _limit_for,
|
|
@@ -64,7 +64,7 @@ from _common.runner import (
|
|
|
64
64
|
)
|
|
65
65
|
from _common.emit import (
|
|
66
66
|
_brief, _BRIEF_DROP, _brief_summary, _CURRENT_CTX, die, emit, info, _plan_at_exit, _plan_inputs, _PLAN_STRIP,
|
|
67
|
-
PLAN_VERSION, print_json,
|
|
67
|
+
PLAN_VERSION, print_json, _set_current_ctx, write_plan
|
|
68
68
|
)
|
|
69
69
|
from _common.probe import (
|
|
70
70
|
analyze_levels, _aspect_string, _bit_depth, decode_pcm_mono, detect_scenes, detect_silences, fingerprint,
|
|
@@ -144,7 +144,7 @@ class _Facade(_types.ModuleType):
|
|
|
144
144
|
# own copy is the import-time binding and would read stale. Reads of these names go to the
|
|
145
145
|
# defining module (audit 14, P1-1); every other name is a plain re-export of the same object.
|
|
146
146
|
_LIVE = {
|
|
147
|
-
"_FFMPEG_VERSION": "runner", "
|
|
147
|
+
"_FFMPEG_VERSION": "runner", "_SIGNALS_INSTALLED": "runner",
|
|
148
148
|
"_DRAWTEXT_TMPDIR": "runner", "_ENCODERS": "runner", "_CURRENT_CTX": "emit",
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -181,7 +181,7 @@ __all__ = [
|
|
|
181
181
|
"brand_states_font", "_brief", "_BRIEF_DROP", "_brief_summary", "bt709_tag_args", "cfr_args", "_char_em",
|
|
182
182
|
"char_script", "_check_existing_output", "_check_no_overwrite_input", "_check_output_path", "child_args",
|
|
183
183
|
"child_limit", "_CHILDREN", "_cleanup_partial_output", "_cmdline", "CODECS", "color_hex", "_COLOR_TOKEN_RE",
|
|
184
|
-
"concat_list_line", "Context", "
|
|
184
|
+
"concat_list_line", "Context", "_CURRENT_CTX", "db_to_linear", "decode_pcm_mono", "description_block", "_evidence_rank", "fmt_chapter_time", "propose_chapters",
|
|
185
185
|
"filler_spans", "FILLER_WORDS", "FILLER_AMBIGUOUS", "FILLER_DISCOURSE_MARKERS",
|
|
186
186
|
"FILLER_MAX_WORD", "FILLER_MIN_GAP", "FILLER_PAD", "normalise_filler_token",
|
|
187
187
|
"beat_grid", "snap_points", "BEAT_MIN_CONFIDENCE", "BEAT_ONSET_K", "BEAT_OCTAVE_MARGIN",
|
|
@@ -205,13 +205,13 @@ __all__ = [
|
|
|
205
205
|
"_odd_dimension_retry", "_on_signal", "_output_failed", "_OutputLock", "pad_filters", "parse_time", "_pid_dead",
|
|
206
206
|
"place_output", "_plan_at_exit", "_plan_inputs", "_PLAN_STRIP", "PLAN_VERSION", "PREFERRED_FAMILIES",
|
|
207
207
|
"print_json", "probe", "PROBE_TIMEOUT", "_progress_line", "read_text_or_die", "refuse_output_is_input",
|
|
208
|
-
"_remember_output", "require_tool", "resolve_emoji_assets", "
|
|
208
|
+
"_remember_output", "require_tool", "resolve_emoji_assets", "rms_envelope", "run", "run_analysis",
|
|
209
209
|
"decode_gray_frames", "frame_flow", "label_shot_flow", "MOTION_GRID", "MOTION_SEARCH", "MOTION_STATIC_PX", "MOTION_PAN_SPREAD",
|
|
210
210
|
"_run_captured", "run_keeping_subtitles", "run_tool", "_run_with_progress", "_SCRIPT_FONT_CACHE",
|
|
211
211
|
"_script_font_entry", "script_font_for_text", "script_font_status", "_script_font_uncached", "_SCRIPT_RANGES",
|
|
212
212
|
"SCRIPTS", "_sdr_bt709", "_set_current_ctx", "_SHAPING_BUILD_CACHE", "SHAPING_SCRIPTS", "shell_quote",
|
|
213
213
|
"_SIGNALS_INSTALLED", "signed_time_arg", "_stage_existing_output", "STATE", "SVT_PRESET", "text_width_em",
|
|
214
|
-
"time_arg", "_timed_out", "_to_float", "_to_int", "_unwatch", "
|
|
214
|
+
"time_arg", "_timed_out", "_to_float", "_to_int", "_unwatch", "validate_color", "verify_output",
|
|
215
215
|
"video_args", "_VS15", "_VS16", "_watch", "WINDOWS_FONTS", "write_plan", "x264_args", "X264_PRESETS",
|
|
216
216
|
"_x264_raw", "_ZWJ",
|
|
217
217
|
"ASR_ENGINES", "ASR_INSTALL_HINT", "_asr_run", "die_no_engine", "parse_srt", "transcribe",
|
|
@@ -230,7 +230,7 @@ def encoder_args(codec: str, crf: int, preset: str, meta: Optional[Dict[str, Any
|
|
|
230
230
|
it video_args() does what it always did (x264 for SDR, x265 Main10 for HDR).
|
|
231
231
|
"""
|
|
232
232
|
v = (meta or {}).get("video") or {}
|
|
233
|
-
hdr = bool(v.get("
|
|
233
|
+
hdr = bool(v.get("bt2020_or_hdr"))
|
|
234
234
|
cs = v.get("color_space") or "bt2020nc"
|
|
235
235
|
prim = v.get("color_primaries") or "bt2020"
|
|
236
236
|
trc = v.get("color_transfer") or "arib-std-b67"
|
|
@@ -298,7 +298,7 @@ def video_args(meta: Optional[Dict[str, Any]], crf: int = 18, preset: str = "med
|
|
|
298
298
|
if STATE.codec:
|
|
299
299
|
return encoder_args(STATE.codec, crf, preset, meta)
|
|
300
300
|
v = (meta or {}).get("video") or {}
|
|
301
|
-
if not v.get("
|
|
301
|
+
if not v.get("bt2020_or_hdr"):
|
|
302
302
|
return x264_args(crf, preset)
|
|
303
303
|
cs = v.get("color_space") or "bt2020nc"
|
|
304
304
|
prim = v.get("color_primaries") or "bt2020"
|
package/scripts/_common/emit.py
CHANGED
|
@@ -89,8 +89,6 @@ def emit(output: Optional[str], *, ctx: "Optional[Context]" = None, **extra: Any
|
|
|
89
89
|
doc["verified"] = not ctx.dry_run and bool(steps) and all(s.get("ok") for s in steps)
|
|
90
90
|
doc["verification"] = steps
|
|
91
91
|
doc.update(extra)
|
|
92
|
-
if os.environ.get("FFMPEG_SKILL_RESULT_V2", "") not in ("", "0"):
|
|
93
|
-
doc["result_v2"] = _result_v2(output, meta, dict(extra, verified=doc["verified"], verification=steps))
|
|
94
92
|
if ctx.plan:
|
|
95
93
|
doc["plan"] = write_plan(ctx.plan, output, extra, ctx=ctx)
|
|
96
94
|
print_json(_brief(doc, meta) if ctx.json_brief else doc)
|
|
@@ -101,8 +99,8 @@ def emit(output: Optional[str], *, ctx: "Optional[Context]" = None, **extra: Any
|
|
|
101
99
|
|
|
102
100
|
|
|
103
101
|
# Keys the brief document replaces or drops: the full probe (summarised), the command lines
|
|
104
|
-
# (counted)
|
|
105
|
-
_BRIEF_DROP = ("probe", "commands", "verification"
|
|
102
|
+
# (counted) and the per-step verification list (its verdict stays as `verified`).
|
|
103
|
+
_BRIEF_DROP = ("probe", "commands", "verification")
|
|
106
104
|
|
|
107
105
|
|
|
108
106
|
def _brief_summary(meta: Dict[str, Any], extra: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -245,39 +243,6 @@ def write_plan(path: str, output: Optional[str], extra: Dict[str, Any], ctx: "Op
|
|
|
245
243
|
return path
|
|
246
244
|
|
|
247
245
|
|
|
248
|
-
_V2_HANDLED = ("result", "measured", "notes", "dropped_non_av_streams", "verified", "verification")
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def _result_v2(output: Optional[str], meta: Dict[str, Any], extra: Dict[str, Any]) -> Dict[str, Any]:
|
|
252
|
-
"""The 2.0 success-document shape, previewed in 1.x as a parallel `result_v2` key when
|
|
253
|
-
FFMPEG_SKILL_RESULT_V2=1 (issue #189 B). Every tool gets the same six slots: `output`,
|
|
254
|
-
`probe`, `commands`, `metrics` (numbers a caller keys on: loudness's `result`/`measured`
|
|
255
|
-
dicts flattened, plus every top-level numeric extra such as `expected_duration` or
|
|
256
|
-
`offset_seconds`), `notes` (free text), `dropped` (what did not make it into the output),
|
|
257
|
-
and `details` (the tool's remaining extras, unchanged). The 1.x keys stay where they are;
|
|
258
|
-
this key is additive and its shape is what 2.0 promotes to the top level."""
|
|
259
|
-
metrics: Dict[str, Any] = {}
|
|
260
|
-
for key in ("measured", "result"):
|
|
261
|
-
if isinstance(extra.get(key), dict):
|
|
262
|
-
metrics.update(extra[key])
|
|
263
|
-
for key, value in extra.items():
|
|
264
|
-
if key not in _V2_HANDLED and isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
265
|
-
metrics[key] = value
|
|
266
|
-
notes = extra.get("notes")
|
|
267
|
-
return {
|
|
268
|
-
"schema": 2,
|
|
269
|
-
"output": output,
|
|
270
|
-
"probe": meta or None,
|
|
271
|
-
"commands": list(STATE.commands),
|
|
272
|
-
"metrics": metrics,
|
|
273
|
-
"notes": list(notes) if isinstance(notes, (list, tuple)) else ([notes] if notes else []),
|
|
274
|
-
"dropped": {"non_av_streams": bool(extra.get("dropped_non_av_streams", False))},
|
|
275
|
-
"verified": bool(extra.get("verified", False)),
|
|
276
|
-
"verification": list(extra.get("verification") or []),
|
|
277
|
-
"details": {k: v for k, v in extra.items() if k not in _V2_HANDLED and k not in metrics},
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
246
|
def print_json(obj: Any) -> None:
|
|
282
247
|
sys.stdout.write(json.dumps(obj, indent=2, ensure_ascii=False) + "\n")
|
|
283
248
|
|
package/scripts/_common/probe.py
CHANGED
|
@@ -149,7 +149,7 @@ def probe(path: str, role: str = "input") -> Dict[str, Any]:
|
|
|
149
149
|
# sites are now guarded to treat 0 as "unknown" and fall back sanely instead of dividing
|
|
150
150
|
# by it, so the stub can finally report the honest, unknown value.
|
|
151
151
|
return {"file": path, "dry_run": True, "format": None, "duration": 0.0, "size_bytes": 0, "bitrate": None,
|
|
152
|
-
"video": {"codec": None, "width": 0, "height": 0, "fps": 0.0, "pix_fmt": None, "hdr": False,
|
|
152
|
+
"video": {"codec": None, "width": 0, "height": 0, "fps": 0.0, "pix_fmt": None, "hdr": False, "bt2020_or_hdr": False,
|
|
153
153
|
"color_transfer": None, "color_primaries": None, "rotation": 0, "variable_frame_rate_suspected": False},
|
|
154
154
|
"audio": {"codec": None, "channels": 0, "sample_rate": 0}, "subtitle_streams": 0, "data_streams": 0}
|
|
155
155
|
die(f"input not found: {path}")
|
|
@@ -250,6 +250,7 @@ def probe(path: str, role: str = "input") -> Dict[str, Any]:
|
|
|
250
250
|
dovi = {"profile": sd.get("dv_profile"), "level": sd.get("dv_level"), "bl_compatibility_id": sd.get("dv_bl_signal_compatibility_id")}
|
|
251
251
|
if dovi: # a Dolby Vision stream is HDR even when its base layer tags are missing
|
|
252
252
|
hdr = True
|
|
253
|
+
hdr_signal = trc in ("smpte2084", "arib-std-b67") or bool(dovi)
|
|
253
254
|
out["video"] = {
|
|
254
255
|
"codec": video.get("codec_name"),
|
|
255
256
|
"profile": video.get("profile"),
|
|
@@ -262,11 +263,14 @@ def probe(path: str, role: str = "input") -> Dict[str, Any]:
|
|
|
262
263
|
"variable_frame_rate_suspected": vfr,
|
|
263
264
|
"pix_fmt": video.get("pix_fmt"),
|
|
264
265
|
"bit_depth": _bit_depth(pix),
|
|
265
|
-
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
|
|
266
|
+
# 2.0: `hdr` is true only for a PQ / HLG transfer or Dolby Vision, i.e. a genuinely HDR
|
|
267
|
+
# signal (1.x also counted BT.2020 primaries on an SDR transfer). `hdr_signal` (1.9) is
|
|
268
|
+
# kept, equal to it, for callers that moved to it. `bt2020_or_hdr` is the 1.x meaning,
|
|
269
|
+
# which every editing tool still routes on: a BT.2020 SDR source goes through the
|
|
270
|
+
# 10-bit path with its own tags, since x264 with BT.709 tags would shift its colours.
|
|
271
|
+
"hdr": hdr_signal,
|
|
272
|
+
"hdr_signal": hdr_signal,
|
|
273
|
+
"bt2020_or_hdr": hdr,
|
|
270
274
|
"hdr_format": (("Dolby Vision %s" % (("profile %s" % dovi["profile"]) if dovi and dovi.get("profile") is not None else "")).strip() if dovi else
|
|
271
275
|
"HDR10/PQ" if trc == "smpte2084" else "HLG" if trc == "arib-std-b67" else "BT.2020 SDR" if hdr else None),
|
|
272
276
|
"dolby_vision": dovi,
|