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
|
@@ -195,45 +195,20 @@ def add_common(ap: "argparse.ArgumentParser", codec: bool = True) -> None:
|
|
|
195
195
|
g.add_argument("--timeout", type=float, default=None, metavar="SECONDS",
|
|
196
196
|
help=f"kill an ffmpeg run past this many seconds, kind=timeout (default {DEFAULT_TIMEOUT:.0f}; 0 = no limit)")
|
|
197
197
|
g.add_argument("--overwrite", action="store_true",
|
|
198
|
-
help="allow replacing an existing output (
|
|
198
|
+
help="allow replacing an existing output (refused without it)")
|
|
199
199
|
g.add_argument("--plan", metavar="FILE",
|
|
200
200
|
help="write the dry run as a plan (inputs fingerprinted, commands, expected output, verify steps) that render.py FILE executes later; implies --dry-run")
|
|
201
|
-
if codec and "
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
crf = ap._option_string_actions["--crf"]
|
|
206
|
-
# The flag's own default moves aside so apply_common() can tell an explicit --crf (in any
|
|
207
|
-
# spelling argparse accepts, including the --cr / --c abbreviations) from the default;
|
|
208
|
-
# apply_common() puts _CRF_DEFAULT back when the flag was absent.
|
|
209
|
-
global _CRF_DEFAULT
|
|
210
|
-
_CRF_DEFAULT = crf.default
|
|
211
|
-
crf.deprecated_default = crf.default # the schema still advertises it (_contract._json_type)
|
|
212
|
-
crf.default = None
|
|
213
|
-
if "deprecated" not in (crf.help or ""):
|
|
214
|
-
# the nine tools that declare --crf with no help string used to fall through this and
|
|
215
|
-
# never show the mark at all (review 9)
|
|
216
|
-
crf.help = (crf.help or "x264 CRF when re-encoding (default 18)") + " (deprecated: use --quality)"
|
|
217
|
-
# only the tools that re-encode (they declare --crf before add_common): one encoder choice
|
|
218
|
-
# resolved in video_args(), the 2.0 encoder abstraction pre-shipped in 1.8 (docs/roadmap.md)
|
|
201
|
+
if codec and "crf" in ap._defaults:
|
|
202
|
+
# A tool that re-encodes declares its CRF default with set_defaults(crf=N) before calling
|
|
203
|
+
# this; --codec/--quality are its only encoder flags, resolved in video_args(). --crf was
|
|
204
|
+
# the 1.x spelling of --quality (an alias from 1.8, deprecated in 1.10, removed in 2.0).
|
|
219
205
|
g.add_argument("--codec", choices=CODECS, default=None,
|
|
220
206
|
help="video encoder for the re-encode: h264 (x264, the default for SDR), hevc (x265, the default for HDR), av1 (SVT-AV1 or libaom), prores (422 HQ, needs a .mov/.mkv output); HDR sources keep their colour on hevc/av1/prores")
|
|
221
|
-
g.add_argument("--quality", type=int, default=
|
|
222
|
-
help="encoder quality on the CRF scale (lower = better; 18 visually lossless for x264/x265, up to 63 for av1);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
# The declared default of a deprecated --crf, parked by add_common() (one parser per process).
|
|
226
|
-
_CRF_DEFAULT: Optional[int] = None
|
|
207
|
+
g.add_argument("--quality", type=int, default=ap._defaults["crf"], metavar="N",
|
|
208
|
+
help="encoder quality on the CRF scale (default %(default)s; lower = better; 18 visually lossless for x264/x265, up to 63 for av1); ignored by prores")
|
|
227
209
|
|
|
228
210
|
|
|
229
211
|
def apply_common(args: "argparse.Namespace") -> None:
|
|
230
|
-
# Was --crf typed? add_common() parked the flag's default (None in its place) on every tool
|
|
231
|
-
# whose --crf is deprecated, i.e. the ones that also have --quality; export.py's --crf is not
|
|
232
|
-
# an alias and keeps its own default. Scanning sys.argv for "--crf" instead missed the unique
|
|
233
|
-
# prefixes argparse accepts (--cr, --c) and never ran for batch.py's recipe steps (review 9).
|
|
234
|
-
crf_explicit = hasattr(args, "quality") and getattr(args, "crf", None) is not None
|
|
235
|
-
if hasattr(args, "quality") and hasattr(args, "crf") and args.crf is None:
|
|
236
|
-
args.crf = _CRF_DEFAULT
|
|
237
212
|
STATE.plan = getattr(args, "plan", None) or None
|
|
238
213
|
STATE.dry_run = bool(getattr(args, "dry_run", False)) or bool(STATE.plan)
|
|
239
214
|
if STATE.plan:
|
|
@@ -269,12 +244,7 @@ def apply_common(args: "argparse.Namespace") -> None:
|
|
|
269
244
|
if os.path.splitext(str(out))[1].lower() not in (".mov", ".mkv"):
|
|
270
245
|
die(f"--codec prores needs a .mov (or .mkv) output; {os.path.basename(str(out))} cannot hold ProRes",
|
|
271
246
|
hint="give -o NAME.mov")
|
|
272
|
-
crf = getattr(args, "crf", None)
|
|
273
|
-
# The warning the deprecation policy asks for, only when the caller typed the flag (see
|
|
274
|
-
# crf_explicit above). export.py has no --quality (its preset chooses the encoder), so its
|
|
275
|
-
# --crf is not an alias and is not deprecated: warn only where --quality exists.
|
|
276
|
-
if crf is not None and crf_explicit:
|
|
277
|
-
info("warning: --crf is deprecated since 1.10.0; use --quality N (the same CRF scale, codec-neutral). --crf is removed in 2.0.")
|
|
247
|
+
crf = getattr(args, "crf", None) # export.py's own --crf (its preset chooses the encoder)
|
|
278
248
|
top = 63 if STATE.codec == "av1" else 51
|
|
279
249
|
if crf is not None and not 0 <= int(crf) <= top:
|
|
280
250
|
die(f"--crf must be between 0 and {top} ({'SVT-AV1' if STATE.codec == 'av1' else 'x264/x265'} scale; 18 is visually lossless), got {crf}")
|
|
@@ -565,9 +535,9 @@ def _odd_dimension_retry(cmd: List[str], stderr: str) -> Optional[List[str]]:
|
|
|
565
535
|
def _check_existing_output(cmd: Sequence[str]) -> None:
|
|
566
536
|
"""An output path that already exists is someone's file: a previous result, a source the
|
|
567
537
|
agent mis-named, a deliverable from another run. ffmpeg's -y (which every command carries so
|
|
568
|
-
a run never blocks on a y/N prompt) would replace it without a word.
|
|
569
|
-
|
|
570
|
-
|
|
538
|
+
a run never blocks on a y/N prompt) would replace it without a word. Since 2.0 it is refused
|
|
539
|
+
(kind: input, before anything runs, dry runs included) unless --overwrite gives explicit
|
|
540
|
+
consent; 1.x only warned, and FFMPEG_SKILL_NO_OVERWRITE=1 was the opt-in to this. Paths this
|
|
571
541
|
process wrote itself (a two-pass tool, a copy-then-re-encode fallback) are never in question."""
|
|
572
542
|
output = cmd[-1]
|
|
573
543
|
if output in ("-",) or output.startswith("pipe:") or output.startswith("-"):
|
|
@@ -586,10 +556,8 @@ def _check_existing_output(cmd: Sequence[str]) -> None:
|
|
|
586
556
|
pass
|
|
587
557
|
if STATE.overwrite:
|
|
588
558
|
return
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
info(f"warning: {output} already exists and will be overwritten (pass --overwrite to confirm; "
|
|
592
|
-
f"from 2.0 an existing output is refused without it, FFMPEG_SKILL_NO_OVERWRITE=1 enables that now)")
|
|
559
|
+
die(f"refusing to overwrite existing output {output!r}: pass --overwrite to replace it, or choose another -o path",
|
|
560
|
+
kind="input", hint="pass --overwrite, or choose another -o path")
|
|
593
561
|
|
|
594
562
|
|
|
595
563
|
def _remember_output(cmd: Sequence[str]) -> None:
|
package/scripts/_contract.py
CHANGED
|
@@ -351,9 +351,7 @@ def input_schema(parser: argparse.ArgumentParser) -> Dict[str, Any]:
|
|
|
351
351
|
if isinstance(action, argparse._HelpAction):
|
|
352
352
|
continue
|
|
353
353
|
prop: Dict[str, Any] = _json_type(action)
|
|
354
|
-
|
|
355
|
-
# distinguishable from the default); the schema still advertises the real one
|
|
356
|
-
default = getattr(action, "deprecated_default", action.default)
|
|
354
|
+
default = action.default
|
|
357
355
|
if action.help and action.help != argparse.SUPPRESS:
|
|
358
356
|
prop["description"] = action.help % {"default": default} if "%(default)" in action.help else action.help
|
|
359
357
|
if action.choices:
|
|
@@ -1087,34 +1085,39 @@ def tool_spec(name: str, version: str) -> Dict[str, Any]:
|
|
|
1087
1085
|
# tools that print JSON without --json (probe) or whose primary output is a file path (look): the transport
|
|
1088
1086
|
# does not append --json for them (stated in invocation.structured.argument_mapping.json)
|
|
1089
1087
|
# ----------------------------------------------------------------------------- deprecations
|
|
1090
|
-
# What
|
|
1088
|
+
# What the next major removes, announced here per docs/contract.md's three-step deprecation policy:
|
|
1091
1089
|
# step 1 (this list, --help text and the CHANGELOG) in a minor, step 2 keeps it working, step 3
|
|
1092
|
-
# removes it in the major. `where` says which surface a caller sees it on.
|
|
1093
|
-
#
|
|
1094
|
-
DEPRECATED: List[Dict[str, str]] = [
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1090
|
+
# removes it in the major. `where` says which surface a caller sees it on. Empty at 2.0.0: all
|
|
1091
|
+
# five 1.10.0 deprecations were settled by it (REMOVED below).
|
|
1092
|
+
DEPRECATED: List[Dict[str, str]] = []
|
|
1093
|
+
|
|
1094
|
+
# What 2.0.0 removed or changed, with the version that deprecated each: the "Removed" half of the
|
|
1095
|
+
# policy's step 3, kept in the contract so a caller migrating from 1.x can diff it instead of reading
|
|
1096
|
+
# a CHANGELOG. docs/contract.md's "What 2.0 changed" table is written from this list.
|
|
1097
|
+
REMOVED: List[Dict[str, str]] = [
|
|
1098
1098
|
{"what": "--crf as an alias of --quality on every re-encoding tool that takes --quality (export.py keeps --crf: its preset chooses the encoder)",
|
|
1099
|
-
"since": "1.10.0", "
|
|
1100
|
-
"
|
|
1099
|
+
"since": "1.10.0", "removed_in": "2.0.0", "where": "cli",
|
|
1100
|
+
"replacement": "--quality N (the same CRF scale, codec-neutral; each tool's old --crf default is its --quality default)"},
|
|
1101
1101
|
{"what": "json and progress in the MCP inputSchema (they are CLI transport flags, not tool arguments)",
|
|
1102
|
-
"since": "1.10.0", "
|
|
1103
|
-
"
|
|
1102
|
+
"since": "1.10.0", "removed_in": "2.0.0", "where": "mcp",
|
|
1103
|
+
"replacement": "nothing: the MCP transport sets them itself; a client that still sends them is not refused"},
|
|
1104
1104
|
{"what": "probe's hdr meaning BT.2020 primaries or a PQ/HLG transfer",
|
|
1105
|
-
"since": "1.10.0", "
|
|
1106
|
-
"
|
|
1107
|
-
{"what": "overwriting an existing output without --overwrite (warned
|
|
1108
|
-
"since": "1.10.0", "
|
|
1109
|
-
"
|
|
1105
|
+
"since": "1.10.0", "removed_in": "2.0.0", "where": "json",
|
|
1106
|
+
"replacement": "hdr is now true only for PQ / HLG / Dolby Vision (as hdr_signal, which stays); bt2020_or_hdr carries the 1.x meaning"},
|
|
1107
|
+
{"what": "overwriting an existing output without --overwrite (1.x warned)",
|
|
1108
|
+
"since": "1.10.0", "removed_in": "2.0.0", "where": "behaviour",
|
|
1109
|
+
"replacement": "--overwrite as explicit consent; without it an existing output is refused (kind: input) before anything runs"},
|
|
1110
|
+
{"what": "the result_v2 preview key and FFMPEG_SKILL_RESULT_V2 (the v1 top-level keys it was to replace were un-deprecated: they are the 2.0 shape)",
|
|
1111
|
+
"since": "1.10.0", "removed_in": "2.0.0", "where": "json",
|
|
1112
|
+
"replacement": "the top-level keys, unchanged from 1.x; --json-brief for a short document"},
|
|
1110
1113
|
]
|
|
1111
1114
|
|
|
1112
1115
|
|
|
1113
1116
|
MCP_JSON_EXEMPT = ("look", "probe")
|
|
1114
|
-
#
|
|
1115
|
-
#
|
|
1116
|
-
#
|
|
1117
|
-
|
|
1117
|
+
# json/progress are CLI transport flags the MCP server sets itself, not tool arguments: 2.0 leaves
|
|
1118
|
+
# them out of every inputSchema (1.10.0-1.x kept them unless FFMPEG_SKILL_MCP_LEAN=1). A client
|
|
1119
|
+
# that still sends them is not refused -- build_argv() maps them like any other key.
|
|
1120
|
+
MCP_TRANSPORT_FLAGS = ("json", "progress")
|
|
1118
1121
|
# default MCP tools/list surface (roadmap P1-7, shipped 1.18.3): the 42-tool schema dump costs
|
|
1119
1122
|
# every MCP session context whether it needs it or not. These 12 are the tools eval iterations
|
|
1120
1123
|
# 17-20's ground-truth `expect` lists actually name most often across the 118 agent prompts in
|
|
@@ -1185,7 +1188,7 @@ MCP_PROMPTS: List[Dict[str, Any]] = [
|
|
|
1185
1188
|
"template": ("Bring {input} from HDR/Dolby Vision down to SDR: `color.py {input} --to-sdr` (or `--strip-dovi` "
|
|
1186
1189
|
"to keep HDR and only drop the Dolby Vision layer, when strip_dovi is asked for). A flat, washed-out "
|
|
1187
1190
|
"look after a re-encode usually means HDR metadata was lost upstream; `probe` shows `hdr: true` "
|
|
1188
|
-
"for
|
|
1191
|
+
"only for a real PQ/HLG/Dolby Vision signal, and `bt2020_or_hdr: true` also for BT.2020 SDR."),
|
|
1189
1192
|
},
|
|
1190
1193
|
]
|
|
1191
1194
|
|
|
@@ -1259,13 +1262,12 @@ def mcp_input_schema(spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1259
1262
|
one_of = [[{"required": [d]} for d in group] for group in src.get("one_of_required", [])]
|
|
1260
1263
|
if one_of:
|
|
1261
1264
|
structured["anyOf"] = one_of[0] if len(one_of) == 1 else [{"allOf": [{"anyOf": g} for g in one_of]}]
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
del structured["required"]
|
|
1265
|
+
for dest in MCP_TRANSPORT_FLAGS:
|
|
1266
|
+
props.pop(dest, None)
|
|
1267
|
+
if structured.get("required"):
|
|
1268
|
+
structured["required"] = [d for d in structured["required"] if d not in MCP_TRANSPORT_FLAGS]
|
|
1269
|
+
if not structured["required"]:
|
|
1270
|
+
del structured["required"]
|
|
1269
1271
|
schema: Dict[str, Any] = {"type": "object", "properties": props, "additionalProperties": False}
|
|
1270
1272
|
if structured:
|
|
1271
1273
|
schema["anyOf"] = [{"required": ["argv"]}, structured]
|
|
@@ -1297,6 +1299,7 @@ def build(detect: bool = True) -> Dict[str, Any]:
|
|
|
1297
1299
|
return {
|
|
1298
1300
|
"contract_version": CONTRACT_VERSION,
|
|
1299
1301
|
"deprecated": [dict(d) for d in DEPRECATED],
|
|
1302
|
+
"removed": [dict(d) for d in REMOVED],
|
|
1300
1303
|
"skill": {
|
|
1301
1304
|
"id": SKILL_ID,
|
|
1302
1305
|
"version": version,
|
package/scripts/background.py
CHANGED
|
@@ -28,7 +28,7 @@ def main() -> int:
|
|
|
28
28
|
src.add_argument("--color", default="black", help="solid background colour, e.g. black, 0x101010 (default black)")
|
|
29
29
|
src.add_argument("--gradient", help="two colours as C1:C2 for a linear gradient, e.g. 0xff6a00:0x0057ff")
|
|
30
30
|
ap.add_argument("--angle", type=float, default=0.0, help="gradient angle in degrees (with --gradient, default 0 = left to right)")
|
|
31
|
-
ap.
|
|
31
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
32
32
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS, help="x264 preset")
|
|
33
33
|
add_common(ap)
|
|
34
34
|
args = ap.parse_args()
|
package/scripts/broll.py
CHANGED
|
@@ -35,7 +35,7 @@ def main() -> int:
|
|
|
35
35
|
ap.add_argument("--from", dest="from_", action="append", metavar="TIME", help="where in B to start from (default 0); one per --insert, or omit")
|
|
36
36
|
ap.add_argument("--audio", choices=["a", "b", "mix"], default="a", help="under a cutaway: A's audio (default), B's audio, or both mixed")
|
|
37
37
|
ap.add_argument("--pad-color", default="black", help="pad colour when B's aspect differs from A's (default black)")
|
|
38
|
-
ap.
|
|
38
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
39
39
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS, help="x264 preset")
|
|
40
40
|
add_common(ap)
|
|
41
41
|
args = ap.parse_args()
|
|
@@ -104,7 +104,7 @@ def main() -> int:
|
|
|
104
104
|
for i, c in enumerate(cutaways):
|
|
105
105
|
geo = f"scale={w}:{h}:force_original_aspect_ratio=decrease,pad={w}:{h}:(ow-iw)/2:(oh-ih)/2:color={args.pad_color}"
|
|
106
106
|
parts.append(f"[{i + 1}:v]trim=start={c['from']:.3f}:duration={c['length']:.3f},setpts=PTS-STARTPTS+{c['at']:.3f}/TB,"
|
|
107
|
-
f"{geo},setsar=1,fps={fps:g},format={'yuv420p10le' if (meta_a.get('video') or {}).get('
|
|
107
|
+
f"{geo},setsar=1,fps={fps:g},format={'yuv420p10le' if (meta_a.get('video') or {}).get('bt2020_or_hdr') else 'yuv420p'}[b{i}]")
|
|
108
108
|
parts.append(f"{cur}[b{i}]overlay=0:0:eof_action=pass:enable='between(t,{c['at']:.3f},{c['at'] + c['length']:.3f})'[v{i}]")
|
|
109
109
|
cur = f"[v{i}]"
|
|
110
110
|
vout = cur
|
package/scripts/caption.py
CHANGED
|
@@ -939,7 +939,7 @@ def main() -> int:
|
|
|
939
939
|
help="how words are timed inside a cue: 'energy' follows the speech loudness in the audio (default), 'even' splits time equally")
|
|
940
940
|
anim.add_argument("--write-ass", help="where to save the generated ASS (default: next to the output)")
|
|
941
941
|
enc = ap.add_argument_group("encoding")
|
|
942
|
-
enc.
|
|
942
|
+
enc.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
943
943
|
enc.add_argument("--preset", default="medium", choices=X264_PRESETS)
|
|
944
944
|
add_common(ap)
|
|
945
945
|
args = ap.parse_args(_glue_negative_offset(sys.argv[1:]))
|
package/scripts/check.py
CHANGED
|
@@ -137,14 +137,14 @@ def main() -> int:
|
|
|
137
137
|
if args.platform in ("reels", "tiktok", "x", "linkedin", "facebook"):
|
|
138
138
|
row("pixel format", "PASS" if pf == "yuv420p" else "FAIL", pf, "yuv420p (8-bit 4:2:0)", "export.py preset re-encodes to yuv420p",
|
|
139
139
|
reason="QuickTime and iOS commonly reject video that isn't 8-bit 4:2:0")
|
|
140
|
-
if spec["sdr_only"] and v.get("
|
|
140
|
+
if spec["sdr_only"] and v.get("bt2020_or_hdr"):
|
|
141
141
|
row("colour", "FAIL", v.get("hdr_format"), "SDR BT.709", "color.py --to-sdr",
|
|
142
142
|
reason="a platform or player without HDR support will show this washed-out, too dark, or with wrong colours -- not a rendering glitch, a colour space mismatch")
|
|
143
143
|
else:
|
|
144
144
|
tags = (v.get("color_primaries"), v.get("color_transfer"))
|
|
145
145
|
untagged = not tags[0] and not tags[1]
|
|
146
|
-
if v.get("
|
|
147
|
-
row("colour", "PASS", f"{tags[0]}/{tags[1]}" + (f" ({v.get('hdr_format')})" if v.get("
|
|
146
|
+
if v.get("bt2020_or_hdr") or tags == ("bt709", "bt709") or args.platform in ("podcast", "custom"):
|
|
147
|
+
row("colour", "PASS", f"{tags[0]}/{tags[1]}" + (f" ({v.get('hdr_format')})" if v.get("bt2020_or_hdr") else ""), "bt709/bt709 tagged (or HDR)")
|
|
148
148
|
elif untagged and (v.get("bit_depth") or 8) == 8:
|
|
149
149
|
# untagged 8-bit video is treated as BT.709 by every player and platform; nothing to fix
|
|
150
150
|
row("colour", "PASS", "untagged (players assume bt709)", "bt709/bt709 tagged (or HDR)")
|
package/scripts/color.py
CHANGED
|
@@ -203,7 +203,7 @@ def main() -> int:
|
|
|
203
203
|
"the first track, same as leaving it unset always did. Only affects modes that re-encode "
|
|
204
204
|
"audio (--to-sdr, --lut, --correct, and --retag's re-encode fallback) -- --strip-dovi and "
|
|
205
205
|
"a successful --retag stream-copy all streams untouched, so the flag has nothing to select there.")
|
|
206
|
-
ap.
|
|
206
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
207
207
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS)
|
|
208
208
|
add_common(ap)
|
|
209
209
|
args = ap.parse_args()
|
|
@@ -279,13 +279,13 @@ def main() -> int:
|
|
|
279
279
|
|
|
280
280
|
measurements = None
|
|
281
281
|
if args.to_sdr:
|
|
282
|
-
if not v.get("
|
|
282
|
+
if not v.get("bt2020_or_hdr") and not args.force:
|
|
283
283
|
die(f"{args.input} is not tagged as HDR (transfer={v.get('color_transfer')}, primaries={v.get('color_primaries')}). Use --force to tone-map anyway.")
|
|
284
284
|
vf = hdr_to_sdr_chain(meta, args.tonemap, args.peak, args.desat)
|
|
285
285
|
output = args.output or default_output(args.input, "sdr")
|
|
286
286
|
tag = "sdr"
|
|
287
287
|
elif args.correct:
|
|
288
|
-
if v.get("
|
|
288
|
+
if v.get("bt2020_or_hdr") and not args.force:
|
|
289
289
|
die(f"{args.input} is HDR ({v.get('hdr_format')}); --correct works on SDR pixels and would tag PQ/HLG data as BT.709 "
|
|
290
290
|
f"without a tone map (sweep F10). Run --to-sdr first, or --force to grade the raw values anyway")
|
|
291
291
|
vf = correction_chain(args)
|
|
@@ -295,7 +295,7 @@ def main() -> int:
|
|
|
295
295
|
# "looks better" judgement -- the same primitive probe.py --analyze uses for Log detection.
|
|
296
296
|
measurements = None if STATE.dry_run else {"input": analyze_levels(args.input)}
|
|
297
297
|
else:
|
|
298
|
-
if v.get("
|
|
298
|
+
if v.get("bt2020_or_hdr") and not args.force:
|
|
299
299
|
die(f"{args.input} is HDR ({v.get('hdr_format')}); a LUT made for SDR applied to PQ/HLG pixels gives a wrong picture "
|
|
300
300
|
f"tagged BT.709 (sweep F10). Run --to-sdr first (or chain it), or --force if the LUT expects HDR input")
|
|
301
301
|
if not os.path.exists(args.lut):
|
package/scripts/crop.py
CHANGED
|
@@ -32,7 +32,7 @@ def main() -> int:
|
|
|
32
32
|
ap.add_argument("--y", type=int, required=True, help="top edge of the crop rectangle, in source pixels")
|
|
33
33
|
ap.add_argument("--width", type=int, required=True, help="crop width in px (must be even)")
|
|
34
34
|
ap.add_argument("--height", type=int, required=True, help="crop height in px (must be even)")
|
|
35
|
-
ap.
|
|
35
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
36
36
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS, help="x264 preset")
|
|
37
37
|
ap.add_argument("--fps", type=float, help="force a constant output frame rate (recommended for VFR sources)")
|
|
38
38
|
add_common(ap)
|
package/scripts/cut.py
CHANGED
|
@@ -292,7 +292,7 @@ def main() -> int:
|
|
|
292
292
|
"this media file) instead of measuring the input again")
|
|
293
293
|
snap.add_argument("--min-confidence", type=float, default=BEAT_MIN_CONFIDENCE,
|
|
294
294
|
help=f"refuse to snap below this measured beat confidence (default {BEAT_MIN_CONFIDENCE})")
|
|
295
|
-
ap.
|
|
295
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
296
296
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS, help="x264 preset when re-encoding")
|
|
297
297
|
add_common(ap)
|
|
298
298
|
args = ap.parse_args()
|
package/scripts/deinterlace.py
CHANGED
|
@@ -40,7 +40,7 @@ def main() -> int:
|
|
|
40
40
|
help="only deinterlace frames the source marks as interlaced; leave the rest untouched")
|
|
41
41
|
ap.add_argument("--audio-stream", type=int, default=0,
|
|
42
42
|
help="which audio stream of the input to keep, 0-based in file order (default 0)")
|
|
43
|
-
ap.
|
|
43
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
44
44
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS, help="x264 preset")
|
|
45
45
|
add_common(ap)
|
|
46
46
|
args = ap.parse_args()
|
package/scripts/denoise.py
CHANGED
|
@@ -42,7 +42,7 @@ def main() -> int:
|
|
|
42
42
|
ap.add_argument("--chroma-temporal", type=float, help="temporal chroma denoising strength (default from --strength)")
|
|
43
43
|
ap.add_argument("--audio-stream", type=int, default=0,
|
|
44
44
|
help="which audio stream of the input to keep, 0-based in file order (default 0)")
|
|
45
|
-
ap.
|
|
45
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
46
46
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS, help="x264 preset")
|
|
47
47
|
ap.add_argument("--fps", type=float, help="force a constant output frame rate (recommended for VFR sources)")
|
|
48
48
|
add_common(ap)
|
package/scripts/export.py
CHANGED
|
@@ -141,13 +141,13 @@ def main() -> int:
|
|
|
141
141
|
if not meta.get("video"):
|
|
142
142
|
die("input has no video stream")
|
|
143
143
|
notes: List[str] = []
|
|
144
|
-
if p.get("hdr_only") and not meta["video"].get("
|
|
144
|
+
if p.get("hdr_only") and not meta["video"].get("bt2020_or_hdr"):
|
|
145
145
|
# The point of this preset is that the delivery stays HDR. Running it on an SDR source
|
|
146
146
|
# would write a 10-bit HEVC file labelled with SDR tags and call it an HDR delivery.
|
|
147
147
|
die(f"--preset {args.preset} delivers HDR and this source is SDR ({meta['video'].get('codec')}, "
|
|
148
148
|
f"{meta['video'].get('color_transfer') or 'untagged'})",
|
|
149
149
|
hint="use --preset youtube for an SDR delivery; there is no way to invent HDR range from an SDR master")
|
|
150
|
-
if meta["video"].get("
|
|
150
|
+
if meta["video"].get("bt2020_or_hdr") and args.preset not in ("prores", "copy", "youtube-hdr"):
|
|
151
151
|
notes.append("source is HDR (%s). This preset outputs SDR BT.709 tags without tone mapping; run color.py --to-sdr first for correct colours." % meta["video"].get("hdr_format"))
|
|
152
152
|
info("warning: " + notes[-1])
|
|
153
153
|
has_audio = bool(meta.get("audio"))
|
package/scripts/fit.py
CHANGED
|
@@ -106,7 +106,7 @@ def main() -> int:
|
|
|
106
106
|
r.add_argument("--rotate", type=int, choices=[90, 180, 270], help="rotate the picture clockwise by this many degrees")
|
|
107
107
|
r.add_argument("--flip", choices=["h", "v"], help="mirror the picture horizontally (h) or vertically (v)")
|
|
108
108
|
e = ap.add_argument_group("encoding")
|
|
109
|
-
e.
|
|
109
|
+
e.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
110
110
|
e.add_argument("--preset", default="medium", choices=X264_PRESETS)
|
|
111
111
|
e.add_argument("--fps", type=float, help="force a constant output frame rate (recommended for VFR sources)")
|
|
112
112
|
add_common(ap)
|
|
@@ -227,7 +227,7 @@ def main() -> int:
|
|
|
227
227
|
# (gamma-encoded) signal and something else entirely on a PQ/HLG one -- so an HDR
|
|
228
228
|
# source keeps its blurred background undimmed rather than being silently altered
|
|
229
229
|
# (review 12). The blur itself is neutral either way, and no tone mapping happens.
|
|
230
|
-
darken = 0.0 if meta["video"].get("
|
|
230
|
+
darken = 0.0 if meta["video"].get("bt2020_or_hdr") else BLUR_DARKEN
|
|
231
231
|
if not darken:
|
|
232
232
|
info("--fit blur: HDR source, so the blurred background is not dimmed "
|
|
233
233
|
"(an eq on PQ/HLG code values is not the -15%% dim it is on SDR); "
|
package/scripts/freeze.py
CHANGED
|
@@ -30,7 +30,7 @@ def main() -> int:
|
|
|
30
30
|
ap.add_argument("--hold", type=float, required=True, help="how long the freeze lasts, in seconds")
|
|
31
31
|
ap.add_argument("--mode", choices=["insert", "extend"], default="insert",
|
|
32
32
|
help="insert (default): hold pushes the rest of the clip later; extend: only valid at/after the clip's end, makes the last frame last longer with nothing pushed")
|
|
33
|
-
ap.
|
|
33
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
34
34
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS, help="x264 preset")
|
|
35
35
|
add_common(ap)
|
|
36
36
|
args = ap.parse_args()
|
package/scripts/graphics.py
CHANGED
|
@@ -115,7 +115,7 @@ def main() -> int:
|
|
|
115
115
|
emo.add_argument("--emoji-scale", type=float, default=1.0,
|
|
116
116
|
help="emoji box as a multiple of the line's font size (default 1.0)")
|
|
117
117
|
emo.add_argument("--emoji-max", type=int, default=60, help="most emoji overlays one run may build (default 60)")
|
|
118
|
-
ap.
|
|
118
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
119
119
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS)
|
|
120
120
|
add_common(ap)
|
|
121
121
|
args = ap.parse_args()
|
package/scripts/grid.py
CHANGED
|
@@ -49,7 +49,7 @@ def main() -> int:
|
|
|
49
49
|
ap.add_argument("--audio-from", type=int, help="0-based index into inputs to take audio from (default: no audio)")
|
|
50
50
|
ap.add_argument("--gap", type=int, default=0, help="gap between cells in px, must be even (default 0, cells touch)")
|
|
51
51
|
ap.add_argument("--background", default="black", help="colour of the gap/pad borders (default black)")
|
|
52
|
-
ap.
|
|
52
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
53
53
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS, help="x264 preset")
|
|
54
54
|
add_common(ap)
|
|
55
55
|
args = ap.parse_args()
|
|
@@ -124,7 +124,7 @@ def main() -> int:
|
|
|
124
124
|
# A grid is an 8-bit SDR composite by design (a comparison/contact artefact, not a
|
|
125
125
|
# deliverable); an HDR input is flattened like look.py's contact sheet flattens it. Say so
|
|
126
126
|
# once so the caller is not surprised by the 8-bit output.
|
|
127
|
-
if any((m.get("video") or {}).get("
|
|
127
|
+
if any((m.get("video") or {}).get("bt2020_or_hdr") for m in metas):
|
|
128
128
|
info("note: an HDR input is composited into an 8-bit SDR grid (grid.py is a comparison artefact); use color.py --to-sdr first for a graded conversion")
|
|
129
129
|
cmd += video_args(None, args.crf, args.preset)
|
|
130
130
|
cmd += cfr_args(None, args.fps)
|
package/scripts/insert.py
CHANGED
|
@@ -46,7 +46,7 @@ def main() -> int:
|
|
|
46
46
|
ap.add_argument("--zoom", choices=["in", "out"], help="Ken Burns: slow linear zoom in or out across the clip")
|
|
47
47
|
ap.add_argument("--zoom-amount", type=float, default=1.3, help="end (zoom in) or start (zoom out) zoom factor, > 1.0 (default 1.3)")
|
|
48
48
|
ap.add_argument("--pan", choices=["left", "right", "up", "down"], help="drift the visible window this direction while zoomed (needs --zoom)")
|
|
49
|
-
ap.
|
|
49
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
50
50
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS, help="x264 preset")
|
|
51
51
|
add_common(ap)
|
|
52
52
|
args = ap.parse_args()
|
package/scripts/join.py
CHANGED
|
@@ -93,7 +93,7 @@ def main() -> int:
|
|
|
93
93
|
ap.add_argument("--fps", type=float, help="output frame rate (default: first clip)")
|
|
94
94
|
ap.add_argument("--fit", choices=["pad", "crop"], default="pad", help="how clips of another aspect reach the frame (default pad)")
|
|
95
95
|
ap.add_argument("--pad-color", default="black")
|
|
96
|
-
ap.
|
|
96
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
97
97
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS)
|
|
98
98
|
aud = ap.add_argument_group("audio-only inputs")
|
|
99
99
|
aud.add_argument("--sample-rate", type=int, help="output sample rate in Hz (default: first clip's)")
|
|
@@ -170,7 +170,7 @@ def main() -> int:
|
|
|
170
170
|
geo = f"scale={w}:{h}:force_original_aspect_ratio=decrease,pad={w}:{h}:(ow-iw)/2:(oh-ih)/2:color={args.pad_color}"
|
|
171
171
|
# Any HDR input makes the join HDR (10-bit, HEVC via video_args on that clip's tags): an SDR
|
|
172
172
|
# first clip used to drag an HDR second clip down to 8-bit without a tone map.
|
|
173
|
-
hdr_meta = next((m for m in metas if (m.get("video") or {}).get("
|
|
173
|
+
hdr_meta = next((m for m in metas if (m.get("video") or {}).get("bt2020_or_hdr")), None)
|
|
174
174
|
pixfmt = "yuv420p10le" if hdr_meta else "yuv420p"
|
|
175
175
|
# the audio-only join keeps the widest layout; the video join used to force stereo and
|
|
176
176
|
# silently dropped the centre/LFE of 5.1 material
|
package/scripts/look.py
CHANGED
|
@@ -191,7 +191,7 @@ def main() -> int:
|
|
|
191
191
|
f"left {z['left'] * 100:.0f}% / right {z['right'] * 100:.0f}% of the frame -- keep text out of those")
|
|
192
192
|
tc = safe_filter + tc
|
|
193
193
|
# HDR sources: tone-map for the PNG so the agent judges representative colours, not raw HLG/PQ
|
|
194
|
-
if meta["video"].get("
|
|
194
|
+
if meta["video"].get("bt2020_or_hdr"):
|
|
195
195
|
v = meta["video"]
|
|
196
196
|
tm = (f"zscale=tin={v.get('color_transfer') or 'arib-std-b67'}:pin={v.get('color_primaries') or 'bt2020'}:min={v.get('color_space') or 'bt2020nc'}:rin=tv:t=linear:npl=1000,"
|
|
197
197
|
"format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p,")
|
package/scripts/loop.py
CHANGED
|
@@ -28,7 +28,7 @@ def main() -> int:
|
|
|
28
28
|
group = ap.add_mutually_exclusive_group(required=True)
|
|
29
29
|
group.add_argument("--times", type=int, help="repeat the whole clip this many times (2 = original + 1 repeat)")
|
|
30
30
|
group.add_argument("--duration", help="loop (and trim the last repeat) to hit exactly this target duration (seconds or mm:ss)")
|
|
31
|
-
ap.
|
|
31
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
32
32
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS, help="x264 preset")
|
|
33
33
|
add_common(ap)
|
|
34
34
|
args = ap.parse_args()
|
package/scripts/multicam.py
CHANGED
|
@@ -144,7 +144,7 @@ def main() -> int:
|
|
|
144
144
|
ap.add_argument("--width", type=int, help="output width (default: reference)")
|
|
145
145
|
ap.add_argument("--height", type=int, help="output height (default: reference)")
|
|
146
146
|
ap.add_argument("--fps", type=float, help="output fps (default: reference)")
|
|
147
|
-
ap.
|
|
147
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
148
148
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS)
|
|
149
149
|
add_common(ap)
|
|
150
150
|
args = ap.parse_args()
|
|
@@ -260,7 +260,7 @@ def main() -> int:
|
|
|
260
260
|
w, h = h, w
|
|
261
261
|
fps = args.fps or v0.get("fps") or 30.0
|
|
262
262
|
fps = round(fps) if abs(fps - round(fps)) < 0.02 else fps
|
|
263
|
-
pixfmt = "yuv420p10le" if v0.get("
|
|
263
|
+
pixfmt = "yuv420p10le" if v0.get("bt2020_or_hdr") else "yuv420p"
|
|
264
264
|
geo = f"scale={w}:{h}:force_original_aspect_ratio=decrease,pad={w}:{h}:(ow-iw)/2:(oh-ih)/2,setsar=1,fps={fps:g},format={pixfmt}"
|
|
265
265
|
|
|
266
266
|
cmd = ffmpeg_base()
|
package/scripts/overlay.py
CHANGED
|
@@ -137,7 +137,7 @@ def main() -> int:
|
|
|
137
137
|
emo.add_argument("--emoji-max", type=int, default=60, help="most emoji overlays one run may build")
|
|
138
138
|
txt.add_argument("--box-color", default="black@0.5")
|
|
139
139
|
enc = ap.add_argument_group("encoding")
|
|
140
|
-
enc.
|
|
140
|
+
enc.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
141
141
|
enc.add_argument("--preset", default="medium", choices=X264_PRESETS)
|
|
142
142
|
add_common(ap)
|
|
143
143
|
args = ap.parse_args()
|
package/scripts/pad.py
CHANGED
|
@@ -26,7 +26,7 @@ def main() -> int:
|
|
|
26
26
|
ap.add_argument("--start", default="0", help="padding to add before the clip: seconds or mm:ss (default 0)")
|
|
27
27
|
ap.add_argument("--end", default="0", help="padding to add after the clip: seconds or mm:ss (default 0)")
|
|
28
28
|
ap.add_argument("--color", default="black", help="padding colour (default black)")
|
|
29
|
-
ap.
|
|
29
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
30
30
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS, help="x264 preset")
|
|
31
31
|
add_common(ap)
|
|
32
32
|
args = ap.parse_args()
|
package/scripts/probe.py
CHANGED
|
@@ -48,7 +48,7 @@ def main() -> int:
|
|
|
48
48
|
line += f" | {v.get('width')}x{v.get('height')} @ {v.get('fps')}fps {v.get('codec')} {v.get('pix_fmt')}"
|
|
49
49
|
if v.get("variable_frame_rate_suspected"):
|
|
50
50
|
line += " (VFR?)"
|
|
51
|
-
if v.get("
|
|
51
|
+
if v.get("bt2020_or_hdr"): # hdr_format also names the BT.2020 SDR case
|
|
52
52
|
line += f" [{v.get('hdr_format')}]"
|
|
53
53
|
if r.get("levels", {}).get("looks_like_log"):
|
|
54
54
|
line += " [Log?]"
|
package/scripts/proxy.py
CHANGED
|
@@ -5,7 +5,7 @@ editing-decision workflows that only need to look at (or feed a model) a
|
|
|
5
5
|
much smaller stand-in for the original.
|
|
6
6
|
|
|
7
7
|
Resizes to --width (default 640px, height follows the source aspect) or by
|
|
8
|
-
--scale factor, re-encodes at a proxy-grade --
|
|
8
|
+
--scale factor, re-encodes at a proxy-grade --quality (default 30 - well above any
|
|
9
9
|
delivery preset's 18-24 in export.py, since a proxy trades visual quality for
|
|
10
10
|
size and speed), and always uses the fastest x264/x265 preset. Keeps the
|
|
11
11
|
source's own dynamic range (an HDR source proxies to HEVC10, same as every
|
|
@@ -39,7 +39,7 @@ def main() -> int:
|
|
|
39
39
|
ap.add_argument("-o", "--output", help="output file (default: <name>_proxy.<ext>)")
|
|
40
40
|
ap.add_argument("--width", type=int, default=640, help="output width in px, height follows the source aspect (default 640)")
|
|
41
41
|
ap.add_argument("--scale", type=float, help="scale factor applied to the source dimensions instead of --width (0 < scale <= 1)")
|
|
42
|
-
ap.
|
|
42
|
+
ap.set_defaults(crf=30) # --quality's default (the --crf alias was removed in 2.0)
|
|
43
43
|
ap.add_argument("--fps", type=float, help="force a constant output frame rate")
|
|
44
44
|
ap.add_argument("--no-audio", action="store_true", help="drop audio entirely (default: keep it)")
|
|
45
45
|
add_common(ap)
|
package/scripts/redact.py
CHANGED
|
@@ -36,7 +36,7 @@ def main() -> int:
|
|
|
36
36
|
ap.add_argument("--block-size", type=int, default=12, help="mosaic block size in px, --mode pixelate only (default 12)")
|
|
37
37
|
ap.add_argument("--audio-stream", type=int, default=0,
|
|
38
38
|
help="which audio stream of the input to keep, 0-based in file order (default 0)")
|
|
39
|
-
ap.
|
|
39
|
+
ap.set_defaults(crf=18) # --quality's default (the --crf alias was removed in 2.0)
|
|
40
40
|
ap.add_argument("--preset", default="medium", choices=X264_PRESETS, help="x264 preset")
|
|
41
41
|
ap.add_argument("--fps", type=float, help="force a constant output frame rate (recommended for VFR sources)")
|
|
42
42
|
add_common(ap)
|
package/scripts/render.py
CHANGED
|
@@ -64,7 +64,7 @@ from typing import Any, Dict, List, Optional, Sequence
|
|
|
64
64
|
|
|
65
65
|
from export import PRESETS, PLATFORM_OF
|
|
66
66
|
from _platforms import PLATFORMS, caption_defaults, resolve as resolve_platform
|
|
67
|
-
from _common import STATE, add_common, brand_caption_style, load_brand, apply_common, child_args, die, emit, info, probe, run_tool, place_output, refuse_output_is_input, fingerprint, PLAN_VERSION, ffmpeg_version
|
|
67
|
+
from _common import STATE, add_common, brand_caption_style, load_brand, apply_common, child_args, die, emit, info, probe, run_tool, place_output, refuse_output_is_input, _check_existing_output, fingerprint, PLAN_VERSION, ffmpeg_version
|
|
68
68
|
import subprocess
|
|
69
69
|
from _contract import CONTRACT_VERSION
|
|
70
70
|
from batch import file_key
|
|
@@ -816,6 +816,9 @@ def main() -> int:
|
|
|
816
816
|
output = rel(proj.get("output") or "final.mp4")
|
|
817
817
|
# the final stage is a copy from the work dir, so run()'s own guard never sees the sources (review 5)
|
|
818
818
|
refuse_output_is_input(output, *[rel(c.get("src")) for c in clips if c.get("src")])
|
|
819
|
+
# 2.0 refuses an existing output without --overwrite; the final place_output() would, but
|
|
820
|
+
# only after every stage had run -- say so before the first one instead
|
|
821
|
+
_check_existing_output(["ffmpeg", output])
|
|
819
822
|
# The default work dir name comes only from the output path, with no PID or timestamp --
|
|
820
823
|
# two concurrent render.py runs targeting the same output (a batch.py "project" recipe
|
|
821
824
|
# processing several files in parallel, or simply running render.py twice by mistake) shared
|
package/scripts/report.py
CHANGED
|
@@ -71,7 +71,7 @@ def media_rows(meta: Dict[str, Any], ld: Dict[str, Any]) -> List[List[str]]:
|
|
|
71
71
|
["Duration", fmt_dur(meta.get("duration"))],
|
|
72
72
|
["Size", f"{(meta.get('size_bytes') or 0) / 1024 / 1024:.1f} MB"],
|
|
73
73
|
["Video", f"{v.get('codec')} {v.get('width')}×{v.get('height')} @ {v.get('fps')} fps, {v.get('pix_fmt')}" if v else "none"],
|
|
74
|
-
["Colour", (f"{v.get('color_primaries')}/{v.get('color_transfer')}" + (f" — {v.get('hdr_format')}" if v.get("
|
|
74
|
+
["Colour", (f"{v.get('color_primaries')}/{v.get('color_transfer')}" + (f" — {v.get('hdr_format')}" if v.get("bt2020_or_hdr") else " (SDR)")) if v else "—"],
|
|
75
75
|
["Frame rate", ("variable (suspected)" if v.get("variable_frame_rate_suspected") else "constant") if v else "—"],
|
|
76
76
|
["Audio", f"{a.get('codec')} {a.get('channels')} ch {a.get('sample_rate')} Hz" if a else "none"],
|
|
77
77
|
]
|