Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
89
gif-sticker-maker/scripts/convert_mp4_to_gif.py
Normal file
89
gif-sticker-maker/scripts/convert_mp4_to_gif.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
Batch MP4 → GIF converter using ffmpeg.
|
||||
|
||||
Usage:
|
||||
python convert_mp4_to_gif.py sticker_hi.mp4 sticker_laugh.mp4 sticker_cry.mp4 sticker_love.mp4
|
||||
python convert_mp4_to_gif.py *.mp4 --fps 12 --width 320
|
||||
python convert_mp4_to_gif.py input.mp4 -o custom_output.gif
|
||||
|
||||
Requires: ffmpeg (must be on PATH)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
|
||||
def check_ffmpeg():
|
||||
if not shutil.which("ffmpeg"):
|
||||
raise SystemExit("ERROR: ffmpeg not found. Install via: brew install ffmpeg / apt install ffmpeg")
|
||||
|
||||
|
||||
def mp4_to_gif(input_path: str, output_path: str, fps: int = 15, width: int = 360):
|
||||
"""Convert a single MP4 to GIF via ffmpeg two-pass (palette for quality)."""
|
||||
if not os.path.isfile(input_path):
|
||||
print(f"SKIP: {input_path} not found", file=sys.stderr)
|
||||
return False
|
||||
|
||||
palette = output_path + ".palette.png"
|
||||
scale_filter = f"fps={fps},scale={width}:-1:flags=lanczos"
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["ffmpeg", "-y", "-i", input_path,
|
||||
"-vf", f"{scale_filter},palettegen=stats_mode=diff",
|
||||
palette],
|
||||
check=True, capture_output=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["ffmpeg", "-y", "-i", input_path, "-i", palette,
|
||||
"-lavfi", f"{scale_filter} [x]; [x][1:v] paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle",
|
||||
output_path],
|
||||
check=True, capture_output=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"FAIL: {input_path} -> {e.stderr.decode()[-200:]}", file=sys.stderr)
|
||||
return False
|
||||
finally:
|
||||
if os.path.exists(palette):
|
||||
os.remove(palette)
|
||||
|
||||
size = os.path.getsize(output_path)
|
||||
print(f"OK: {size:,} bytes -> {output_path}")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Batch MP4 → GIF converter (ffmpeg two-pass palette)")
|
||||
p.add_argument("inputs", nargs="+", help="MP4 file(s) to convert")
|
||||
p.add_argument("-o", "--output", default=None, help="Output path (only for single file input)")
|
||||
p.add_argument("--fps", type=int, default=15, help="GIF frame rate (default: 15)")
|
||||
p.add_argument("--width", type=int, default=360, help="GIF width in pixels, height auto-scaled (default: 360)")
|
||||
args = p.parse_args()
|
||||
|
||||
if args.output and len(args.inputs) > 1:
|
||||
raise SystemExit("ERROR: -o/--output only works with a single input file")
|
||||
|
||||
check_ffmpeg()
|
||||
|
||||
ok, fail = 0, 0
|
||||
for mp4 in args.inputs:
|
||||
if args.output:
|
||||
gif_path = args.output
|
||||
else:
|
||||
gif_path = os.path.splitext(mp4)[0] + ".gif"
|
||||
|
||||
if mp4_to_gif(mp4, gif_path, fps=args.fps, width=args.width):
|
||||
ok += 1
|
||||
else:
|
||||
fail += 1
|
||||
|
||||
print(f"\nDone: {ok} converted, {fail} failed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user