122 lines
4.0 KiB
Python
Executable File
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()
|