99 lines
3.3 KiB
Python
99 lines
3.3 KiB
Python
|
|
||
|
import argparse
|
||
|
import glob
|
||
|
import os
|
||
|
from pathlib import Path
|
||
|
import random
|
||
|
import string
|
||
|
import subprocess
|
||
|
from typing import Any, Dict, List
|
||
|
|
||
|
|
||
|
def get_args() -> Dict[str, Any]:
|
||
|
parser = argparse.ArgumentParser(
|
||
|
prog='GIF Make',
|
||
|
description='Use ffmpeg to make GIFs from videos',
|
||
|
epilog='— Be gay and do crime')
|
||
|
parser.add_argument('-i', '--input', type=str, required=True, help="Input pattern like '/users/MyName/Videos/*.mov'")
|
||
|
parser.add_argument('-w', '--width', type=str, default='960', help='Width of the GIF in pixels, respecting aspect ratio')
|
||
|
parser.add_argument('-r', '--framerate', type=str, default='12', help='Framerate of GIF')
|
||
|
parser.add_argument('-t', '--tag', type=str, default=get_tag(), help='Optional tag included in file name')
|
||
|
parser.add_argument('-f', '--force_palette', action='store_const', const=True, help='Force regeneration of GIF color palettes')
|
||
|
parser.add_argument('-c', '--clean_up', action='store_const', const=True, help='Delete color palettes after finishing GIFs')
|
||
|
return vars(parser.parse_args())
|
||
|
|
||
|
|
||
|
def get_tag() -> str:
|
||
|
return ''.join(random.choices(string.ascii_uppercase + string.digits, k = 5))
|
||
|
|
||
|
|
||
|
def get_inputs(args: Dict[str, Any]) -> List[str]:
|
||
|
return glob.glob(args['input'])
|
||
|
|
||
|
|
||
|
def clean_up_palette_files(palette_map: Dict[str, str]) -> None:
|
||
|
for input in palette_map:
|
||
|
os.remove(palette_map[input])
|
||
|
|
||
|
|
||
|
def call_ffmpeg(command: str) -> str:
|
||
|
print("ffmpeg", *command.split(' '))
|
||
|
# If we want to silence the command output: stdout=open(os.devnull, 'wb')
|
||
|
return str(subprocess.run(["ffmpeg", *command.split(' ')]))
|
||
|
|
||
|
|
||
|
def generate_palette_files_and_map(inputs: List[str], args: Dict[str, Any]) -> Dict[str, str]:
|
||
|
force_palette = args['force_palette']
|
||
|
width = args['width']
|
||
|
tag = args['tag']
|
||
|
|
||
|
palette_map = {}
|
||
|
for input in inputs:
|
||
|
file_name = f"{Path(input).stem}_{tag}_palette.png"
|
||
|
full_path = f"{Path(input).parent}/{file_name}"
|
||
|
if force_palette or not os.path.exists(full_path):
|
||
|
call_ffmpeg(f"-i {input} -lavfi scale={width}:-1,palettegen -sws_flags ewa_lanczos -y {full_path}")
|
||
|
else:
|
||
|
print(f"Skipped regenerating palette file {file_name}...")
|
||
|
palette_map[input] = full_path
|
||
|
|
||
|
return palette_map
|
||
|
|
||
|
|
||
|
def generate_gif_files(palette_map: Dict[str, str], args: Dict[str, Any]) -> Dict[str, str]:
|
||
|
tag = args['tag']
|
||
|
width = args['width']
|
||
|
framerate = args['framerate']
|
||
|
|
||
|
output_map = {}
|
||
|
for input in palette_map:
|
||
|
palette = palette_map[input]
|
||
|
file_name = f"{Path(input).stem}_{tag}.gif"
|
||
|
full_path = f"{Path(input).parent}/{file_name}"
|
||
|
call_ffmpeg(f"-i {input} -i {palette} -f gif -r {framerate} -lavfi scale={width}:-1,paletteuse -sws_flags ewa_lanczos {full_path}")
|
||
|
output_map[input] = full_path
|
||
|
|
||
|
return output_map
|
||
|
|
||
|
|
||
|
def make_gifs(args: Dict[str, Any]) -> None:
|
||
|
clean_up = args['clean_up']
|
||
|
inputs = get_inputs(args)
|
||
|
|
||
|
palette_map = generate_palette_files_and_map(inputs, args)
|
||
|
output_map = generate_gif_files(palette_map, args)
|
||
|
|
||
|
if clean_up:
|
||
|
clean_up_palette_files(palette_map)
|
||
|
|
||
|
for input in output_map:
|
||
|
print(f"Created {output_map[input]} from {input}")
|
||
|
|
||
|
|
||
|
def main() -> None:
|
||
|
make_gifs(get_args())
|
||
|
return
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
main()
|