commit 33670e06597d738fdfd6a7811d2c2baa872704d6 Author: jlw4049 Date: Wed Jan 24 12:11:06 2024 -0500 feat: initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..353a315 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.idea/ +.venv/ +__pycache__/ +env/ +*.bin +*.exe +*.zip +*.dll +pyinstaller_build/ +scrap_files/ +.vscode/ +custom-pyinstaller/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b2eab6e --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 jlw_4049 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7dd1c2 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Image-Generator + +A CLI to generate comparison image sets with + +## Usage + +``` +usage: Comparison Image Generator [-h] [-v] [--source SOURCE] [--encode ENCODE] [--image-dir IMAGE_DIR] + [--indexer {lsmash,ffms2}] [--index-dir INDEX_DIR] [--sub-size SUB_SIZE] + [--left-crop LEFT_CROP] [--right-crop RIGHT_CROP] [--top-crop TOP_CROP] + [--bottom-crop BOTTOM_CROP] [--adv-resize-left ADV_RESIZE_LEFT] + [--adv-resize-right ADV_RESIZE_RIGHT] [--adv-resize-top ADV_RESIZE_TOP] + [--adv-resize-bottom ADV_RESIZE_BOTTOM] [--tone-map] [--re-sync RE_SYNC] + [--comparison-count COMPARISON_COUNT] [--subtitle-color SUBTITLE_COLOR] + [--release-sub-title RELEASE_SUB_TITLE] + +options: + -h, --help show this help message and exit + -v, --version show program's version number and exit + --source SOURCE Path to source file + --encode ENCODE Path to encode file + --image-dir IMAGE_DIR + Path to base image folder + --indexer {lsmash,ffms2} + Indexer choice + --index-dir INDEX_DIR + Path to look/create indexes + --sub-size SUB_SIZE Size of subtitles + --left-crop LEFT_CROP + Left crop + --right-crop RIGHT_CROP + Right crop + --top-crop TOP_CROP Top crop + --bottom-crop BOTTOM_CROP + crop + --adv-resize-left ADV_RESIZE_LEFT + Advanced resize left + --adv-resize-right ADV_RESIZE_RIGHT + Advanced resize right + --adv-resize-top ADV_RESIZE_TOP + Advanced resize top + --adv-resize-bottom ADV_RESIZE_BOTTOM + Advanced resize bottom + --tone-map HDR tone-mapping + --re-sync RE_SYNC Sync offset for image generation in frames (i.e. --re-sync=-3) + --comparison-count COMPARISON_COUNT + Amount of comparisons to generate + --subtitle-color SUBTITLE_COLOR + Hex color code for subtitle color + --release-sub-title RELEASE_SUB_TITLE + Release group subtitle name (this will show on the encode images) +``` + +## Supports + +Windows 8 and up + +## Requirements + +You will need lsmash, ffms2, libfpng, libimwri, and SubText vapoursynth plugins. +Place all of these in a folder 'img_plugins' beside the script/executable. diff --git a/build.py b/build.py new file mode 100644 index 0000000..a1048f5 --- /dev/null +++ b/build.py @@ -0,0 +1,78 @@ +from pathlib import Path +from subprocess import run +import os +import shutil +import sys + + +def build_app(): + # Change directory to the project's root directory + project_root = Path(__file__).parent + os.chdir(project_root) + + # Ensure we're in a virtual env, if we are, install dependencies using Poetry + if sys.prefix == sys.base_prefix: + raise Exception("You must activate your virtual environment first") + else: + # Use Poetry to install dependencies + run(["poetry", "install"]) + + # Define the PyInstaller output path + pyinstaller_folder = project_root / "pyinstaller_build" + + # Delete the old build folder if it exists + shutil.rmtree(pyinstaller_folder, ignore_errors=True) + + # Create a folder for the PyInstaller output + pyinstaller_folder.mkdir(exist_ok=True) + + # Define paths before changing directory + img_gen = project_root / "image_generator.py" + icon_path = project_root / "images" / "icon.ico" + additional_hooks_path = Path(Path.cwd() / "hooks") + + # paths to needed vapoursynth files + vapoursynth_64 = project_root / ".venv" / "Lib" / "site-packages" / "vapoursynth64" + vapoursynth_64_portable = ( + project_root / ".venv" / "Lib" / "site-packages" / "portable.vs" + ) + + # Change directory so PyInstaller outputs all of its files in its own folder + os.chdir(pyinstaller_folder) + + # Run PyInstaller command with Poetry's virtual environment + build_job = run( + [ + "poetry", + "run", + "pyinstaller", + # "-w", + "--onefile", + f"--icon={icon_path}", + "--add-data", + f"{vapoursynth_64};vapoursynth64", + "--add-data", + f"{vapoursynth_64_portable};.", + "--name", + "Image-Generator", + str(img_gen), + f"--additional-hooks-dir={str(additional_hooks_path)}", + ] + ) + + # Ensure the output of the .exe + success = "Did not complete successfully" + exe_path = project_root / pyinstaller_folder / "dist" / "Image-Generator.exe" + if exe_path.is_file() and str(build_job.returncode) == "0": + success = f"\nSuccess!\nPath to exe: {str(exe_path)}" + + # Change directory back to the original directory + os.chdir(project_root) + + # Return a success message + return success + + +if __name__ == "__main__": + build = build_app() + print(build) diff --git a/hooks/hook-vapoursynth.py b/hooks/hook-vapoursynth.py new file mode 100644 index 0000000..73bca5d --- /dev/null +++ b/hooks/hook-vapoursynth.py @@ -0,0 +1,27 @@ +hiddenimports = [ + "os", + "enum", + "ctypes", + "threading", + "traceback", + "gc", + "sys", + "inspect", + "weakref", + "atexit", + "contextlib", + "logging", + "functools", + "typing", + "traceback", + "warnings", + "threading.local", + "threading.Lock", + "threading.RLock", + "types.MappingProxyType", + "collections.namedtuple", + "collections.abc.Iterable", + "collections.abc.Mapping", + "concurrent.futures.Future", + "fractions", +] diff --git a/image_generator.py b/image_generator.py new file mode 100644 index 0000000..f09adb0 --- /dev/null +++ b/image_generator.py @@ -0,0 +1,110 @@ +from pathlib import Path +from argparse import ArgumentParser +from image_generator import GenerateImages +from image_generator.exceptions import ImageGeneratorError +from image_generator.utils import exit_application + + +program_name = "Comparison Image Generator" +__version__ = "1.1.0" + + +if __name__ == "__main__": + parser = ArgumentParser(prog=program_name) + + parser.add_argument( + "-v", "--version", action="version", version=f"{program_name} v{__version__}" + ) + + parser.add_argument("--source", type=str, help="Path to source file") + parser.add_argument("--encode", type=str, help="Path to encode file") + parser.add_argument("--image-dir", type=str, help="Path to base image folder") + parser.add_argument( + "--indexer", + type=str, + choices=["lsmash", "ffms2"], + help="Indexer choice", + ) + parser.add_argument("--index-dir", type=str, help="Path to look/create indexes") + parser.add_argument("--sub-size", type=int, help="Size of subtitles") + parser.add_argument("--left-crop", type=int, help="Left crop") + parser.add_argument("--right-crop", type=int, help="Right crop") + parser.add_argument("--top-crop", type=int, help="Top crop") + parser.add_argument("--bottom-crop", type=int, help=" crop") + parser.add_argument("--adv-resize-left", type=float, help="Advanced resize left") + parser.add_argument("--adv-resize-right", type=float, help="Advanced resize right") + parser.add_argument("--adv-resize-top", type=float, help="Advanced resize top") + parser.add_argument( + "--adv-resize-bottom", type=float, help="Advanced resize bottom" + ) + parser.add_argument("--tone-map", action="store_true", help="HDR tone-mapping") + parser.add_argument( + "--re-sync", + type=str, + help="Sync offset for image generation in frames (i.e. --re-sync=-3)", + ) + parser.add_argument( + "--comparison-count", type=int, help="Amount of comparisons to generate" + ) + parser.add_argument( + "--subtitle-color", type=str, help="Hex color code for subtitle color" + ) + parser.add_argument( + "--release-sub-title", + type=str, + help="Release group subtitle name (this will show on the encode images)", + ) + + args = parser.parse_args() + + if not any(vars(args).values()): + parser.print_help() + exit_application("", 1) + + if not args.source or not Path(args.source).is_file(): + exit_application( + "Source input is not detected (--source 'path to file')", + 1, + ) + + if not args.encode or not Path(args.encode).is_file(): + exit_application( + "Encode input is not detected (--encode 'path to file')", + 1, + ) + + if args.image_dir: + image_dir = Path(args.image_dir) + else: + image_dir = Path(Path(args.encode).parent / f"{Path(args.encode).stem}_images") + image_dir.mkdir(parents=True, exist_ok=True) + + try: + img_generator = GenerateImages( + source_file=Path(args.source), + encode_file=Path(args.encode), + image_dir=image_dir, + indexer=args.indexer, + index_directory=args.index_dir, + sub_size=args.sub_size, + left_crop=args.left_crop, + right_crop=args.right_crop, + top_crop=args.top_crop, + bottom_crop=args.bottom_crop, + adv_resize_left=args.adv_resize_left, + adv_resize_right=args.adv_resize_right, + adv_resize_top=args.adv_resize_top, + adv_resize_bottom=args.adv_resize_bottom, + tone_map=args.tone_map, + re_sync=args.re_sync, + comparison_count=int(args.comparison_count) + if args.comparison_count + else 20, + subtitle_color=args.subtitle_color, + release_sub_title=args.release_sub_title, + ) + img_gen = img_generator.process_images() + if img_gen: + exit_application(f"Output: {img_gen}", 0) + except ImageGeneratorError as error: + exit_application(error, 1) diff --git a/image_generator/__init__.py b/image_generator/__init__.py new file mode 100644 index 0000000..6974dc8 --- /dev/null +++ b/image_generator/__init__.py @@ -0,0 +1,585 @@ +import re +import shutil +from random import choice +from pathlib import Path +from numpy import linspace +from unidecode import unidecode +import awsmfunc +import vapoursynth as vs +from image_generator.exceptions import ImageGeneratorError +from image_generator.utils import get_working_dir, hex_to_bgr + + +class GenerateImages: + def __init__( + self, + source_file: Path, + encode_file: Path, + image_dir: Path, + indexer: str, + index_directory: None | str, + sub_size: int, + left_crop: int, + right_crop: int, + top_crop: int, + bottom_crop: int, + adv_resize_left: float, + adv_resize_right: float, + adv_resize_top: float, + adv_resize_bottom: float, + tone_map: bool, + re_sync: str, + comparison_count: int, + subtitle_color: str, + release_sub_title: str | None, + ): + self.source_file = source_file + self.source_node = None + self.reference_source_file = None + self.encode_file = encode_file + self.encode_node = None + self.image_dir = image_dir + self.indexer = indexer + self.index_dir = index_directory + self.sub_size = sub_size + self.left_crop = left_crop + self.right_crop = right_crop + self.top_crop = top_crop + self.bottom_crop = bottom_crop + self.adv_resize_left = adv_resize_left + self.adv_resize_right = adv_resize_right + self.adv_resize_top = adv_resize_top + self.adv_resize_bottom = adv_resize_bottom + self.tone_map = tone_map + self.re_sync = re_sync + self.comparison_count = comparison_count + self.subtitle_color = subtitle_color + self.release_sub_title = release_sub_title + + self.core = vs.core + self.load_plugins() + + def process_images(self): + if self.indexer == "lsmash": + self.index_lsmash() + + elif self.indexer == "ffms2": + self.index_ffms2() + + num_source_frames = len(self.source_node) + num_encode_frames = len(self.encode_node) + + # Format: Name, Fontname, Font-size, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, + # Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, + # MarginR, MarginV, + + # bgr color + color = "&H000ac7f5" + if self.subtitle_color: + color = hex_to_bgr(self.subtitle_color) + + selected_sub_style = ( + f"Segoe UI,{self.sub_size},{color},&H00000000,&H00000000,&H00000000," + "1,0,0,0,100,100,0,0,1,1,0,7,5,0,0,1" + ) + + self.check_de_interlaced(num_source_frames, num_encode_frames) + + b_frames = self.get_b_frames(num_source_frames) + + screenshot_comparison_dir, screenshot_sync_dir = self.generate_folders() + + self.handle_crop() + + self.handle_resize() + + self.handle_hdr() + + vs_source_info, vs_encode_info = self.handle_subtitles(selected_sub_style) + + img_job = self.generate_screens( + b_frames, + vs_source_info, + vs_encode_info, + screenshot_comparison_dir, + screenshot_sync_dir, + selected_sub_style, + ) + + return img_job + + @staticmethod + def screen_gen_callback(sg_call_back): + print( + str(sg_call_back).replace("ScreenGen: ", "").replace("\n", "").strip(), + flush=True, + ) + + def generate_screens( + self, + b_frames, + vs_source_info, + vs_encode_info, + screenshot_comparison_dir, + screenshot_sync_dir, + selected_sub_style, + ): + print("\nGenerating screenshots, please wait", flush=True) + + # handle re_sync if needed + sync_frames = [] + if self.re_sync: + get_sync_digits = re.search(r"(\d+)", self.re_sync) + sync_digits = int(get_sync_digits.group(1)) if get_sync_digits else 0 + for x_frames in b_frames: + if self.re_sync.startswith("-"): + sync_frames.append(int(x_frames) - sync_digits) + else: + sync_frames.append(int(x_frames) + sync_digits) + else: + sync_frames = b_frames + + # generate source images + awsmfunc.ScreenGen( + vs_source_info, + frame_numbers=sync_frames, + fpng_compression=1, + folder=screenshot_comparison_dir, + suffix="a_source__%d", + callback=self.screen_gen_callback, + ) + + # generate encode images + awsmfunc.ScreenGen( + vs_encode_info, + frame_numbers=b_frames, + fpng_compression=1, + folder=screenshot_comparison_dir, + suffix="b_encode__%d", + callback=self.screen_gen_callback, + ) + + # generate some sync frames + print("\nGenerating a few sync frames", flush=True) + + # select two frames randomly from list + sync_1 = choice(b_frames) + remove_sync1 = b_frames.copy() + remove_sync1.remove(sync_1) + sync_2 = choice(remove_sync1) + + # reference subs + vs_source_ref_info = self.core.sub.Subtitle( + clip=self.source_node, text="Sync", style=selected_sub_style + ) + vs_encode_ref_info = self.core.sub.Subtitle( + clip=self.encode_node, text="Reference", style=selected_sub_style + ) + + # generate screens for the two reference frames + awsmfunc.ScreenGen( + vs_encode_ref_info, + frame_numbers=[sync_1, sync_2], + fpng_compression=1, + folder=screenshot_sync_dir, + suffix="b_encode__%d", + callback=self.screen_gen_callback, + ) + + # generate 10 source frames around those reference frames + awsmfunc.ScreenGen( + vs_source_ref_info, + frame_numbers=[ + sync_1 - 4, + sync_1 - 3, + sync_1 - 2, + sync_1 - 1, + sync_1, + sync_1 + 1, + sync_1 + 2, + sync_1 + 3, + sync_1 + 4, + ], + fpng_compression=1, + folder=Path(Path(screenshot_sync_dir) / "sync1"), + suffix="a_source__%d", + callback=self.screen_gen_callback, + ) + + awsmfunc.ScreenGen( + vs_source_ref_info, + frame_numbers=[ + sync_2 - 4, + sync_2 - 3, + sync_2 - 2, + sync_2 - 1, + sync_2, + sync_2 + 1, + sync_2 + 2, + sync_2 + 3, + sync_2 + 4, + ], + fpng_compression=1, + folder=Path(Path(screenshot_sync_dir) / "sync2"), + suffix="a_source__%d", + callback=self.screen_gen_callback, + ) + + print("Screen generation completed", flush=True) + return str(screenshot_comparison_dir) + + def handle_subtitles(self, selected_sub_style): + vs_source_info = self.core.sub.Subtitle( + clip=self.source_node, text="Source", style=selected_sub_style + ) + vs_encode_info = awsmfunc.FrameInfo( + clip=self.encode_node, + title=self.release_sub_title if self.release_sub_title else "", + style=selected_sub_style, + ) + + return vs_source_info, vs_encode_info + + def handle_hdr(self): + if self.tone_map: + self.source_node = awsmfunc.DynamicTonemap( + clip=self.source_node, libplacebo=False + ) + self.encode_node = awsmfunc.DynamicTonemap( + clip=self.encode_node, + reference=self.reference_source_file, + libplacebo=False, + ) + + def handle_resize(self): + if ( + self.source_node.width != self.encode_node.width + and self.source_node.height != self.encode_node.height + or any( + [ + self.adv_resize_left, + self.adv_resize_right, + self.adv_resize_top, + self.adv_resize_bottom, + ] + ) + ): + # advanced resize offset vars + advanced_resize_left = self.adv_resize_left if self.adv_resize_left else 0 + advanced_resize_top = self.adv_resize_top if self.adv_resize_top else 0 + advanced_resize_width = ( + self.adv_resize_right if self.adv_resize_right else 0 + ) + advanced_resize_height = ( + self.adv_resize_bottom if self.adv_resize_bottom else 0 + ) + + # resize source to match encode for screenshots + self.source_node = self.core.resize.Spline36( + self.source_node, + width=int(self.encode_node.width), + height=int(self.encode_node.height), + src_left=advanced_resize_left, + src_top=advanced_resize_top, + src_width=float( + self.source_node.width + - (advanced_resize_left + advanced_resize_width) + ), + src_height=float( + self.source_node.height + - (advanced_resize_top + advanced_resize_height) + ), + dither_type="error_diffusion", + ) + + def handle_crop(self): + if any([self.left_crop, self.right_crop, self.top_crop, self.bottom_crop]): + self.source_node = self.core.std.Crop( + self.source_node, + left=self.left_crop if self.left_crop else 0, + right=self.right_crop if self.right_crop else 0, + top=self.top_crop if self.top_crop else 0, + bottom=self.bottom_crop if self.bottom_crop else 0, + ) + + def generate_folders(self): + print("\nCreating folders for images", flush=True) + if self.image_dir: + image_output_dir = Path(self.image_dir) + else: + image_output_dir = Path( + Path(self.encode_file).parent / f"{Path(self.encode_file).stem}_images" + ) + + # remove any accent characters from path + image_output_dir = Path(unidecode(str(image_output_dir))) + + # check if temp image dir exists, if so delete it! + if image_output_dir.exists(): + shutil.rmtree(image_output_dir, ignore_errors=True) + + # create main image dir + image_output_dir.mkdir(exist_ok=True, parents=True) + + # create comparison image directory and define it as variable + Path(Path(image_output_dir) / "img_comparison").mkdir(exist_ok=True) + screenshot_comparison_dir = str(Path(Path(image_output_dir) / "img_comparison")) + + # create selected image directory and define it as variable + Path(Path(image_output_dir) / "img_selected").mkdir(exist_ok=True) + + # create sync image directory and define it as variable + Path(Path(image_output_dir) / "img_sync").mkdir(exist_ok=True) + screenshot_sync_dir = str(Path(Path(image_output_dir) / "img_sync")) + + # create sub directories + Path(Path(image_output_dir) / "img_sync/sync1").mkdir(exist_ok=True) + Path(Path(image_output_dir) / "img_sync/sync2").mkdir(exist_ok=True) + + print("Folder creation completed", flush=True) + + return screenshot_comparison_dir, screenshot_sync_dir + + def get_b_frames(self, num_source_frames): + print( + f"\nGenerating {self.comparison_count} 'B' frames for " "comparison images", + flush=True, + ) + + b_frames = list( + linspace( + int(num_source_frames * 0.15), + int(num_source_frames * 0.75), + int(self.comparison_count), + ).astype(int) + ) + + try: + for i, frame in enumerate(b_frames): + while ( + self.encode_node.get_frame(frame).props["_PictType"].decode() != "B" + ): + frame += 1 + b_frames[i] = frame + except ValueError: + raise ImageGeneratorError( + "Error! Your encode file is likely an incomplete or corrupted encode" + ) + + print(f"Finished generating {self.comparison_count} 'B' frames", flush=True) + + return b_frames + + def check_de_interlaced(self, num_source_frames, num_encode_frames): + print("\nChecking if encode has been de-interlaced", flush=True) + try: + source_fps = float(self.source_node.fps) + encode_fps = float(self.encode_node.fps) + + if source_fps != encode_fps: + if num_source_frames == num_encode_frames: + self.source_node = self.core.std.AssumeFPS( + self.source_node, + fpsnum=self.encode_node.fps.numerator, + fpsden=self.encode_node.fps.denominator, + ) + print( + "Adjusting source fps to match the encode using AssumeFPS() on the source", + flush=True, + ) + else: + even_frames_for = "" + if num_source_frames != num_encode_frames: + file_differences = float(num_source_frames / num_encode_frames) + if file_differences > 1.01: + even_frames_for = "source" + self.source_node = self.core.std.SelectEvery( + self.source_node, cycle=2, offsets=0 + ) + elif file_differences < 0.99: + even_frames_for = "encode" + self.encode_node = self.core.std.SelectEvery( + self.encode_node, cycle=2, offsets=0 + ) + print( + f"Source: FPS={source_fps} Frames={num_source_frames}\n" + f"Encode: FPS={encode_fps} Frames={num_encode_frames}\n" + "Source vs Encode appears to be different, we're going to assume encode has been" + f" de-interlaced, automatically generating even frames for {even_frames_for}", + flush=True, + ) + else: + print("No de-interlacing detected", flush=True) + except ValueError: + print( + "There was an error while detecting source or encode fps, attempting to continue", + flush=True, + ) + + def index_lsmash(self): + print("Indexing source", flush=True) + + # index source file + # if index is found in the StaxRip temp working directory, attempt to use it + if ( + Path(str(Path(self.source_file).with_suffix("")) + "_temp/").is_dir() + and Path( + str(Path(self.source_file).with_suffix("")) + "_temp/temp.lwi" + ).is_file() + ): + print("Index found in StaxRip temp, attempting to use", flush=True) + + # define cache path + lwi_cache_path = Path( + str(Path(self.source_file).with_suffix("")) + "_temp/temp.lwi" + ) + + # try to use index on source file with the cache path + try: + self.source_node = self.core.lsmas.LWLibavSource( + source=self.source_file, cachefile=lwi_cache_path + ) + self.reference_source_file = self.core.lsmas.LWLibavSource( + source=self.source_file, cachefile=lwi_cache_path + ) + print("Using existing index", flush=True) + # if index cannot be used + except vs.Error: + print("L-Smash version miss-match, indexing source again", flush=True) + + # index source file + self.source_node = self.core.lsmas.LWLibavSource(self.source_file) + self.reference_source_file = self.core.lsmas.LWLibavSource( + self.source_file + ) + + # if no existing index is found index source file + else: + cache_path = Path(Path(self.source_file).with_suffix(".lwi")) + try: + # create index + self.source_node = self.core.lsmas.LWLibavSource( + self.source_file, cachefile=cache_path + ) + self.reference_source_file = self.core.lsmas.LWLibavSource( + self.source_file, cachefile=cache_path + ) + except vs.Error: + # delete index + Path(self.source_file).with_suffix(".lwi").unlink(missing_ok=True) + # create index + self.source_node = self.core.lsmas.LWLibavSource( + self.source_file, cachefile=cache_path + ) + self.reference_source_file = self.core.lsmas.LWLibavSource( + self.source_file, cachefile=cache_path + ) + + print("Source index completed\n\nIndexing encode", flush=True) + + # define a path for encode index to go + if self.index_dir: + index_base_path = Path(self.index_dir) / Path(self.encode_file).name + cache_path_enc = index_base_path.with_suffix(".lwi") + else: + cache_path_enc = Path(Path(self.encode_file).with_suffix(".lwi")) + + try: + # create index + self.encode_node = self.core.lsmas.LWLibavSource( + self.encode_file, cachefile=cache_path_enc + ) + except vs.Error: + # delete index + cache_path_enc.unlink(missing_ok=True) + # create index + self.encode_node = self.core.lsmas.LWLibavSource( + self.encode_file, cachefile=cache_path_enc + ) + + print("Encode index completed", flush=True) + + def index_ffms2(self): + print("Indexing source", flush=True) + + # index source file + # if index is found in the StaxRip temp working directory, attempt to use it + if ( + Path(str(Path(self.source_file).with_suffix("")) + "_temp/").is_dir() + and Path( + str(Path(self.source_file).with_suffix("")) + "_temp/temp.ffindex" + ).is_file() + ): + print("Index found in StaxRip temp, attempting to use", flush=True) + + # define cache path + ffindex_cache_path = Path( + str(Path(self.source_file).with_suffix("")) + "_temp/temp.ffindex" + ) + + # try to use index on source file with the cache path + try: + self.source_node = self.core.ffms2.Source( + source=self.source_file, cachefile=ffindex_cache_path + ) + self.reference_source_file = self.core.ffms2.Source( + source=self.source_file, cachefile=ffindex_cache_path + ) + print("Using existing index", flush=True) + # if index cannot be used + except vs.Error: + print("FFMS2 version miss-match, indexing source again", flush=True) + + # index source file + self.source_node = self.core.ffms2.Source(self.source_file) + self.reference_source_file = self.core.ffms2.Source(self.source_file) + + # if no existing index is found index source file + else: + try: + # create index + print( + "FFMS2 library doesn't allow progress, please wait while the index is completed", + flush=True, + ) + self.source_node = self.core.ffms2.Source(self.source_file) + self.reference_source_file = self.core.ffms2.Source(self.source_file) + except vs.Error: + # delete index + Path(self.source_file).with_suffix(".ffindex").unlink(missing_ok=True) + # create index + print( + "FFMS2 library doesn't allow progress, please wait while the index is completed", + flush=True, + ) + self.source_node = self.core.ffms2.Source(self.source_file) + self.reference_source_file = self.core.ffms2.Source(self.source_file) + + print("Source index completed\n\nIndexing encode", flush=True) + + # define a path for encode index to go + if self.index_dir: + index_base_path = Path(self.index_dir) / Path(self.encode_file).name + cache_path_enc = Path(str(index_base_path) + ".ffindex") + else: + cache_path_enc = Path(self.encode_file + ".ffindex") + + try: + self.encode_node = self.core.ffms2.Source( + self.encode_file, cachefile=cache_path_enc + ) + except vs.Error: + cache_path_enc.unlink(missing_ok=True) + self.encode_node = self.core.ffms2.Source( + self.encode_file, cachefile=cache_path_enc + ) + + print("Encode index completed", flush=True) + + def load_plugins(self): + plugin_path = get_working_dir() / "img_plugins" + if not plugin_path.is_dir() and not plugin_path.exists(): + raise ImageGeneratorError("Can not detect plugin directory") + else: + for plugin in plugin_path.glob("*.dll"): + self.core.std.LoadPlugin(Path(plugin).resolve()) diff --git a/image_generator/exceptions.py b/image_generator/exceptions.py new file mode 100644 index 0000000..cbc4cd1 --- /dev/null +++ b/image_generator/exceptions.py @@ -0,0 +1,2 @@ +class ImageGeneratorError(Exception): + """Handle all exceptions thrown by image generation""" diff --git a/image_generator/utils.py b/image_generator/utils.py new file mode 100644 index 0000000..25d3ba9 --- /dev/null +++ b/image_generator/utils.py @@ -0,0 +1,48 @@ +import sys +from pathlib import Path + + +def exit_application(msg: str, exit_code: int = 0): + """A clean way to exit the program without raising traceback errors + + Args: + msg (str): Success or Error message you'd like to display in the console + exit_code (int): Can either be 0 (success) or 1 (fail) + """ + if exit_code not in {0, 1}: + raise ValueError("exit_code must only be '0' or '1' (int)") + + if exit_code == 0: + output = sys.stdout + elif exit_code == 1: + output = sys.stderr + + print(msg, file=output, flush=True) + sys.exit(exit_code) + + +def get_working_dir(): + """ + Used to determine the correct working directory automatically. + This way we can utilize files/relative paths easily. + + Returns: + (Path): Current working directory + """ + # we're in a pyinstaller bundle + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + return Path(sys.executable).parent + + # we're running from a *.py file + else: + return Path.cwd() + + +def hex_to_bgr(hex_code): + # Remove the '#' if present + hex_code = hex_code.lstrip("#") + + # Convert hex to BGR format + bgr_values = f"&H{int(hex_code[4:6], 16):02X}{int(hex_code[2:4], 16):02X}{int(hex_code[0:2], 16):02X}" + + return bgr_values diff --git a/images/icon.ico b/images/icon.ico new file mode 100644 index 0000000..f03dfc0 Binary files /dev/null and b/images/icon.ico differ diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..712e170 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,349 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "altgraph" +version = "0.17.4" +description = "Python graph (network) package" +optional = false +python-versions = "*" +files = [ + {file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"}, + {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, +] + +[[package]] +name = "awsmfunc" +version = "1.3.4" +description = "awesome VapourSynth functions" +optional = false +python-versions = ">=3.9" +files = [ + {file = "awsmfunc-1.3.4-py3-none-any.whl", hash = "sha256:d9ce9cf90dfdb66b4561a5d3b011232e663ad0d879e2a276827bff9b8b3b37e1"}, + {file = "awsmfunc-1.3.4.tar.gz", hash = "sha256:8330332f5c4818322b4090b24499b1dc4e4e371460de70c4bd62a112f4157255"}, +] + +[package.dependencies] +numpy = "*" +VapourSynth = ">=57" +vs-rekt = ">=1.0.0" +vsutil = ">=0.7.0" + +[package.extras] +dev = ["pylint", "toml", "yapf"] + +[[package]] +name = "black" +version = "23.10.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, + {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, + {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, + {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, + {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, + {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"}, + {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"}, + {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, + {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, + {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, + {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, + {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "macholib" +version = "1.16.3" +description = "Mach-O header analysis and editing" +optional = false +python-versions = "*" +files = [ + {file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"}, + {file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"}, +] + +[package.dependencies] +altgraph = ">=0.17" + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "numpy" +version = "1.26.2" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3703fc9258a4a122d17043e57b35e5ef1c5a5837c3db8be396c82e04c1cf9b0f"}, + {file = "numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc392fdcbd21d4be6ae1bb4475a03ce3b025cd49a9be5345d76d7585aea69440"}, + {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36340109af8da8805d8851ef1d74761b3b88e81a9bd80b290bbfed61bd2b4f75"}, + {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc008217145b3d77abd3e4d5ef586e3bdfba8fe17940769f8aa09b99e856c00"}, + {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ced40d4e9e18242f70dd02d739e44698df3dcb010d31f495ff00a31ef6014fe"}, + {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b272d4cecc32c9e19911891446b72e986157e6a1809b7b56518b4f3755267523"}, + {file = "numpy-1.26.2-cp310-cp310-win32.whl", hash = "sha256:22f8fc02fdbc829e7a8c578dd8d2e15a9074b630d4da29cda483337e300e3ee9"}, + {file = "numpy-1.26.2-cp310-cp310-win_amd64.whl", hash = "sha256:26c9d33f8e8b846d5a65dd068c14e04018d05533b348d9eaeef6c1bd787f9919"}, + {file = "numpy-1.26.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b96e7b9c624ef3ae2ae0e04fa9b460f6b9f17ad8b4bec6d7756510f1f6c0c841"}, + {file = "numpy-1.26.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa18428111fb9a591d7a9cc1b48150097ba6a7e8299fb56bdf574df650e7d1f1"}, + {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06fa1ed84aa60ea6ef9f91ba57b5ed963c3729534e6e54055fc151fad0423f0a"}, + {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ca5482c3dbdd051bcd1fce8034603d6ebfc125a7bd59f55b40d8f5d246832b"}, + {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:854ab91a2906ef29dc3925a064fcd365c7b4da743f84b123002f6139bcb3f8a7"}, + {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f43740ab089277d403aa07567be138fc2a89d4d9892d113b76153e0e412409f8"}, + {file = "numpy-1.26.2-cp311-cp311-win32.whl", hash = "sha256:a2bbc29fcb1771cd7b7425f98b05307776a6baf43035d3b80c4b0f29e9545186"}, + {file = "numpy-1.26.2-cp311-cp311-win_amd64.whl", hash = "sha256:2b3fca8a5b00184828d12b073af4d0fc5fdd94b1632c2477526f6bd7842d700d"}, + {file = "numpy-1.26.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a4cd6ed4a339c21f1d1b0fdf13426cb3b284555c27ac2f156dfdaaa7e16bfab0"}, + {file = "numpy-1.26.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d5244aabd6ed7f312268b9247be47343a654ebea52a60f002dc70c769048e75"}, + {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3cdb4d9c70e6b8c0814239ead47da00934666f668426fc6e94cce869e13fd7"}, + {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa317b2325f7aa0a9471663e6093c210cb2ae9c0ad824732b307d2c51983d5b6"}, + {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:174a8880739c16c925799c018f3f55b8130c1f7c8e75ab0a6fa9d41cab092fd6"}, + {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f79b231bf5c16b1f39c7f4875e1ded36abee1591e98742b05d8a0fb55d8a3eec"}, + {file = "numpy-1.26.2-cp312-cp312-win32.whl", hash = "sha256:4a06263321dfd3598cacb252f51e521a8cb4b6df471bb12a7ee5cbab20ea9167"}, + {file = "numpy-1.26.2-cp312-cp312-win_amd64.whl", hash = "sha256:b04f5dc6b3efdaab541f7857351aac359e6ae3c126e2edb376929bd3b7f92d7e"}, + {file = "numpy-1.26.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4eb8df4bf8d3d90d091e0146f6c28492b0be84da3e409ebef54349f71ed271ef"}, + {file = "numpy-1.26.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a13860fdcd95de7cf58bd6f8bc5a5ef81c0b0625eb2c9a783948847abbef2c2"}, + {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64308ebc366a8ed63fd0bf426b6a9468060962f1a4339ab1074c228fa6ade8e3"}, + {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf8aab04a2c0e859da118f0b38617e5ee65d75b83795055fb66c0d5e9e9b818"}, + {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d73a3abcac238250091b11caef9ad12413dab01669511779bc9b29261dd50210"}, + {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b361d369fc7e5e1714cf827b731ca32bff8d411212fccd29ad98ad622449cc36"}, + {file = "numpy-1.26.2-cp39-cp39-win32.whl", hash = "sha256:bd3f0091e845164a20bd5a326860c840fe2af79fa12e0469a12768a3ec578d80"}, + {file = "numpy-1.26.2-cp39-cp39-win_amd64.whl", hash = "sha256:2beef57fb031dcc0dc8fa4fe297a742027b954949cabb52a2a376c144e5e6060"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1cc3d5029a30fb5f06704ad6b23b35e11309491c999838c31f124fee32107c79"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94cc3c222bb9fb5a12e334d0479b97bb2df446fbe622b470928f5284ffca3f8d"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe6b44fb8fcdf7eda4ef4461b97b3f63c466b27ab151bec2366db8b197387841"}, + {file = "numpy-1.26.2.tar.gz", hash = "sha256:f65738447676ab5777f11e6bbbdb8ce11b785e105f690bc45966574816b6d3ea"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "pefile" +version = "2023.2.7" +description = "Python PE parsing module" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, + {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, +] + +[[package]] +name = "platformdirs" +version = "3.11.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pyinstaller" +version = "6.3.0" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +optional = false +python-versions = "<3.13,>=3.8" +files = [] +develop = true + +[package.dependencies] +altgraph = "*" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +packaging = ">=22.0" +pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2021.4" +pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} +setuptools = ">=42.0.0" + +[package.extras] +completion = ["argcomplete"] +hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] + +[package.source] +type = "directory" +url = "custom-pyinstaller" + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2023.11" +description = "Community maintained hooks for PyInstaller" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyinstaller-hooks-contrib-2023.11.tar.gz", hash = "sha256:5dd7a8a054a65c19cdaa381cabcfbe76f44d5f88d18214b0c570a0cd139be77f"}, + {file = "pyinstaller_hooks_contrib-2023.11-py2.py3-none-any.whl", hash = "sha256:f2a75dac2968ec81f92dcd3768906f654fa4204bc496126ae8483e87a5d89602"}, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.2" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, + {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, +] + +[[package]] +name = "setuptools" +version = "69.0.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "unidecode" +version = "1.3.7" +description = "ASCII transliterations of Unicode text" +optional = false +python-versions = ">=3.5" +files = [ + {file = "Unidecode-1.3.7-py3-none-any.whl", hash = "sha256:663a537f506834ed836af26a81b210d90cbde044c47bfbdc0fbbc9f94c86a6e4"}, + {file = "Unidecode-1.3.7.tar.gz", hash = "sha256:3c90b4662aa0de0cb591884b934ead8d2225f1800d8da675a7750cbc3bd94610"}, +] + +[[package]] +name = "vapoursynth" +version = "64" +description = "A frameserver for the 21st century" +optional = false +python-versions = "*" +files = [ + {file = "VapourSynth-64-cp311-cp311-win_amd64.whl", hash = "sha256:ad046a704537276f7ebb4132e1210e8922ec4b2403a4b44f5fe30fd9456a6412"}, + {file = "VapourSynth-64-cp38-cp38-win_amd64.whl", hash = "sha256:794f93bcaa1ce79510c11962f677a7d18685a0c29aa36586cb307d1eb9d7f2e0"}, + {file = "VapourSynth-64.zip", hash = "sha256:29425a135ca68cbb17b3dfcad0097375b75f94af1f499ba89bcd2b03b2651846"}, +] + +[[package]] +name = "vapoursynth-portable" +version = "64" +description = "A frameserver for the 21st century" +optional = false +python-versions = "*" +files = [ + {file = "VapourSynth_portable-64-py2.py3-none-win_amd64.whl", hash = "sha256:82b26b38774197a7ef53cbd3206bafb02d197100fad513d635bf83f9981dad6c"}, +] + +[package.dependencies] +vapoursynth = "64" + +[[package]] +name = "vs-rekt" +version = "1.0.0" +description = "VapourSynth wrapper for Cropping and Stacking clips." +optional = false +python-versions = "*" +files = [ + {file = "vs-rekt-1.0.0.tar.gz", hash = "sha256:24fbc0b577074e841b80c7d02b7ab2c88c1a5c703276d37dea530d6ba109ae31"}, + {file = "vs_rekt-1.0.0-py3-none-any.whl", hash = "sha256:e217d00c5bdb746d5124f9a10a50e90fc0f1f170bef02c89a4cb036547d2d132"}, +] + +[package.dependencies] +VapourSynth = ">=57" +vsutil = ">=0.7.0" + +[[package]] +name = "vsutil" +version = "0.8.0" +description = "A collection of general-purpose Vapoursynth functions to be reused in modules and scripts." +optional = false +python-versions = ">=3.8" +files = [ + {file = "vsutil-0.8.0-py3-none-any.whl", hash = "sha256:b14751680f1aa47a8671a943932e160fc3b6029b3e60227875fd73d7a47dac86"}, + {file = "vsutil-0.8.0.tar.gz", hash = "sha256:e01831203dfb9c3af86a101fba5e7fe04e3686bddec94bb3057ca73e84c98768"}, +] + +[package.dependencies] +vapoursynth = "*" + +[metadata] +lock-version = "2.0" +python-versions = "3.11.5" +content-hash = "820bfb62c5fba1b17221bc1a02144932dbce64fa44c698f24f7205c2fc4a3d18" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a77b4bc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[tool.poetry] +name = "image-generator" +version = "1.1.0" +description = "CLI to offload image generation to" +authors = ["jlw4049 "] +license = "MIT" +readme = "README.md" + +[tool.poetry.dependencies] +python = "3.11.5" +awsmfunc = "^1.3.4" +unidecode = "^1.3.7" +vapoursynth-portable = "64" +numpy = "^1.26.2" + + +[tool.poetry.group.dev.dependencies] +black = "^23.10.1" +pyinstaller = {path = "custom-pyinstaller", develop = true} + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api"