initial commit
This commit is contained in:
Executable
+127
@@ -0,0 +1,127 @@
|
||||
#!/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=98",
|
||||
"--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=int, help="Duration of gif in seconds")
|
||||
arger.add_argument("--fps", default=15, type=int, 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")
|
||||
|
||||
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("~/Downloads").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()
|
||||
Reference in New Issue
Block a user