ffmpeg-skill 3.1.4 → 3.1.6
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/docs/contract.md +4 -3
- package/package.json +1 -1
- package/references/ci-platform-pitfalls.md +3 -2
- package/references/gotchas.md +5 -1
- package/references/scripts.md +5 -4
- package/scripts/_common/__init__.py +9 -7
- package/scripts/_common/color.py +40 -1
- package/scripts/_common/decision.py +199 -14
- package/scripts/_common/emit.py +12 -1
- package/scripts/_common/probe.py +98 -4
- package/scripts/_common/runner.py +38 -3
- package/scripts/_common/timeline.py +5 -4
- package/scripts/_contract.py +2 -2
- package/scripts/broll.py +35 -14
- package/scripts/caption.py +49 -22
- package/scripts/color.py +37 -8
- package/scripts/crop.py +8 -3
- package/scripts/cropdetect.py +13 -9
- package/scripts/cut.py +4 -4
- package/scripts/export.py +11 -2
- package/scripts/fit.py +8 -4
- package/scripts/freeze.py +38 -10
- package/scripts/graphics.py +109 -68
- package/scripts/grid.py +49 -7
- package/scripts/insert.py +11 -2
- package/scripts/join.py +29 -19
- package/scripts/look.py +44 -14
- package/scripts/loop.py +45 -6
- package/scripts/multicam.py +16 -7
- package/scripts/overlay.py +70 -15
- package/scripts/proxy.py +8 -4
- package/scripts/redact.py +10 -5
- package/scripts/render.py +8 -0
- package/scripts/reverse.py +27 -2
- package/scripts/scenes.py +16 -5
- package/scripts/sequence.py +6 -2
- package/scripts/silence.py +61 -30
- package/scripts/speedramp.py +17 -5
- package/scripts/straighten.py +17 -5
- package/scripts/sync.py +8 -2
- package/scripts/verify.py +7 -2
- package/scripts/waveform.py +22 -10
package/scripts/sync.py
CHANGED
|
@@ -34,6 +34,7 @@ from typing import Dict, List
|
|
|
34
34
|
|
|
35
35
|
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, run_analysis, x264_args, decode_pcm_mono, rms_envelope
|
|
36
36
|
from _common.emit import record_skip, warn
|
|
37
|
+
from _common.decision import stream_starts
|
|
37
38
|
|
|
38
39
|
SR = 8000 # decode sample rate
|
|
39
40
|
|
|
@@ -355,6 +356,11 @@ def main() -> int:
|
|
|
355
356
|
if abs(drift_ratio - 1.0) > 1e-7:
|
|
356
357
|
# the second file runs long by drift_ratio -> play it faster by that ratio (pitch shift is ~ppm, inaudible)
|
|
357
358
|
drift_af = [f"asetrate={sec_sr * drift_ratio:.6f}", f"aresample={sec_sr}"]
|
|
359
|
+
# the offset is measured between the files' clocks (decode_pcm_mono pads a late sound), so
|
|
360
|
+
# the second's sound is placed on its own clock first: one that starts 0.3 s after its
|
|
361
|
+
# file's start keeps that 0.3 s instead of being delayed from its first sample (#409).
|
|
362
|
+
# Only then, so the command for an ordinary file is the one it always was.
|
|
363
|
+
on_clock = ["aresample=async=1:first_pts=0"] if stream_starts(second_meta)[1] > 0.0005 else []
|
|
358
364
|
|
|
359
365
|
if args.replace_audio:
|
|
360
366
|
cmd = ffmpeg_base() + ["-i", args.reference]
|
|
@@ -367,7 +373,7 @@ def main() -> int:
|
|
|
367
373
|
# its length instead of -shortest, which cut the reference's tail whenever the second
|
|
368
374
|
# file started earlier (head_trim > 0) or was simply shorter
|
|
369
375
|
ref_dur = probe(args.reference).get("duration")
|
|
370
|
-
af_parts = drift_af + ([f"adelay={delay_ms}:all=1"] if delay_ms > 0 else []) + ["apad"]
|
|
376
|
+
af_parts = on_clock + drift_af + ([f"adelay={delay_ms}:all=1"] if delay_ms > 0 else []) + ["apad"]
|
|
371
377
|
cmd += ["-af", ",".join(af_parts)]
|
|
372
378
|
cmd += aac_args() + (["-t", f"{ref_dur:.3f}"] if ref_dur else ["-shortest"]) + [output]
|
|
373
379
|
proc = run(cmd, check=False)
|
|
@@ -391,7 +397,7 @@ def main() -> int:
|
|
|
391
397
|
if head_trim > 0:
|
|
392
398
|
cmd += ["-ss", f"{head_trim:.4f}"]
|
|
393
399
|
cmd += ["-i", args.second]
|
|
394
|
-
af_parts =
|
|
400
|
+
af_parts = on_clock + drift_af
|
|
395
401
|
if delay_ms > 0:
|
|
396
402
|
af_parts.append(f"adelay={delay_ms}:all=1")
|
|
397
403
|
if has_video:
|
package/scripts/verify.py
CHANGED
|
@@ -26,7 +26,7 @@ import time
|
|
|
26
26
|
from pathlib import Path
|
|
27
27
|
from typing import Any, Dict, List, Optional, Tuple
|
|
28
28
|
|
|
29
|
-
from _common import STATE, add_common, apply_common, die, emit, info, probe
|
|
29
|
+
from _common import STATE, add_common, apply_common, die, emit, guard_sidecar, info, probe
|
|
30
30
|
|
|
31
31
|
HERE = Path(__file__).resolve().parent
|
|
32
32
|
MEDIA_EXT = {".mp4", ".mov", ".m4v", ".mkv", ".webm", ".avi", ".mts", ".m2ts", ".mxf", ".wav", ".m4a", ".mp3", ".flac", ".aac"}
|
|
@@ -160,9 +160,14 @@ def main() -> int:
|
|
|
160
160
|
files = collect(args.paths)
|
|
161
161
|
tmp = None
|
|
162
162
|
if args.out:
|
|
163
|
+
# made first, so a --report inside a new --out folder passes the guard below (#413 review)
|
|
163
164
|
outdir = Path(args.out)
|
|
164
165
|
outdir.mkdir(parents=True, exist_ok=True)
|
|
165
|
-
|
|
166
|
+
if args.report:
|
|
167
|
+
# a file this run writes itself (#408): not one of the media it verifies (a folder's
|
|
168
|
+
# files included), nor an existing file without --overwrite
|
|
169
|
+
guard_sidecar(args.report, *args.paths, *map(str, files))
|
|
170
|
+
if not args.out:
|
|
166
171
|
tmp = tempfile.TemporaryDirectory(prefix="ffskill_verify_")
|
|
167
172
|
outdir = Path(tmp.name)
|
|
168
173
|
|
package/scripts/waveform.py
CHANGED
|
@@ -35,11 +35,12 @@ import os
|
|
|
35
35
|
import sys
|
|
36
36
|
|
|
37
37
|
from _platforms import PLATFORMS, PLATFORM_CHOICES, resolve as resolve_platform
|
|
38
|
-
from _common.probe import SILENT_PEAK_DBFS
|
|
38
|
+
from _common.probe import SILENT_PEAK_DBFS, square_pixels, square_pixels_note
|
|
39
|
+
from _common import still_input_args # noqa: E402
|
|
39
40
|
from _common import (add_common, apply_common, aac_args, default_output, die, draws_chroma, emit, ffmpeg_base,
|
|
40
41
|
info, load_brand, pad_filters, probe, rgb_chain_to_bt709, run, STATE, validate_color, video_args, X264_PRESETS,
|
|
41
|
-
fmt_secs, child_args, run_tool, measured_level_dbfs)
|
|
42
|
-
from _common.emit import absorb_stage_hw, warn
|
|
42
|
+
fmt_secs, child_args, run_tool, measured_level_dbfs, sibling_temp)
|
|
43
|
+
from _common.emit import absorb_stage_hw, note, warn
|
|
43
44
|
|
|
44
45
|
WAVEFORM_MODES = ["point", "line", "p2p", "cline"]
|
|
45
46
|
|
|
@@ -183,7 +184,12 @@ def main() -> int:
|
|
|
183
184
|
plate = pad_filters(args.width, args.height,
|
|
184
185
|
"blur" if args.image_fit == "blur" else "color",
|
|
185
186
|
args.background, 20)
|
|
186
|
-
|
|
187
|
+
# a non-square-pixel still is squared at its display aspect before it is fitted (#411)
|
|
188
|
+
plate_v = probe(args.image, role="input").get("video") if not STATE.dry_run else None
|
|
189
|
+
plate_sq = square_pixels(plate_v)
|
|
190
|
+
if plate_sq:
|
|
191
|
+
note(square_pixels_note(plate_v, "--image"))
|
|
192
|
+
graph = f"[1:v]{plate_sq + ',' if plate_sq else ''}{plate},format=rgba[plate];[0:a:{args.audio_stream}]{vf},format=rgba"
|
|
187
193
|
if args.opacity < 1.0:
|
|
188
194
|
graph += f",colorchannelmixer=aa={args.opacity:g}"
|
|
189
195
|
# the composite is RGBA (the plate is): converted to the BT.709 the output is tagged with,
|
|
@@ -207,14 +213,15 @@ def main() -> int:
|
|
|
207
213
|
if args.title or args.srt or args.text:
|
|
208
214
|
# the visualisation is rendered first and the label/captions are drawn on it by the tools
|
|
209
215
|
# that own those code paths, so neither is re-implemented here
|
|
210
|
-
|
|
211
|
-
|
|
216
|
+
# hidden, pid-named siblings (#408): a fixed `<out>_vis` name was removed afterwards
|
|
217
|
+
# whoever had written it, the input or another file of the caller's included
|
|
218
|
+
render = sibling_temp(output, "vis")
|
|
212
219
|
cmd = ffmpeg_base() + ["-i", args.input]
|
|
213
220
|
if args.image:
|
|
214
221
|
# -framerate before -loop: a looped still defaults to 25 fps, and overlay takes its rate
|
|
215
222
|
# from the FIRST input -- so without this the file came out 25 fps however loud --fps or
|
|
216
223
|
# --platform said otherwise (the colour-plate path never had the bug: `color=` carries r=).
|
|
217
|
-
cmd += ["-framerate", f"{args.fps:g}", "-loop", "1", "-i", args.image]
|
|
224
|
+
cmd += ["-framerate", f"{args.fps:g}", "-loop", "1", *still_input_args(args.image), "-i", args.image]
|
|
218
225
|
cmd += ["-filter_complex", vf]
|
|
219
226
|
if args.image:
|
|
220
227
|
cmd += ["-map", "[v]"]
|
|
@@ -227,15 +234,20 @@ def main() -> int:
|
|
|
227
234
|
if meta.get("duration"):
|
|
228
235
|
cmd += ["-t", f"{float(meta['duration']):.3f}"]
|
|
229
236
|
cmd += ["-shortest", render]
|
|
230
|
-
|
|
231
|
-
intermediates = [p for p in (render,
|
|
237
|
+
titled_tmp = sibling_temp(output, "titled")
|
|
238
|
+
intermediates = [p for p in (render, titled_tmp) if p != output]
|
|
232
239
|
try:
|
|
233
240
|
run(cmd)
|
|
234
241
|
|
|
235
242
|
stages = ["waveform"]
|
|
236
243
|
current = render
|
|
237
244
|
if args.title:
|
|
238
|
-
titled =
|
|
245
|
+
titled = titled_tmp if (args.srt or args.text) else output
|
|
246
|
+
if titled == titled_tmp:
|
|
247
|
+
# graphics.py's libass route writes <its output stem>_gfx.ass beside it (#413 review)
|
|
248
|
+
titled_ass = os.path.splitext(titled)[0] + "_gfx.ass"
|
|
249
|
+
if not os.path.exists(titled_ass):
|
|
250
|
+
intermediates.append(titled_ass)
|
|
239
251
|
# a dry run rendered no _vis file for graphics.py to probe, so its default --end (the
|
|
240
252
|
# clip's end) came out 0 and the plan failed: give it the source's duration, which
|
|
241
253
|
# is what -t caps the render at. A real run is unchanged.
|