ffmpeg-skill 0.10.0 → 0.12.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.
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """Stabilize shaky video (FFmpeg's vidstab, two-pass).
3
+
4
+ Pass 1 (vidstabdetect) analyses camera motion and writes the transforms to a
5
+ temporary file; pass 2 (vidstabtransform) smooths that motion and re-renders
6
+ the frames. The transforms file lives in a temp directory for the duration of
7
+ this run only -- it is not a caller-facing artifact.
8
+
9
+ --shakiness (1 = barely shaky, fast; 10 = very shaky, slow analysis) and
10
+ --smoothing (how many neighbouring frames to average the camera path over)
11
+ are the two knobs that matter most; --zoom crops in slightly to hide the
12
+ black edges stabilizing can introduce (0 = keep the original framing and let
13
+ edges show; ffmpeg's own --crop-mode is not exposed as a raw flag here).
14
+
15
+ Examples:
16
+ python3 stabilize.py shaky.mp4
17
+ python3 stabilize.py shaky.mp4 --shakiness 8 --smoothing 20 --zoom 5
18
+ """
19
+ import argparse
20
+ import sys
21
+ import tempfile
22
+ from pathlib import Path
23
+
24
+ from _common import STATE, add_common, apply_common, aac_args, cfr_args, default_output, die, emit, escape_filter_path, ffmpeg_base, info, probe, require_tool, run, video_args
25
+
26
+
27
+ def main() -> int:
28
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
29
+ ap.add_argument("input")
30
+ ap.add_argument("-o", "--output", help="output file (default: <name>_stab.<ext>)")
31
+ ap.add_argument("--shakiness", type=int, default=5, help="1 (barely shaky) .. 10 (very shaky), default 5")
32
+ ap.add_argument("--smoothing", type=int, default=15, help="frames of camera-path smoothing on each side, default 15")
33
+ ap.add_argument("--zoom", type=float, default=0.0, help="percent to zoom in to hide stabilization edges, 0..100 (default 0)")
34
+ ap.add_argument("--crf", type=int, default=18, help="x264 CRF (default 18)")
35
+ ap.add_argument("--preset", default="medium", help="x264 preset")
36
+ add_common(ap)
37
+ args = ap.parse_args()
38
+ apply_common(args)
39
+
40
+ if not 1 <= args.shakiness <= 10:
41
+ die(f"--shakiness must be 1..10, got {args.shakiness}")
42
+ if not 0 <= args.smoothing <= 1000:
43
+ die(f"--smoothing must be 0..1000, got {args.smoothing}")
44
+ if not 0 <= args.zoom <= 100:
45
+ die(f"--zoom must be 0..100, got {args.zoom}")
46
+
47
+ meta = probe(args.input)
48
+ if not meta.get("video"):
49
+ die("input has no video stream")
50
+ has_audio = bool(meta.get("audio"))
51
+ output = args.output or default_output(args.input, "stab")
52
+
53
+ with tempfile.TemporaryDirectory(prefix="ffmpeg-skill-vidstab-") as tmp:
54
+ trf = str(Path(tmp) / "transforms.trf")
55
+ trf_arg = escape_filter_path(trf)
56
+
57
+ if not STATE["dry_run"]:
58
+ ffmpeg = require_tool("ffmpeg")
59
+ detect_cmd = [ffmpeg, "-hide_banner", "-loglevel", "error", "-nostdin", "-y", "-i", args.input,
60
+ "-vf", f"vidstabdetect=shakiness={args.shakiness}:result={trf_arg}", "-f", "null", "-"]
61
+ proc = run(detect_cmd, check=False)
62
+ if proc.returncode != 0:
63
+ die(f"stabilization analysis (pass 1) failed:\n{proc.stderr.strip()[-1500:]}")
64
+
65
+ transform_vf = f"vidstabtransform=input={trf_arg}:smoothing={args.smoothing}:zoom={args.zoom:g}:optzoom=1"
66
+ cmd = ffmpeg_base() + ["-i", args.input, "-vf", transform_vf]
67
+ cmd += video_args(meta, args.crf, args.preset)
68
+ cmd += cfr_args(meta)
69
+ if has_audio:
70
+ cmd += aac_args()
71
+ else:
72
+ cmd += ["-an"]
73
+ cmd.append(output)
74
+ run(cmd)
75
+
76
+ result = probe(output, role="output")
77
+ info(f"wrote {output} ({result['duration']:.3f}s, {result['video']['width']}x{result['video']['height']})")
78
+ emit(output)
79
+ return 0
80
+
81
+
82
+ if __name__ == "__main__":
83
+ sys.exit(main())
package/scripts/sync.py CHANGED
@@ -61,7 +61,7 @@ def decode_mono(path: str, seconds: float, start: float = 0.0) -> List[float]:
61
61
  "-vn", "-ac", "1", "-ar", str(SR), "-f", "s16le", "-"]
62
62
  proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
63
63
  if proc.returncode != 0 or not proc.stdout:
64
- die(f"could not decode audio from {path}:\n{proc.stderr.decode(errors='replace').strip()}")
64
+ die(f"could not decode audio from {path}:\n{proc.stderr.decode(errors='replace').strip()}", kind="ffmpeg")
65
65
  n = len(proc.stdout) // 2
66
66
  return [v / 32768.0 for v in struct.unpack(f"<{n}h", proc.stdout[: n * 2])]
67
67