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.
- package/README.md +33 -9
- package/SKILL.md +77 -12
- package/bin/install.js +15 -1
- package/mcp/server.py +2 -0
- package/package.json +2 -2
- package/references/ci-platform-pitfalls.md +111 -0
- package/references/process-pitfalls.md +85 -0
- package/references/scripts.md +122 -11
- package/scripts/_common.py +200 -12
- package/scripts/_contract.py +204 -12
- package/scripts/audio.py +1 -1
- package/scripts/background.py +73 -0
- package/scripts/caption.py +97 -24
- package/scripts/color.py +36 -9
- package/scripts/crop.py +79 -0
- package/scripts/cut.py +2 -2
- package/scripts/export.py +1 -1
- package/scripts/fit.py +60 -10
- package/scripts/graphics.py +12 -3
- package/scripts/insert.py +128 -0
- package/scripts/join.py +14 -5
- package/scripts/loudness.py +3 -3
- package/scripts/multicam.py +1 -1
- package/scripts/overlay.py +59 -5
- package/scripts/proxy.py +82 -0
- package/scripts/reverse.py +56 -0
- package/scripts/sequence.py +124 -0
- package/scripts/silence.py +2 -2
- package/scripts/stabilize.py +83 -0
- package/scripts/sync.py +1 -1
|
@@ -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
|
|