#!/usr/bin/env python3.9 import argparse import datetime import hashlib import math import re import subprocess import sys import typing from pathlib import Path import ffmpeg def create_gif( video_path: Path, from_time: str, to_time: typing.Optional[str] = None, save_path: typing.Optional[Path] = None, duration: typing.Optional[int] = 10, fps: int = 15, width: int = 320, ) -> Path: if not save_path: # strip extra decimals time_clean = re.sub(r"\.(\d)\d+", r".\1", from_time).replace(":", "") save_path = video_path.with_name(f"{video_path.stem}_{time_clean}.gif") info = ffmpeg.ffprobe(video_path) scaled_width, scaled_height = width, math.ceil(width / info.ar / info.sample_ar) subprocess.run( [ "ffmpeg", "-v", "warning", "-y", "-ss", from_time, *(["-to", to_time] if to_time else ["-t", str(duration)]), "-ignore_chapters", "1", "-i", str(video_path), "-filter_complex", # f"fps={fps},scale={scale}:-1:flags=lanczos[x];[x]split[x1][x2];[x1]palettegen[p];[x2][p]paletteuse", f"[0:v] fps={fps},scale={scaled_width}:{scaled_height},split [a][b];[a] palettegen=stats_mode=full [p];[b][p] paletteuse=new=1", str(save_path), ], check=True, ) return save_path def optimize_gif(gif_path: Path) -> Path: subprocess.run( [ "gifsicle", "--lossy=200", "--optimize=3", "--batch", "-i", str(gif_path), ], check=True, ) return gif_path def parse_args(argv: typing.List[str] = None) -> argparse.Namespace: arger = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) arger.add_argument("video_path", type=str, help="Path to video file") arger.add_argument( "--start", "-s", dest="from_time", type=str, help="Time to start from, e.g. 02:59", required=True ) arger.add_argument("--duration", "-d", default=3, type=float, help="Duration of gif in seconds") arger.add_argument("--fps", default=20, type=int, help="Frames per second") arger.add_argument("--width", default=376, type=int, help="Image width") arger.add_argument("--optimize", default=True, action="store_true", help="Reduce GIF filesize") if len(argv) == 0: argv = ["--help"] return arger.parse_args(argv) def md5(text: str) -> str: return hashlib.md5(text.encode("utf-8")).hexdigest() def main(): args = parse_args(sys.argv[1:]) video_path = Path(args.video_path) from_time = args.from_time try: time = datetime.timedelta(seconds=round(float(from_time), 1)) from_time = str(time) if time.total_seconds() < 3600: from_time = from_time[2:] except ValueError: pass save_path = None if str(args.video_path).startswith("http"): save_path = Path("~/Pictures/Screenshots/").expanduser() / f"{md5(str(video_path))}__{from_time.replace(':', '')}.gif" gif_path = create_gif( video_path=video_path, save_path=save_path, from_time=from_time, duration=args.duration, fps=args.fps, width=args.width, ) if args.optimize: try: gif_path = optimize_gif(gif_path) except: pass print(gif_path.resolve()) if __name__ == "__main__": main()