#!/usr/bin/env python3.9 import argparse import datetime import math import subprocess import sys import typing from pathlib import Path import re import ffmpeg_gif def parse_duration(text: str) -> datetime.timedelta: re_duration = re.compile(r"(\d+(?:\.\d+)?(?:sec|min|hour|hr|h|m|s))") re_digits = re.compile(r"([\d.]+)") time_scales = { "s": 1, "sec": 1, "m": 60, "min": 60, "h": 3600, "hr": 3600, } parts = re_duration.findall(text) if parts: total_secs = 0 for it in parts: if m := re_digits.search(it): _, end = m.span() scale = it[end:] total_secs += math.ceil(float(m.group(1)) * time_scales[scale]) return datetime.timedelta(seconds=total_secs) parts = [float(it) for it in text.split(":")] if len(parts) == 1: return datetime.timedelta(seconds=parts[0]) if len(parts) == 2: return datetime.timedelta(minutes=parts[0], seconds=parts[1]) if len(parts) == 3: return datetime.timedelta(hours=parts[0], minutes=parts[1], seconds=parts[2]) def parse_args(argv: typing.List[str] = None) -> argparse.Namespace: arger = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) arger.add_argument("video_path", type=Path, help="Path to video file") arger.add_argument("--interval", "-i", default=60, type=int, help="Interval between GIFs in seconds") arger.add_argument("--duration", "-d", default=3, type=int, help="GIF duration in seconds") arger.add_argument("--fps", default=18, type=float, help="Frames per second") arger.add_argument("--width", default=400, type=int, help="Image width") arger.add_argument("--optimize", default=True, action="store_true", help="Reduce GIF filesize") arger.add_argument("--from", dest='from_time', type=parse_duration, help="Start from timestamp") arger.add_argument("--to", dest='to_time', type=parse_duration, help="Stop at timestamp") if len(argv) == 0: argv = ["--help"] return arger.parse_args(argv) def get_video_duration(video_path: Path) -> datetime.timedelta: # fmt: off args = [ 'ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', video_path, ] # fmt: on p = subprocess.run(args, stdout=subprocess.PIPE, check=True) return datetime.timedelta(seconds=round(float(p.stdout), 1)) def summarize(video_path: Path, width: int = 400, gif_duration: int = 3, fps: int = 15, interval_seconds: int = 60, optimize: bool = True, from_time: typing.Optional[datetime.timedelta] = None, to_time: typing.Optional[datetime.timedelta] = None) -> None: duration = get_video_duration(video_path) if not from_time: from_time = datetime.timedelta(minutes=2) if not to_time: to_time = duration count = math.ceil((to_time - from_time).total_seconds() // interval_seconds) interval = (to_time - from_time) / count for i in range(count): start = from_time + datetime.timedelta(seconds=round(interval.total_seconds() * i, 1)) end = start + datetime.timedelta(seconds=gif_duration) if end >= to_time: break gif_path = ffmpeg_gif.create_gif( video_path=video_path, from_time=str(start), to_time=str(end), width=width, fps=fps, duration=duration, ) if optimize: gif_path = ffmpeg_gif.optimize_gif(gif_path) print(str(gif_path.absolute())) def main(): args = parse_args(sys.argv[1:]) video_path: Path = args.video_path summarize( video_path=video_path, width=args.width, gif_duration=args.duration, fps=args.fps, optimize=args.optimize, interval_seconds=args.interval, from_time=args.from_time, to_time=args.to_time, ) if __name__ == "__main__": main()