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/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 = list(drift_af)
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
- else:
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
 
@@ -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
- graph = f"[1:v]{plate},format=rgba[plate];[0:a:{args.audio_stream}]{vf},format=rgba"
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
- stem, ext = os.path.splitext(output)
211
- render = stem + "_vis" + ext
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
- stem, ext = os.path.splitext(output)
231
- intermediates = [p for p in (render, stem + "_titled" + ext) if p != output]
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 = stem + "_titled" + ext if (args.srt or args.text) else output
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.