Files
playground/ffmpeg_gif_summary.py
2023-02-18 07:39:08 +01:00

122 lines
4.0 KiB
Python
Executable File

#!/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()