ffmpeg-skill 0.4.0 → 0.4.1
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/SKILL.md +8 -4
- package/package.json +1 -1
- package/scripts/__pycache__/_common.cpython-311.pyc +0 -0
- package/scripts/__pycache__/caption.cpython-311.pyc +0 -0
- package/scripts/__pycache__/color.cpython-311.pyc +0 -0
- package/scripts/__pycache__/cut.cpython-311.pyc +0 -0
- package/scripts/__pycache__/fit.cpython-311.pyc +0 -0
- package/scripts/__pycache__/join.cpython-311.pyc +0 -0
- package/scripts/__pycache__/look.cpython-311.pyc +0 -0
- package/scripts/__pycache__/multicam.cpython-311.pyc +0 -0
- package/scripts/__pycache__/overlay.cpython-311.pyc +0 -0
- package/scripts/__pycache__/probe.cpython-311.pyc +0 -0
- package/scripts/__pycache__/silence.cpython-311.pyc +0 -0
- package/scripts/__pycache__/sync.cpython-311.pyc +0 -0
- package/scripts/__pycache__/verify.cpython-311.pyc +0 -0
- package/scripts/_common.py +28 -3
- package/scripts/caption.py +2 -2
- package/scripts/color.py +2 -2
- package/scripts/cut.py +3 -3
- package/scripts/fit.py +2 -2
- package/scripts/join.py +4 -3
- package/scripts/look.py +7 -0
- package/scripts/multicam.py +4 -3
- package/scripts/overlay.py +3 -3
- package/scripts/silence.py +2 -2
- package/scripts/sync.py +4 -4
- package/scripts/verify.py +10 -1
package/SKILL.md
CHANGED
|
@@ -263,10 +263,14 @@ trims to platform maximums (Reels 90 s, X 140 s) unless `--allow-long`.
|
|
|
263
263
|
After `sync.py`, verify by running it again on the output: offset (and drift
|
|
264
264
|
ppm with `--fix-drift`) should be ~0. Recordings longer than ~10 minutes from
|
|
265
265
|
separate devices: always use `--fix-drift`.
|
|
266
|
-
- **Colour.**
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
266
|
+
- **Colour.** SDR outputs are H.264 tagged BT.709 `yuv420p`. When `probe.py`
|
|
267
|
+
reports `hdr: true` (HDR10/PQ, HLG, Dolby Vision, BT.2020), every editing
|
|
268
|
+
script keeps the output HDR (HEVC Main10, source colour tags) so nothing is
|
|
269
|
+
silently flattened. Decide with the user: keep HDR (fine for YouTube/phones)
|
|
270
|
+
or run `color.py --to-sdr` first for SDR-only destinations, LUT work or
|
|
271
|
+
H.264 deliverables. `export.py` platform presets are SDR and warn on HDR
|
|
272
|
+
input. iPhone `.mov` files also carry timecode/metadata tracks; scripts map
|
|
273
|
+
only the first audio track, so extra tracks are dropped on re-encode.
|
|
270
274
|
For Log footage (S-Log, V-Log, C-Log: looks grey and low-contrast but is
|
|
271
275
|
tagged SDR) run `probe.py --analyze`; `looks_like_log: true` means apply the
|
|
272
276
|
manufacturer's `.cube` with `color.py --lut` before anything else. Keep
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ffmpeg-skill",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Agent Skill that lets coding agents (Claude Code, Cursor, Codex) do professional video editing with local FFmpeg: cut, silence removal, transitions, multicam, captions, sync with drift correction, HDR to SDR, LUTs, audio clean-up and ducking, loudness, platform exports. No API keys, no cloud, no dependencies.",
|
|
5
5
|
"keywords": ["ffmpeg", "video", "agent-skill", "claude-code", "cursor", "codex", "skill", "video-editing"],
|
|
6
6
|
"license": "MIT",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/scripts/_common.py
CHANGED
|
@@ -331,6 +331,25 @@ def x264_args(crf: int = 18, preset: str = "medium", keep_bt709: bool = True) ->
|
|
|
331
331
|
return args
|
|
332
332
|
|
|
333
333
|
|
|
334
|
+
def video_args(meta: Optional[Dict[str, Any]], crf: int = 18, preset: str = "medium") -> List[str]:
|
|
335
|
+
"""Encoder args that preserve what the source is.
|
|
336
|
+
|
|
337
|
+
SDR sources -> H.264 8-bit tagged BT.709 (x264_args). HDR sources (HDR10/PQ, HLG,
|
|
338
|
+
Dolby Vision base layer, BT.2020) -> HEVC Main10 with the source's own colour tags,
|
|
339
|
+
so cutting/captioning/fitting an iPhone HDR clip stays HDR instead of becoming a
|
|
340
|
+
washed-out file mislabelled as BT.709. Use color.py --to-sdr when SDR is wanted.
|
|
341
|
+
"""
|
|
342
|
+
v = (meta or {}).get("video") or {}
|
|
343
|
+
if not v.get("hdr"):
|
|
344
|
+
return x264_args(crf, preset)
|
|
345
|
+
cs = v.get("color_space") or "bt2020nc"
|
|
346
|
+
prim = v.get("color_primaries") or "bt2020"
|
|
347
|
+
trc = v.get("color_transfer") or "arib-std-b67"
|
|
348
|
+
x265 = f"log-level=error:colorprim={prim}:transfer={trc}:colormatrix={cs}:range=limited:hdr10-opt=1" if trc == "smpte2084" else f"log-level=error:colorprim={prim}:transfer={trc}:colormatrix={cs}"
|
|
349
|
+
return ["-c:v", "libx265", "-preset", preset, "-crf", str(crf + 2), "-pix_fmt", "yuv420p10le", "-tag:v", "hvc1",
|
|
350
|
+
"-x265-params", x265, "-colorspace", cs, "-color_primaries", prim, "-color_trc", trc, "-movflags", "+faststart"]
|
|
351
|
+
|
|
352
|
+
|
|
334
353
|
def aac_args(bitrate: str = "192k") -> List[str]:
|
|
335
354
|
return ["-c:a", "aac", "-b:a", bitrate]
|
|
336
355
|
|
|
@@ -376,14 +395,20 @@ def analyze_levels(path: str, seconds: float = 20.0) -> Dict[str, Any]:
|
|
|
376
395
|
v = vals.get(k) or [0.0]
|
|
377
396
|
return sum(v) / len(v)
|
|
378
397
|
ymin, ymax, yavg, sat = min(vals.get("YMIN") or [0]), max(vals.get("YMAX") or [255]), mean("YAVG"), mean("SATAVG")
|
|
398
|
+
# signalstats reports in the source bit depth; normalise everything to an 8-bit scale
|
|
399
|
+
scale = 1.0
|
|
400
|
+
if ymax > 255 or yavg > 255:
|
|
401
|
+
scale = 1 / 4.0 if ymax <= 1023 else 1 / 16.0
|
|
402
|
+
ymin, ymax, yavg, sat = ymin * scale, ymax * scale, yavg * scale, sat * scale
|
|
379
403
|
# 5th/95th percentile of per-frame lows/highs is more robust than the absolute min/max
|
|
380
|
-
lows = sorted(vals.get("YLOW") or vals.get("YMIN") or [0])
|
|
381
|
-
highs = sorted(vals.get("YHIGH") or vals.get("YMAX") or [255])
|
|
404
|
+
lows = sorted(x * scale for x in (vals.get("YLOW") or vals.get("YMIN") or [0]))
|
|
405
|
+
highs = sorted(x * scale for x in (vals.get("YHIGH") or vals.get("YMAX") or [255]))
|
|
382
406
|
p_low = lows[len(lows) // 20]
|
|
383
407
|
p_high = highs[-1 - len(highs) // 20]
|
|
384
408
|
looks_log = p_low >= 64 and p_high <= 235 and sat < 40
|
|
385
409
|
return {
|
|
386
|
-
"
|
|
410
|
+
"scale": "8-bit equivalent",
|
|
411
|
+
"y_min": round(ymin, 1), "y_max": round(ymax, 1), "y_avg": round(yavg, 1), "y_low_p5": round(p_low, 1), "y_high_p95": round(p_high, 1),
|
|
387
412
|
"saturation_avg": round(sat, 1),
|
|
388
413
|
"looks_like_log": looks_log,
|
|
389
414
|
"note": ("flat, low-contrast, desaturated picture tagged as SDR: probably a Log profile (S-Log/V-Log/C-Log). "
|
package/scripts/caption.py
CHANGED
|
@@ -22,7 +22,7 @@ import re
|
|
|
22
22
|
import sys
|
|
23
23
|
from typing import List, Tuple
|
|
24
24
|
|
|
25
|
-
from _common import add_common, apply_common, emit, aac_args, cfr_args, default_output, die, escape_filter_path, ffmpeg_base, fmt_srt_time, info, parse_time, probe, run, x264_args
|
|
25
|
+
from _common import video_args, add_common, apply_common, emit, aac_args, cfr_args, default_output, die, escape_filter_path, ffmpeg_base, fmt_srt_time, info, parse_time, probe, run, x264_args
|
|
26
26
|
|
|
27
27
|
ALIGN = {"bottom": 2, "top": 8, "center": 5, "bottom-left": 1, "bottom-right": 3, "top-left": 7, "top-right": 9}
|
|
28
28
|
|
|
@@ -302,7 +302,7 @@ def main() -> int:
|
|
|
302
302
|
if args.fonts_dir:
|
|
303
303
|
vf += f":fontsdir={escape_filter_path(args.fonts_dir)}"
|
|
304
304
|
|
|
305
|
-
cmd = ffmpeg_base() + ["-i", args.input, "-vf", vf] +
|
|
305
|
+
cmd = ffmpeg_base() + ["-i", args.input, "-vf", vf] + video_args(meta, args.crf, args.preset) + cfr_args(meta)
|
|
306
306
|
cmd += (aac_args() if meta.get("audio") else ["-an"]) + [output]
|
|
307
307
|
run(cmd)
|
|
308
308
|
result = probe(output)
|
package/scripts/color.py
CHANGED
|
@@ -99,7 +99,7 @@ def main() -> int:
|
|
|
99
99
|
if proc.returncode != 0:
|
|
100
100
|
# some codecs cannot carry retagged colour info without a bitstream filter; fall back to re-encode
|
|
101
101
|
info("stream copy could not rewrite tags, re-encoding")
|
|
102
|
-
cmd = ffmpeg_base() + ["-i", args.input, "-map", "0:v:0", "-map", "0:a?"] + x264_args(args.crf, args.preset, keep_bt709=False)
|
|
102
|
+
cmd = ffmpeg_base() + ["-i", args.input, "-map", "0:v:0", "-map", "0:a:0?"] + x264_args(args.crf, args.preset, keep_bt709=False)
|
|
103
103
|
cmd += ["-colorspace", tags[0], "-color_primaries", tags[1], "-color_trc", tags[2]] + (aac_args() if has_audio else []) + [output]
|
|
104
104
|
run(cmd)
|
|
105
105
|
info(f"wrote {output} (tags -> {args.retag})")
|
|
@@ -124,7 +124,7 @@ def main() -> int:
|
|
|
124
124
|
output = args.output or default_output(args.input, "lut")
|
|
125
125
|
tag = "lut"
|
|
126
126
|
|
|
127
|
-
cmd = ffmpeg_base() + ["-i", args.input, "-vf", vf, "-map", "0:v:0", "-map", "0:a?"]
|
|
127
|
+
cmd = ffmpeg_base() + ["-i", args.input, "-vf", vf, "-map", "0:v:0", "-map", "0:a:0?"]
|
|
128
128
|
cmd += x264_args(args.crf, args.preset) + cfr_args(meta) + (aac_args() if has_audio else []) + [output]
|
|
129
129
|
run(cmd)
|
|
130
130
|
r = probe(output)
|
package/scripts/cut.py
CHANGED
|
@@ -16,7 +16,7 @@ import sys
|
|
|
16
16
|
import tempfile
|
|
17
17
|
from typing import List, Tuple
|
|
18
18
|
|
|
19
|
-
from _common import STATE, add_common, apply_common, emit, aac_args, cfr_args, default_output, die, ffmpeg_base, info, parse_time, probe, run, x264_args
|
|
19
|
+
from _common import video_args, STATE, add_common, apply_common, emit, aac_args, cfr_args, default_output, die, ffmpeg_base, info, parse_time, probe, run, x264_args
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def parse_segments(spec: str) -> List[Tuple[float, float]]:
|
|
@@ -43,7 +43,7 @@ def cut_one(src: str, start: float, end: float, dst: str, reencode: bool, crf: i
|
|
|
43
43
|
meta = meta or probe(src)
|
|
44
44
|
if reencode:
|
|
45
45
|
cmd = ffmpeg_base() + ["-ss", f"{start:.3f}", "-i", src, "-t", f"{dur:.3f}"]
|
|
46
|
-
cmd +=
|
|
46
|
+
cmd += video_args(meta, crf, preset) + cfr_args(meta) + aac_args() + ["-avoid_negative_ts", "make_zero", dst]
|
|
47
47
|
else:
|
|
48
48
|
cmd = ffmpeg_base() + ["-ss", f"{start:.3f}", "-i", src, "-t", f"{dur:.3f}", "-c", "copy", "-avoid_negative_ts", "make_zero", dst]
|
|
49
49
|
proc = run(cmd, check=False)
|
|
@@ -126,7 +126,7 @@ def main() -> int:
|
|
|
126
126
|
proc = run(cmd, check=False)
|
|
127
127
|
if proc.returncode != 0:
|
|
128
128
|
info("concat with stream copy failed, re-encoding the join")
|
|
129
|
-
cmd = ffmpeg_base() + ["-f", "concat", "-safe", "0", "-i", listfile] +
|
|
129
|
+
cmd = ffmpeg_base() + ["-f", "concat", "-safe", "0", "-i", listfile] + video_args(meta, args.crf, args.preset) + cfr_args(meta) + aac_args() + [output]
|
|
130
130
|
run(cmd)
|
|
131
131
|
|
|
132
132
|
result = probe(output)
|
package/scripts/fit.py
CHANGED
|
@@ -19,7 +19,7 @@ import sys
|
|
|
19
19
|
from fractions import Fraction
|
|
20
20
|
from typing import List
|
|
21
21
|
|
|
22
|
-
from _common import STATE, add_common, apply_common, emit, aac_args, cfr_args, default_output, die, ffmpeg_base, info, parse_time, probe, run, x264_args
|
|
22
|
+
from _common import video_args, STATE, add_common, apply_common, emit, aac_args, cfr_args, default_output, die, ffmpeg_base, info, parse_time, probe, run, x264_args
|
|
23
23
|
|
|
24
24
|
ASPECT_PRESETS = {"16:9": Fraction(16, 9), "9:16": Fraction(9, 16), "1:1": Fraction(1, 1), "4:5": Fraction(4, 5), "4:3": Fraction(4, 3), "21:9": Fraction(21, 9)}
|
|
25
25
|
|
|
@@ -153,7 +153,7 @@ def main() -> int:
|
|
|
153
153
|
cmd += ["-vf", ",".join(vf)]
|
|
154
154
|
if af:
|
|
155
155
|
cmd += ["-af", ",".join(af)]
|
|
156
|
-
cmd +=
|
|
156
|
+
cmd += video_args(meta, args.crf, args.preset)
|
|
157
157
|
cmd += cfr_args(meta, args.fps) if not args.fps else []
|
|
158
158
|
if has_audio:
|
|
159
159
|
cmd += aac_args()
|
package/scripts/join.py
CHANGED
|
@@ -14,7 +14,7 @@ import argparse
|
|
|
14
14
|
import sys
|
|
15
15
|
from typing import List
|
|
16
16
|
|
|
17
|
-
from _common import aac_args, add_common, apply_common, default_output, die, emit, ffmpeg_base, info, probe, run, x264_args
|
|
17
|
+
from _common import video_args, aac_args, add_common, apply_common, default_output, die, emit, ffmpeg_base, info, probe, run, x264_args
|
|
18
18
|
|
|
19
19
|
TRANSITIONS = ["fade", "dissolve", "wipeleft", "wiperight", "wipeup", "wipedown", "slideleft", "slideright",
|
|
20
20
|
"circleopen", "circleclose", "fadeblack", "fadewhite", "smoothleft", "smoothright", "radial", "none"]
|
|
@@ -78,8 +78,9 @@ def main() -> int:
|
|
|
78
78
|
geo = f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h}"
|
|
79
79
|
else:
|
|
80
80
|
geo = f"scale={w}:{h}:force_original_aspect_ratio=decrease,pad={w}:{h}:(ow-iw)/2:(oh-ih)/2:color={args.pad_color}"
|
|
81
|
+
pixfmt = "yuv420p10le" if (metas[0].get("video") or {}).get("hdr") else "yuv420p"
|
|
81
82
|
for i in range(n):
|
|
82
|
-
parts.append(f"[{i}:v]{geo},setsar=1,fps={fps:g},format=
|
|
83
|
+
parts.append(f"[{i}:v]{geo},setsar=1,fps={fps:g},format={pixfmt},settb=AVTB[v{i}]")
|
|
83
84
|
parts.append(f"[{audio_src[i]}]aformat=sample_rates=48000:channel_layouts=stereo,asetpts=PTS-STARTPTS[a{i}]")
|
|
84
85
|
|
|
85
86
|
if args.transition == "none":
|
|
@@ -98,7 +99,7 @@ def main() -> int:
|
|
|
98
99
|
|
|
99
100
|
output = args.output or default_output(args.inputs[0], "joined", "mp4")
|
|
100
101
|
cmd += ["-filter_complex", ";".join(parts), "-map", "[vout]", "-map", "[aout]"]
|
|
101
|
-
cmd +=
|
|
102
|
+
cmd += video_args(metas[0], args.crf, args.preset) + aac_args() + [output]
|
|
102
103
|
run(cmd)
|
|
103
104
|
expected = sum(durs) - d * (n - 1)
|
|
104
105
|
r = probe(output)
|
package/scripts/look.py
CHANGED
|
@@ -44,6 +44,13 @@ def main() -> int:
|
|
|
44
44
|
stem = Path(args.input).stem
|
|
45
45
|
outdir = str(Path(args.output).parent) if args.output else str(Path(args.input).parent)
|
|
46
46
|
tc = "" if args.no_timecode else "," + timecode_filter()
|
|
47
|
+
# HDR sources: tone-map for the PNG so the agent judges representative colours, not raw HLG/PQ
|
|
48
|
+
if meta["video"].get("hdr"):
|
|
49
|
+
v = meta["video"]
|
|
50
|
+
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,"
|
|
51
|
+
"format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p,")
|
|
52
|
+
tc = "," + tm.rstrip(",") + tc
|
|
53
|
+
info("HDR source: frames are tone-mapped to SDR for display")
|
|
47
54
|
outputs: List[str] = []
|
|
48
55
|
|
|
49
56
|
if args.compare:
|
package/scripts/multicam.py
CHANGED
|
@@ -21,7 +21,7 @@ import argparse
|
|
|
21
21
|
import sys
|
|
22
22
|
from typing import List, Tuple
|
|
23
23
|
|
|
24
|
-
from _common import aac_args, add_common, apply_common, default_output, die, emit, ffmpeg_base, info, parse_time, probe, run, x264_args
|
|
24
|
+
from _common import video_args, aac_args, add_common, apply_common, default_output, die, emit, ffmpeg_base, info, parse_time, probe, run, x264_args
|
|
25
25
|
from sync import measure_offset
|
|
26
26
|
|
|
27
27
|
|
|
@@ -154,7 +154,8 @@ def main() -> int:
|
|
|
154
154
|
w, h = h, w
|
|
155
155
|
fps = args.fps or v0.get("fps") or 30.0
|
|
156
156
|
fps = round(fps) if abs(fps - round(fps)) < 0.02 else fps
|
|
157
|
-
|
|
157
|
+
pixfmt = "yuv420p10le" if v0.get("hdr") else "yuv420p"
|
|
158
|
+
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}"
|
|
158
159
|
|
|
159
160
|
cmd = ffmpeg_base()
|
|
160
161
|
for p in args.inputs:
|
|
@@ -184,7 +185,7 @@ def main() -> int:
|
|
|
184
185
|
|
|
185
186
|
output = args.output or default_output(args.inputs[0], "multicam", "mp4")
|
|
186
187
|
cmd += ["-filter_complex", ";".join(parts), "-map", "[vout]", "-map", "[aout]"]
|
|
187
|
-
cmd +=
|
|
188
|
+
cmd += video_args(metas[0], args.crf, args.preset) + aac_args() + ["-shortest", output]
|
|
188
189
|
run(cmd)
|
|
189
190
|
r = probe(output)
|
|
190
191
|
info(f"wrote {output} ({r['duration']:.3f}s, {len(filled)} cuts, audio from input {a})")
|
package/scripts/overlay.py
CHANGED
|
@@ -15,7 +15,7 @@ import argparse
|
|
|
15
15
|
import sys
|
|
16
16
|
from typing import List, Optional
|
|
17
17
|
|
|
18
|
-
from _common import add_common, apply_common, emit, aac_args, cfr_args, default_output, die, escape_drawtext, escape_filter_path, ffmpeg_base, info, parse_time, probe, run, x264_args
|
|
18
|
+
from _common import video_args, add_common, apply_common, emit, aac_args, cfr_args, default_output, die, escape_drawtext, escape_filter_path, ffmpeg_base, info, parse_time, probe, run, x264_args
|
|
19
19
|
|
|
20
20
|
POS = {
|
|
21
21
|
"top-left": ("{m}", "{m}"),
|
|
@@ -140,7 +140,7 @@ def main() -> int:
|
|
|
140
140
|
# -loop 1 turns the still into a timed stream so fade/enable expressions see real timestamps
|
|
141
141
|
cmd = ffmpeg_base() + ["-i", args.input, "-loop", "1", "-i", args.image]
|
|
142
142
|
fc = f"[1:v]{','.join(chain)},setpts=PTS-STARTPTS[ov];[0:v][ov]{ov}[out]"
|
|
143
|
-
cmd += ["-filter_complex", fc, "-map", "[out]", "-map", "0:a?", "-shortest"]
|
|
143
|
+
cmd += ["-filter_complex", fc, "-map", "[out]", "-map", "0:a:0?", "-shortest"]
|
|
144
144
|
else:
|
|
145
145
|
x, y = position_exprs(args.position, args.margin, text_mode=True)
|
|
146
146
|
opts = [f"text='{escape_drawtext(args.text)}'", f"fontsize={args.font_size}", f"x={x}", f"y={y}",
|
|
@@ -159,7 +159,7 @@ def main() -> int:
|
|
|
159
159
|
opts.append(f"enable='{enable}'")
|
|
160
160
|
cmd += ["-vf", "drawtext=" + ":".join(opts)]
|
|
161
161
|
|
|
162
|
-
cmd +=
|
|
162
|
+
cmd += video_args(meta, args.crf, args.preset) + cfr_args(meta)
|
|
163
163
|
cmd += aac_args() if meta.get("audio") else ["-an"]
|
|
164
164
|
cmd.append(output)
|
|
165
165
|
run(cmd)
|
package/scripts/silence.py
CHANGED
|
@@ -16,7 +16,7 @@ import re
|
|
|
16
16
|
import sys
|
|
17
17
|
from typing import List, Tuple
|
|
18
18
|
|
|
19
|
-
from _common import aac_args, add_common, apply_common, cfr_args, default_output, die, emit, ffmpeg_base, info, print_json, probe, require_tool, run, x264_args
|
|
19
|
+
from _common import video_args, aac_args, add_common, apply_common, cfr_args, default_output, die, emit, ffmpeg_base, info, print_json, probe, require_tool, run, x264_args
|
|
20
20
|
|
|
21
21
|
SIL_RE = re.compile(r"silence_(start|end): ([0-9.]+)")
|
|
22
22
|
|
|
@@ -110,7 +110,7 @@ def main() -> int:
|
|
|
110
110
|
af = f"aselect='{expr}',asetpts=N/SR/TB"
|
|
111
111
|
cmd = ffmpeg_base() + ["-i", args.input]
|
|
112
112
|
if meta.get("video"):
|
|
113
|
-
cmd += ["-vf", vf] +
|
|
113
|
+
cmd += ["-vf", vf] + video_args(meta, args.crf, args.preset) + cfr_args(meta)
|
|
114
114
|
cmd += ["-af", af] + aac_args() + [output]
|
|
115
115
|
run(cmd)
|
|
116
116
|
r = probe(output)
|
package/scripts/sync.py
CHANGED
|
@@ -26,7 +26,7 @@ import subprocess
|
|
|
26
26
|
import sys
|
|
27
27
|
from typing import List
|
|
28
28
|
|
|
29
|
-
from _common import add_common, apply_common, emit, aac_args, audio_codec_for, default_output, die, ffmpeg_base, info, probe, require_tool, run, x264_args
|
|
29
|
+
from _common import video_args, add_common, apply_common, emit, aac_args, audio_codec_for, default_output, die, ffmpeg_base, info, probe, require_tool, run, x264_args
|
|
30
30
|
|
|
31
31
|
SR = 8000 # decode sample rate
|
|
32
32
|
|
|
@@ -253,14 +253,14 @@ def main() -> int:
|
|
|
253
253
|
if proc.returncode != 0:
|
|
254
254
|
cmd = [c for c in cmd if c != "copy"]
|
|
255
255
|
idx = cmd.index("-c:v"); del cmd[idx]
|
|
256
|
-
cmd = cmd[:-1] +
|
|
256
|
+
cmd = cmd[:-1] + video_args(probe(args.reference) if args.replace_audio else probe(args.second), args.crf) + [output]
|
|
257
257
|
run(cmd)
|
|
258
258
|
else:
|
|
259
259
|
if head_trim > 0 and not drift_af:
|
|
260
260
|
cmd = ffmpeg_base() + ["-ss", f"{head_trim:.4f}", "-i", args.second, "-c", "copy", "-avoid_negative_ts", "make_zero", output]
|
|
261
261
|
proc = run(cmd, check=False)
|
|
262
262
|
if proc.returncode != 0:
|
|
263
|
-
cmd = ffmpeg_base() + ["-ss", f"{head_trim:.4f}", "-i", args.second] + (
|
|
263
|
+
cmd = ffmpeg_base() + ["-ss", f"{head_trim:.4f}", "-i", args.second] + (video_args(probe(args.reference) if args.replace_audio else probe(args.second), args.crf) if has_video else []) + audio_codec_for(output) + [output]
|
|
264
264
|
run(cmd)
|
|
265
265
|
else:
|
|
266
266
|
cmd = ffmpeg_base()
|
|
@@ -278,7 +278,7 @@ def main() -> int:
|
|
|
278
278
|
vf.append(f"setpts=PTS/{drift_ratio:.9f}")
|
|
279
279
|
if vf:
|
|
280
280
|
cmd += ["-vf", ",".join(vf)]
|
|
281
|
-
cmd +=
|
|
281
|
+
cmd += video_args(probe(args.reference) if args.replace_audio else probe(args.second), args.crf)
|
|
282
282
|
if af_parts:
|
|
283
283
|
cmd += ["-af", ",".join(af_parts)]
|
|
284
284
|
cmd += audio_codec_for(output) + [output]
|
package/scripts/verify.py
CHANGED
|
@@ -47,6 +47,14 @@ def collect(paths: List[str]) -> List[Path]:
|
|
|
47
47
|
|
|
48
48
|
def step(name: str, argv: List[str], timeout: float) -> Dict:
|
|
49
49
|
t0 = time.time()
|
|
50
|
+
if argv[0] == "__check_hdr__":
|
|
51
|
+
try:
|
|
52
|
+
v = probe(argv[1]).get("video") or {}
|
|
53
|
+
ok = bool(v.get("hdr")) and v.get("bit_depth", 8) >= 10
|
|
54
|
+
err = "" if ok else f"re-encode lost HDR: {v.get('color_transfer')}/{v.get('pix_fmt')}"
|
|
55
|
+
except SystemExit:
|
|
56
|
+
ok, err = False, "output missing"
|
|
57
|
+
return {"step": name, "ok": ok, "seconds": round(time.time() - t0, 1), "error": err}
|
|
50
58
|
try:
|
|
51
59
|
proc = subprocess.run([sys.executable, str(HERE / argv[0])] + argv[1:], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=timeout)
|
|
52
60
|
ok = proc.returncode == 0
|
|
@@ -110,7 +118,8 @@ def main() -> int:
|
|
|
110
118
|
plan.append(("overlay text", ["overlay.py", cut, "--text", "verify", "--position", "top-left", "-o", f"{stem}_ovl.mp4"] + fast))
|
|
111
119
|
plan.append(("look sheet", ["look.py", cut, "-o", f"{stem}_sheet.png"]))
|
|
112
120
|
if (meta.get("video") or {}).get("hdr"):
|
|
113
|
-
plan.append(("color to-sdr", ["color.py",
|
|
121
|
+
plan.append(("color to-sdr", ["color.py", str(f), "--to-sdr", "-o", f"{stem}_sdr.mp4"] + fast))
|
|
122
|
+
plan.append(("hdr preserved", ["__check_hdr__", f"{stem}_acc.mp4"]))
|
|
114
123
|
plan.append(("probe analyze", ["probe.py", cut, "--analyze"]))
|
|
115
124
|
plan.append(("export x", ["export.py", cut, "--preset", "x", "-o", f"{stem}_x.mp4"]))
|
|
116
125
|
if has_a and not args.quick:
|