ffmpeg-skill 2.13.0 → 2.15.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 +2 -2
- package/bin/install.js +15 -2
- package/docs/contract.md +10 -4
- package/package.json +1 -1
- package/references/gotchas.md +2 -1
- package/references/scripts.md +11 -1
- package/scripts/_contract.py +24 -3
- package/scripts/render.py +46 -12
package/README.md
CHANGED
|
@@ -420,7 +420,7 @@ The encode path a tool will actually take is Apple VideoToolbox, for h264, hevc
|
|
|
420
420
|
|
|
421
421
|
## FFmpeg compatibility
|
|
422
422
|
|
|
423
|
-
The tools need FFmpeg 5.0 or later and Python 3.9 or later (standard library only)
|
|
423
|
+
The tools need FFmpeg 5.0 or later and Python 3.9 or later (standard library only). FFmpeg 6.1+ is recommended: FFmpeg 5.x still works but is deprecated since 2.14.0, `doctor` and the `npx` installer warn on it, and 3.0 requires 6.1. Python 3.10+ is recommended too: 3.9 still works but is deprecated since 2.10.9 (CPython 3.9 is end of life), `doctor` and the `npx` installer warn on it, and 3.0 drops it. What CI actually exercises on every pull request is FFmpeg 5.1.1 (static build), 6.1 (Ubuntu apt), 7.1 (Debian trixie apt), 8.x (macOS Homebrew) and 9.x (Windows gyan.dev), on Python 3.9 and 3.13 (the two ends of the supported range). The capability parser has been run against the listings of these builds:
|
|
424
424
|
|
|
425
425
|
| FFmpeg | `-filters` row layout | Source |
|
|
426
426
|
|---|---|---|
|
|
@@ -527,7 +527,7 @@ FFmpeg itself:
|
|
|
527
527
|
|
|
528
528
|
## Requirements
|
|
529
529
|
|
|
530
|
-
- FFmpeg 5.0
|
|
530
|
+
- FFmpeg 5.0+ (FFmpeg 5.x is deprecated since 2.14.0 and 3.0 requires 6.1; 6.1+ recommended). 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.
|
|
531
531
|
- Python 3.10+ recommended, standard library only. 3.9 still works but is deprecated (end of life upstream; support ends in 3.0)
|
|
532
532
|
- Node 16+ only for the `npx` installer
|
|
533
533
|
|
package/bin/install.js
CHANGED
|
@@ -55,6 +55,16 @@ function pythonDeprecation(version) {
|
|
|
55
55
|
return version === '3.9' ? PY39_DEPRECATION : null;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// 2.14.0 deprecates FFmpeg 5.x the same way. The same line as scripts/_contract.py's
|
|
59
|
+
// FFMPEG5_DEPRECATION (a test keeps them equal).
|
|
60
|
+
const FFMPEG5_DEPRECATION = 'warning: FFmpeg 5.x is deprecated since ffmpeg-skill 2.14.0 (the 5.x branches date from 2022; Ubuntu 24.04 ships 6.1); it still works in 2.x and support ends in 3.0.0: use FFmpeg 6.1+.';
|
|
61
|
+
|
|
62
|
+
// The line for `ffmpeg -version`'s first line, or null (not 5.x, or not a release number).
|
|
63
|
+
function ffmpegDeprecation(versionLine) {
|
|
64
|
+
const m = /^ffmpeg version n?(\d+)\.\d+/.exec(versionLine || '');
|
|
65
|
+
return m && m[1] === '5' ? FFMPEG5_DEPRECATION : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
function pythonCandidates(platform) {
|
|
59
69
|
return platform === 'win32' ? ['python3', 'python', 'py'] : ['python3'];
|
|
60
70
|
}
|
|
@@ -146,7 +156,10 @@ function checkFfmpeg() {
|
|
|
146
156
|
console.warn(' Windows: winget install Gyan.FFmpeg\n');
|
|
147
157
|
return false;
|
|
148
158
|
}
|
|
149
|
-
|
|
159
|
+
const first = r.stdout.split('\n')[0];
|
|
160
|
+
console.log(` found ${first}`);
|
|
161
|
+
const line = ffmpegDeprecation(first);
|
|
162
|
+
if (line) console.warn(`\n ${line}`);
|
|
150
163
|
return true;
|
|
151
164
|
}
|
|
152
165
|
|
|
@@ -211,4 +224,4 @@ if (require.main === module) {
|
|
|
211
224
|
main();
|
|
212
225
|
}
|
|
213
226
|
|
|
214
|
-
module.exports = { pythonCandidates, runWithPython, findPython, pythonDeprecation, warnDeprecatedPython, PY39_DEPRECATION };
|
|
227
|
+
module.exports = { pythonCandidates, runWithPython, findPython, pythonDeprecation, warnDeprecatedPython, PY39_DEPRECATION, ffmpegDeprecation, FFMPEG5_DEPRECATION };
|
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 (`2.
|
|
24
|
+
| `skill.version` | the npm / package.json version (`2.15.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`
|
|
@@ -88,6 +88,11 @@ in October 2025 and gets no security fixes, but raising the floor is a break, so
|
|
|
88
88
|
recommended: on 3.9, `doctor` and the `npx` installer print one stderr line,
|
|
89
89
|
`warning: Python 3.9 is deprecated since ffmpeg-skill 2.10.9 (...); it still works in 2.x and support ends in 3.0.0: use Python 3.10+.`
|
|
90
90
|
Like every deprecation warning it is stderr only: `doctor --json`'s document has no key for it.
|
|
91
|
+
Since 2.14.0 there is a second entry, **running on FFmpeg 5.x** (`where: behaviour`, `removed_in: 3.0.0`),
|
|
92
|
+
handled the same way: `requirements.ffmpeg` stays `>=5.0` and CI keeps its 5.1.1 job until 3.0, which
|
|
93
|
+
requires 6.1. When `ffmpeg -version` reads 5.x, `doctor` and the `npx` installer print one stderr line,
|
|
94
|
+
`warning: FFmpeg 5.x is deprecated since ffmpeg-skill 2.14.0 (...); it still works in 2.x and support ends in 3.0.0: use FFmpeg 6.1+.`
|
|
95
|
+
A version that is not a release number (a git snapshot) gets no line.
|
|
91
96
|
`tests/test_contract.py` refuses a package.json major bump that still lists an entry that major should remove, or that
|
|
92
97
|
comes before step 2's window has passed -- unless the waiver is written here, as 2.0.0's is.
|
|
93
98
|
|
|
@@ -123,12 +128,13 @@ JSON would have been rewritten for that. The flat keys, typed per tool in `outpu
|
|
|
123
128
|
```json
|
|
124
129
|
{
|
|
125
130
|
"contract_version": "1.0",
|
|
126
|
-
"deprecated": [{"what": "running on Python 3.9 (the requirements.python floor)", "since": "2.10.9", "replacement": "Python 3.10 or later; ...", "removed_in": "3.0.0", "where": "behaviour"}
|
|
131
|
+
"deprecated": [{"what": "running on Python 3.9 (the requirements.python floor)", "since": "2.10.9", "replacement": "Python 3.10 or later; ...", "removed_in": "3.0.0", "where": "behaviour"},
|
|
132
|
+
{"what": "running on FFmpeg 5.x (the requirements.ffmpeg floor)", "since": "2.14.0", "replacement": "FFmpeg 6.1 or later; ...", "removed_in": "3.0.0", "where": "behaviour"}],
|
|
127
133
|
"removed": [{"what": "...", "since": "1.10.0", "replacement": "...", "removed_in": "2.0.0", "where": "cli | json | mcp | behaviour"}],
|
|
128
|
-
"skill": {"id": "ffmpeg-skill", "version": "2.
|
|
134
|
+
"skill": {"id": "ffmpeg-skill", "version": "2.15.0", "execution_mode": "local", "kind": "execution",
|
|
129
135
|
"entrypoints": {"cli": "...", "mcp": "...", "contract": "...", "doctor": "..."},
|
|
130
136
|
"not_provided": ["AI reasoning", "decisions", "production plans", "project IR", "approvals", "network access", "transcription engine"]},
|
|
131
|
-
"requirements": {"python": ">=3.9 (standard library only; 3.9 deprecated since 2.10.9, removed in 3.0.0; 3.10+ recommended)", "ffmpeg": ">=5.0", "ffprobe": ">=5.0"},
|
|
137
|
+
"requirements": {"python": ">=3.9 (standard library only; 3.9 deprecated since 2.10.9, removed in 3.0.0; 3.10+ recommended)", "ffmpeg": ">=5.0 (5.x deprecated since 2.14.0, removed in 3.0.0; 6.1+ recommended)", "ffprobe": ">=5.0"},
|
|
132
138
|
"execution": {"shell": false, "arbitrary_executables": false, "network": false,
|
|
133
139
|
"model_downloads": {"when": "...", "in_process": {"faster-whisper": "..."},
|
|
134
140
|
"child_process": {"parakeet-mlx": "...", "openai-whisper": "..."},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ffmpeg-skill",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.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
|
@@ -301,7 +301,8 @@ the contact sheet (`--sheet`) before treating the picks as final.
|
|
|
301
301
|
Anything chained by hand through three re-encodes should be one `render.py`
|
|
302
302
|
project instead, so the plan is one file and the user can change one number.
|
|
303
303
|
Re-encodes use x264 `medium`; for long files add `--preset veryfast` to
|
|
304
|
-
intermediates and keep the default for the final export.
|
|
304
|
+
intermediates and keep the default for the final export. `render.py` does that by itself since
|
|
305
|
+
2.15 when its project ends in an encoding export (`--intermediate-preset medium` turns it off). On Apple Silicon,
|
|
305
306
|
`--hw` (or `FFMPEG_SKILL_HW=1` for the machine) moves h264/hevc/prores encodes to
|
|
306
307
|
VideoToolbox: 2–7× faster, 1.1–2.9× the bytes at the same quality (1.9–3.5× on HDR). The variable
|
|
307
308
|
leaves `export.py`'s delivery presets on the CPU, which is the usual split (GPU
|
package/references/scripts.md
CHANGED
|
@@ -717,7 +717,17 @@ render.py project.json [--fast] [--dry-run] [--stop-after STAGE] [--work DIR --k
|
|
|
717
717
|
render.py project.json --cache DIR [--from STAGE] # reuse identical stages from a previous run
|
|
718
718
|
render.py plan.json [--audit-log FILE] # execute a plan written by <tool> --plan plan.json
|
|
719
719
|
render.py project.json --export-timeline edit.fcpxml|edit.edl|edit.otio # the cut for an editor, nothing rendered
|
|
720
|
-
|
|
720
|
+
render.py project.json --intermediate-preset medium # the stages' own encoder preset instead of veryfast
|
|
721
|
+
```
|
|
722
|
+
`--intermediate-preset` (2.15, default `veryfast`): when the project has an export stage that
|
|
723
|
+
re-encodes (any preset but `copy`) and the run goes that far (no `--stop-after` before it), the
|
|
724
|
+
stages before it that encode with x264 or x265 (clips, fit, join, silence, captions, graphics,
|
|
725
|
+
overlays, audiogram; x265 on the HDR path) encode with this preset instead of `medium`. export.py re-encodes their output and its own
|
|
726
|
+
preset sets the delivered quality, so the earlier `medium` passes cost time the file does not
|
|
727
|
+
keep: a 30 s 1080p reels render took 41.7 s instead of 51.8 s on FFmpeg 8.0.1 (SSIM 0.997
|
|
728
|
+
between the two deliveries). `medium` restores the stages' default; `--fast` still overrides
|
|
729
|
+
everything; a stage given its own `--preset` keeps it. The result names it as
|
|
730
|
+
`intermediate_preset` (`null` when not applied).
|
|
721
731
|
`--export-timeline FILE` (2.1) writes the project's cut as an editor timeline instead of
|
|
722
732
|
rendering it: `.fcpxml` (FCPXML 1.10: Final Cut Pro, DaVinci Resolve), `.edl` (CMX 3600:
|
|
723
733
|
Premiere, Resolve, Avid) or `.otio` (OpenTimelineIO JSON: Resolve natively, others through
|
package/scripts/_contract.py
CHANGED
|
@@ -987,6 +987,20 @@ def python_deprecation(version_info: Optional[tuple] = None) -> Optional[str]:
|
|
|
987
987
|
return PY39_DEPRECATION if vi == (3, 9) else None
|
|
988
988
|
|
|
989
989
|
|
|
990
|
+
# 2.14.0 deprecates FFmpeg 5.x the same way (DEPRECATED below): 2.x keeps running on it and CI keeps
|
|
991
|
+
# its 5.1.1 job, and the line names 3.0.0 as the release that raises the floor to 6.1. bin/install.js
|
|
992
|
+
# prints it too (FFMPEG5_DEPRECATION there; a test keeps the two equal).
|
|
993
|
+
FFMPEG5_DEPRECATION = ("warning: FFmpeg 5.x is deprecated since ffmpeg-skill 2.14.0 (the 5.x branches date from 2022; "
|
|
994
|
+
"Ubuntu 24.04 ships 6.1); it still works in 2.x and support ends in 3.0.0: use FFmpeg 6.1+.")
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def ffmpeg_deprecation(version: Optional[str]) -> Optional[str]:
|
|
998
|
+
"""The deprecation line for this ffmpeg's version token, or None (not 5.x, or not a release
|
|
999
|
+
number: a git snapshot gets no line, as with the security advisory)."""
|
|
1000
|
+
v = _parse_ffmpeg_version(version)
|
|
1001
|
+
return FFMPEG5_DEPRECATION if v and v[0] == 5 else None
|
|
1002
|
+
|
|
1003
|
+
|
|
990
1004
|
def _exe_report() -> Dict[str, Any]:
|
|
991
1005
|
"""`ffmpeg_path` / `ffmpeg_source` / `ffprobe_path` / `ffprobe_source`: which binary the tools run
|
|
992
1006
|
and why ("path": what PATH resolves; "homebrew-ffmpeg-full": macOS, a lean Homebrew ffmpeg on
|
|
@@ -1549,6 +1563,12 @@ DEPRECATED: List[Dict[str, str]] = [
|
|
|
1549
1563
|
{"what": "running on Python 3.9 (the requirements.python floor)",
|
|
1550
1564
|
"since": "2.10.9", "removed_in": "3.0.0", "where": "behaviour",
|
|
1551
1565
|
"replacement": "Python 3.10 or later; on 3.9, doctor and the npx installer print a one-line warning"},
|
|
1566
|
+
# FFmpeg 5.x: the 5.x branches date from 2022 and keep version-specific workarounds alive.
|
|
1567
|
+
# 2.x keeps running on 5.0+ (CI still tests 5.1.1) and doctor / bin/install.js print
|
|
1568
|
+
# FFMPEG5_DEPRECATION; 3.0.0 raises requirements.ffmpeg to 6.1.
|
|
1569
|
+
{"what": "running on FFmpeg 5.x (the requirements.ffmpeg floor)",
|
|
1570
|
+
"since": "2.14.0", "removed_in": "3.0.0", "where": "behaviour",
|
|
1571
|
+
"replacement": "FFmpeg 6.1 or later; on 5.x, doctor and the npx installer print a one-line warning"},
|
|
1552
1572
|
]
|
|
1553
1573
|
|
|
1554
1574
|
# What 2.0.0 removed or changed, with the version that deprecated each: the "Removed" half of the
|
|
@@ -1787,7 +1807,7 @@ def build(detect: bool = True) -> Dict[str, Any]:
|
|
|
1787
1807
|
},
|
|
1788
1808
|
"not_provided": ["AI reasoning", "decisions", "production plans", "project IR", "approvals", "network access", "transcription engine"],
|
|
1789
1809
|
},
|
|
1790
|
-
"requirements": {"python": ">=3.9 (standard library only; 3.9 deprecated since 2.10.9, removed in 3.0.0; 3.10+ recommended)", "ffmpeg": ">=5.0", "ffprobe": ">=5.0", "node": ">=16 (npx installer only)"},
|
|
1810
|
+
"requirements": {"python": ">=3.9 (standard library only; 3.9 deprecated since 2.10.9, removed in 3.0.0; 3.10+ recommended)", "ffmpeg": ">=5.0 (5.x deprecated since 2.14.0, removed in 3.0.0; 6.1+ recommended)", "ffprobe": ">=5.0", "node": ">=16 (npx installer only)"},
|
|
1791
1811
|
"execution": {
|
|
1792
1812
|
"shell": False,
|
|
1793
1813
|
"arbitrary_executables": False,
|
|
@@ -1872,8 +1892,9 @@ def main() -> int:
|
|
|
1872
1892
|
# Advisories, never failures: on stderr in both modes, so `doctor --json`'s stdout stays one
|
|
1873
1893
|
# JSON document. The FFmpeg ones are also in the document (`ffmpeg_security.warnings`); the
|
|
1874
1894
|
# Python 3.9 deprecation is stderr-only, as every deprecation warning is (docs/contract.md
|
|
1875
|
-
# "Deprecation policy" step 1), and machine-readable in `contract --json`'s `deprecated
|
|
1876
|
-
|
|
1895
|
+
# "Deprecation policy" step 1), and machine-readable in `contract --json`'s `deprecated`;
|
|
1896
|
+
# so is the FFmpeg 5.x one.
|
|
1897
|
+
for line in [python_deprecation(), ffmpeg_deprecation(d.get("ffmpeg"))] + d["ffmpeg_security"]["warnings"]:
|
|
1877
1898
|
if line:
|
|
1878
1899
|
print(line, file=sys.stderr)
|
|
1879
1900
|
if args.json:
|
package/scripts/render.py
CHANGED
|
@@ -74,7 +74,7 @@ from typing import Any, Dict, List, Optional, Sequence
|
|
|
74
74
|
|
|
75
75
|
from export import PRESETS, PLATFORM_OF
|
|
76
76
|
from _platforms import PLATFORMS, caption_defaults, resolve as resolve_platform
|
|
77
|
-
from _common import STATE, add_common, aspect_ratio, brand_caption_style, load_brand, apply_common, child_args, die, emit, info, probe, require_tool, run, run_tool, place_output, refuse_output_is_input, _check_existing_output, _check_output_path, fingerprint, PLAN_VERSION, ffmpeg_version, ffmpeg_exe
|
|
77
|
+
from _common import STATE, add_common, aspect_ratio, brand_caption_style, load_brand, apply_common, child_args, die, emit, info, probe, require_tool, run, run_tool, place_output, refuse_output_is_input, _check_existing_output, _check_output_path, fingerprint, PLAN_VERSION, ffmpeg_version, ffmpeg_exe, X264_PRESETS
|
|
78
78
|
from _common.runner import HW_ENV, HW_FORCED_ENV, add_hw_orchestrator_args
|
|
79
79
|
from _common.emit import absorb_stage, audit, record_fallback, record_skip, refuse, warn
|
|
80
80
|
import subprocess
|
|
@@ -353,7 +353,8 @@ def render_pack(names: List[str], args) -> int:
|
|
|
353
353
|
argv = [str(HERE / "render.py"), args.input, "--template", name, "-o", dest_out]
|
|
354
354
|
for flag, value in (("--cues", args.cues), ("--srt", args.srt), ("--logo", args.logo),
|
|
355
355
|
("--title", args.title), ("--brand", args.brand), ("--fit", args.fit),
|
|
356
|
-
("--chapters", args.chapters), ("--image", args.image)
|
|
356
|
+
("--chapters", args.chapters), ("--image", args.image),
|
|
357
|
+
("--intermediate-preset", args.intermediate_preset)):
|
|
357
358
|
if value:
|
|
358
359
|
argv += [flag, str(value)]
|
|
359
360
|
info(f"→ pack: {name}")
|
|
@@ -545,6 +546,13 @@ def sh(script: str, *argv: Any, extra: List[str] = None, stage: str = None) -> s
|
|
|
545
546
|
cache instead of re-encoded. `stages_done` is unchanged either way: a cached stage is still
|
|
546
547
|
a stage that happened.
|
|
547
548
|
"""
|
|
549
|
+
if (INTERMEDIATE["preset"] and script in _X264_STAGES
|
|
550
|
+
and "--preset" not in [str(a) for a in argv] + (extra or [])):
|
|
551
|
+
extra = (extra or []) + ["--preset", INTERMEDIATE["preset"]]
|
|
552
|
+
if not INTERMEDIATE["applied"]:
|
|
553
|
+
info(f"intermediate stages: preset {INTERMEDIATE['preset']} (the export re-encodes them; "
|
|
554
|
+
"--intermediate-preset medium for the stages' own default)")
|
|
555
|
+
INTERMEDIATE["applied"] = True
|
|
548
556
|
full = [str(a) for a in argv] + (extra or [])
|
|
549
557
|
dest = full[full.index("-o") + 1] if "-o" in full[:-1] else None
|
|
550
558
|
key = None
|
|
@@ -627,6 +635,21 @@ def _fresh_cache() -> "Dict[str, Any]":
|
|
|
627
635
|
# inherit each other's hit/miss lists.
|
|
628
636
|
CACHE: Dict[str, Any] = _fresh_cache()
|
|
629
637
|
|
|
638
|
+
# The stages whose x264 encode (x265 on the HDR path, which takes the same --preset) is an
|
|
639
|
+
# intermediate when an encoding export follows: export.py
|
|
640
|
+
# re-encodes their output, so their own `medium` pass bought nothing the delivery keeps. Measured
|
|
641
|
+
# on a 30 s 1080p reels render (4 cores): fit 18 s and captions 27 s of a 70 s run at `medium`;
|
|
642
|
+
# `veryfast` at the same CRF encodes in about half the time (SSIM 0.997 against `medium`) and
|
|
643
|
+
# the export, still `medium`, sets the delivered quality. `--intermediate-preset medium` restores it.
|
|
644
|
+
_X264_STAGES = frozenset({"cut.py", "fit.py", "join.py", "silence.py", "caption.py", "graphics.py", "overlay.py", "waveform.py"})
|
|
645
|
+
INTERMEDIATE: Dict[str, Any] = {"preset": None, "applied": False}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _intermediate_used() -> Optional[str]:
|
|
649
|
+
"""The preset `intermediate_preset` reports: only once a stage actually received it (a project
|
|
650
|
+
whose only encode is the export reports null)."""
|
|
651
|
+
return INTERMEDIATE["preset"] if INTERMEDIATE["applied"] else None
|
|
652
|
+
|
|
630
653
|
|
|
631
654
|
def _content_hash(path: str) -> str:
|
|
632
655
|
"""The same cheap content fingerprint batch.py caches on: name, size, mtime, first and last
|
|
@@ -1043,6 +1066,10 @@ def main() -> int:
|
|
|
1043
1066
|
help="append the audit records of this run (a refused plan, a lock taken over, ...) to FILE as JSON lines; "
|
|
1044
1067
|
"default for a refused plan: <plan>.audit.jsonl next to it. A record that cannot be written is a "
|
|
1045
1068
|
"warning, never a failure")
|
|
1069
|
+
ap.add_argument("--intermediate-preset", default="veryfast", choices=X264_PRESETS,
|
|
1070
|
+
help="x264/x265 preset of the stages before an encoding export (default veryfast: the export "
|
|
1071
|
+
"re-encodes them, and its own preset sets the delivered quality); medium = the stages' own default. "
|
|
1072
|
+
"Not used without an export stage, with --stop-after before it, with --fast, or for a copy export")
|
|
1046
1073
|
ap.add_argument("--stop-after", choices=["clips", "join", "silence", "fit", "captions", "graphics", "overlays", "audio", "loudness", "export"], help="stop after this stage (for iterating)")
|
|
1047
1074
|
tpl = ap.add_argument_group("delivery templates (one command per destination)")
|
|
1048
1075
|
tpl.add_argument("--template", metavar="NAME", help="render INPUT with a shipped template: " + ", ".join(template_names())
|
|
@@ -1175,6 +1202,13 @@ def main() -> int:
|
|
|
1175
1202
|
# unchanged -- it never gets a --platform it did not ask for.
|
|
1176
1203
|
dest = str(((proj.get("check") or {}).get("platform") or "")) if proj.get("template") else ""
|
|
1177
1204
|
platform_args: List[str] = ["--platform", dest] if dest in PLATFORMS and PLATFORMS[dest].get("frame") else []
|
|
1205
|
+
ex_cfg = proj.get("export")
|
|
1206
|
+
# a malformed "export" is refused by the export stage with its own message; here it only means
|
|
1207
|
+
# "no encoding export", never a traceback before that refusal
|
|
1208
|
+
ex_preset = str(ex_cfg.get("preset") or "") if isinstance(ex_cfg, dict) else ""
|
|
1209
|
+
if (ex_preset and "copy" not in PRESETS.get(ex_preset, {}).get("video", [])
|
|
1210
|
+
and args.stop_after in (None, "export") and args.intermediate_preset != "medium" and not STATE.fast):
|
|
1211
|
+
INTERMEDIATE["preset"] = args.intermediate_preset
|
|
1178
1212
|
stages_done: List[str] = []
|
|
1179
1213
|
# what the caption stage reported (wrapped/split counts and the fit-size keys), so a caller
|
|
1180
1214
|
# reading render.py's JSON can see whether the type was shrunk to fit or a cue was split.
|
|
@@ -1306,7 +1340,7 @@ def main() -> int:
|
|
|
1306
1340
|
stages_done.append("audiogram")
|
|
1307
1341
|
|
|
1308
1342
|
if args.stop_after == "clips":
|
|
1309
|
-
emit(current, stages=stages_done)
|
|
1343
|
+
emit(current, stages=stages_done, intermediate_preset=_intermediate_used())
|
|
1310
1344
|
return 0
|
|
1311
1345
|
|
|
1312
1346
|
# ---- join
|
|
@@ -1322,7 +1356,7 @@ def main() -> int:
|
|
|
1322
1356
|
sh("join.py", *argv, stage="join")
|
|
1323
1357
|
stages_done.append("join")
|
|
1324
1358
|
if args.stop_after == "join":
|
|
1325
|
-
emit(current, stages=stages_done)
|
|
1359
|
+
emit(current, stages=stages_done, intermediate_preset=_intermediate_used())
|
|
1326
1360
|
return 0
|
|
1327
1361
|
|
|
1328
1362
|
# ---- silence
|
|
@@ -1337,7 +1371,7 @@ def main() -> int:
|
|
|
1337
1371
|
current = nxt
|
|
1338
1372
|
stages_done.append("silence")
|
|
1339
1373
|
if args.stop_after == "silence":
|
|
1340
|
-
emit(current, stages=stages_done)
|
|
1374
|
+
emit(current, stages=stages_done, intermediate_preset=_intermediate_used())
|
|
1341
1375
|
return 0
|
|
1342
1376
|
|
|
1343
1377
|
# ---- fit (duration and/or frame)
|
|
@@ -1374,7 +1408,7 @@ def main() -> int:
|
|
|
1374
1408
|
current = nxt
|
|
1375
1409
|
stages_done.append("fit")
|
|
1376
1410
|
if args.stop_after == "fit":
|
|
1377
|
-
emit(current, stages=stages_done)
|
|
1411
|
+
emit(current, stages=stages_done, intermediate_preset=_intermediate_used())
|
|
1378
1412
|
return 0
|
|
1379
1413
|
|
|
1380
1414
|
# ---- captions
|
|
@@ -1403,7 +1437,7 @@ def main() -> int:
|
|
|
1403
1437
|
current = nxt
|
|
1404
1438
|
stages_done.append("captions")
|
|
1405
1439
|
if args.stop_after == "captions":
|
|
1406
|
-
emit(current, stages=stages_done)
|
|
1440
|
+
emit(current, stages=stages_done, intermediate_preset=_intermediate_used())
|
|
1407
1441
|
return 0
|
|
1408
1442
|
|
|
1409
1443
|
# ---- graphics
|
|
@@ -1422,7 +1456,7 @@ def main() -> int:
|
|
|
1422
1456
|
if "graphics" not in stages_done:
|
|
1423
1457
|
stages_done.append("graphics")
|
|
1424
1458
|
if args.stop_after == "graphics":
|
|
1425
|
-
emit(current, stages=stages_done)
|
|
1459
|
+
emit(current, stages=stages_done, intermediate_preset=_intermediate_used())
|
|
1426
1460
|
return 0
|
|
1427
1461
|
|
|
1428
1462
|
# ---- overlays
|
|
@@ -1453,7 +1487,7 @@ def main() -> int:
|
|
|
1453
1487
|
if "overlays" not in stages_done:
|
|
1454
1488
|
stages_done.append("overlays")
|
|
1455
1489
|
if args.stop_after == "overlays":
|
|
1456
|
-
emit(current, stages=stages_done)
|
|
1490
|
+
emit(current, stages=stages_done, intermediate_preset=_intermediate_used())
|
|
1457
1491
|
return 0
|
|
1458
1492
|
|
|
1459
1493
|
# ---- audio
|
|
@@ -1490,7 +1524,7 @@ def main() -> int:
|
|
|
1490
1524
|
current = nxt
|
|
1491
1525
|
stages_done.append("audio")
|
|
1492
1526
|
if args.stop_after == "audio":
|
|
1493
|
-
emit(current, stages=stages_done)
|
|
1527
|
+
emit(current, stages=stages_done, intermediate_preset=_intermediate_used())
|
|
1494
1528
|
return 0
|
|
1495
1529
|
|
|
1496
1530
|
# ---- loudness
|
|
@@ -1506,7 +1540,7 @@ def main() -> int:
|
|
|
1506
1540
|
current = nxt
|
|
1507
1541
|
stages_done.append("loudness")
|
|
1508
1542
|
if args.stop_after == "loudness":
|
|
1509
|
-
emit(current, stages=stages_done)
|
|
1543
|
+
emit(current, stages=stages_done, intermediate_preset=_intermediate_used())
|
|
1510
1544
|
return 0
|
|
1511
1545
|
|
|
1512
1546
|
# ---- export
|
|
@@ -1612,7 +1646,7 @@ def main() -> int:
|
|
|
1612
1646
|
snap_report = dict(snap_reports[0])
|
|
1613
1647
|
snap_report["clips"] = snap_reports
|
|
1614
1648
|
emit(output, stages=stages_done, check=check_result, snap=snap_report, cache=cache_report,
|
|
1615
|
-
caption=caption_report,
|
|
1649
|
+
caption=caption_report, intermediate_preset=_intermediate_used(),
|
|
1616
1650
|
verification=[{"step": "check", "ok": True, "platform": ck.get("platform")}] if check_result else [])
|
|
1617
1651
|
return 0
|
|
1618
1652
|
|