Adding more docs, archiving the first version of the script, changing the second version to be the main version, Python 3.12→3.13, updating the Dockerfile to support the main version of the script, updating README.md to reflect the most recent --help
This commit is contained in:
parent
2dcf1fba8a
commit
d4573a72cb
14
Dockerfile
14
Dockerfile
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.12-slim-bookworm
|
FROM python:3.13-slim-trixie
|
||||||
|
|
||||||
VOLUME /home
|
VOLUME /home
|
||||||
|
|
||||||
@ -6,8 +6,18 @@ COPY video_to_gif.py /home/video_to_gif.py
|
|||||||
|
|
||||||
WORKDIR /home
|
WORKDIR /home
|
||||||
|
|
||||||
|
# https://stackoverflow.com/a/52445962
|
||||||
RUN apt update -y && \
|
RUN apt update -y && \
|
||||||
apt upgrade -y && \
|
apt upgrade -y && \
|
||||||
apt install ffmpeg -y
|
apt install ffmpeg -y && \
|
||||||
|
apt install curl -y && \
|
||||||
|
apt install build-essential -y
|
||||||
|
|
||||||
|
# https://stackoverflow.com/a/49676568
|
||||||
|
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
|
||||||
|
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||||
|
|
||||||
|
# https://crates.io/crates/gifski
|
||||||
|
RUN cargo install gifski
|
||||||
|
|
||||||
ENTRYPOINT ["python", "video_to_gif.py"]
|
ENTRYPOINT ["python", "video_to_gif.py"]
|
11
Pipfile
Normal file
11
Pipfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.13"
|
||||||
|
|
||||||
|
[dev-packages]
|
30
README.md
30
README.md
@ -1,33 +1,7 @@
|
|||||||
# video-to-gif
|
# video-to-gif
|
||||||
## video_to_gif.py usage
|
## video_to_gif.py usage
|
||||||
```
|
```
|
||||||
usage: video_to_gif.py [-h] [-w WIDTH] [-r FRAMERATE]
|
usage: video_to_gif.py [-h] [-w WIDTH] [-r FRAMERATE] [-e] [--no_loop] [-t TAG] INPUT [INPUT ...]
|
||||||
[-s {fast_bilinear,bilinear,bicubic,experimental,neighbor,area,bicublin,gauss,sinc,lanczos,spline}]
|
|
||||||
[-i {none,dup,blend,mci}] [-t TAG]
|
|
||||||
INPUT [INPUT ...]
|
|
||||||
|
|
||||||
Use ffmpeg to make GIFs from videos
|
|
||||||
|
|
||||||
positional arguments:
|
|
||||||
INPUT input file, supports passing a glob like /Users/MyName/Videos/*.mov
|
|
||||||
|
|
||||||
options:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
-w, --width WIDTH width of the GIF in pixels, respecting aspect ratio (default: 960)
|
|
||||||
-r, --framerate FRAMERATE
|
|
||||||
framerate of GIF (default: 12)
|
|
||||||
-s, --scaler {fast_bilinear,bilinear,bicubic,experimental,neighbor,area,bicublin,gauss,sinc,lanczos,spline}
|
|
||||||
scaling algorithm to use (default: lanczos)
|
|
||||||
-i, --interpolate {none,dup,blend,mci}
|
|
||||||
interpolation method to use (default: none)
|
|
||||||
-t, --tag TAG optional tag included in file name
|
|
||||||
|
|
||||||
— Be gay and do crime
|
|
||||||
```
|
|
||||||
|
|
||||||
## video_to_gif2.py usage
|
|
||||||
```
|
|
||||||
usage: video_to_gif2.py [-h] [-w WIDTH] [-r FRAMERATE] [-e] [-t TAG] INPUT [INPUT ...]
|
|
||||||
|
|
||||||
Use ffmpeg and gifski to make GIFs from videos
|
Use ffmpeg and gifski to make GIFs from videos
|
||||||
|
|
||||||
@ -40,13 +14,13 @@ options:
|
|||||||
-r, --framerate FRAMERATE
|
-r, --framerate FRAMERATE
|
||||||
framerate of GIF (default: 12)
|
framerate of GIF (default: 12)
|
||||||
-e, --extra increase quality at the expense of file size and encoding time
|
-e, --extra increase quality at the expense of file size and encoding time
|
||||||
|
--no_loop don't loop the animated GIF
|
||||||
-t, --tag TAG optional tag included in file name
|
-t, --tag TAG optional tag included in file name
|
||||||
|
|
||||||
— Be gay and do crime
|
— Be gay and do crime
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
Docker only works for the video_to_gif.py script.
|
|
||||||
```sh
|
```sh
|
||||||
docker build -t video_to_gif:latest .
|
docker build -t video_to_gif:latest .
|
||||||
docker run --rm -it video_to_gif --help
|
docker run --rm -it video_to_gif --help
|
||||||
|
@ -9,24 +9,38 @@ from typing import Any, Dict, List, Tuple
|
|||||||
|
|
||||||
def get_args() -> Dict[str, Any]:
|
def get_args() -> Dict[str, Any]:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog='video_to_gif2.py',
|
prog='video_to_gif.py',
|
||||||
description='Use ffmpeg and gifski to make GIFs from videos',
|
description='Use ffmpeg to make GIFs from videos',
|
||||||
epilog='— Be gay and do crime')
|
epilog='— Be gay and do crime')
|
||||||
parser.add_argument('input', type=str, metavar='INPUT', nargs='+', \
|
parser.add_argument('input', type=str, metavar='INPUT', nargs='+',
|
||||||
help="input file, supports passing a glob like /Users/MyName/Videos/*.mov")
|
help="input file, supports passing a glob like /Users/MyName/Videos/*.mov")
|
||||||
parser.add_argument('-w', '--width', type=str, default='960', \
|
parser.add_argument('-w', '--width', type=str, default='960',
|
||||||
help='width of the GIF in pixels, respecting aspect ratio (default: 960)')
|
help='width of the GIF in pixels, respecting aspect ratio (default: 960)')
|
||||||
parser.add_argument('-r', '--framerate', type=str, \
|
parser.add_argument('-r', '--framerate', type=str,
|
||||||
default='12', help='framerate of GIF (default: 12)')
|
default='12', help='framerate of GIF (default: 12)')
|
||||||
parser.add_argument('-e', '--extra', default=False, action='store_true', \
|
parser.add_argument('-s', '--scaler', type=str, choices=get_scaling_algorithms(),
|
||||||
help='increase quality at the expense of file size and encoding time')
|
default='lanczos', help='scaling algorithm to use (default: lanczos)')
|
||||||
parser.add_argument('--no_loop', default=False, action='store_true', \
|
parser.add_argument('-i', '--interpolate', type=str, choices=get_interpolation_methods(),
|
||||||
help='don\'t loop the animated GIF')
|
default='none', help='interpolation method to use (default: none)')
|
||||||
parser.add_argument('-t', '--tag', type=str, default=get_tag(), \
|
parser.add_argument('-t', '--tag', type=str, default=get_tag(),
|
||||||
help='optional tag included in file name')
|
help='optional tag included in file name')
|
||||||
return vars(parser.parse_args())
|
return vars(parser.parse_args())
|
||||||
|
|
||||||
|
|
||||||
|
def get_scaling_algorithms() -> List[str]:
|
||||||
|
return ['fast_bilinear', 'bilinear', 'bicubic', 'experimental', 'neighbor',
|
||||||
|
'area', 'bicublin', 'gauss', 'sinc', 'lanczos', 'spline']
|
||||||
|
|
||||||
|
|
||||||
|
def get_interpolation_methods() -> List[str]:
|
||||||
|
return ['none', 'dup', 'blend', 'mci']
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Figure out how to specify both scaling and dithering algorithms.
|
||||||
|
def get_dithering_algorithms() -> List[str]:
|
||||||
|
return ['none', 'auto', 'bayer', 'ed', 'a_dither', 'x_dither']
|
||||||
|
|
||||||
|
|
||||||
def get_tag() -> str:
|
def get_tag() -> str:
|
||||||
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=5))
|
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=5))
|
||||||
|
|
||||||
@ -34,10 +48,7 @@ def get_tag() -> str:
|
|||||||
def call_ffmpeg(command: List[str]) -> bool:
|
def call_ffmpeg(command: List[str]) -> bool:
|
||||||
print("ffmpeg", ' '.join(command))
|
print("ffmpeg", ' '.join(command))
|
||||||
full_command = ["ffmpeg", '-hide_banner', '-loglevel', 'error'] + command
|
full_command = ["ffmpeg", '-hide_banner', '-loglevel', 'error'] + command
|
||||||
|
process = subprocess.run(full_command)
|
||||||
# TODO: Find a way to avoid shell=True usage.
|
|
||||||
process = subprocess.run(' '.join(full_command), shell=True)
|
|
||||||
|
|
||||||
return process.returncode == 0
|
return process.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
@ -45,26 +56,20 @@ def generate_gif_files(inputs: List[str], args: Dict[str, Any]) -> Dict[str, Tup
|
|||||||
tag = args['tag']
|
tag = args['tag']
|
||||||
width = args['width']
|
width = args['width']
|
||||||
framerate = args['framerate']
|
framerate = args['framerate']
|
||||||
extra = args['extra']
|
scaler = args['scaler']
|
||||||
no_loop = args['no_loop']
|
interpolate = args['interpolate']
|
||||||
|
|
||||||
extra_cmd = ''
|
interpolate_cmd = ''
|
||||||
quality = '90'
|
if interpolate != 'none':
|
||||||
if extra:
|
interpolate_cmd = f"minterpolate=fps={ \
|
||||||
extra_cmd = '--extra'
|
framerate}:mi_mode={interpolate},"
|
||||||
quality = '100'
|
|
||||||
|
|
||||||
no_loop_cmd = ''
|
|
||||||
if no_loop: no_loop_cmd = '--repeat -1'
|
|
||||||
|
|
||||||
output_map = {}
|
output_map = {}
|
||||||
for input in inputs:
|
for input in inputs:
|
||||||
file_name = f"{Path(input).stem}_{tag}.gif"
|
file_name = f"{Path(input).stem}_{tag}.gif"
|
||||||
full_path = f"{Path(input).parent}/{file_name}"
|
full_path = f"{Path(input).parent}/{file_name}"
|
||||||
command = ['-i', input, '-pix_fmt', 'yuv422p', '-f', 'yuv4mpegpipe', '-', '|', 'gifski', \
|
command = ['-i', input, '-f', 'gif', '-r', f"{framerate}", '-filter_complex', f"{interpolate_cmd}scale={ \
|
||||||
'--width', f"{width}", '-r', f"{framerate}", extra_cmd, '-Q', quality, no_loop_cmd, \
|
width}:-1:flags={scaler},split[v1][v2];[v1]palettegen[plt];[v2][plt]paletteuse", f"{full_path}"]
|
||||||
'-o', f"{full_path}", '-']
|
|
||||||
command = list(filter(None, command))
|
|
||||||
success = call_ffmpeg(command)
|
success = call_ffmpeg(command)
|
||||||
# Tuple of (was it successful?, path of output)
|
# Tuple of (was it successful?, path of output)
|
||||||
output_map[input] = (success, full_path)
|
output_map[input] = (success, full_path)
|
@ -8,68 +8,68 @@ from typing import Any, Dict, List, Tuple
|
|||||||
|
|
||||||
|
|
||||||
def get_args() -> Dict[str, Any]:
|
def get_args() -> Dict[str, Any]:
|
||||||
|
"""Pull arguments from command line, turn them into a dictionary of <arg, value>"""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog='video_to_gif.py',
|
prog='video_to_gif.py',
|
||||||
description='Use ffmpeg to make GIFs from videos',
|
description='Use ffmpeg and gifski to make GIFs from videos',
|
||||||
epilog='— Be gay and do crime')
|
epilog='— Be gay and do crime')
|
||||||
parser.add_argument('input', type=str, metavar='INPUT', nargs='+',
|
parser.add_argument('input', type=str, metavar='INPUT', nargs='+', \
|
||||||
help="input file, supports passing a glob like /Users/MyName/Videos/*.mov")
|
help="input file, supports passing a glob like /Users/MyName/Videos/*.mov")
|
||||||
parser.add_argument('-w', '--width', type=str, default='960',
|
parser.add_argument('-w', '--width', type=str, default='960', \
|
||||||
help='width of the GIF in pixels, respecting aspect ratio (default: 960)')
|
help='width of the GIF in pixels, respecting aspect ratio (default: 960)')
|
||||||
parser.add_argument('-r', '--framerate', type=str,
|
parser.add_argument('-r', '--framerate', type=str, \
|
||||||
default='12', help='framerate of GIF (default: 12)')
|
default='12', help='framerate of GIF (default: 12)')
|
||||||
parser.add_argument('-s', '--scaler', type=str, choices=get_scaling_algorithms(),
|
parser.add_argument('-e', '--extra', default=False, action='store_true', \
|
||||||
default='lanczos', help='scaling algorithm to use (default: lanczos)')
|
help='increase quality at the expense of file size and encoding time')
|
||||||
parser.add_argument('-i', '--interpolate', type=str, choices=get_interpolation_methods(),
|
parser.add_argument('--no_loop', default=False, action='store_true', \
|
||||||
default='none', help='interpolation method to use (default: none)')
|
help='don\'t loop the animated GIF')
|
||||||
parser.add_argument('-t', '--tag', type=str, default=get_tag(),
|
parser.add_argument('-t', '--tag', type=str, default=get_tag(), \
|
||||||
help='optional tag included in file name')
|
help='optional tag included in file name')
|
||||||
return vars(parser.parse_args())
|
return vars(parser.parse_args())
|
||||||
|
|
||||||
|
|
||||||
def get_scaling_algorithms() -> List[str]:
|
|
||||||
return ['fast_bilinear', 'bilinear', 'bicubic', 'experimental', 'neighbor',
|
|
||||||
'area', 'bicublin', 'gauss', 'sinc', 'lanczos', 'spline']
|
|
||||||
|
|
||||||
|
|
||||||
def get_interpolation_methods() -> List[str]:
|
|
||||||
return ['none', 'dup', 'blend', 'mci']
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Figure out how to specify both scaling and dithering algorithms.
|
|
||||||
def get_dithering_algorithms() -> List[str]:
|
|
||||||
return ['none', 'auto', 'bayer', 'ed', 'a_dither', 'x_dither']
|
|
||||||
|
|
||||||
|
|
||||||
def get_tag() -> str:
|
def get_tag() -> str:
|
||||||
|
"""Get a random, five-character string to prevent overwriting files with the same name"""
|
||||||
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=5))
|
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=5))
|
||||||
|
|
||||||
|
|
||||||
def call_ffmpeg(command: List[str]) -> bool:
|
def call_ffmpeg(command: List[str]) -> bool:
|
||||||
|
"""Pipe commands to ffmpeg. It unfortunately has to use `shell=True` 🤷"""
|
||||||
print("ffmpeg", ' '.join(command))
|
print("ffmpeg", ' '.join(command))
|
||||||
full_command = ["ffmpeg", '-hide_banner', '-loglevel', 'error'] + command
|
full_command = ["ffmpeg", '-hide_banner', '-loglevel', 'error'] + command
|
||||||
process = subprocess.run(full_command)
|
|
||||||
|
# TODO: Find a way to avoid shell=True usage.
|
||||||
|
process = subprocess.run(' '.join(full_command), shell=True)
|
||||||
|
|
||||||
return process.returncode == 0
|
return process.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
def generate_gif_files(inputs: List[str], args: Dict[str, Any]) -> Dict[str, Tuple[bool, str]]:
|
def generate_gif_files(inputs: List[str], args: Dict[str, Any]) -> Dict[str, Tuple[bool, str]]:
|
||||||
|
"""Generate GIFs, return a dictionary of <input_file, tuple>—the tuple is a boolean
|
||||||
|
indicating whether creating the GIF was successful"""
|
||||||
tag = args['tag']
|
tag = args['tag']
|
||||||
width = args['width']
|
width = args['width']
|
||||||
framerate = args['framerate']
|
framerate = args['framerate']
|
||||||
scaler = args['scaler']
|
extra = args['extra']
|
||||||
interpolate = args['interpolate']
|
no_loop = args['no_loop']
|
||||||
|
|
||||||
interpolate_cmd = ''
|
extra_cmd = ''
|
||||||
if interpolate != 'none':
|
quality = '90'
|
||||||
interpolate_cmd = f"minterpolate=fps={ \
|
if extra:
|
||||||
framerate}:mi_mode={interpolate},"
|
extra_cmd = '--extra'
|
||||||
|
quality = '100'
|
||||||
|
|
||||||
|
no_loop_cmd = ''
|
||||||
|
if no_loop: no_loop_cmd = '--repeat -1'
|
||||||
|
|
||||||
output_map = {}
|
output_map = {}
|
||||||
for input in inputs:
|
for input in inputs:
|
||||||
file_name = f"{Path(input).stem}_{tag}.gif"
|
file_name = f"{Path(input).stem}_{tag}.gif"
|
||||||
full_path = f"{Path(input).parent}/{file_name}"
|
full_path = f"{Path(input).parent}/{file_name}"
|
||||||
command = ['-i', input, '-f', 'gif', '-r', f"{framerate}", '-filter_complex', f"{interpolate_cmd}scale={ \
|
command = ['-i', input, '-pix_fmt', 'yuv422p', '-f', 'yuv4mpegpipe', '-', '|', 'gifski', \
|
||||||
width}:-1:flags={scaler},split[v1][v2];[v1]palettegen[plt];[v2][plt]paletteuse", f"{full_path}"]
|
'--width', f"{width}", '-r', f"{framerate}", extra_cmd, '-Q', quality, no_loop_cmd, \
|
||||||
|
'-o', f"{full_path}", '-']
|
||||||
|
command = list(filter(None, command))
|
||||||
success = call_ffmpeg(command)
|
success = call_ffmpeg(command)
|
||||||
# Tuple of (was it successful?, path of output)
|
# Tuple of (was it successful?, path of output)
|
||||||
output_map[input] = (success, full_path)
|
output_map[input] = (success, full_path)
|
||||||
@ -78,6 +78,8 @@ def generate_gif_files(inputs: List[str], args: Dict[str, Any]) -> Dict[str, Tup
|
|||||||
|
|
||||||
|
|
||||||
def make_gifs(args: Dict[str, Any]) -> None:
|
def make_gifs(args: Dict[str, Any]) -> None:
|
||||||
|
"""Take in command line arguments, do the process to create GIFs, and echo
|
||||||
|
the files and their statuses"""
|
||||||
inputs = args['input']
|
inputs = args['input']
|
||||||
output_map = generate_gif_files(inputs, args)
|
output_map = generate_gif_files(inputs, args)
|
||||||
successes = 0
|
successes = 0
|
||||||
@ -91,6 +93,7 @@ def make_gifs(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
"""The main, driving function"""
|
||||||
make_gifs(get_args())
|
make_gifs(get_args())
|
||||||
return
|
return
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user