From bfda833bcf2b211f6a2771b1bd53f137ed86c917 Mon Sep 17 00:00:00 2001 From: K4YT3X Date: Sat, 19 Dec 2020 18:11:11 -0500 Subject: [PATCH 1/4] formatted code with black --- src/bilogger.py | 9 +- src/image_cleaner.py | 10 +- src/progress_monitor.py | 20 +- src/upscaler.py | 608 ++++++--- src/video2x.py | 195 ++- src/video2x_gui.py | 1702 +++++++++++++++++-------- src/wrappers/anime4kcpp.py | 91 +- src/wrappers/ffmpeg.py | 206 +-- src/wrappers/gifski.py | 35 +- src/wrappers/realsr_ncnn_vulkan.py | 54 +- src/wrappers/srmd_ncnn_vulkan.py | 56 +- src/wrappers/waifu2x_caffe.py | 79 +- src/wrappers/waifu2x_converter_cpp.py | 84 +- src/wrappers/waifu2x_ncnn_vulkan.py | 58 +- 14 files changed, 2117 insertions(+), 1090 deletions(-) diff --git a/src/bilogger.py b/src/bilogger.py index a97ceb7..bbc73d1 100755 --- a/src/bilogger.py +++ b/src/bilogger.py @@ -12,14 +12,14 @@ import io class BiLogger(io.TextIOWrapper): - """ A bidirectional logger that both prints the output + """A bidirectional logger that both prints the output and log all output to file. Original code from: https://stackoverflow.com/a/14906787 """ def __init__(self, terminal: io.TextIOWrapper, log_file: io.BufferedRandom): - """ initialize BiLogger + """initialize BiLogger Args: terminal (_io.TextIOWrapper): original terminal IO wrapper @@ -30,7 +30,7 @@ class BiLogger(io.TextIOWrapper): self.fileno = self.log_file.fileno def write(self, message: str): - """ write message to original terminal output and log file + """write message to original terminal output and log file Args: message (str): message to write @@ -41,6 +41,5 @@ class BiLogger(io.TextIOWrapper): self.log_file.flush() def flush(self): - """ flush logger (for compability only) - """ + """flush logger (for compability only)""" pass diff --git a/src/image_cleaner.py b/src/image_cleaner.py index afec7f9..087ce4f 100755 --- a/src/image_cleaner.py +++ b/src/image_cleaner.py @@ -22,7 +22,7 @@ import time class ImageCleaner(threading.Thread): - """ Video2X Image Cleaner + """Video2X Image Cleaner This class creates an object that keeps track of extracted frames that has already been upscaled and are not needed @@ -40,8 +40,7 @@ class ImageCleaner(threading.Thread): self.running = False def run(self): - """ Run image cleaner - """ + """Run image cleaner""" self.running = True while self.running: @@ -49,13 +48,12 @@ class ImageCleaner(threading.Thread): time.sleep(1) def stop(self): - """ Stop the image cleaner - """ + """Stop the image cleaner""" self.running = False self.join() def remove_upscaled_frames(self): - """ remove frames that have already been upscaled + """remove frames that have already been upscaled This method compares the files in the extracted frames directory with the upscaled frames directory, and removes diff --git a/src/progress_monitor.py b/src/progress_monitor.py index 1c8e7f2..2a8f0a0 100755 --- a/src/progress_monitor.py +++ b/src/progress_monitor.py @@ -17,7 +17,7 @@ from tqdm import tqdm class ProgressMonitor(threading.Thread): - """ progress monitor + """progress monitor This class provides progress monitoring functionalities by keeping track of the amount of frames in the input @@ -34,7 +34,15 @@ class ProgressMonitor(threading.Thread): def run(self): self.running = True - with tqdm(total=self.upscaler.total_frames, ascii=True, desc=_('Processing: {} (pass {}/{})').format(self.upscaler.current_input_file.name, self.upscaler.current_pass, len(self.upscaler.scaling_jobs))) as progress_bar: + with tqdm( + total=self.upscaler.total_frames, + ascii=True, + desc=_("Processing: {} (pass {}/{})").format( + self.upscaler.current_input_file.name, + self.upscaler.current_pass, + len(self.upscaler.scaling_jobs), + ), + ) as progress_bar: # tqdm update method adds the value to the progress # bar instead of setting the value. Therefore, a delta # needs to be calculated. @@ -42,7 +50,13 @@ class ProgressMonitor(threading.Thread): while self.running: with contextlib.suppress(FileNotFoundError): - upscaled_frames = [f for f in self.upscaler.upscaled_frames.iterdir() if str(f).lower().endswith(self.upscaler.extracted_frame_format.lower())] + upscaled_frames = [ + f + for f in self.upscaler.upscaled_frames.iterdir() + if str(f) + .lower() + .endswith(self.upscaler.extracted_frame_format.lower()) + ] if len(upscaled_frames) >= 1: self.upscaler.last_frame_upscaled = sorted(upscaled_frames)[-1] self.upscaler.total_frames_upscaled = len(upscaled_frames) diff --git a/src/upscaler.py b/src/upscaler.py index 306b9bc..9688a2b 100755 --- a/src/upscaler.py +++ b/src/upscaler.py @@ -43,40 +43,44 @@ from tqdm import tqdm import magic # internationalization constants -DOMAIN = 'video2x' -LOCALE_DIRECTORY = pathlib.Path(__file__).parent.absolute() / 'locale' +DOMAIN = "video2x" +LOCALE_DIRECTORY = pathlib.Path(__file__).parent.absolute() / "locale" # getting default locale settings default_locale, encoding = locale.getdefaultlocale() -language = gettext.translation(DOMAIN, LOCALE_DIRECTORY, [default_locale], fallback=True) +language = gettext.translation( + DOMAIN, LOCALE_DIRECTORY, [default_locale], fallback=True +) language.install() _ = language.gettext # version information -UPSCALER_VERSION = '4.4.1' +UPSCALER_VERSION = "4.4.1" # these names are consistent for # - driver selection in command line # - driver wrapper file names # - config file keys -AVAILABLE_DRIVERS = ['waifu2x_caffe', - 'waifu2x_converter_cpp', - 'waifu2x_ncnn_vulkan', - 'srmd_ncnn_vulkan', - 'realsr_ncnn_vulkan', - 'anime4kcpp'] +AVAILABLE_DRIVERS = [ + "waifu2x_caffe", + "waifu2x_converter_cpp", + "waifu2x_ncnn_vulkan", + "srmd_ncnn_vulkan", + "realsr_ncnn_vulkan", + "anime4kcpp", +] # fixed scaling ratios supported by the drivers # that only support certain fixed scale ratios DRIVER_FIXED_SCALING_RATIOS = { - 'waifu2x_ncnn_vulkan': [1, 2], - 'srmd_ncnn_vulkan': [2, 3, 4], - 'realsr_ncnn_vulkan': [4], + "waifu2x_ncnn_vulkan": [1, 2], + "srmd_ncnn_vulkan": [2, 3, 4], + "realsr_ncnn_vulkan": [4], } class Upscaler: - """ An instance of this class is a upscaler that will + """An instance of this class is a upscaler that will upscale all images in the given directory. Raises: @@ -91,17 +95,18 @@ class Upscaler: driver_settings: dict, ffmpeg_settings: dict, gifski_settings: dict, - driver: str = 'waifu2x_caffe', + driver: str = "waifu2x_caffe", scale_ratio: float = None, scale_width: int = None, scale_height: int = None, processes: int = 1, - video2x_cache_directory: pathlib.Path = pathlib.Path(tempfile.gettempdir()) / 'video2x', - extracted_frame_format: str = 'png', - output_file_name_format_string: str = '{original_file_name}_output{extension}', - image_output_extension: str = '.png', - video_output_extension: str = '.mp4', - preserve_frames: bool = False + video2x_cache_directory: pathlib.Path = pathlib.Path(tempfile.gettempdir()) + / "video2x", + extracted_frame_format: str = "png", + output_file_name_format_string: str = "{original_file_name}_output{extension}", + image_output_extension: str = ".png", + video_output_extension: str = ".mp4", + preserve_frames: bool = False, ): # required parameters @@ -137,109 +142,155 @@ class Upscaler: self.last_frame_upscaled = pathlib.Path() def create_temp_directories(self): - """create temporary directories - """ + """create temporary directories""" # if cache directory unspecified, use %TEMP%\video2x if self.video2x_cache_directory is None: - self.video2x_cache_directory = pathlib.Path(tempfile.gettempdir()) / 'video2x' + self.video2x_cache_directory = ( + pathlib.Path(tempfile.gettempdir()) / "video2x" + ) # if specified cache path exists and isn't a directory - if self.video2x_cache_directory.exists() and not self.video2x_cache_directory.is_dir(): - Avalon.error(_('Specified or default cache directory is a file/link')) - raise FileExistsError('Specified or default cache directory is a file/link') + if ( + self.video2x_cache_directory.exists() + and not self.video2x_cache_directory.is_dir() + ): + Avalon.error(_("Specified or default cache directory is a file/link")) + raise FileExistsError("Specified or default cache directory is a file/link") # if cache directory doesn't exist, try creating it if not self.video2x_cache_directory.exists(): try: - Avalon.debug_info(_('Creating cache directory {}').format(self.video2x_cache_directory)) + Avalon.debug_info( + _("Creating cache directory {}").format( + self.video2x_cache_directory + ) + ) self.video2x_cache_directory.mkdir(parents=True, exist_ok=True) except Exception as exception: - Avalon.error(_('Unable to create {}').format(self.video2x_cache_directory)) + Avalon.error( + _("Unable to create {}").format(self.video2x_cache_directory) + ) raise exception # create temp directories for extracted frames and upscaled frames - self.extracted_frames = pathlib.Path(tempfile.mkdtemp(dir=self.video2x_cache_directory)) - Avalon.debug_info(_('Extracted frames are being saved to: {}').format(self.extracted_frames)) - self.upscaled_frames = pathlib.Path(tempfile.mkdtemp(dir=self.video2x_cache_directory)) - Avalon.debug_info(_('Upscaled frames are being saved to: {}').format(self.upscaled_frames)) + self.extracted_frames = pathlib.Path( + tempfile.mkdtemp(dir=self.video2x_cache_directory) + ) + Avalon.debug_info( + _("Extracted frames are being saved to: {}").format(self.extracted_frames) + ) + self.upscaled_frames = pathlib.Path( + tempfile.mkdtemp(dir=self.video2x_cache_directory) + ) + Avalon.debug_info( + _("Upscaled frames are being saved to: {}").format(self.upscaled_frames) + ) def cleanup_temp_directories(self): - """delete temp directories when done - """ + """delete temp directories when done""" if not self.preserve_frames: - for directory in [self.extracted_frames, self.upscaled_frames, self.video2x_cache_directory]: + for directory in [ + self.extracted_frames, + self.upscaled_frames, + self.video2x_cache_directory, + ]: try: # avalon framework cannot be used if python is shutting down # therefore, plain print is used - print(_('Cleaning up cache directory: {}').format(directory)) + print(_("Cleaning up cache directory: {}").format(directory)) shutil.rmtree(directory) except FileNotFoundError: pass except OSError: - print(_('Unable to delete: {}').format(directory)) + print(_("Unable to delete: {}").format(directory)) traceback.print_exc() def _check_arguments(self): if isinstance(self.input, list): if self.output.exists() and not self.output.is_dir(): - Avalon.error(_('Input and output path type mismatch')) - Avalon.error(_('Input is multiple files but output is not directory')) - raise ArgumentError('input output path type mismatch') + Avalon.error(_("Input and output path type mismatch")) + Avalon.error(_("Input is multiple files but output is not directory")) + raise ArgumentError("input output path type mismatch") for input_path in self.input: if not input_path.is_file() and not input_path.is_dir(): - Avalon.error(_('Input path {} is neither a file nor a directory').format(input_path)) - raise FileNotFoundError(f'{input_path} is neither file nor directory') + Avalon.error( + _("Input path {} is neither a file nor a directory").format( + input_path + ) + ) + raise FileNotFoundError( + f"{input_path} is neither file nor directory" + ) with contextlib.suppress(FileNotFoundError): if input_path.samefile(self.output): - Avalon.error(_('Input directory and output directory cannot be the same')) - raise FileExistsError('input directory and output directory are the same') + Avalon.error( + _("Input directory and output directory cannot be the same") + ) + raise FileExistsError( + "input directory and output directory are the same" + ) # if input is a file elif self.input.is_file(): if self.output.is_dir(): - Avalon.error(_('Input and output path type mismatch')) - Avalon.error(_('Input is single file but output is directory')) - raise ArgumentError('input output path type mismatch') - if self.output.suffix == '': - Avalon.error(_('No suffix found in output file path')) - Avalon.error(_('Suffix must be specified')) - raise ArgumentError('no output file suffix specified') + Avalon.error(_("Input and output path type mismatch")) + Avalon.error(_("Input is single file but output is directory")) + raise ArgumentError("input output path type mismatch") + if self.output.suffix == "": + Avalon.error(_("No suffix found in output file path")) + Avalon.error(_("Suffix must be specified")) + raise ArgumentError("no output file suffix specified") # if input is a directory elif self.input.is_dir(): if self.output.is_file(): - Avalon.error(_('Input and output path type mismatch')) - Avalon.error(_('Input is directory but output is existing single file')) - raise ArgumentError('input output path type mismatch') + Avalon.error(_("Input and output path type mismatch")) + Avalon.error(_("Input is directory but output is existing single file")) + raise ArgumentError("input output path type mismatch") with contextlib.suppress(FileNotFoundError): if self.input.samefile(self.output): - Avalon.error(_('Input directory and output directory cannot be the same')) - raise FileExistsError('input directory and output directory are the same') + Avalon.error( + _("Input directory and output directory cannot be the same") + ) + raise FileExistsError( + "input directory and output directory are the same" + ) # if input is neither else: - Avalon.error(_('Input path is neither a file nor a directory')) - raise FileNotFoundError(f'{self.input} is neither file nor directory') + Avalon.error(_("Input path is neither a file nor a directory")) + raise FileNotFoundError(f"{self.input} is neither file nor directory") # check FFmpeg settings - ffmpeg_path = pathlib.Path(self.ffmpeg_settings['ffmpeg_path']) - if not ((pathlib.Path(ffmpeg_path / 'ffmpeg.exe').is_file() and - pathlib.Path(ffmpeg_path / 'ffprobe.exe').is_file()) or - (pathlib.Path(ffmpeg_path / 'ffmpeg').is_file() and - pathlib.Path(ffmpeg_path / 'ffprobe').is_file())): - Avalon.error(_('FFmpeg or FFprobe cannot be found under the specified path')) - Avalon.error(_('Please check the configuration file settings')) - raise FileNotFoundError(self.ffmpeg_settings['ffmpeg_path']) + ffmpeg_path = pathlib.Path(self.ffmpeg_settings["ffmpeg_path"]) + if not ( + ( + pathlib.Path(ffmpeg_path / "ffmpeg.exe").is_file() + and pathlib.Path(ffmpeg_path / "ffprobe.exe").is_file() + ) + or ( + pathlib.Path(ffmpeg_path / "ffmpeg").is_file() + and pathlib.Path(ffmpeg_path / "ffprobe").is_file() + ) + ): + Avalon.error( + _("FFmpeg or FFprobe cannot be found under the specified path") + ) + Avalon.error(_("Please check the configuration file settings")) + raise FileNotFoundError(self.ffmpeg_settings["ffmpeg_path"]) # check if driver settings driver_settings = copy.deepcopy(self.driver_settings) - driver_path = driver_settings.pop('path') + driver_path = driver_settings.pop("path") # check if driver path exists - if not (pathlib.Path(driver_path).is_file() or pathlib.Path(f'{driver_path}.exe').is_file()): - Avalon.error(_('Specified driver executable directory doesn\'t exist')) - Avalon.error(_('Please check the configuration file settings')) + if not ( + pathlib.Path(driver_path).is_file() + or pathlib.Path(f"{driver_path}.exe").is_file() + ): + Avalon.error(_("Specified driver executable directory doesn't exist")) + Avalon.error(_("Please check the configuration file settings")) raise FileNotFoundError(driver_path) # parse driver arguments using driver's parser @@ -255,21 +306,25 @@ class Upscaler: else: if len(key) == 1: - driver_arguments.append(f'-{key}') + driver_arguments.append(f"-{key}") else: - driver_arguments.append(f'--{key}') + driver_arguments.append(f"--{key}") # true means key is an option if value is not True: driver_arguments.append(str(value)) - DriverWrapperMain = getattr(importlib.import_module(f'wrappers.{self.driver}'), 'WrapperMain') + DriverWrapperMain = getattr( + importlib.import_module(f"wrappers.{self.driver}"), "WrapperMain" + ) DriverWrapperMain.parse_arguments(driver_arguments) except AttributeError as e: - Avalon.error(_('Failed to parse driver argument: {}').format(e.args[0])) + Avalon.error(_("Failed to parse driver argument: {}").format(e.args[0])) raise e - def _upscale_frames(self, input_directory: pathlib.Path, output_directory: pathlib.Path): - """ Upscale video frames with waifu2x-caffe + def _upscale_frames( + self, input_directory: pathlib.Path, output_directory: pathlib.Path + ): + """Upscale video frames with waifu2x-caffe This function upscales all the frames extracted by ffmpeg using the waifu2x-caffe binary. @@ -285,7 +340,9 @@ class Upscaler: # initialize waifu2x driver if self.driver not in AVAILABLE_DRIVERS: - raise UnrecognizedDriverError(_('Unrecognized driver: {}').format(self.driver)) + raise UnrecognizedDriverError( + _("Unrecognized driver: {}").format(self.driver) + ) # list all images in the extracted frames frames = [(input_directory / f) for f in input_directory.iterdir() if f.is_file] @@ -308,7 +365,13 @@ class Upscaler: process_directory.mkdir(parents=True, exist_ok=True) # waifu2x-converter-cpp will perform multi-threading within its own process - if self.driver in ['waifu2x_converter_cpp', 'waifu2x_ncnn_vulkan', 'srmd_ncnn_vulkan', 'realsr_ncnn_vulkan', 'anime4kcpp']: + if self.driver in [ + "waifu2x_converter_cpp", + "waifu2x_ncnn_vulkan", + "srmd_ncnn_vulkan", + "realsr_ncnn_vulkan", + "anime4kcpp", + ]: process_directories = [input_directory] else: @@ -318,20 +381,26 @@ class Upscaler: # move image image.rename(process_directories[0] / image.name) # rotate list - process_directories = process_directories[-1:] + process_directories[:-1] + process_directories = ( + process_directories[-1:] + process_directories[:-1] + ) # create driver processes and start them for process_directory in process_directories: - self.process_pool.append(self.driver_object.upscale(process_directory, output_directory)) + self.process_pool.append( + self.driver_object.upscale(process_directory, output_directory) + ) # start progress bar in a different thread - Avalon.debug_info(_('Starting progress monitor')) + Avalon.debug_info(_("Starting progress monitor")) self.progress_monitor = ProgressMonitor(self, process_directories) self.progress_monitor.start() # create the clearer and start it - Avalon.debug_info(_('Starting upscaled image cleaner')) - self.image_cleaner = ImageCleaner(input_directory, output_directory, len(self.process_pool)) + Avalon.debug_info(_("Starting upscaled image cleaner")) + self.image_cleaner = ImageCleaner( + input_directory, output_directory, len(self.process_pool) + ) self.image_cleaner.start() # wait for all process to exit @@ -339,38 +408,39 @@ class Upscaler: self._wait() except (Exception, KeyboardInterrupt, SystemExit) as e: # cleanup - Avalon.debug_info(_('Killing progress monitor')) + Avalon.debug_info(_("Killing progress monitor")) self.progress_monitor.stop() - Avalon.debug_info(_('Killing upscaled image cleaner')) + Avalon.debug_info(_("Killing upscaled image cleaner")) self.image_cleaner.stop() raise e # if the driver is waifu2x-converter-cpp # images need to be renamed to be recognizable for FFmpeg - if self.driver == 'waifu2x_converter_cpp': + if self.driver == "waifu2x_converter_cpp": for image in [f for f in output_directory.iterdir() if f.is_file()]: - renamed = re.sub(f'_\\[.*\\]\\[x(\\d+(\\.\\d+)?)\\]\\.{self.extracted_frame_format}', - f'.{self.extracted_frame_format}', - str(image.name)) + renamed = re.sub( + f"_\\[.*\\]\\[x(\\d+(\\.\\d+)?)\\]\\.{self.extracted_frame_format}", + f".{self.extracted_frame_format}", + str(image.name), + ) (output_directory / image).rename(output_directory / renamed) # upscaling done, kill helper threads - Avalon.debug_info(_('Killing progress monitor')) + Avalon.debug_info(_("Killing progress monitor")) self.progress_monitor.stop() - Avalon.debug_info(_('Killing upscaled image cleaner')) + Avalon.debug_info(_("Killing upscaled image cleaner")) self.image_cleaner.stop() def _terminate_subprocesses(self): - Avalon.warning(_('Terminating all processes')) + Avalon.warning(_("Terminating all processes")) for process in self.process_pool: process.terminate() def _wait(self): - """ wait for subprocesses in process pool to complete - """ - Avalon.debug_info(_('Main process waiting for subprocesses to exit')) + """wait for subprocesses in process pool to complete""" + Avalon.debug_info(_("Main process waiting for subprocesses to exit")) try: # while process pool not empty @@ -389,27 +459,37 @@ class Upscaler: # if return code is not 0 elif process_status != 0: - Avalon.error(_('Subprocess {} exited with code {}').format(process.pid, process_status)) - raise subprocess.CalledProcessError(process_status, process.args) + Avalon.error( + _("Subprocess {} exited with code {}").format( + process.pid, process_status + ) + ) + raise subprocess.CalledProcessError( + process_status, process.args + ) else: - Avalon.debug_info(_('Subprocess {} exited with code {}').format(process.pid, process_status)) + Avalon.debug_info( + _("Subprocess {} exited with code {}").format( + process.pid, process_status + ) + ) self.process_pool.remove(process) time.sleep(0.1) except (KeyboardInterrupt, SystemExit) as e: - Avalon.warning(_('Stop signal received')) + Avalon.warning(_("Stop signal received")) self._terminate_subprocesses() raise e except (Exception, subprocess.CalledProcessError) as e: - Avalon.error(_('Subprocess execution ran into an error')) + Avalon.error(_("Subprocess execution ran into an error")) self._terminate_subprocesses() raise e def run(self): - """ Main controller for Video2X + """Main controller for Video2X This function controls the flow of video conversion and handles all necessary functions. @@ -422,20 +502,24 @@ class Upscaler: self.process_pool = [] # load driver modules - DriverWrapperMain = getattr(importlib.import_module(f'wrappers.{self.driver}'), 'WrapperMain') + DriverWrapperMain = getattr( + importlib.import_module(f"wrappers.{self.driver}"), "WrapperMain" + ) self.driver_object = DriverWrapperMain(self.driver_settings) # load options from upscaler class into driver settings self.driver_object.load_configurations(self) # initialize FFmpeg object - self.ffmpeg_object = Ffmpeg(self.ffmpeg_settings, extracted_frame_format=self.extracted_frame_format) + self.ffmpeg_object = Ffmpeg( + self.ffmpeg_settings, extracted_frame_format=self.extracted_frame_format + ) # define processing queue self.processing_queue = queue.Queue() - Avalon.info(_('Loading files into processing queue')) - Avalon.debug_info(_('Input path(s): {}').format(self.input)) + Avalon.info(_("Loading files into processing queue")) + Avalon.debug_info(_("Input path(s): {}").format(self.input)) # make output directory if the input is a list or a directory if isinstance(self.input, list) or self.input.is_dir(): @@ -470,39 +554,53 @@ class Upscaler: # get file type # try python-magic if it's available try: - input_file_mime_type = magic.from_file(str(input_path.absolute()), mime=True) - input_file_type = input_file_mime_type.split('/')[0] - input_file_subtype = input_file_mime_type.split('/')[1] + input_file_mime_type = magic.from_file( + str(input_path.absolute()), mime=True + ) + input_file_type = input_file_mime_type.split("/")[0] + input_file_subtype = input_file_mime_type.split("/")[1] except Exception: - input_file_mime_type = input_file_type = input_file_subtype = '' + input_file_mime_type = input_file_type = input_file_subtype = "" # if python-magic doesn't determine the file to be an image/video file # fall back to mimetypes to guess the file type based on the extension - if input_file_type not in ['image', 'video']: + if input_file_type not in ["image", "video"]: # in case python-magic fails to detect file type # try guessing file mime type with mimetypes input_file_mime_type = mimetypes.guess_type(input_path.name)[0] - input_file_type = input_file_mime_type.split('/')[0] - input_file_subtype = input_file_mime_type.split('/')[1] + input_file_type = input_file_mime_type.split("/")[0] + input_file_subtype = input_file_mime_type.split("/")[1] - Avalon.debug_info(_('File MIME type: {}').format(input_file_mime_type)) + Avalon.debug_info(_("File MIME type: {}").format(input_file_mime_type)) # set default output file suffixes # if image type is GIF, default output suffix is also .gif - if input_file_mime_type == 'image/gif': - output_path = self.output / self.output_file_name_format_string.format(original_file_name=input_path.stem, extension='.gif') + if input_file_mime_type == "image/gif": + output_path = self.output / self.output_file_name_format_string.format( + original_file_name=input_path.stem, extension=".gif" + ) - elif input_file_type == 'image': - output_path = self.output / self.output_file_name_format_string.format(original_file_name=input_path.stem, extension=self.image_output_extension) + elif input_file_type == "image": + output_path = self.output / self.output_file_name_format_string.format( + original_file_name=input_path.stem, + extension=self.image_output_extension, + ) - elif input_file_type == 'video': - output_path = self.output / self.output_file_name_format_string.format(original_file_name=input_path.stem, extension=self.video_output_extension) + elif input_file_type == "video": + output_path = self.output / self.output_file_name_format_string.format( + original_file_name=input_path.stem, + extension=self.video_output_extension, + ) # if file is none of: image, image/gif, video # skip to the next task else: - Avalon.error(_('File {} ({}) neither an image nor a video').format(input_path, input_file_mime_type)) - Avalon.warning(_('Skipping this file')) + Avalon.error( + _("File {} ({}) neither an image nor a video").format( + input_path, input_file_mime_type + ) + ) + Avalon.warning(_("Skipping this file")) continue # if there is only one input file @@ -512,14 +610,24 @@ class Upscaler: output_path_id = 0 while str(output_path) in output_paths: - output_path = output_path.parent / pathlib.Path(f'{output_path.stem}_{output_path_id}{output_path.suffix}') + output_path = output_path.parent / pathlib.Path( + f"{output_path.stem}_{output_path_id}{output_path.suffix}" + ) output_path_id += 1 # record output path output_paths.append(str(output_path)) # push file information into processing queue - self.processing_queue.put((input_path.absolute(), output_path.absolute(), input_file_mime_type, input_file_type, input_file_subtype)) + self.processing_queue.put( + ( + input_path.absolute(), + output_path.absolute(), + input_file_mime_type, + input_file_type, + input_file_subtype, + ) + ) # check argument sanity before running self._check_arguments() @@ -527,22 +635,28 @@ class Upscaler: # record file count for external calls self.total_files = self.processing_queue.qsize() - Avalon.info(_('Loaded files into processing queue')) + Avalon.info(_("Loaded files into processing queue")) # print all files in queue for debugging for job in self.processing_queue.queue: - Avalon.debug_info(_('Input file: {}').format(job[0].absolute())) + Avalon.debug_info(_("Input file: {}").format(job[0].absolute())) try: while not self.processing_queue.empty(): # get new job from queue - self.current_input_file, output_path, input_file_mime_type, input_file_type, input_file_subtype = self.processing_queue.get() + ( + self.current_input_file, + output_path, + input_file_mime_type, + input_file_type, + input_file_subtype, + ) = self.processing_queue.get() # get current job starting time for GUI calculations self.current_processing_starting_time = time.time() # get video information JSON using FFprobe - Avalon.info(_('Reading file information')) + Avalon.info(_("Reading file information")) file_info = self.ffmpeg_object.probe_file_info(self.current_input_file) # create temporary directories for storing frames @@ -550,50 +664,61 @@ class Upscaler: # start handling input # if input file is a static image - if input_file_type == 'image' and input_file_subtype != 'gif': - Avalon.info(_('Starting upscaling image')) + if input_file_type == "image" and input_file_subtype != "gif": + Avalon.info(_("Starting upscaling image")) # copy original file into the pre-processing directory - shutil.copy(self.current_input_file, self.extracted_frames / self.current_input_file.name) + shutil.copy( + self.current_input_file, + self.extracted_frames / self.current_input_file.name, + ) - width = int(file_info['streams'][0]['width']) - height = int(file_info['streams'][0]['height']) + width = int(file_info["streams"][0]["width"]) + height = int(file_info["streams"][0]["height"]) framerate = self.total_frames = 1 # elif input_file_mime_type == 'image/gif' or input_file_type == 'video': else: - Avalon.info(_('Starting upscaling video/GIF')) + Avalon.info(_("Starting upscaling video/GIF")) # find index of video stream video_stream_index = None - for stream in file_info['streams']: - if stream['codec_type'] == 'video': - video_stream_index = stream['index'] + for stream in file_info["streams"]: + if stream["codec_type"] == "video": + video_stream_index = stream["index"] break # exit if no video stream found if video_stream_index is None: - Avalon.error(_('Aborting: No video stream found')) - raise StreamNotFoundError('no video stream found') + Avalon.error(_("Aborting: No video stream found")) + raise StreamNotFoundError("no video stream found") # get average frame rate of video stream - framerate = float(Fraction(file_info['streams'][video_stream_index]['r_frame_rate'])) - width = int(file_info['streams'][video_stream_index]['width']) - height = int(file_info['streams'][video_stream_index]['height']) + framerate = float( + Fraction( + file_info["streams"][video_stream_index]["r_frame_rate"] + ) + ) + width = int(file_info["streams"][video_stream_index]["width"]) + height = int(file_info["streams"][video_stream_index]["height"]) # get total number of frames - Avalon.info(_('Getting total number of frames in the file')) + Avalon.info(_("Getting total number of frames in the file")) # if container stores total number of frames in nb_frames, fetch it directly - if 'nb_frames' in file_info['streams'][video_stream_index]: - self.total_frames = int(file_info['streams'][video_stream_index]['nb_frames']) + if "nb_frames" in file_info["streams"][video_stream_index]: + self.total_frames = int( + file_info["streams"][video_stream_index]["nb_frames"] + ) # otherwise call FFprobe to count the total number of frames else: - self.total_frames = self.ffmpeg_object.get_number_of_frames(self.current_input_file, video_stream_index) + self.total_frames = self.ffmpeg_object.get_number_of_frames( + self.current_input_file, video_stream_index + ) # calculate scale width/height/ratio and scaling jobs if required - Avalon.info(_('Calculating scaling parameters')) + Avalon.info(_("Calculating scaling parameters")) # create a local copy of the global output settings output_scale = self.scale_ratio @@ -624,7 +749,9 @@ class Upscaler: if self.driver in DRIVER_FIXED_SCALING_RATIOS: # select the optimal driver scaling ratio to use - supported_scaling_ratios = sorted(DRIVER_FIXED_SCALING_RATIOS[self.driver]) + supported_scaling_ratios = sorted( + DRIVER_FIXED_SCALING_RATIOS[self.driver] + ) remaining_scaling_ratio = math.ceil(output_scale) self.scaling_jobs = [] @@ -654,46 +781,68 @@ class Upscaler: break if found is False: - self.scaling_jobs.append(supported_scaling_ratios[-1]) - remaining_scaling_ratio /= supported_scaling_ratios[-1] + self.scaling_jobs.append( + supported_scaling_ratios[-1] + ) + remaining_scaling_ratio /= supported_scaling_ratios[ + -1 + ] else: self.scaling_jobs = [output_scale] # print file information - Avalon.debug_info(_('Framerate: {}').format(framerate)) - Avalon.debug_info(_('Width: {}').format(width)) - Avalon.debug_info(_('Height: {}').format(height)) - Avalon.debug_info(_('Total number of frames: {}').format(self.total_frames)) - Avalon.debug_info(_('Output width: {}').format(output_width)) - Avalon.debug_info(_('Output height: {}').format(output_height)) - Avalon.debug_info(_('Required scale ratio: {}').format(output_scale)) - Avalon.debug_info(_('Upscaling jobs queue: {}').format(self.scaling_jobs)) + Avalon.debug_info(_("Framerate: {}").format(framerate)) + Avalon.debug_info(_("Width: {}").format(width)) + Avalon.debug_info(_("Height: {}").format(height)) + Avalon.debug_info( + _("Total number of frames: {}").format(self.total_frames) + ) + Avalon.debug_info(_("Output width: {}").format(output_width)) + Avalon.debug_info(_("Output height: {}").format(output_height)) + Avalon.debug_info(_("Required scale ratio: {}").format(output_scale)) + Avalon.debug_info( + _("Upscaling jobs queue: {}").format(self.scaling_jobs) + ) # extract frames from video - if input_file_mime_type == 'image/gif' or input_file_type == 'video': - self.process_pool.append((self.ffmpeg_object.extract_frames(self.current_input_file, self.extracted_frames))) + if input_file_mime_type == "image/gif" or input_file_type == "video": + self.process_pool.append( + ( + self.ffmpeg_object.extract_frames( + self.current_input_file, self.extracted_frames + ) + ) + ) self._wait() # if driver is waifu2x-caffe # pass pixel format output depth information - if self.driver == 'waifu2x_caffe': + if self.driver == "waifu2x_caffe": # get a dict of all pixel formats and corresponding bit depth pixel_formats = self.ffmpeg_object.get_pixel_formats() # try getting pixel format's corresponding bti depth try: - self.driver_settings['output_depth'] = pixel_formats[self.ffmpeg_object.pixel_format] + self.driver_settings["output_depth"] = pixel_formats[ + self.ffmpeg_object.pixel_format + ] except KeyError: - Avalon.error(_('Unsupported pixel format: {}').format(self.ffmpeg_object.pixel_format)) - raise UnsupportedPixelError(f'unsupported pixel format {self.ffmpeg_object.pixel_format}') + Avalon.error( + _("Unsupported pixel format: {}").format( + self.ffmpeg_object.pixel_format + ) + ) + raise UnsupportedPixelError( + f"unsupported pixel format {self.ffmpeg_object.pixel_format}" + ) # upscale images one by one using waifu2x - Avalon.info(_('Starting to upscale extracted frames')) + Avalon.info(_("Starting to upscale extracted frames")) upscale_begin_time = time.time() self.current_pass = 1 - if self.driver == 'waifu2x_caffe': + if self.driver == "waifu2x_caffe": self.driver_object.set_scale_resolution(output_width, output_height) else: self.driver_object.set_scale_ratio(self.scaling_jobs[0]) @@ -706,22 +855,39 @@ class Upscaler: self.upscaled_frames.mkdir(parents=True, exist_ok=True) self._upscale_frames(self.extracted_frames, self.upscaled_frames) - Avalon.info(_('Upscaling completed')) - Avalon.info(_('Average processing speed: {} seconds per frame').format(self.total_frames / (time.time() - upscale_begin_time))) + Avalon.info(_("Upscaling completed")) + Avalon.info( + _("Average processing speed: {} seconds per frame").format( + self.total_frames / (time.time() - upscale_begin_time) + ) + ) # downscale frames with Lanczos - Avalon.info(_('Lanczos downscaling frames')) + Avalon.info(_("Lanczos downscaling frames")) shutil.rmtree(self.extracted_frames) shutil.move(self.upscaled_frames, self.extracted_frames) self.upscaled_frames.mkdir(parents=True, exist_ok=True) - for image in tqdm([i for i in self.extracted_frames.iterdir() if i.is_file() and i.name.endswith(self.extracted_frame_format)], ascii=True, desc=_('Downscaling')): + for image in tqdm( + [ + i + for i in self.extracted_frames.iterdir() + if i.is_file() and i.name.endswith(self.extracted_frame_format) + ], + ascii=True, + desc=_("Downscaling"), + ): image_object = Image.open(image) # if the image dimensions are not equal to the output size # resize the image using Lanczos - if (image_object.width, image_object.height) != (output_width, output_height): - image_object.resize((output_width, output_height), Image.LANCZOS).save(self.upscaled_frames / image.name) + if (image_object.width, image_object.height) != ( + output_width, + output_height, + ): + image_object.resize( + (output_width, output_height), Image.LANCZOS + ).save(self.upscaled_frames / image.name) image_object.close() # if the image's dimensions are already equal to the output size @@ -732,71 +898,117 @@ class Upscaler: # start handling output # output can be either GIF or video - if input_file_type == 'image' and input_file_subtype != 'gif': + if input_file_type == "image" and input_file_subtype != "gif": - Avalon.info(_('Exporting image')) + Avalon.info(_("Exporting image")) # there should be only one image in the directory - shutil.move([f for f in self.upscaled_frames.iterdir() if f.is_file()][0], output_path) + shutil.move( + [f for f in self.upscaled_frames.iterdir() if f.is_file()][0], + output_path, + ) # elif input_file_mime_type == 'image/gif' or input_file_type == 'video': else: # if the desired output is gif file - if output_path.suffix.lower() == '.gif': - Avalon.info(_('Converting extracted frames into GIF image')) + if output_path.suffix.lower() == ".gif": + Avalon.info(_("Converting extracted frames into GIF image")) gifski_object = Gifski(self.gifski_settings) - self.process_pool.append(gifski_object.make_gif(self.upscaled_frames, output_path, framerate, self.extracted_frame_format, output_width, output_height)) + self.process_pool.append( + gifski_object.make_gif( + self.upscaled_frames, + output_path, + framerate, + self.extracted_frame_format, + output_width, + output_height, + ) + ) self._wait() - Avalon.info(_('Conversion completed')) + Avalon.info(_("Conversion completed")) # if the desired output is video else: # frames to video - Avalon.info(_('Converting extracted frames into video')) - self.process_pool.append(self.ffmpeg_object.assemble_video(framerate, self.upscaled_frames)) + Avalon.info(_("Converting extracted frames into video")) + self.process_pool.append( + self.ffmpeg_object.assemble_video( + framerate, self.upscaled_frames + ) + ) # f'{scale_width}x{scale_height}' self._wait() - Avalon.info(_('Conversion completed')) + Avalon.info(_("Conversion completed")) try: # migrate audio tracks and subtitles - Avalon.info(_('Migrating audio, subtitles and other streams to upscaled video')) - self.process_pool.append(self.ffmpeg_object.migrate_streams(self.current_input_file, - output_path, - self.upscaled_frames)) + Avalon.info( + _( + "Migrating audio, subtitles and other streams to upscaled video" + ) + ) + self.process_pool.append( + self.ffmpeg_object.migrate_streams( + self.current_input_file, + output_path, + self.upscaled_frames, + ) + ) self._wait() # if failed to copy streams # use file with only video stream except subprocess.CalledProcessError: traceback.print_exc() - Avalon.error(_('Failed to migrate streams')) - Avalon.warning(_('Trying to output video without additional streams')) + Avalon.error(_("Failed to migrate streams")) + Avalon.warning( + _("Trying to output video without additional streams") + ) - if input_file_mime_type == 'image/gif': + if input_file_mime_type == "image/gif": # copy will overwrite destination content if exists - shutil.copy(self.upscaled_frames / self.ffmpeg_object.intermediate_file_name, output_path) + shutil.copy( + self.upscaled_frames + / self.ffmpeg_object.intermediate_file_name, + output_path, + ) else: # construct output file path - output_file_name = f'{output_path.stem}{self.ffmpeg_object.intermediate_file_name.suffix}' - output_video_path = output_path.parent / output_file_name + output_file_name = f"{output_path.stem}{self.ffmpeg_object.intermediate_file_name.suffix}" + output_video_path = ( + output_path.parent / output_file_name + ) # if output file already exists # create temporary directory in output folder # temporary directories generated by tempfile are guaranteed to be unique # and won't conflict with other files if output_video_path.exists(): - Avalon.error(_('Output video file exists')) + Avalon.error(_("Output video file exists")) - temporary_directory = pathlib.Path(tempfile.mkdtemp(dir=output_path.parent)) - output_video_path = temporary_directory / output_file_name - Avalon.info(_('Created temporary directory to contain file')) + temporary_directory = pathlib.Path( + tempfile.mkdtemp(dir=output_path.parent) + ) + output_video_path = ( + temporary_directory / output_file_name + ) + Avalon.info( + _("Created temporary directory to contain file") + ) # move file to new destination - Avalon.info(_('Writing intermediate file to: {}').format(output_video_path.absolute())) - shutil.move(self.upscaled_frames / self.ffmpeg_object.intermediate_file_name, output_video_path) + Avalon.info( + _("Writing intermediate file to: {}").format( + output_video_path.absolute() + ) + ) + shutil.move( + self.upscaled_frames + / self.ffmpeg_object.intermediate_file_name, + output_video_path, + ) # increment total number of files processed self.cleanup_temp_directories() diff --git a/src/video2x.py b/src/video2x.py index c2e6ed9..cd1a597 100755 --- a/src/video2x.py +++ b/src/video2x.py @@ -71,75 +71,143 @@ import yaml from avalon_framework import Avalon # internationalization constants -DOMAIN = 'video2x' -LOCALE_DIRECTORY = pathlib.Path(__file__).parent.absolute() / 'locale' +DOMAIN = "video2x" +LOCALE_DIRECTORY = pathlib.Path(__file__).parent.absolute() / "locale" # getting default locale settings default_locale, encoding = locale.getdefaultlocale() -language = gettext.translation(DOMAIN, LOCALE_DIRECTORY, [default_locale], fallback=True) +language = gettext.translation( + DOMAIN, LOCALE_DIRECTORY, [default_locale], fallback=True +) language.install() _ = language.gettext -CLI_VERSION = '4.3.1' +CLI_VERSION = "4.3.1" -LEGAL_INFO = _('''Video2X CLI Version: {} +LEGAL_INFO = _( + """Video2X CLI Version: {} Upscaler Version: {} Author: K4YT3X License: GNU GPL v3 Github Page: https://github.com/k4yt3x/video2x -Contact: k4yt3x@k4yt3x.com''').format(CLI_VERSION, UPSCALER_VERSION) +Contact: k4yt3x@k4yt3x.com""" +).format(CLI_VERSION, UPSCALER_VERSION) -LOGO = r''' +LOGO = r""" __ __ _ _ ___ __ __ \ \ / / (_) | | |__ \ \ \ / / \ \ / / _ __| | ___ ___ ) | \ V / \ \/ / | | / _` | / _ \ / _ \ / / > < \ / | | | (_| | | __/ | (_) | / /_ / . \ \/ |_| \__,_| \___| \___/ |____| /_/ \_\ -''' +""" def parse_arguments(): - """ parse CLI arguments - """ - parser = argparse.ArgumentParser(prog='video2x', formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) + """parse CLI arguments""" + parser = argparse.ArgumentParser( + prog="video2x", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + add_help=False, + ) # video options - video2x_options = parser.add_argument_group(_('Video2X Options')) - video2x_options.add_argument('--help', action='help', help=_('show this help message and exit')) + video2x_options = parser.add_argument_group(_("Video2X Options")) + + video2x_options.add_argument( + "--help", action="help", help=_("show this help message and exit") + ) # if help is in arguments list # do not require input and output path to be specified require_input_output = True - if '-h' in sys.argv or '--help' in sys.argv: + if "-h" in sys.argv or "--help" in sys.argv: require_input_output = False - video2x_options.add_argument('-i', '--input', type=pathlib.Path, help=_('source video file/directory'), required=require_input_output) - video2x_options.add_argument('-o', '--output', type=pathlib.Path, help=_('output video file/directory'), required=require_input_output) - video2x_options.add_argument('-c', '--config', type=pathlib.Path, help=_('Video2X config file path'), action='store', - default=pathlib.Path(__file__).parent.absolute() / 'video2x.yaml') - video2x_options.add_argument('--log', type=pathlib.Path, help=_('log file path')) - video2x_options.add_argument('-v', '--version', help=_('display version, lawful information and exit'), action='store_true') + video2x_options.add_argument( + "-i", + "--input", + type=pathlib.Path, + help=_("source video file/directory"), + required=require_input_output, + ) + + video2x_options.add_argument( + "-o", + "--output", + type=pathlib.Path, + help=_("output video file/directory"), + required=require_input_output, + ) + + video2x_options.add_argument( + "-c", + "--config", + type=pathlib.Path, + help=_("Video2X config file path"), + action="store", + default=pathlib.Path(__file__).parent.absolute() / "video2x.yaml", + ) + + video2x_options.add_argument("--log", type=pathlib.Path, help=_("log file path")) + + video2x_options.add_argument( + "-v", + "--version", + help=_("display version, lawful information and exit"), + action="store_true", + ) # scaling options - upscaling_options = parser.add_argument_group(_('Upscaling Options')) - upscaling_options.add_argument('-r', '--ratio', help=_('scaling ratio'), action='store', type=float) - upscaling_options.add_argument('-w', '--width', help=_('output width'), action='store', type=float) - upscaling_options.add_argument('-h', '--height', help=_('output height'), action='store', type=float) - upscaling_options.add_argument('-d', '--driver', help=_('upscaling driver'), choices=AVAILABLE_DRIVERS, default='waifu2x_ncnn_vulkan') - upscaling_options.add_argument('-p', '--processes', help=_('number of processes to use for upscaling'), action='store', type=int, default=1) - upscaling_options.add_argument('--preserve_frames', help=_('preserve extracted and upscaled frames'), action='store_true') + upscaling_options = parser.add_argument_group(_("Upscaling Options")) + + upscaling_options.add_argument( + "-r", "--ratio", help=_("scaling ratio"), action="store", type=float + ) + + upscaling_options.add_argument( + "-w", "--width", help=_("output width"), action="store", type=float + ) + + upscaling_options.add_argument( + "-h", "--height", help=_("output height"), action="store", type=float + ) + + upscaling_options.add_argument( + "-d", + "--driver", + help=_("upscaling driver"), + choices=AVAILABLE_DRIVERS, + default="waifu2x_ncnn_vulkan", + ) + + upscaling_options.add_argument( + "-p", + "--processes", + help=_("number of processes to use for upscaling"), + action="store", + type=int, + default=1, + ) + + upscaling_options.add_argument( + "--preserve_frames", + help=_("preserve extracted and upscaled frames"), + action="store_true", + ) # if no driver arguments are specified - if '--' not in sys.argv: + if "--" not in sys.argv: video2x_args = parser.parse_args() return video2x_args, None # if driver arguments are specified else: - video2x_args = parser.parse_args(sys.argv[1:sys.argv.index('--')]) - wrapper = getattr(importlib.import_module(f'wrappers.{video2x_args.driver}'), 'WrapperMain') - driver_args = wrapper.parse_arguments(sys.argv[sys.argv.index('--') + 1:]) + video2x_args = parser.parse_args(sys.argv[1 : sys.argv.index("--")]) + wrapper = getattr( + importlib.import_module(f"wrappers.{video2x_args.driver}"), "WrapperMain" + ) + driver_args = wrapper.parse_arguments(sys.argv[sys.argv.index("--") + 1 :]) return video2x_args, driver_args @@ -151,7 +219,7 @@ def print_logo(): def read_config(config_file: pathlib.Path) -> dict: - """ read video2x configurations from config file + """read video2x configurations from config file Arguments: config_file {pathlib.Path} -- video2x configuration file pathlib.Path @@ -160,16 +228,16 @@ def read_config(config_file: pathlib.Path) -> dict: dict -- dictionary of video2x configuration """ - with open(config_file, 'r') as config: + with open(config_file, "r") as config: return yaml.load(config, Loader=yaml.FullLoader) # /////////////////// Execution /////////////////// # # this is not a library -if __name__ != '__main__': - Avalon.error(_('This file cannot be imported')) - raise ImportError(f'{__file__} cannot be imported') +if __name__ != "__main__": + Avalon.error(_("This file cannot be imported")) + raise ImportError(f"{__file__} cannot be imported") # print video2x logo print_logo() @@ -183,15 +251,19 @@ if video2x_args.version: sys.exit(0) # additional checks on upscaling arguments -if video2x_args.ratio is not None and (video2x_args.width is not None or video2x_args.height is not None): - Avalon.error(_('Specify either scaling ratio or scaling resolution, not both')) +if video2x_args.ratio is not None and ( + video2x_args.width is not None or video2x_args.height is not None +): + Avalon.error(_("Specify either scaling ratio or scaling resolution, not both")) sys.exit(1) # redirect output to both terminal and log file if video2x_args.log is not None: - log_file = video2x_args.log.open(mode='a+', encoding='utf-8') + log_file = video2x_args.log.open(mode="a+", encoding="utf-8") else: - log_file = tempfile.TemporaryFile(mode='a+', suffix='.log', prefix='video2x_', encoding='utf-8') + log_file = tempfile.TemporaryFile( + mode="a+", suffix=".log", prefix="video2x_", encoding="utf-8" + ) original_stdout = sys.stdout original_stderr = sys.stderr @@ -203,22 +275,22 @@ config = read_config(video2x_args.config) # load waifu2x configuration driver_settings = config[video2x_args.driver] -driver_settings['path'] = os.path.expandvars(driver_settings['path']) +driver_settings["path"] = os.path.expandvars(driver_settings["path"]) # read FFmpeg configuration -ffmpeg_settings = config['ffmpeg'] -ffmpeg_settings['ffmpeg_path'] = os.path.expandvars(ffmpeg_settings['ffmpeg_path']) +ffmpeg_settings = config["ffmpeg"] +ffmpeg_settings["ffmpeg_path"] = os.path.expandvars(ffmpeg_settings["ffmpeg_path"]) # read Gifski configuration -gifski_settings = config['gifski'] -gifski_settings['gifski_path'] = os.path.expandvars(gifski_settings['gifski_path']) +gifski_settings = config["gifski"] +gifski_settings["gifski_path"] = os.path.expandvars(gifski_settings["gifski_path"]) # load video2x settings -extracted_frame_format = config['video2x']['extracted_frame_format'].lower() -output_file_name_format_string = config['video2x']['output_file_name_format_string'] -image_output_extension = config['video2x']['image_output_extension'] -video_output_extension = config['video2x']['video_output_extension'] -preserve_frames = config['video2x']['preserve_frames'] +extracted_frame_format = config["video2x"]["extracted_frame_format"].lower() +output_file_name_format_string = config["video2x"]["output_file_name_format_string"] +image_output_extension = config["video2x"]["image_output_extension"] +video_output_extension = config["video2x"]["video_output_extension"] +preserve_frames = config["video2x"]["preserve_frames"] # if preserve frames specified in command line # overwrite config file options @@ -227,10 +299,10 @@ if video2x_args.preserve_frames is True: # if cache directory not specified # use default path: %TEMP%\video2x -if config['video2x']['video2x_cache_directory'] is None: - video2x_cache_directory = (pathlib.Path(tempfile.gettempdir()) / 'video2x') +if config["video2x"]["video2x_cache_directory"] is None: + video2x_cache_directory = pathlib.Path(tempfile.gettempdir()) / "video2x" else: - video2x_cache_directory = pathlib.Path(config['video2x']['video2x_cache_directory']) + video2x_cache_directory = pathlib.Path(config["video2x"]["video2x_cache_directory"]) # overwrite driver_settings with driver_args if driver_args is not None: @@ -252,7 +324,6 @@ try: driver_settings=driver_settings, ffmpeg_settings=ffmpeg_settings, gifski_settings=gifski_settings, - # optional parameters driver=video2x_args.driver, scale_ratio=video2x_args.ratio, @@ -264,17 +335,21 @@ try: output_file_name_format_string=output_file_name_format_string, image_output_extension=image_output_extension, video_output_extension=video_output_extension, - preserve_frames=preserve_frames + preserve_frames=preserve_frames, ) # run upscaler upscaler.run() - Avalon.info(_('Program completed, taking {} seconds').format(round((time.time() - begin_time), 5))) + Avalon.info( + _("Program completed, taking {} seconds").format( + round((time.time() - begin_time), 5) + ) + ) except Exception: - Avalon.error(_('An exception has occurred')) + Avalon.error(_("An exception has occurred")) traceback.print_exc() if video2x_args.log is not None: @@ -284,12 +359,12 @@ except Exception: # tempfile.TempFile does not have a name attribute and is not guaranteed to have # a visible name on the file system else: - log_file_path = tempfile.mkstemp(suffix='.log', prefix='video2x_')[1] - with open(log_file_path, 'w', encoding='utf-8') as permanent_log_file: + log_file_path = tempfile.mkstemp(suffix=".log", prefix="video2x_")[1] + with open(log_file_path, "w", encoding="utf-8") as permanent_log_file: log_file.seek(0) permanent_log_file.write(log_file.read()) - Avalon.error(_('The error log file can be found at: {}').format(log_file_path)) + Avalon.error(_("The error log file can be found at: {}").format(log_file_path)) finally: sys.stdout = original_stdout diff --git a/src/video2x_gui.py b/src/video2x_gui.py index 3c570d6..d97fa7c 100755 --- a/src/video2x_gui.py +++ b/src/video2x_gui.py @@ -33,22 +33,22 @@ from PyQt5.QtGui import * from PyQt5.QtWidgets import * import magic -GUI_VERSION = '2.8.1' +GUI_VERSION = "2.8.1" -LEGAL_INFO = f'''Video2X GUI Version: {GUI_VERSION}\\ +LEGAL_INFO = f"""Video2X GUI Version: {GUI_VERSION}\\ Upscaler Version: {UPSCALER_VERSION}\\ Author: K4YT3X\\ License: GNU GPL v3\\ Github Page: [https://github.com/k4yt3x/video2x](https://github.com/k4yt3x/video2x)\\ -Contact: [k4yt3x@k4yt3x.com](mailto:k4yt3x@k4yt3x.com)''' +Contact: [k4yt3x@k4yt3x.com](mailto:k4yt3x@k4yt3x.com)""" AVAILABLE_DRIVERS = { - 'Waifu2X Caffe': 'waifu2x_caffe', - 'Waifu2X Converter CPP': 'waifu2x_converter_cpp', - 'Waifu2X NCNN Vulkan': 'waifu2x_ncnn_vulkan', - 'SRMD NCNN Vulkan': 'srmd_ncnn_vulkan', - 'RealSR NCNN Vulkan': 'realsr_ncnn_vulkan', - 'Anime4KCPP': 'anime4kcpp' + "Waifu2X Caffe": "waifu2x_caffe", + "Waifu2X Converter CPP": "waifu2x_converter_cpp", + "Waifu2X NCNN Vulkan": "waifu2x_ncnn_vulkan", + "SRMD NCNN Vulkan": "srmd_ncnn_vulkan", + "RealSR NCNN Vulkan": "realsr_ncnn_vulkan", + "Anime4KCPP": "anime4kcpp", } # get current working directory before it is changed by drivers @@ -77,7 +77,7 @@ class ProgressMonitorWorkder(QRunnable): self.args = args self.kwargs = kwargs self.signals = WorkerSignals() - self.kwargs['progress_callback'] = self.signals.progress + self.kwargs["progress_callback"] = self.signals.progress @pyqtSlot() def run(self): @@ -88,7 +88,6 @@ class ProgressMonitorWorkder(QRunnable): class UpscalerWorker(QRunnable): - def __init__(self, fn, *args, **kwargs): super(UpscalerWorker, self).__init__() @@ -130,38 +129,40 @@ class InputTableModel(QAbstractTableModel): # determine file type # if path is a folder if file_path.is_dir(): - return 'Folder' + return "Folder" # if path is single file # determine file type elif file_path.is_file(): try: - input_file_mime_type = magic.from_file(str(file_path.absolute()), mime=True) - input_file_type = input_file_mime_type.split('/')[0] - input_file_subtype = input_file_mime_type.split('/')[1] + input_file_mime_type = magic.from_file( + str(file_path.absolute()), mime=True + ) + input_file_type = input_file_mime_type.split("/")[0] + input_file_subtype = input_file_mime_type.split("/")[1] except Exception: input_file_type = input_file_subtype = None # in case python-magic fails to detect file type # try guessing file mime type with mimetypes - if input_file_type not in ['image', 'video']: + if input_file_type not in ["image", "video"]: input_file_mime_type = mimetypes.guess_type(file_path.name)[0] - input_file_type = input_file_mime_type.split('/')[0] - input_file_subtype = input_file_mime_type.split('/')[1] + input_file_type = input_file_mime_type.split("/")[0] + input_file_subtype = input_file_mime_type.split("/")[1] - if input_file_type == 'image': - if input_file_subtype == 'gif': - return 'GIF' - return 'Image' + if input_file_type == "image": + if input_file_subtype == "gif": + return "GIF" + return "Image" - elif input_file_type == 'video': - return 'Video' + elif input_file_type == "video": + return "Video" else: - return 'Unknown' + return "Unknown" else: - return 'Unknown' + return "Unknown" def rowCount(self, index): return len(self._data) @@ -177,7 +178,7 @@ class InputTableModel(QAbstractTableModel): if role != Qt.DisplayRole: return None - horizontal_headers = ['File Path', 'Type'] + horizontal_headers = ["File Path", "Type"] # return the correspondign header if orientation == Qt.Horizontal: @@ -189,13 +190,14 @@ class InputTableModel(QAbstractTableModel): class Video2XMainWindow(QMainWindow): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - uic.loadUi(str(resource_path('video2x_gui.ui')), self) + uic.loadUi(str(resource_path("video2x_gui.ui")), self) # redirect output to both terminal and log file - self.log_file = tempfile.TemporaryFile(mode='a+', suffix='.log', prefix='video2x_', encoding='utf-8') + self.log_file = tempfile.TemporaryFile( + mode="a+", suffix=".log", prefix="video2x_", encoding="utf-8" + ) sys.stdout = BiLogger(sys.stdout, self.log_file) sys.stderr = BiLogger(sys.stderr, self.log_file) @@ -203,8 +205,8 @@ class Video2XMainWindow(QMainWindow): self.threadpool = QThreadPool() # set window title and icon - self.video2x_icon_path = str(resource_path('images/video2x.png')) - self.setWindowTitle(f'Video2X GUI {GUI_VERSION}') + self.video2x_icon_path = str(resource_path("images/video2x.png")) + self.setWindowTitle(f"Video2X GUI {GUI_VERSION}") self.setWindowIcon(QIcon(self.video2x_icon_path)) # register shortcut keys @@ -212,22 +214,26 @@ class Video2XMainWindow(QMainWindow): QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q), self, self.close) QShortcut(QKeySequence(Qt.CTRL + Qt.Key_I), self, self.select_input_file) QShortcut(QKeySequence(Qt.CTRL + Qt.Key_O), self, self.select_output_file) - QShortcut(QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_I), self, self.select_input_folder) - QShortcut(QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_O), self, self.select_output_folder) + QShortcut( + QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_I), self, self.select_input_folder + ) + QShortcut( + QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_O), self, self.select_output_folder + ) # menu bar - self.action_exit = self.findChild(QAction, 'actionExit') + self.action_exit = self.findChild(QAction, "actionExit") self.action_exit.triggered.connect(self.close) - self.action_shortcuts = self.findChild(QAction, 'actionShortcuts') + self.action_shortcuts = self.findChild(QAction, "actionShortcuts") self.action_shortcuts.triggered.connect(self.show_shortcuts) - self.action_about = self.findChild(QAction, 'actionAbout') + self.action_about = self.findChild(QAction, "actionAbout") self.action_about.triggered.connect(self.show_about) # main tab # select input file/folder - self.input_table_view = self.findChild(QTableView, 'inputTableView') + self.input_table_view = self.findChild(QTableView, "inputTableView") self.input_table_view.dragEnterEvent = self.dragEnterEvent self.input_table_view.dropEvent = self.dropEvent @@ -236,223 +242,502 @@ class Video2XMainWindow(QMainWindow): self.input_table_model = InputTableModel(self.input_table_data) self.input_table_view.setModel(self.input_table_model) # stretch file path and fill columns horizontally - self.input_table_view.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) + self.input_table_view.horizontalHeader().setSectionResizeMode( + 0, QHeaderView.Stretch + ) # input table buttons - self.input_select_file_button = self.findChild(QPushButton, 'inputSelectFileButton') + self.input_select_file_button = self.findChild( + QPushButton, "inputSelectFileButton" + ) self.input_select_file_button.clicked.connect(self.select_input_file) - self.input_select_folder_button = self.findChild(QPushButton, 'inputSelectFolderButton') + self.input_select_folder_button = self.findChild( + QPushButton, "inputSelectFolderButton" + ) self.input_select_folder_button.clicked.connect(self.select_input_folder) - self.input_delete_selected_button = self.findChild(QPushButton, 'inputDeleteSelectedButton') - self.input_delete_selected_button.clicked.connect(self.input_table_delete_selected) - self.input_clear_all_button = self.findChild(QPushButton, 'inputClearAllButton') + self.input_delete_selected_button = self.findChild( + QPushButton, "inputDeleteSelectedButton" + ) + self.input_delete_selected_button.clicked.connect( + self.input_table_delete_selected + ) + self.input_clear_all_button = self.findChild(QPushButton, "inputClearAllButton") self.input_clear_all_button.clicked.connect(self.input_table_clear_all) # other paths selection # select output file/folder - self.output_line_edit = self.findChild(QLineEdit, 'outputLineEdit') + self.output_line_edit = self.findChild(QLineEdit, "outputLineEdit") self.enable_line_edit_file_drop(self.output_line_edit) - self.output_line_edit.setText(str((CWD / 'output').absolute())) - self.output_select_file_button = self.findChild(QPushButton, 'outputSelectFileButton') + self.output_line_edit.setText(str((CWD / "output").absolute())) + self.output_select_file_button = self.findChild( + QPushButton, "outputSelectFileButton" + ) self.output_select_file_button.clicked.connect(self.select_output_file) - self.output_select_folder_button = self.findChild(QPushButton, 'outputSelectFolderButton') + self.output_select_folder_button = self.findChild( + QPushButton, "outputSelectFolderButton" + ) self.output_select_folder_button.clicked.connect(self.select_output_folder) # config file - self.config_line_edit = self.findChild(QLineEdit, 'configLineEdit') + self.config_line_edit = self.findChild(QLineEdit, "configLineEdit") self.enable_line_edit_file_drop(self.config_line_edit) - if getattr(sys, 'frozen', False): - self.config_line_edit.setText(str((pathlib.Path(sys.executable).parent / 'video2x.yaml').absolute())) + if getattr(sys, "frozen", False): + self.config_line_edit.setText( + str((pathlib.Path(sys.executable).parent / "video2x.yaml").absolute()) + ) elif __file__: - self.config_line_edit.setText(str((pathlib.Path(__file__).parent / 'video2x.yaml').absolute())) + self.config_line_edit.setText( + str((pathlib.Path(__file__).parent / "video2x.yaml").absolute()) + ) - self.config_select_file_button = self.findChild(QPushButton, 'configSelectButton') + self.config_select_file_button = self.findChild( + QPushButton, "configSelectButton" + ) self.config_select_file_button.clicked.connect(self.select_config_file) # cache directory - self.cache_line_edit = self.findChild(QLineEdit, 'cacheLineEdit') + self.cache_line_edit = self.findChild(QLineEdit, "cacheLineEdit") self.enable_line_edit_file_drop(self.cache_line_edit) - self.cache_select_folder_button = self.findChild(QPushButton, 'cacheSelectFolderButton') + self.cache_select_folder_button = self.findChild( + QPushButton, "cacheSelectFolderButton" + ) self.cache_select_folder_button.clicked.connect(self.select_cache_folder) # express settings - self.driver_combo_box = self.findChild(QComboBox, 'driverComboBox') + self.driver_combo_box = self.findChild(QComboBox, "driverComboBox") self.driver_combo_box.currentTextChanged.connect(self.update_gui_for_driver) - self.processes_spin_box = self.findChild(QSpinBox, 'processesSpinBox') - self.scale_ratio_double_spin_box = self.findChild(QDoubleSpinBox, 'scaleRatioDoubleSpinBox') - self.output_width_spin_box = self.findChild(QSpinBox, 'outputWidthSpinBox') - self.output_width_spin_box.valueChanged.connect(self.mutually_exclude_scale_ratio_resolution) - self.output_height_spin_box = self.findChild(QSpinBox, 'outputHeightSpinBox') - self.output_height_spin_box.valueChanged.connect(self.mutually_exclude_scale_ratio_resolution) - self.output_file_name_format_string_line_edit = self.findChild(QLineEdit, 'outputFileNameFormatStringLineEdit') - self.image_output_extension_line_edit = self.findChild(QLineEdit, 'imageOutputExtensionLineEdit') - self.video_output_extension_line_edit = self.findChild(QLineEdit, 'videoOutputExtensionLineEdit') - self.preserve_frames_check_box = self.findChild(QCheckBox, 'preserveFramesCheckBox') + self.processes_spin_box = self.findChild(QSpinBox, "processesSpinBox") + self.scale_ratio_double_spin_box = self.findChild( + QDoubleSpinBox, "scaleRatioDoubleSpinBox" + ) + self.output_width_spin_box = self.findChild(QSpinBox, "outputWidthSpinBox") + self.output_width_spin_box.valueChanged.connect( + self.mutually_exclude_scale_ratio_resolution + ) + self.output_height_spin_box = self.findChild(QSpinBox, "outputHeightSpinBox") + self.output_height_spin_box.valueChanged.connect( + self.mutually_exclude_scale_ratio_resolution + ) + self.output_file_name_format_string_line_edit = self.findChild( + QLineEdit, "outputFileNameFormatStringLineEdit" + ) + self.image_output_extension_line_edit = self.findChild( + QLineEdit, "imageOutputExtensionLineEdit" + ) + self.video_output_extension_line_edit = self.findChild( + QLineEdit, "videoOutputExtensionLineEdit" + ) + self.preserve_frames_check_box = self.findChild( + QCheckBox, "preserveFramesCheckBox" + ) # frame preview - self.frame_preview_show_preview_check_box = self.findChild(QCheckBox, 'framePreviewShowPreviewCheckBox') - self.frame_preview_keep_aspect_ratio_check_box = self.findChild(QCheckBox, 'framePreviewKeepAspectRatioCheckBox') - self.frame_preview_label = self.findChild(QLabel, 'framePreviewLabel') + self.frame_preview_show_preview_check_box = self.findChild( + QCheckBox, "framePreviewShowPreviewCheckBox" + ) + self.frame_preview_keep_aspect_ratio_check_box = self.findChild( + QCheckBox, "framePreviewKeepAspectRatioCheckBox" + ) + self.frame_preview_label = self.findChild(QLabel, "framePreviewLabel") # currently processing - self.currently_processing_label = self.findChild(QLabel, 'currentlyProcessingLabel') - self.current_progress_bar = self.findChild(QProgressBar, 'currentProgressBar') - self.time_elapsed_label = self.findChild(QLabel, 'timeElapsedLabel') - self.time_remaining_label = self.findChild(QLabel, 'timeRemainingLabel') - self.rate_label = self.findChild(QLabel, 'rateLabel') - self.frames_label = self.findChild(QLabel, 'framesLabel') + self.currently_processing_label = self.findChild( + QLabel, "currentlyProcessingLabel" + ) + self.current_progress_bar = self.findChild(QProgressBar, "currentProgressBar") + self.time_elapsed_label = self.findChild(QLabel, "timeElapsedLabel") + self.time_remaining_label = self.findChild(QLabel, "timeRemainingLabel") + self.rate_label = self.findChild(QLabel, "rateLabel") + self.frames_label = self.findChild(QLabel, "framesLabel") # overall progress - self.overall_progress_bar = self.findChild(QProgressBar, 'overallProgressBar') - self.overall_progress_label = self.findChild(QLabel, 'overallProgressLabel') - self.start_button = self.findChild(QPushButton, 'startButton') + self.overall_progress_bar = self.findChild(QProgressBar, "overallProgressBar") + self.overall_progress_label = self.findChild(QLabel, "overallProgressLabel") + self.start_button = self.findChild(QPushButton, "startButton") self.start_button.clicked.connect(self.start) - self.stop_button = self.findChild(QPushButton, 'stopButton') + self.stop_button = self.findChild(QPushButton, "stopButton") self.stop_button.clicked.connect(self.stop) # driver settings # waifu2x-caffe - self.waifu2x_caffe_path_line_edit = self.findChild(QLineEdit, 'waifu2xCaffePathLineEdit') + self.waifu2x_caffe_path_line_edit = self.findChild( + QLineEdit, "waifu2xCaffePathLineEdit" + ) self.enable_line_edit_file_drop(self.waifu2x_caffe_path_line_edit) - self.waifu2x_caffe_path_select_button = self.findChild(QPushButton, 'waifu2xCaffePathSelectButton') - self.waifu2x_caffe_path_select_button.clicked.connect(lambda: self.select_driver_binary_path(self.waifu2x_caffe_path_line_edit)) - self.waifu2x_caffe_mode_combo_box = self.findChild(QComboBox, 'waifu2xCaffeModeComboBox') - self.waifu2x_caffe_noise_level_spin_box = self.findChild(QSpinBox, 'waifu2xCaffeNoiseLevelSpinBox') - self.waifu2x_caffe_process_combo_box = self.findChild(QComboBox, 'waifu2xCaffeProcessComboBox') - self.waifu2x_caffe_model_combobox = self.findChild(QComboBox, 'waifu2xCaffeModelComboBox') - self.waifu2x_caffe_crop_size_spin_box = self.findChild(QSpinBox, 'waifu2xCaffeCropSizeSpinBox') - self.waifu2x_caffe_output_quality_spin_box = self.findChild(QSpinBox, 'waifu2xCaffeOutputQualitySpinBox') - self.waifu2x_caffe_output_depth_spin_box = self.findChild(QSpinBox, 'waifu2xCaffeOutputDepthSpinBox') - self.waifu2x_caffe_batch_size_spin_box = self.findChild(QSpinBox, 'waifu2xCaffeBatchSizeSpinBox') - self.waifu2x_caffe_gpu_spin_box = self.findChild(QSpinBox, 'waifu2xCaffeGpuSpinBox') - self.waifu2x_caffe_tta_check_box = self.findChild(QCheckBox, 'waifu2xCaffeTtaCheckBox') + self.waifu2x_caffe_path_select_button = self.findChild( + QPushButton, "waifu2xCaffePathSelectButton" + ) + self.waifu2x_caffe_path_select_button.clicked.connect( + lambda: self.select_driver_binary_path(self.waifu2x_caffe_path_line_edit) + ) + self.waifu2x_caffe_mode_combo_box = self.findChild( + QComboBox, "waifu2xCaffeModeComboBox" + ) + self.waifu2x_caffe_noise_level_spin_box = self.findChild( + QSpinBox, "waifu2xCaffeNoiseLevelSpinBox" + ) + self.waifu2x_caffe_process_combo_box = self.findChild( + QComboBox, "waifu2xCaffeProcessComboBox" + ) + self.waifu2x_caffe_model_combobox = self.findChild( + QComboBox, "waifu2xCaffeModelComboBox" + ) + self.waifu2x_caffe_crop_size_spin_box = self.findChild( + QSpinBox, "waifu2xCaffeCropSizeSpinBox" + ) + self.waifu2x_caffe_output_quality_spin_box = self.findChild( + QSpinBox, "waifu2xCaffeOutputQualitySpinBox" + ) + self.waifu2x_caffe_output_depth_spin_box = self.findChild( + QSpinBox, "waifu2xCaffeOutputDepthSpinBox" + ) + self.waifu2x_caffe_batch_size_spin_box = self.findChild( + QSpinBox, "waifu2xCaffeBatchSizeSpinBox" + ) + self.waifu2x_caffe_gpu_spin_box = self.findChild( + QSpinBox, "waifu2xCaffeGpuSpinBox" + ) + self.waifu2x_caffe_tta_check_box = self.findChild( + QCheckBox, "waifu2xCaffeTtaCheckBox" + ) # waifu2x-converter-cpp - self.waifu2x_converter_cpp_path_line_edit = self.findChild(QLineEdit, 'waifu2xConverterCppPathLineEdit') + self.waifu2x_converter_cpp_path_line_edit = self.findChild( + QLineEdit, "waifu2xConverterCppPathLineEdit" + ) self.enable_line_edit_file_drop(self.waifu2x_converter_cpp_path_line_edit) - self.waifu2x_converter_cpp_path_edit_button = self.findChild(QPushButton, 'waifu2xConverterCppPathSelectButton') - self.waifu2x_converter_cpp_path_edit_button.clicked.connect(lambda: self.select_driver_binary_path(self.waifu2x_converter_cpp_path_line_edit)) - self.waifu2x_converter_cpp_png_compression_spin_box = self.findChild(QSpinBox, 'waifu2xConverterCppPngCompressionSpinBox') - self.waifu2x_converter_cpp_image_quality_spin_box = self.findChild(QSpinBox, 'waifu2xConverterCppImageQualitySpinBox') - self.waifu2x_converter_cpp_block_size_spin_box = self.findChild(QSpinBox, 'waifu2xConverterCppBlockSizeSpinBox') - self.waifu2x_converter_cpp_processor_spin_box = self.findChild(QSpinBox, 'waifu2xConverterCppProcessorSpinBox') - self.waifu2x_converter_cpp_model_combo_box = self.findChild(QComboBox, 'waifu2xConverterCppModelComboBox') - self.waifu2x_converter_cpp_noise_level_spin_box = self.findChild(QSpinBox, 'waifu2xConverterCppNoiseLevelSpinBox') - self.waifu2x_converter_cpp_mode_combo_box = self.findChild(QComboBox, 'waifu2xConverterCppModeComboBox') - self.waifu2x_converter_cpp_log_level_spin_box = self.findChild(QSpinBox, 'waifu2xConverterCppLogLevelSpinBox') - self.waifu2x_converter_cpp_disable_gpu_check_box = self.findChild(QCheckBox, 'waifu2xConverterCppDisableGpuCheckBox') - self.waifu2x_converter_cpp_force_opencl_check_box = self.findChild(QCheckBox, 'waifu2xConverterCppForceOpenclCheckBox') - self.waifu2x_converter_cpp_tta_check_box = self.findChild(QCheckBox, 'waifu2xConverterCppTtaCheckBox') + self.waifu2x_converter_cpp_path_edit_button = self.findChild( + QPushButton, "waifu2xConverterCppPathSelectButton" + ) + self.waifu2x_converter_cpp_path_edit_button.clicked.connect( + lambda: self.select_driver_binary_path( + self.waifu2x_converter_cpp_path_line_edit + ) + ) + self.waifu2x_converter_cpp_png_compression_spin_box = self.findChild( + QSpinBox, "waifu2xConverterCppPngCompressionSpinBox" + ) + self.waifu2x_converter_cpp_image_quality_spin_box = self.findChild( + QSpinBox, "waifu2xConverterCppImageQualitySpinBox" + ) + self.waifu2x_converter_cpp_block_size_spin_box = self.findChild( + QSpinBox, "waifu2xConverterCppBlockSizeSpinBox" + ) + self.waifu2x_converter_cpp_processor_spin_box = self.findChild( + QSpinBox, "waifu2xConverterCppProcessorSpinBox" + ) + self.waifu2x_converter_cpp_model_combo_box = self.findChild( + QComboBox, "waifu2xConverterCppModelComboBox" + ) + self.waifu2x_converter_cpp_noise_level_spin_box = self.findChild( + QSpinBox, "waifu2xConverterCppNoiseLevelSpinBox" + ) + self.waifu2x_converter_cpp_mode_combo_box = self.findChild( + QComboBox, "waifu2xConverterCppModeComboBox" + ) + self.waifu2x_converter_cpp_log_level_spin_box = self.findChild( + QSpinBox, "waifu2xConverterCppLogLevelSpinBox" + ) + self.waifu2x_converter_cpp_disable_gpu_check_box = self.findChild( + QCheckBox, "waifu2xConverterCppDisableGpuCheckBox" + ) + self.waifu2x_converter_cpp_force_opencl_check_box = self.findChild( + QCheckBox, "waifu2xConverterCppForceOpenclCheckBox" + ) + self.waifu2x_converter_cpp_tta_check_box = self.findChild( + QCheckBox, "waifu2xConverterCppTtaCheckBox" + ) # waifu2x-ncnn-vulkan - self.waifu2x_ncnn_vulkan_path_line_edit = self.findChild(QLineEdit, 'waifu2xNcnnVulkanPathLineEdit') + self.waifu2x_ncnn_vulkan_path_line_edit = self.findChild( + QLineEdit, "waifu2xNcnnVulkanPathLineEdit" + ) self.enable_line_edit_file_drop(self.waifu2x_ncnn_vulkan_path_line_edit) - self.waifu2x_ncnn_vulkan_path_select_button = self.findChild(QPushButton, 'waifu2xNcnnVulkanPathSelectButton') - self.waifu2x_ncnn_vulkan_path_select_button.clicked.connect(lambda: self.select_driver_binary_path(self.waifu2x_ncnn_vulkan_path_line_edit)) - self.waifu2x_ncnn_vulkan_noise_level_spin_box = self.findChild(QSpinBox, 'waifu2xNcnnVulkanNoiseLevelSpinBox') - self.waifu2x_ncnn_vulkan_tile_size_spin_box = self.findChild(QSpinBox, 'waifu2xNcnnVulkanTileSizeSpinBox') - self.waifu2x_ncnn_vulkan_model_combo_box = self.findChild(QComboBox, 'waifu2xNcnnVulkanModelComboBox') - self.waifu2x_ncnn_vulkan_gpu_id_spin_box = self.findChild(QSpinBox, 'waifu2xNcnnVulkanGpuIdSpinBox') - self.waifu2x_ncnn_vulkan_jobs_line_edit = self.findChild(QLineEdit, 'waifu2xNcnnVulkanJobsLineEdit') - self.waifu2x_ncnn_vulkan_tta_check_box = self.findChild(QCheckBox, 'waifu2xNcnnVulkanTtaCheckBox') + self.waifu2x_ncnn_vulkan_path_select_button = self.findChild( + QPushButton, "waifu2xNcnnVulkanPathSelectButton" + ) + self.waifu2x_ncnn_vulkan_path_select_button.clicked.connect( + lambda: self.select_driver_binary_path( + self.waifu2x_ncnn_vulkan_path_line_edit + ) + ) + self.waifu2x_ncnn_vulkan_noise_level_spin_box = self.findChild( + QSpinBox, "waifu2xNcnnVulkanNoiseLevelSpinBox" + ) + self.waifu2x_ncnn_vulkan_tile_size_spin_box = self.findChild( + QSpinBox, "waifu2xNcnnVulkanTileSizeSpinBox" + ) + self.waifu2x_ncnn_vulkan_model_combo_box = self.findChild( + QComboBox, "waifu2xNcnnVulkanModelComboBox" + ) + self.waifu2x_ncnn_vulkan_gpu_id_spin_box = self.findChild( + QSpinBox, "waifu2xNcnnVulkanGpuIdSpinBox" + ) + self.waifu2x_ncnn_vulkan_jobs_line_edit = self.findChild( + QLineEdit, "waifu2xNcnnVulkanJobsLineEdit" + ) + self.waifu2x_ncnn_vulkan_tta_check_box = self.findChild( + QCheckBox, "waifu2xNcnnVulkanTtaCheckBox" + ) # srmd-ncnn-vulkan - self.srmd_ncnn_vulkan_path_line_edit = self.findChild(QLineEdit, 'srmdNcnnVulkanPathLineEdit') + self.srmd_ncnn_vulkan_path_line_edit = self.findChild( + QLineEdit, "srmdNcnnVulkanPathLineEdit" + ) self.enable_line_edit_file_drop(self.srmd_ncnn_vulkan_path_line_edit) - self.srmd_ncnn_vulkan_path_select_button = self.findChild(QPushButton, 'srmdNcnnVulkanPathSelectButton') - self.srmd_ncnn_vulkan_path_select_button.clicked.connect(lambda: self.select_driver_binary_path(self.srmd_ncnn_vulkan_path_line_edit)) - self.srmd_ncnn_vulkan_noise_level_spin_box = self.findChild(QSpinBox, 'srmdNcnnVulkanNoiseLevelSpinBox') - self.srmd_ncnn_vulkan_tile_size_spin_box = self.findChild(QSpinBox, 'srmdNcnnVulkanTileSizeSpinBox') - self.srmd_ncnn_vulkan_model_combo_box = self.findChild(QComboBox, 'srmdNcnnVulkanModelComboBox') - self.srmd_ncnn_vulkan_gpu_id_spin_box = self.findChild(QSpinBox, 'srmdNcnnVulkanGpuIdSpinBox') - self.srmd_ncnn_vulkan_jobs_line_edit = self.findChild(QLineEdit, 'srmdNcnnVulkanJobsLineEdit') - self.srmd_ncnn_vulkan_tta_check_box = self.findChild(QCheckBox, 'srmdNcnnVulkanTtaCheckBox') + self.srmd_ncnn_vulkan_path_select_button = self.findChild( + QPushButton, "srmdNcnnVulkanPathSelectButton" + ) + self.srmd_ncnn_vulkan_path_select_button.clicked.connect( + lambda: self.select_driver_binary_path(self.srmd_ncnn_vulkan_path_line_edit) + ) + self.srmd_ncnn_vulkan_noise_level_spin_box = self.findChild( + QSpinBox, "srmdNcnnVulkanNoiseLevelSpinBox" + ) + self.srmd_ncnn_vulkan_tile_size_spin_box = self.findChild( + QSpinBox, "srmdNcnnVulkanTileSizeSpinBox" + ) + self.srmd_ncnn_vulkan_model_combo_box = self.findChild( + QComboBox, "srmdNcnnVulkanModelComboBox" + ) + self.srmd_ncnn_vulkan_gpu_id_spin_box = self.findChild( + QSpinBox, "srmdNcnnVulkanGpuIdSpinBox" + ) + self.srmd_ncnn_vulkan_jobs_line_edit = self.findChild( + QLineEdit, "srmdNcnnVulkanJobsLineEdit" + ) + self.srmd_ncnn_vulkan_tta_check_box = self.findChild( + QCheckBox, "srmdNcnnVulkanTtaCheckBox" + ) # realsr-ncnn-vulkan - self.realsr_ncnn_vulkan_path_line_edit = self.findChild(QLineEdit, 'realsrNcnnVulkanPathLineEdit') + self.realsr_ncnn_vulkan_path_line_edit = self.findChild( + QLineEdit, "realsrNcnnVulkanPathLineEdit" + ) self.enable_line_edit_file_drop(self.realsr_ncnn_vulkan_path_line_edit) - self.realsr_ncnn_vulkan_path_select_button = self.findChild(QPushButton, 'realsrNcnnVulkanPathSelectButton') - self.realsr_ncnn_vulkan_path_select_button.clicked.connect(lambda: self.select_driver_binary_path(self.realsr_ncnn_vulkan_path_line_edit)) - self.realsr_ncnn_vulkan_tile_size_spin_box = self.findChild(QSpinBox, 'realsrNcnnVulkanTileSizeSpinBox') - self.realsr_ncnn_vulkan_model_combo_box = self.findChild(QComboBox, 'realsrNcnnVulkanModelComboBox') - self.realsr_ncnn_vulkan_gpu_id_spin_box = self.findChild(QSpinBox, 'realsrNcnnVulkanGpuIdSpinBox') - self.realsr_ncnn_vulkan_jobs_line_edit = self.findChild(QLineEdit, 'realsrNcnnVulkanJobsLineEdit') - self.realsr_ncnn_vulkan_tta_check_box = self.findChild(QCheckBox, 'realsrNcnnVulkanTtaCheckBox') + self.realsr_ncnn_vulkan_path_select_button = self.findChild( + QPushButton, "realsrNcnnVulkanPathSelectButton" + ) + self.realsr_ncnn_vulkan_path_select_button.clicked.connect( + lambda: self.select_driver_binary_path( + self.realsr_ncnn_vulkan_path_line_edit + ) + ) + self.realsr_ncnn_vulkan_tile_size_spin_box = self.findChild( + QSpinBox, "realsrNcnnVulkanTileSizeSpinBox" + ) + self.realsr_ncnn_vulkan_model_combo_box = self.findChild( + QComboBox, "realsrNcnnVulkanModelComboBox" + ) + self.realsr_ncnn_vulkan_gpu_id_spin_box = self.findChild( + QSpinBox, "realsrNcnnVulkanGpuIdSpinBox" + ) + self.realsr_ncnn_vulkan_jobs_line_edit = self.findChild( + QLineEdit, "realsrNcnnVulkanJobsLineEdit" + ) + self.realsr_ncnn_vulkan_tta_check_box = self.findChild( + QCheckBox, "realsrNcnnVulkanTtaCheckBox" + ) # anime4k - self.anime4kcpp_path_line_edit = self.findChild(QLineEdit, 'anime4kCppPathLineEdit') + self.anime4kcpp_path_line_edit = self.findChild( + QLineEdit, "anime4kCppPathLineEdit" + ) self.enable_line_edit_file_drop(self.anime4kcpp_path_line_edit) - self.anime4kcpp_path_select_button = self.findChild(QPushButton, 'anime4kCppPathSelectButton') - self.anime4kcpp_path_select_button.clicked.connect(lambda: self.select_driver_binary_path(self.anime4kcpp_path_line_edit)) - self.anime4kcpp_passes_spin_box = self.findChild(QSpinBox, 'anime4kCppPassesSpinBox') - self.anime4kcpp_push_color_count_spin_box = self.findChild(QSpinBox, 'anime4kCppPushColorCountSpinBox') - self.anime4kcpp_strength_color_spin_box = self.findChild(QDoubleSpinBox, 'anime4kCppStrengthColorSpinBox') - self.anime4kcpp_strength_gradient_spin_box = self.findChild(QDoubleSpinBox, 'anime4kCppStrengthGradientSpinBox') - self.anime4kcpp_threads_spin_box = self.findChild(QSpinBox, 'anime4kCppThreadsSpinBox') - self.anime4kcpp_pre_filters_spin_box = self.findChild(QSpinBox, 'anime4kCppPreFiltersSpinBox') - self.anime4kcpp_post_filters_spin_box = self.findChild(QSpinBox, 'anime4kCppPostFiltersSpinBox') - self.anime4kcpp_platform_id_spin_box = self.findChild(QSpinBox, 'anime4kCppPlatformIdSpinBox') - self.anime4kcpp_device_id_spin_box = self.findChild(QSpinBox, 'anime4kCppDeviceIdSpinBox') - self.anime4kcpp_codec_combo_box = self.findChild(QComboBox, 'anime4kCppCodecComboBox') - self.anime4kcpp_fast_mode_check_box = self.findChild(QCheckBox, 'anime4kCppFastModeCheckBox') - self.anime4kcpp_pre_processing_check_box = self.findChild(QCheckBox, 'anime4kCppPreProcessingCheckBox') - self.anime4kcpp_post_processing_check_box = self.findChild(QCheckBox, 'anime4kCppPostProcessingCheckBox') - self.anime4kcpp_gpu_mode_check_box = self.findChild(QCheckBox, 'anime4kCppGpuModeCheckBox') - self.anime4kcpp_cnn_mode_check_box = self.findChild(QCheckBox, 'anime4kCppCnnModeCheckBox') - self.anime4kcpp_hdn_check_box = self.findChild(QCheckBox, 'anime4kCppHdnCheckBox') - self.anime4kcpp_hdn_level_spin_box = self.findChild(QSpinBox, 'anime4kCppHdnLevelSpinBox') - self.anime4kcpp_force_fps_double_spin_box = self.findChild(QDoubleSpinBox, 'anime4kCppForceFpsDoubleSpinBox') - self.anime4kcpp_disable_progress_check_box = self.findChild(QCheckBox, 'anime4kCppDisableProgressCheckBox') - self.anime4kcpp_alpha_check_box = self.findChild(QCheckBox, 'anime4kCppAlphaCheckBox') + self.anime4kcpp_path_select_button = self.findChild( + QPushButton, "anime4kCppPathSelectButton" + ) + self.anime4kcpp_path_select_button.clicked.connect( + lambda: self.select_driver_binary_path(self.anime4kcpp_path_line_edit) + ) + self.anime4kcpp_passes_spin_box = self.findChild( + QSpinBox, "anime4kCppPassesSpinBox" + ) + self.anime4kcpp_push_color_count_spin_box = self.findChild( + QSpinBox, "anime4kCppPushColorCountSpinBox" + ) + self.anime4kcpp_strength_color_spin_box = self.findChild( + QDoubleSpinBox, "anime4kCppStrengthColorSpinBox" + ) + self.anime4kcpp_strength_gradient_spin_box = self.findChild( + QDoubleSpinBox, "anime4kCppStrengthGradientSpinBox" + ) + self.anime4kcpp_threads_spin_box = self.findChild( + QSpinBox, "anime4kCppThreadsSpinBox" + ) + self.anime4kcpp_pre_filters_spin_box = self.findChild( + QSpinBox, "anime4kCppPreFiltersSpinBox" + ) + self.anime4kcpp_post_filters_spin_box = self.findChild( + QSpinBox, "anime4kCppPostFiltersSpinBox" + ) + self.anime4kcpp_platform_id_spin_box = self.findChild( + QSpinBox, "anime4kCppPlatformIdSpinBox" + ) + self.anime4kcpp_device_id_spin_box = self.findChild( + QSpinBox, "anime4kCppDeviceIdSpinBox" + ) + self.anime4kcpp_codec_combo_box = self.findChild( + QComboBox, "anime4kCppCodecComboBox" + ) + self.anime4kcpp_fast_mode_check_box = self.findChild( + QCheckBox, "anime4kCppFastModeCheckBox" + ) + self.anime4kcpp_pre_processing_check_box = self.findChild( + QCheckBox, "anime4kCppPreProcessingCheckBox" + ) + self.anime4kcpp_post_processing_check_box = self.findChild( + QCheckBox, "anime4kCppPostProcessingCheckBox" + ) + self.anime4kcpp_gpu_mode_check_box = self.findChild( + QCheckBox, "anime4kCppGpuModeCheckBox" + ) + self.anime4kcpp_cnn_mode_check_box = self.findChild( + QCheckBox, "anime4kCppCnnModeCheckBox" + ) + self.anime4kcpp_hdn_check_box = self.findChild( + QCheckBox, "anime4kCppHdnCheckBox" + ) + self.anime4kcpp_hdn_level_spin_box = self.findChild( + QSpinBox, "anime4kCppHdnLevelSpinBox" + ) + self.anime4kcpp_force_fps_double_spin_box = self.findChild( + QDoubleSpinBox, "anime4kCppForceFpsDoubleSpinBox" + ) + self.anime4kcpp_disable_progress_check_box = self.findChild( + QCheckBox, "anime4kCppDisableProgressCheckBox" + ) + self.anime4kcpp_alpha_check_box = self.findChild( + QCheckBox, "anime4kCppAlphaCheckBox" + ) # FFmpeg settings # global options - self.ffmpeg_path_line_edit = self.findChild(QLineEdit, 'ffmpegPathLineEdit') + self.ffmpeg_path_line_edit = self.findChild(QLineEdit, "ffmpegPathLineEdit") self.enable_line_edit_file_drop(self.ffmpeg_path_line_edit) - self.ffmpeg_path_select_button = self.findChild(QPushButton, 'ffmpegPathSelectButton') - self.ffmpeg_path_select_button.clicked.connect(lambda: self.select_driver_binary_path(self.ffmpeg_path_line_edit)) - self.ffmpeg_intermediate_file_name_line_edit = self.findChild(QLineEdit, 'ffmpegIntermediateFileNameLineEdit') + self.ffmpeg_path_select_button = self.findChild( + QPushButton, "ffmpegPathSelectButton" + ) + self.ffmpeg_path_select_button.clicked.connect( + lambda: self.select_driver_binary_path(self.ffmpeg_path_line_edit) + ) + self.ffmpeg_intermediate_file_name_line_edit = self.findChild( + QLineEdit, "ffmpegIntermediateFileNameLineEdit" + ) # extract frames - self.ffmpeg_extract_frames_output_options_pixel_format_line_edit = self.findChild(QLineEdit, 'ffmpegExtractFramesOutputOptionsPixelFormatLineEdit') - self.ffmpeg_extract_frames_hardware_acceleration_check_box = self.findChild(QCheckBox, 'ffmpegExtractFramesHardwareAccelerationCheckBox') + self.ffmpeg_extract_frames_output_options_pixel_format_line_edit = ( + self.findChild( + QLineEdit, "ffmpegExtractFramesOutputOptionsPixelFormatLineEdit" + ) + ) + self.ffmpeg_extract_frames_hardware_acceleration_check_box = self.findChild( + QCheckBox, "ffmpegExtractFramesHardwareAccelerationCheckBox" + ) # assemble video - self.ffmpeg_assemble_video_input_options_force_format_line_edit = self.findChild(QLineEdit, 'ffmpegAssembleVideoInputOptionsForceFormatLineEdit') - self.ffmpeg_assemble_video_output_options_video_codec_line_edit = self.findChild(QLineEdit, 'ffmpegAssembleVideoOutputOptionsVideoCodecLineEdit') - self.ffmpeg_assemble_video_output_options_pixel_format_line_edit = self.findChild(QLineEdit, 'ffmpegAssembleVideoOutputOptionsPixelFormatLineEdit') - self.ffmpeg_assemble_video_output_options_crf_spin_box = self.findChild(QSpinBox, 'ffmpegAssembleVideoOutputOptionsCrfSpinBox') - self.ffmpeg_assemble_video_output_options_tune_combo_box = self.findChild(QComboBox, 'ffmpegAssembleVideoOutputOptionsTuneComboBox') - self.ffmpeg_assemble_video_output_options_bitrate_line_edit = self.findChild(QLineEdit, 'ffmpegAssembleVideoOutputOptionsBitrateLineEdit') - self.ffmpeg_assemble_video_output_options_ensure_divisible_check_box = self.findChild(QCheckBox, 'ffmpegAssembleVideoOutputOptionsEnsureDivisibleCheckBox') - self.ffmpeg_assemble_video_hardware_acceleration_check_box = self.findChild(QCheckBox, 'ffmpegAssembleVideoHardwareAccelerationCheckBox') + self.ffmpeg_assemble_video_input_options_force_format_line_edit = ( + self.findChild( + QLineEdit, "ffmpegAssembleVideoInputOptionsForceFormatLineEdit" + ) + ) + self.ffmpeg_assemble_video_output_options_video_codec_line_edit = ( + self.findChild( + QLineEdit, "ffmpegAssembleVideoOutputOptionsVideoCodecLineEdit" + ) + ) + self.ffmpeg_assemble_video_output_options_pixel_format_line_edit = ( + self.findChild( + QLineEdit, "ffmpegAssembleVideoOutputOptionsPixelFormatLineEdit" + ) + ) + self.ffmpeg_assemble_video_output_options_crf_spin_box = self.findChild( + QSpinBox, "ffmpegAssembleVideoOutputOptionsCrfSpinBox" + ) + self.ffmpeg_assemble_video_output_options_tune_combo_box = self.findChild( + QComboBox, "ffmpegAssembleVideoOutputOptionsTuneComboBox" + ) + self.ffmpeg_assemble_video_output_options_bitrate_line_edit = self.findChild( + QLineEdit, "ffmpegAssembleVideoOutputOptionsBitrateLineEdit" + ) + self.ffmpeg_assemble_video_output_options_ensure_divisible_check_box = ( + self.findChild( + QCheckBox, "ffmpegAssembleVideoOutputOptionsEnsureDivisibleCheckBox" + ) + ) + self.ffmpeg_assemble_video_hardware_acceleration_check_box = self.findChild( + QCheckBox, "ffmpegAssembleVideoHardwareAccelerationCheckBox" + ) # migrate_streams - self.ffmpeg_migrate_streams_output_options_mapping_video_check_box_check_box = self.findChild(QCheckBox, 'ffmpegMigrateStreamsOutputOptionsMappingVideoCheckBox') - self.ffmpeg_migrate_streams_output_options_mapping_audio_check_box_check_box = self.findChild(QCheckBox, 'ffmpegMigrateStreamsOutputOptionsMappingAudioCheckBox') - self.ffmpeg_migrate_streams_output_options_mapping_subtitle_check_box_check_box = self.findChild(QCheckBox, 'ffmpegMigrateStreamsOutputOptionsMappingSubtitleCheckBox') - self.ffmpeg_migrate_streams_output_options_mapping_data_check_box_check_box = self.findChild(QCheckBox, 'ffmpegMigrateStreamsOutputOptionsMappingDataCheckBox') - self.ffmpeg_migrate_streams_output_options_mapping_font_check_box_check_box = self.findChild(QCheckBox, 'ffmpegMigrateStreamsOutputOptionsMappingFontCheckBox') - self.ffmpeg_migrate_streams_output_options_pixel_format_line_edit = self.findChild(QLineEdit, 'ffmpegMigrateStreamsOutputOptionsPixelFormatLineEdit') - self.ffmpeg_migrate_streams_output_options_frame_interpolation_spin_box = self.findChild(QSpinBox, 'ffmpegMigrateStreamsOutputOptionsFrameInterpolationSpinBox') - self.ffmpeg_migrate_streams_output_options_frame_interpolation_spin_box.valueChanged.connect(self.mutually_exclude_frame_interpolation_stream_copy) - self.ffmpeg_migrate_streams_output_options_frame_interpolation_spin_box.textChanged.connect(self.mutually_exclude_frame_interpolation_stream_copy) - self.ffmpeg_migrate_streams_output_options_copy_streams_check_box = self.findChild(QCheckBox, 'ffmpegMigrateStreamsOutputOptionsCopyStreamsCheckBox') - self.ffmpeg_migrate_streams_output_options_copy_known_metadata_tags_check_box = self.findChild(QCheckBox, 'ffmpegMigrateStreamsOutputOptionsCopyKnownMetadataTagsCheckBox') - self.ffmpeg_migrate_streams_output_options_copy_arbitrary_metadata_tags_check_box = self.findChild(QCheckBox, 'ffmpegMigrateStreamsOutputOptionsCopyArbitraryMetadataTagsCheckBox') - self.ffmpeg_migrate_streams_hardware_acceleration_check_box = self.findChild(QCheckBox, 'ffmpegMigrateStreamsHardwareAccelerationCheckBox') + self.ffmpeg_migrate_streams_output_options_mapping_video_check_box_check_box = ( + self.findChild( + QCheckBox, "ffmpegMigrateStreamsOutputOptionsMappingVideoCheckBox" + ) + ) + self.ffmpeg_migrate_streams_output_options_mapping_audio_check_box_check_box = ( + self.findChild( + QCheckBox, "ffmpegMigrateStreamsOutputOptionsMappingAudioCheckBox" + ) + ) + self.ffmpeg_migrate_streams_output_options_mapping_subtitle_check_box_check_box = self.findChild( + QCheckBox, "ffmpegMigrateStreamsOutputOptionsMappingSubtitleCheckBox" + ) + self.ffmpeg_migrate_streams_output_options_mapping_data_check_box_check_box = ( + self.findChild( + QCheckBox, "ffmpegMigrateStreamsOutputOptionsMappingDataCheckBox" + ) + ) + self.ffmpeg_migrate_streams_output_options_mapping_font_check_box_check_box = ( + self.findChild( + QCheckBox, "ffmpegMigrateStreamsOutputOptionsMappingFontCheckBox" + ) + ) + self.ffmpeg_migrate_streams_output_options_pixel_format_line_edit = ( + self.findChild( + QLineEdit, "ffmpegMigrateStreamsOutputOptionsPixelFormatLineEdit" + ) + ) + self.ffmpeg_migrate_streams_output_options_frame_interpolation_spin_box = ( + self.findChild( + QSpinBox, "ffmpegMigrateStreamsOutputOptionsFrameInterpolationSpinBox" + ) + ) + self.ffmpeg_migrate_streams_output_options_frame_interpolation_spin_box.valueChanged.connect( + self.mutually_exclude_frame_interpolation_stream_copy + ) + self.ffmpeg_migrate_streams_output_options_frame_interpolation_spin_box.textChanged.connect( + self.mutually_exclude_frame_interpolation_stream_copy + ) + self.ffmpeg_migrate_streams_output_options_copy_streams_check_box = ( + self.findChild( + QCheckBox, "ffmpegMigrateStreamsOutputOptionsCopyStreamsCheckBox" + ) + ) + self.ffmpeg_migrate_streams_output_options_copy_known_metadata_tags_check_box = self.findChild( + QCheckBox, "ffmpegMigrateStreamsOutputOptionsCopyKnownMetadataTagsCheckBox" + ) + self.ffmpeg_migrate_streams_output_options_copy_arbitrary_metadata_tags_check_box = self.findChild( + QCheckBox, + "ffmpegMigrateStreamsOutputOptionsCopyArbitraryMetadataTagsCheckBox", + ) + self.ffmpeg_migrate_streams_hardware_acceleration_check_box = self.findChild( + QCheckBox, "ffmpegMigrateStreamsHardwareAccelerationCheckBox" + ) # Gifski settings - self.gifski_path_line_edit = self.findChild(QLineEdit, 'gifskiPathLineEdit') + self.gifski_path_line_edit = self.findChild(QLineEdit, "gifskiPathLineEdit") self.enable_line_edit_file_drop(self.gifski_path_line_edit) - self.gifski_quality_spin_box = self.findChild(QSpinBox, 'gifskiQualitySpinBox') - self.gifski_fast_check_box = self.findChild(QCheckBox, 'gifskiFastCheckBox') - self.gifski_once_check_box = self.findChild(QCheckBox, 'gifskiOnceCheckBox') - self.gifski_quiet_check_box = self.findChild(QCheckBox, 'gifskiQuietCheckBox') + self.gifski_quality_spin_box = self.findChild(QSpinBox, "gifskiQualitySpinBox") + self.gifski_fast_check_box = self.findChild(QCheckBox, "gifskiFastCheckBox") + self.gifski_once_check_box = self.findChild(QCheckBox, "gifskiOnceCheckBox") + self.gifski_quiet_check_box = self.findChild(QCheckBox, "gifskiQuietCheckBox") # Tools - self.ffprobe_plain_text_edit = self.findChild(QPlainTextEdit, 'ffprobePlainTextEdit') + self.ffprobe_plain_text_edit = self.findChild( + QPlainTextEdit, "ffprobePlainTextEdit" + ) self.ffprobe_plain_text_edit.dropEvent = self.show_ffprobe_output # load configurations after GUI initialization @@ -461,321 +746,612 @@ class Video2XMainWindow(QMainWindow): def load_configurations(self): # get config file path from line edit - config_file_path = pathlib.Path(os.path.expandvars(self.config_line_edit.text())) + config_file_path = pathlib.Path( + os.path.expandvars(self.config_line_edit.text()) + ) # if file doesn't exist, return if not config_file_path.is_file(): - QErrorMessage(self).showMessage('Video2X configuration file not found, please specify manually.') + QErrorMessage(self).showMessage( + "Video2X configuration file not found, please specify manually." + ) return # read configuration dict from config file self.config = self.read_config(config_file_path) # load FFmpeg settings - self.ffmpeg_settings = self.config['ffmpeg'] - self.ffmpeg_settings['ffmpeg_path'] = str(pathlib.Path(os.path.expandvars(self.ffmpeg_settings['ffmpeg_path'])).absolute()) + self.ffmpeg_settings = self.config["ffmpeg"] + self.ffmpeg_settings["ffmpeg_path"] = str( + pathlib.Path( + os.path.expandvars(self.ffmpeg_settings["ffmpeg_path"]) + ).absolute() + ) # read Gifski configuration - self.gifski_settings = self.config['gifski'] - self.gifski_settings['gifski_path'] = str(pathlib.Path(os.path.expandvars(self.gifski_settings['gifski_path'])).absolute()) + self.gifski_settings = self.config["gifski"] + self.gifski_settings["gifski_path"] = str( + pathlib.Path( + os.path.expandvars(self.gifski_settings["gifski_path"]) + ).absolute() + ) # set cache directory path - if self.config['video2x']['video2x_cache_directory'] is None: - self.config['video2x']['video2x_cache_directory'] = str((pathlib.Path(tempfile.gettempdir()) / 'video2x').absolute()) - self.cache_line_edit.setText(self.config['video2x']['video2x_cache_directory']) + if self.config["video2x"]["video2x_cache_directory"] is None: + self.config["video2x"]["video2x_cache_directory"] = str( + (pathlib.Path(tempfile.gettempdir()) / "video2x").absolute() + ) + self.cache_line_edit.setText(self.config["video2x"]["video2x_cache_directory"]) - self.output_file_name_format_string_line_edit.setText(self.config['video2x']['output_file_name_format_string']) - self.image_output_extension_line_edit.setText(self.config['video2x']['image_output_extension']) - self.video_output_extension_line_edit.setText(self.config['video2x']['video_output_extension']) + self.output_file_name_format_string_line_edit.setText( + self.config["video2x"]["output_file_name_format_string"] + ) + self.image_output_extension_line_edit.setText( + self.config["video2x"]["image_output_extension"] + ) + self.video_output_extension_line_edit.setText( + self.config["video2x"]["video_output_extension"] + ) # load preserve frames settings - self.preserve_frames_check_box.setChecked(self.config['video2x']['preserve_frames']) + self.preserve_frames_check_box.setChecked( + self.config["video2x"]["preserve_frames"] + ) self.start_button.setEnabled(True) # waifu2x-caffe - settings = self.config['waifu2x_caffe'] - self.waifu2x_caffe_path_line_edit.setText(str(pathlib.Path(os.path.expandvars(settings['path'])).absolute())) - self.waifu2x_caffe_mode_combo_box.setCurrentText(settings['mode']) - self.waifu2x_caffe_noise_level_spin_box.setValue(settings['noise_level']) - self.waifu2x_caffe_process_combo_box.setCurrentText(settings['process']) - self.waifu2x_caffe_crop_size_spin_box.setValue(settings['crop_size']) - self.waifu2x_caffe_output_quality_spin_box.setValue(settings['output_quality']) - self.waifu2x_caffe_output_depth_spin_box.setValue(settings['output_depth']) - self.waifu2x_caffe_batch_size_spin_box.setValue(settings['batch_size']) - self.waifu2x_caffe_gpu_spin_box.setValue(settings['gpu']) - self.waifu2x_caffe_tta_check_box.setChecked(bool(settings['tta'])) + settings = self.config["waifu2x_caffe"] + self.waifu2x_caffe_path_line_edit.setText( + str(pathlib.Path(os.path.expandvars(settings["path"])).absolute()) + ) + self.waifu2x_caffe_mode_combo_box.setCurrentText(settings["mode"]) + self.waifu2x_caffe_noise_level_spin_box.setValue(settings["noise_level"]) + self.waifu2x_caffe_process_combo_box.setCurrentText(settings["process"]) + self.waifu2x_caffe_crop_size_spin_box.setValue(settings["crop_size"]) + self.waifu2x_caffe_output_quality_spin_box.setValue(settings["output_quality"]) + self.waifu2x_caffe_output_depth_spin_box.setValue(settings["output_depth"]) + self.waifu2x_caffe_batch_size_spin_box.setValue(settings["batch_size"]) + self.waifu2x_caffe_gpu_spin_box.setValue(settings["gpu"]) + self.waifu2x_caffe_tta_check_box.setChecked(bool(settings["tta"])) # waifu2x-converter-cpp - settings = self.config['waifu2x_converter_cpp'] - self.waifu2x_converter_cpp_path_line_edit.setText(str(pathlib.Path(os.path.expandvars(settings['path'])).absolute())) - self.waifu2x_converter_cpp_png_compression_spin_box.setValue(settings['png-compression']) - self.waifu2x_converter_cpp_image_quality_spin_box.setValue(settings['image-quality']) - self.waifu2x_converter_cpp_block_size_spin_box.setValue(settings['block-size']) - self.waifu2x_converter_cpp_processor_spin_box.setValue(settings['processor']) - self.waifu2x_converter_cpp_noise_level_spin_box.setValue(settings['noise-level']) - self.waifu2x_converter_cpp_mode_combo_box.setCurrentText(settings['mode']) - self.waifu2x_converter_cpp_log_level_spin_box.setValue(settings['log-level']) - self.waifu2x_converter_cpp_disable_gpu_check_box.setChecked(settings['disable-gpu']) - self.waifu2x_converter_cpp_force_opencl_check_box.setChecked(settings['force-OpenCL']) - self.waifu2x_converter_cpp_tta_check_box.setChecked(bool(settings['tta'])) + settings = self.config["waifu2x_converter_cpp"] + self.waifu2x_converter_cpp_path_line_edit.setText( + str(pathlib.Path(os.path.expandvars(settings["path"])).absolute()) + ) + self.waifu2x_converter_cpp_png_compression_spin_box.setValue( + settings["png-compression"] + ) + self.waifu2x_converter_cpp_image_quality_spin_box.setValue( + settings["image-quality"] + ) + self.waifu2x_converter_cpp_block_size_spin_box.setValue(settings["block-size"]) + self.waifu2x_converter_cpp_processor_spin_box.setValue(settings["processor"]) + self.waifu2x_converter_cpp_noise_level_spin_box.setValue( + settings["noise-level"] + ) + self.waifu2x_converter_cpp_mode_combo_box.setCurrentText(settings["mode"]) + self.waifu2x_converter_cpp_log_level_spin_box.setValue(settings["log-level"]) + self.waifu2x_converter_cpp_disable_gpu_check_box.setChecked( + settings["disable-gpu"] + ) + self.waifu2x_converter_cpp_force_opencl_check_box.setChecked( + settings["force-OpenCL"] + ) + self.waifu2x_converter_cpp_tta_check_box.setChecked(bool(settings["tta"])) # waifu2x-ncnn-vulkan - settings = self.config['waifu2x_ncnn_vulkan'] - self.waifu2x_ncnn_vulkan_path_line_edit.setText(str(pathlib.Path(os.path.expandvars(settings['path'])).absolute())) - self.waifu2x_ncnn_vulkan_noise_level_spin_box.setValue(settings['n']) - self.waifu2x_ncnn_vulkan_tile_size_spin_box.setValue(settings['t']) - self.waifu2x_ncnn_vulkan_gpu_id_spin_box.setValue(settings['g']) - self.waifu2x_ncnn_vulkan_jobs_line_edit.setText(settings['j']) - self.waifu2x_ncnn_vulkan_tta_check_box.setChecked(settings['x']) + settings = self.config["waifu2x_ncnn_vulkan"] + self.waifu2x_ncnn_vulkan_path_line_edit.setText( + str(pathlib.Path(os.path.expandvars(settings["path"])).absolute()) + ) + self.waifu2x_ncnn_vulkan_noise_level_spin_box.setValue(settings["n"]) + self.waifu2x_ncnn_vulkan_tile_size_spin_box.setValue(settings["t"]) + self.waifu2x_ncnn_vulkan_gpu_id_spin_box.setValue(settings["g"]) + self.waifu2x_ncnn_vulkan_jobs_line_edit.setText(settings["j"]) + self.waifu2x_ncnn_vulkan_tta_check_box.setChecked(settings["x"]) # srmd-ncnn-vulkan - settings = self.config['srmd_ncnn_vulkan'] - self.srmd_ncnn_vulkan_path_line_edit.setText(str(pathlib.Path(os.path.expandvars(settings['path'])).absolute())) - self.srmd_ncnn_vulkan_noise_level_spin_box.setValue(settings['n']) - self.srmd_ncnn_vulkan_tile_size_spin_box.setValue(settings['t']) - self.srmd_ncnn_vulkan_gpu_id_spin_box.setValue(settings['g']) - self.srmd_ncnn_vulkan_jobs_line_edit.setText(settings['j']) - self.srmd_ncnn_vulkan_tta_check_box.setChecked(settings['x']) + settings = self.config["srmd_ncnn_vulkan"] + self.srmd_ncnn_vulkan_path_line_edit.setText( + str(pathlib.Path(os.path.expandvars(settings["path"])).absolute()) + ) + self.srmd_ncnn_vulkan_noise_level_spin_box.setValue(settings["n"]) + self.srmd_ncnn_vulkan_tile_size_spin_box.setValue(settings["t"]) + self.srmd_ncnn_vulkan_gpu_id_spin_box.setValue(settings["g"]) + self.srmd_ncnn_vulkan_jobs_line_edit.setText(settings["j"]) + self.srmd_ncnn_vulkan_tta_check_box.setChecked(settings["x"]) # realsr-ncnn-vulkan - settings = self.config['realsr_ncnn_vulkan'] - self.realsr_ncnn_vulkan_path_line_edit.setText(str(pathlib.Path(os.path.expandvars(settings['path'])).absolute())) - self.realsr_ncnn_vulkan_tile_size_spin_box.setValue(settings['t']) - self.realsr_ncnn_vulkan_gpu_id_spin_box.setValue(settings['g']) - self.realsr_ncnn_vulkan_jobs_line_edit.setText(settings['j']) - self.realsr_ncnn_vulkan_tta_check_box.setChecked(settings['x']) + settings = self.config["realsr_ncnn_vulkan"] + self.realsr_ncnn_vulkan_path_line_edit.setText( + str(pathlib.Path(os.path.expandvars(settings["path"])).absolute()) + ) + self.realsr_ncnn_vulkan_tile_size_spin_box.setValue(settings["t"]) + self.realsr_ncnn_vulkan_gpu_id_spin_box.setValue(settings["g"]) + self.realsr_ncnn_vulkan_jobs_line_edit.setText(settings["j"]) + self.realsr_ncnn_vulkan_tta_check_box.setChecked(settings["x"]) # anime4k - settings = self.config['anime4kcpp'] - self.anime4kcpp_path_line_edit.setText(str(pathlib.Path(os.path.expandvars(settings['path'])).absolute())) - self.anime4kcpp_passes_spin_box.setValue(settings['passes']) - self.anime4kcpp_push_color_count_spin_box.setValue(settings['pushColorCount']) - self.anime4kcpp_strength_color_spin_box.setValue(settings['strengthColor']) - self.anime4kcpp_strength_gradient_spin_box.setValue(settings['strengthGradient']) - self.anime4kcpp_threads_spin_box.setValue(settings['threads']) - self.anime4kcpp_pre_filters_spin_box.setValue(settings['preFilters']) - self.anime4kcpp_post_filters_spin_box.setValue(settings['postFilters']) - self.anime4kcpp_platform_id_spin_box.setValue(settings['platformID']) - self.anime4kcpp_device_id_spin_box.setValue(settings['deviceID']) - self.anime4kcpp_codec_combo_box.setCurrentText(settings['codec']) - self.anime4kcpp_fast_mode_check_box.setChecked(settings['fastMode']) - self.anime4kcpp_pre_processing_check_box.setChecked(settings['preprocessing']) - self.anime4kcpp_post_processing_check_box.setChecked(settings['postprocessing']) - self.anime4kcpp_gpu_mode_check_box.setChecked(settings['GPUMode']) - self.anime4kcpp_cnn_mode_check_box.setChecked(settings['CNNMode']) - self.anime4kcpp_hdn_check_box.setChecked(settings['HDN']) - self.anime4kcpp_hdn_level_spin_box.setValue(settings['HDNLevel']) - self.anime4kcpp_force_fps_double_spin_box.setValue(settings['forceFps']) - self.anime4kcpp_disable_progress_check_box.setChecked(settings['disableProgress']) - self.anime4kcpp_alpha_check_box.setChecked(settings['alpha']) + settings = self.config["anime4kcpp"] + self.anime4kcpp_path_line_edit.setText( + str(pathlib.Path(os.path.expandvars(settings["path"])).absolute()) + ) + self.anime4kcpp_passes_spin_box.setValue(settings["passes"]) + self.anime4kcpp_push_color_count_spin_box.setValue(settings["pushColorCount"]) + self.anime4kcpp_strength_color_spin_box.setValue(settings["strengthColor"]) + self.anime4kcpp_strength_gradient_spin_box.setValue( + settings["strengthGradient"] + ) + self.anime4kcpp_threads_spin_box.setValue(settings["threads"]) + self.anime4kcpp_pre_filters_spin_box.setValue(settings["preFilters"]) + self.anime4kcpp_post_filters_spin_box.setValue(settings["postFilters"]) + self.anime4kcpp_platform_id_spin_box.setValue(settings["platformID"]) + self.anime4kcpp_device_id_spin_box.setValue(settings["deviceID"]) + self.anime4kcpp_codec_combo_box.setCurrentText(settings["codec"]) + self.anime4kcpp_fast_mode_check_box.setChecked(settings["fastMode"]) + self.anime4kcpp_pre_processing_check_box.setChecked(settings["preprocessing"]) + self.anime4kcpp_post_processing_check_box.setChecked(settings["postprocessing"]) + self.anime4kcpp_gpu_mode_check_box.setChecked(settings["GPUMode"]) + self.anime4kcpp_cnn_mode_check_box.setChecked(settings["CNNMode"]) + self.anime4kcpp_hdn_check_box.setChecked(settings["HDN"]) + self.anime4kcpp_hdn_level_spin_box.setValue(settings["HDNLevel"]) + self.anime4kcpp_force_fps_double_spin_box.setValue(settings["forceFps"]) + self.anime4kcpp_disable_progress_check_box.setChecked( + settings["disableProgress"] + ) + self.anime4kcpp_alpha_check_box.setChecked(settings["alpha"]) # ffmpeg # global options - settings = self.config['ffmpeg'] - self.ffmpeg_path_line_edit.setText(str(pathlib.Path(os.path.expandvars(settings['ffmpeg_path'])).absolute())) - self.ffmpeg_intermediate_file_name_line_edit.setText(settings['intermediate_file_name']) + settings = self.config["ffmpeg"] + self.ffmpeg_path_line_edit.setText( + str(pathlib.Path(os.path.expandvars(settings["ffmpeg_path"])).absolute()) + ) + self.ffmpeg_intermediate_file_name_line_edit.setText( + settings["intermediate_file_name"] + ) # extract frames - settings = self.config['ffmpeg']['extract_frames'] - self.ffmpeg_extract_frames_output_options_pixel_format_line_edit.setText(settings['output_options']['-pix_fmt']) + settings = self.config["ffmpeg"]["extract_frames"] + self.ffmpeg_extract_frames_output_options_pixel_format_line_edit.setText( + settings["output_options"]["-pix_fmt"] + ) # assemble video - settings = self.config['ffmpeg']['assemble_video'] - self.ffmpeg_assemble_video_input_options_force_format_line_edit.setText(settings['input_options']['-f']) - self.ffmpeg_assemble_video_output_options_video_codec_line_edit.setText(settings['output_options']['-vcodec']) - self.ffmpeg_assemble_video_output_options_pixel_format_line_edit.setText(settings['output_options']['-pix_fmt']) - self.ffmpeg_assemble_video_output_options_crf_spin_box.setValue(settings['output_options']['-crf']) - self.ffmpeg_assemble_video_output_options_tune_combo_box.setCurrentText(settings['output_options']['-tune']) - self.ffmpeg_assemble_video_output_options_bitrate_line_edit.setText(settings['output_options']['-b:v']) + settings = self.config["ffmpeg"]["assemble_video"] + self.ffmpeg_assemble_video_input_options_force_format_line_edit.setText( + settings["input_options"]["-f"] + ) + self.ffmpeg_assemble_video_output_options_video_codec_line_edit.setText( + settings["output_options"]["-vcodec"] + ) + self.ffmpeg_assemble_video_output_options_pixel_format_line_edit.setText( + settings["output_options"]["-pix_fmt"] + ) + self.ffmpeg_assemble_video_output_options_crf_spin_box.setValue( + settings["output_options"]["-crf"] + ) + self.ffmpeg_assemble_video_output_options_tune_combo_box.setCurrentText( + settings["output_options"]["-tune"] + ) + self.ffmpeg_assemble_video_output_options_bitrate_line_edit.setText( + settings["output_options"]["-b:v"] + ) # migrate streams - settings = self.config['ffmpeg']['migrate_streams'] - self.ffmpeg_migrate_streams_output_options_pixel_format_line_edit.setText(settings['output_options']['-pix_fmt']) + settings = self.config["ffmpeg"]["migrate_streams"] + self.ffmpeg_migrate_streams_output_options_pixel_format_line_edit.setText( + settings["output_options"]["-pix_fmt"] + ) # Gifski - settings = self.config['gifski'] - self.gifski_path_line_edit.setText(str(pathlib.Path(os.path.expandvars(settings['gifski_path'])).absolute())) - self.gifski_quality_spin_box.setValue(settings['quality']) - self.gifski_fast_check_box.setChecked(settings['fast']) - self.gifski_once_check_box.setChecked(settings['once']) - self.gifski_quiet_check_box.setChecked(settings['quiet']) + settings = self.config["gifski"] + self.gifski_path_line_edit.setText( + str(pathlib.Path(os.path.expandvars(settings["gifski_path"])).absolute()) + ) + self.gifski_quality_spin_box.setValue(settings["quality"]) + self.gifski_fast_check_box.setChecked(settings["fast"]) + self.gifski_once_check_box.setChecked(settings["once"]) + self.gifski_quiet_check_box.setChecked(settings["quiet"]) def resolve_driver_settings(self): # waifu2x-caffe - self.config['waifu2x_caffe']['path'] = os.path.expandvars(self.waifu2x_caffe_path_line_edit.text()) - self.config['waifu2x_caffe']['mode'] = self.waifu2x_caffe_mode_combo_box.currentText() - self.config['waifu2x_caffe']['noise_level'] = self.waifu2x_caffe_noise_level_spin_box.value() - self.config['waifu2x_caffe']['process'] = self.waifu2x_caffe_process_combo_box.currentText() - self.config['waifu2x_caffe']['model_dir'] = str((pathlib.Path(self.config['waifu2x_caffe']['path']).parent / 'models' / self.waifu2x_caffe_model_combobox.currentText()).absolute()) - self.config['waifu2x_caffe']['crop_size'] = self.waifu2x_caffe_crop_size_spin_box.value() - self.config['waifu2x_caffe']['output_quality'] = self.waifu2x_caffe_output_quality_spin_box.value() - self.config['waifu2x_caffe']['output_depth'] = self.waifu2x_caffe_output_depth_spin_box.value() - self.config['waifu2x_caffe']['batch_size'] = self.waifu2x_caffe_batch_size_spin_box.value() - self.config['waifu2x_caffe']['gpu'] = self.waifu2x_caffe_gpu_spin_box.value() - self.config['waifu2x_caffe']['tta'] = int(self.waifu2x_caffe_tta_check_box.isChecked()) + self.config["waifu2x_caffe"]["path"] = os.path.expandvars( + self.waifu2x_caffe_path_line_edit.text() + ) + self.config["waifu2x_caffe"][ + "mode" + ] = self.waifu2x_caffe_mode_combo_box.currentText() + self.config["waifu2x_caffe"][ + "noise_level" + ] = self.waifu2x_caffe_noise_level_spin_box.value() + self.config["waifu2x_caffe"][ + "process" + ] = self.waifu2x_caffe_process_combo_box.currentText() + self.config["waifu2x_caffe"]["model_dir"] = str( + ( + pathlib.Path(self.config["waifu2x_caffe"]["path"]).parent + / "models" + / self.waifu2x_caffe_model_combobox.currentText() + ).absolute() + ) + self.config["waifu2x_caffe"][ + "crop_size" + ] = self.waifu2x_caffe_crop_size_spin_box.value() + self.config["waifu2x_caffe"][ + "output_quality" + ] = self.waifu2x_caffe_output_quality_spin_box.value() + self.config["waifu2x_caffe"][ + "output_depth" + ] = self.waifu2x_caffe_output_depth_spin_box.value() + self.config["waifu2x_caffe"][ + "batch_size" + ] = self.waifu2x_caffe_batch_size_spin_box.value() + self.config["waifu2x_caffe"]["gpu"] = self.waifu2x_caffe_gpu_spin_box.value() + self.config["waifu2x_caffe"]["tta"] = int( + self.waifu2x_caffe_tta_check_box.isChecked() + ) # waifu2x-converter-cpp - self.config['waifu2x_converter_cpp']['path'] = os.path.expandvars(self.waifu2x_converter_cpp_path_line_edit.text()) - self.config['waifu2x_converter_cpp']['png-compression'] = self.waifu2x_converter_cpp_png_compression_spin_box.value() - self.config['waifu2x_converter_cpp']['image-quality'] = self.waifu2x_converter_cpp_image_quality_spin_box.value() - self.config['waifu2x_converter_cpp']['block-size'] = self.waifu2x_converter_cpp_block_size_spin_box.value() - self.config['waifu2x_converter_cpp']['processor'] = self.waifu2x_converter_cpp_processor_spin_box.value() - self.config['waifu2x_converter_cpp']['model-dir'] = str((pathlib.Path(self.config['waifu2x_converter_cpp']['path']).parent / self.waifu2x_converter_cpp_model_combo_box.currentText()).absolute()) - self.config['waifu2x_converter_cpp']['noise-level'] = self.waifu2x_converter_cpp_noise_level_spin_box.value() - self.config['waifu2x_converter_cpp']['mode'] = self.waifu2x_converter_cpp_mode_combo_box.currentText() - self.config['waifu2x_converter_cpp']['log-level'] = self.waifu2x_converter_cpp_log_level_spin_box.value() - self.config['waifu2x_converter_cpp']['disable-gpu'] = bool(self.waifu2x_converter_cpp_disable_gpu_check_box.isChecked()) - self.config['waifu2x_converter_cpp']['force-OpenCL'] = bool(self.waifu2x_converter_cpp_force_opencl_check_box.isChecked()) - self.config['waifu2x_converter_cpp']['tta'] = int(self.waifu2x_converter_cpp_tta_check_box.isChecked()) + self.config["waifu2x_converter_cpp"]["path"] = os.path.expandvars( + self.waifu2x_converter_cpp_path_line_edit.text() + ) + self.config["waifu2x_converter_cpp"][ + "png-compression" + ] = self.waifu2x_converter_cpp_png_compression_spin_box.value() + self.config["waifu2x_converter_cpp"][ + "image-quality" + ] = self.waifu2x_converter_cpp_image_quality_spin_box.value() + self.config["waifu2x_converter_cpp"][ + "block-size" + ] = self.waifu2x_converter_cpp_block_size_spin_box.value() + self.config["waifu2x_converter_cpp"][ + "processor" + ] = self.waifu2x_converter_cpp_processor_spin_box.value() + self.config["waifu2x_converter_cpp"]["model-dir"] = str( + ( + pathlib.Path(self.config["waifu2x_converter_cpp"]["path"]).parent + / self.waifu2x_converter_cpp_model_combo_box.currentText() + ).absolute() + ) + self.config["waifu2x_converter_cpp"][ + "noise-level" + ] = self.waifu2x_converter_cpp_noise_level_spin_box.value() + self.config["waifu2x_converter_cpp"][ + "mode" + ] = self.waifu2x_converter_cpp_mode_combo_box.currentText() + self.config["waifu2x_converter_cpp"][ + "log-level" + ] = self.waifu2x_converter_cpp_log_level_spin_box.value() + self.config["waifu2x_converter_cpp"]["disable-gpu"] = bool( + self.waifu2x_converter_cpp_disable_gpu_check_box.isChecked() + ) + self.config["waifu2x_converter_cpp"]["force-OpenCL"] = bool( + self.waifu2x_converter_cpp_force_opencl_check_box.isChecked() + ) + self.config["waifu2x_converter_cpp"]["tta"] = int( + self.waifu2x_converter_cpp_tta_check_box.isChecked() + ) # waifu2x-ncnn-vulkan - self.config['waifu2x_ncnn_vulkan']['path'] = os.path.expandvars(self.waifu2x_ncnn_vulkan_path_line_edit.text()) - self.config['waifu2x_ncnn_vulkan']['n'] = self.waifu2x_ncnn_vulkan_noise_level_spin_box.value() - self.config['waifu2x_ncnn_vulkan']['t'] = self.waifu2x_ncnn_vulkan_tile_size_spin_box.value() - self.config['waifu2x_ncnn_vulkan']['m'] = str((pathlib.Path(self.config['waifu2x_ncnn_vulkan']['path']).parent / self.waifu2x_ncnn_vulkan_model_combo_box.currentText()).absolute()) - self.config['waifu2x_ncnn_vulkan']['g'] = self.waifu2x_ncnn_vulkan_gpu_id_spin_box.value() - self.config['waifu2x_ncnn_vulkan']['j'] = self.waifu2x_ncnn_vulkan_jobs_line_edit.text() - self.config['waifu2x_ncnn_vulkan']['x'] = self.waifu2x_ncnn_vulkan_tta_check_box.isChecked() + self.config["waifu2x_ncnn_vulkan"]["path"] = os.path.expandvars( + self.waifu2x_ncnn_vulkan_path_line_edit.text() + ) + self.config["waifu2x_ncnn_vulkan"][ + "n" + ] = self.waifu2x_ncnn_vulkan_noise_level_spin_box.value() + self.config["waifu2x_ncnn_vulkan"][ + "t" + ] = self.waifu2x_ncnn_vulkan_tile_size_spin_box.value() + self.config["waifu2x_ncnn_vulkan"]["m"] = str( + ( + pathlib.Path(self.config["waifu2x_ncnn_vulkan"]["path"]).parent + / self.waifu2x_ncnn_vulkan_model_combo_box.currentText() + ).absolute() + ) + self.config["waifu2x_ncnn_vulkan"][ + "g" + ] = self.waifu2x_ncnn_vulkan_gpu_id_spin_box.value() + self.config["waifu2x_ncnn_vulkan"][ + "j" + ] = self.waifu2x_ncnn_vulkan_jobs_line_edit.text() + self.config["waifu2x_ncnn_vulkan"][ + "x" + ] = self.waifu2x_ncnn_vulkan_tta_check_box.isChecked() # srmd-ncnn-vulkan - self.config['srmd_ncnn_vulkan']['path'] = os.path.expandvars(self.srmd_ncnn_vulkan_path_line_edit.text()) - self.config['srmd_ncnn_vulkan']['n'] = self.srmd_ncnn_vulkan_noise_level_spin_box.value() - self.config['srmd_ncnn_vulkan']['t'] = self.srmd_ncnn_vulkan_tile_size_spin_box.value() - self.config['srmd_ncnn_vulkan']['m'] = str((pathlib.Path(self.config['srmd_ncnn_vulkan']['path']).parent / self.srmd_ncnn_vulkan_model_combo_box.currentText()).absolute()) - self.config['srmd_ncnn_vulkan']['g'] = self.srmd_ncnn_vulkan_gpu_id_spin_box.value() - self.config['srmd_ncnn_vulkan']['j'] = self.srmd_ncnn_vulkan_jobs_line_edit.text() - self.config['srmd_ncnn_vulkan']['x'] = self.srmd_ncnn_vulkan_tta_check_box.isChecked() + self.config["srmd_ncnn_vulkan"]["path"] = os.path.expandvars( + self.srmd_ncnn_vulkan_path_line_edit.text() + ) + self.config["srmd_ncnn_vulkan"][ + "n" + ] = self.srmd_ncnn_vulkan_noise_level_spin_box.value() + self.config["srmd_ncnn_vulkan"][ + "t" + ] = self.srmd_ncnn_vulkan_tile_size_spin_box.value() + self.config["srmd_ncnn_vulkan"]["m"] = str( + ( + pathlib.Path(self.config["srmd_ncnn_vulkan"]["path"]).parent + / self.srmd_ncnn_vulkan_model_combo_box.currentText() + ).absolute() + ) + self.config["srmd_ncnn_vulkan"][ + "g" + ] = self.srmd_ncnn_vulkan_gpu_id_spin_box.value() + self.config["srmd_ncnn_vulkan"][ + "j" + ] = self.srmd_ncnn_vulkan_jobs_line_edit.text() + self.config["srmd_ncnn_vulkan"][ + "x" + ] = self.srmd_ncnn_vulkan_tta_check_box.isChecked() # realsr-ncnn-vulkan - self.config['realsr_ncnn_vulkan']['path'] = os.path.expandvars(self.realsr_ncnn_vulkan_path_line_edit.text()) - self.config['realsr_ncnn_vulkan']['t'] = self.realsr_ncnn_vulkan_tile_size_spin_box.value() - self.config['realsr_ncnn_vulkan']['m'] = str((pathlib.Path(self.config['realsr_ncnn_vulkan']['path']).parent / self.realsr_ncnn_vulkan_model_combo_box.currentText()).absolute()) - self.config['realsr_ncnn_vulkan']['g'] = self.realsr_ncnn_vulkan_gpu_id_spin_box.value() - self.config['realsr_ncnn_vulkan']['j'] = self.realsr_ncnn_vulkan_jobs_line_edit.text() - self.config['realsr_ncnn_vulkan']['x'] = self.realsr_ncnn_vulkan_tta_check_box.isChecked() + self.config["realsr_ncnn_vulkan"]["path"] = os.path.expandvars( + self.realsr_ncnn_vulkan_path_line_edit.text() + ) + self.config["realsr_ncnn_vulkan"][ + "t" + ] = self.realsr_ncnn_vulkan_tile_size_spin_box.value() + self.config["realsr_ncnn_vulkan"]["m"] = str( + ( + pathlib.Path(self.config["realsr_ncnn_vulkan"]["path"]).parent + / self.realsr_ncnn_vulkan_model_combo_box.currentText() + ).absolute() + ) + self.config["realsr_ncnn_vulkan"][ + "g" + ] = self.realsr_ncnn_vulkan_gpu_id_spin_box.value() + self.config["realsr_ncnn_vulkan"][ + "j" + ] = self.realsr_ncnn_vulkan_jobs_line_edit.text() + self.config["realsr_ncnn_vulkan"][ + "x" + ] = self.realsr_ncnn_vulkan_tta_check_box.isChecked() # anime4k - self.config['anime4kcpp']['path'] = os.path.expandvars(self.anime4kcpp_path_line_edit.text()) - self.config['anime4kcpp']['passes'] = self.anime4kcpp_passes_spin_box.value() - self.config['anime4kcpp']['pushColorCount'] = self.anime4kcpp_push_color_count_spin_box.value() - self.config['anime4kcpp']['strengthColor'] = self.anime4kcpp_strength_color_spin_box.value() - self.config['anime4kcpp']['strengthGradient'] = self.anime4kcpp_strength_gradient_spin_box.value() - self.config['anime4kcpp']['threads'] = self.anime4kcpp_threads_spin_box.value() - self.config['anime4kcpp']['preFilters'] = self.anime4kcpp_pre_filters_spin_box.value() - self.config['anime4kcpp']['postFilters'] = self.anime4kcpp_post_filters_spin_box.value() - self.config['anime4kcpp']['platformID'] = self.anime4kcpp_platform_id_spin_box.value() - self.config['anime4kcpp']['deviceID'] = self.anime4kcpp_device_id_spin_box.value() - self.config['anime4kcpp']['codec'] = self.anime4kcpp_codec_combo_box.currentText() - self.config['anime4kcpp']['fastMode'] = bool(self.anime4kcpp_fast_mode_check_box.isChecked()) - self.config['anime4kcpp']['preprocessing'] = bool(self.anime4kcpp_pre_processing_check_box.isChecked()) - self.config['anime4kcpp']['postprocessing'] = bool(self.anime4kcpp_post_processing_check_box.isChecked()) - self.config['anime4kcpp']['GPUMode'] = bool(self.anime4kcpp_gpu_mode_check_box.isChecked()) - self.config['anime4kcpp']['CNNMode'] = bool(self.anime4kcpp_cnn_mode_check_box.isChecked()) - self.config['anime4kcpp']['HDN'] = bool(self.anime4kcpp_hdn_check_box.isChecked()) - self.config['anime4kcpp']['HDNLevel'] = self.anime4kcpp_hdn_level_spin_box.value() - self.config['anime4kcpp']['forceFps'] = self.anime4kcpp_force_fps_double_spin_box.value() - self.config['anime4kcpp']['disableProgress'] = bool(self.anime4kcpp_disable_progress_check_box.isChecked()) - self.config['anime4kcpp']['alpha'] = bool(self.anime4kcpp_alpha_check_box.isChecked()) + self.config["anime4kcpp"]["path"] = os.path.expandvars( + self.anime4kcpp_path_line_edit.text() + ) + self.config["anime4kcpp"]["passes"] = self.anime4kcpp_passes_spin_box.value() + self.config["anime4kcpp"][ + "pushColorCount" + ] = self.anime4kcpp_push_color_count_spin_box.value() + self.config["anime4kcpp"][ + "strengthColor" + ] = self.anime4kcpp_strength_color_spin_box.value() + self.config["anime4kcpp"][ + "strengthGradient" + ] = self.anime4kcpp_strength_gradient_spin_box.value() + self.config["anime4kcpp"]["threads"] = self.anime4kcpp_threads_spin_box.value() + self.config["anime4kcpp"][ + "preFilters" + ] = self.anime4kcpp_pre_filters_spin_box.value() + self.config["anime4kcpp"][ + "postFilters" + ] = self.anime4kcpp_post_filters_spin_box.value() + self.config["anime4kcpp"][ + "platformID" + ] = self.anime4kcpp_platform_id_spin_box.value() + self.config["anime4kcpp"][ + "deviceID" + ] = self.anime4kcpp_device_id_spin_box.value() + self.config["anime4kcpp"][ + "codec" + ] = self.anime4kcpp_codec_combo_box.currentText() + self.config["anime4kcpp"]["fastMode"] = bool( + self.anime4kcpp_fast_mode_check_box.isChecked() + ) + self.config["anime4kcpp"]["preprocessing"] = bool( + self.anime4kcpp_pre_processing_check_box.isChecked() + ) + self.config["anime4kcpp"]["postprocessing"] = bool( + self.anime4kcpp_post_processing_check_box.isChecked() + ) + self.config["anime4kcpp"]["GPUMode"] = bool( + self.anime4kcpp_gpu_mode_check_box.isChecked() + ) + self.config["anime4kcpp"]["CNNMode"] = bool( + self.anime4kcpp_cnn_mode_check_box.isChecked() + ) + self.config["anime4kcpp"]["HDN"] = bool( + self.anime4kcpp_hdn_check_box.isChecked() + ) + self.config["anime4kcpp"][ + "HDNLevel" + ] = self.anime4kcpp_hdn_level_spin_box.value() + self.config["anime4kcpp"][ + "forceFps" + ] = self.anime4kcpp_force_fps_double_spin_box.value() + self.config["anime4kcpp"]["disableProgress"] = bool( + self.anime4kcpp_disable_progress_check_box.isChecked() + ) + self.config["anime4kcpp"]["alpha"] = bool( + self.anime4kcpp_alpha_check_box.isChecked() + ) # ffmpeg - self.config['ffmpeg']['ffmpeg_path'] = os.path.expandvars(self.ffmpeg_path_line_edit.text()) - self.config['ffmpeg']['intermediate_file_name'] = self.ffmpeg_intermediate_file_name_line_edit.text() + self.config["ffmpeg"]["ffmpeg_path"] = os.path.expandvars( + self.ffmpeg_path_line_edit.text() + ) + self.config["ffmpeg"][ + "intermediate_file_name" + ] = self.ffmpeg_intermediate_file_name_line_edit.text() # extract frames - self.config['ffmpeg']['extract_frames']['output_options']['-pix_fmt'] = self.ffmpeg_extract_frames_output_options_pixel_format_line_edit.text() + self.config["ffmpeg"]["extract_frames"]["output_options"][ + "-pix_fmt" + ] = self.ffmpeg_extract_frames_output_options_pixel_format_line_edit.text() if not self.ffmpeg_extract_frames_hardware_acceleration_check_box.isChecked(): - self.config['ffmpeg']['extract_frames'].pop('-hwaccel', None) + self.config["ffmpeg"]["extract_frames"].pop("-hwaccel", None) # assemble video - self.config['ffmpeg']['assemble_video']['input_options']['-f'] = self.ffmpeg_assemble_video_input_options_force_format_line_edit.text() - self.config['ffmpeg']['assemble_video']['output_options']['-vcodec'] = self.ffmpeg_assemble_video_output_options_video_codec_line_edit.text() - self.config['ffmpeg']['assemble_video']['output_options']['-pix_fmt'] = self.ffmpeg_assemble_video_output_options_pixel_format_line_edit.text() - self.config['ffmpeg']['assemble_video']['output_options']['-crf'] = self.ffmpeg_assemble_video_output_options_crf_spin_box.value() - if self.ffmpeg_assemble_video_output_options_tune_combo_box.currentText() == 'none': - self.config['ffmpeg']['assemble_video']['output_options']['-tune'] = None + self.config["ffmpeg"]["assemble_video"]["input_options"][ + "-f" + ] = self.ffmpeg_assemble_video_input_options_force_format_line_edit.text() + self.config["ffmpeg"]["assemble_video"]["output_options"][ + "-vcodec" + ] = self.ffmpeg_assemble_video_output_options_video_codec_line_edit.text() + self.config["ffmpeg"]["assemble_video"]["output_options"][ + "-pix_fmt" + ] = self.ffmpeg_assemble_video_output_options_pixel_format_line_edit.text() + self.config["ffmpeg"]["assemble_video"]["output_options"][ + "-crf" + ] = self.ffmpeg_assemble_video_output_options_crf_spin_box.value() + if ( + self.ffmpeg_assemble_video_output_options_tune_combo_box.currentText() + == "none" + ): + self.config["ffmpeg"]["assemble_video"]["output_options"]["-tune"] = None else: - self.config['ffmpeg']['assemble_video']['output_options']['-tune'] = self.ffmpeg_assemble_video_output_options_tune_combo_box.currentText() - if self.ffmpeg_assemble_video_output_options_bitrate_line_edit.text() != '': - self.config['ffmpeg']['assemble_video']['output_options']['-b:v'] = self.ffmpeg_assemble_video_output_options_bitrate_line_edit.text() + self.config["ffmpeg"]["assemble_video"]["output_options"][ + "-tune" + ] = self.ffmpeg_assemble_video_output_options_tune_combo_box.currentText() + if self.ffmpeg_assemble_video_output_options_bitrate_line_edit.text() != "": + self.config["ffmpeg"]["assemble_video"]["output_options"][ + "-b:v" + ] = self.ffmpeg_assemble_video_output_options_bitrate_line_edit.text() else: - self.config['ffmpeg']['assemble_video']['output_options']['-b:v'] = None + self.config["ffmpeg"]["assemble_video"]["output_options"]["-b:v"] = None - if self.ffmpeg_assemble_video_output_options_ensure_divisible_check_box.isChecked(): + if ( + self.ffmpeg_assemble_video_output_options_ensure_divisible_check_box.isChecked() + ): # if video filter is enabled and is not empty and is not equal to divisible by two filter # append divisible by two filter to the end of existing filter - if ('-vf' in self.config['ffmpeg']['assemble_video']['output_options'] and - len(self.config['ffmpeg']['assemble_video']['output_options']['-vf']) > 0 and - self.config['ffmpeg']['assemble_video']['output_options']['-vf'] != 'pad=ceil(iw/2)*2:ceil(ih/2)*2'): - self.config['ffmpeg']['assemble_video']['output_options']['-vf'] += ',pad=ceil(iw/2)*2:ceil(ih/2)*2' + if ( + "-vf" in self.config["ffmpeg"]["assemble_video"]["output_options"] + and len( + self.config["ffmpeg"]["assemble_video"]["output_options"]["-vf"] + ) + > 0 + and self.config["ffmpeg"]["assemble_video"]["output_options"]["-vf"] + != "pad=ceil(iw/2)*2:ceil(ih/2)*2" + ): + self.config["ffmpeg"]["assemble_video"]["output_options"][ + "-vf" + ] += ",pad=ceil(iw/2)*2:ceil(ih/2)*2" else: - self.config['ffmpeg']['assemble_video']['output_options']['-vf'] = 'pad=ceil(iw/2)*2:ceil(ih/2)*2' + self.config["ffmpeg"]["assemble_video"]["output_options"][ + "-vf" + ] = "pad=ceil(iw/2)*2:ceil(ih/2)*2" else: - self.config['ffmpeg']['assemble_video']['output_options'].pop('-vf', None) + self.config["ffmpeg"]["assemble_video"]["output_options"].pop("-vf", None) if not self.ffmpeg_assemble_video_hardware_acceleration_check_box.isChecked(): - self.config['ffmpeg']['assemble_video'].pop('-hwaccel', None) + self.config["ffmpeg"]["assemble_video"].pop("-hwaccel", None) # migrate streams - self.config['ffmpeg']['migrate_streams']['output_options']['-map'] = [] - if self.ffmpeg_migrate_streams_output_options_mapping_video_check_box_check_box.isChecked(): - self.config['ffmpeg']['migrate_streams']['output_options']['-map'].append('0:v?') - if self.ffmpeg_migrate_streams_output_options_mapping_audio_check_box_check_box.isChecked(): - self.config['ffmpeg']['migrate_streams']['output_options']['-map'].append('1:a?') - if self.ffmpeg_migrate_streams_output_options_mapping_subtitle_check_box_check_box.isChecked(): - self.config['ffmpeg']['migrate_streams']['output_options']['-map'].append('1:s?') - if self.ffmpeg_migrate_streams_output_options_mapping_data_check_box_check_box.isChecked(): - self.config['ffmpeg']['migrate_streams']['output_options']['-map'].append('1:d?') - if self.ffmpeg_migrate_streams_output_options_mapping_font_check_box_check_box.isChecked(): - self.config['ffmpeg']['migrate_streams']['output_options']['-map'].append('1:t?') + self.config["ffmpeg"]["migrate_streams"]["output_options"]["-map"] = [] + if ( + self.ffmpeg_migrate_streams_output_options_mapping_video_check_box_check_box.isChecked() + ): + self.config["ffmpeg"]["migrate_streams"]["output_options"]["-map"].append( + "0:v?" + ) + if ( + self.ffmpeg_migrate_streams_output_options_mapping_audio_check_box_check_box.isChecked() + ): + self.config["ffmpeg"]["migrate_streams"]["output_options"]["-map"].append( + "1:a?" + ) + if ( + self.ffmpeg_migrate_streams_output_options_mapping_subtitle_check_box_check_box.isChecked() + ): + self.config["ffmpeg"]["migrate_streams"]["output_options"]["-map"].append( + "1:s?" + ) + if ( + self.ffmpeg_migrate_streams_output_options_mapping_data_check_box_check_box.isChecked() + ): + self.config["ffmpeg"]["migrate_streams"]["output_options"]["-map"].append( + "1:d?" + ) + if ( + self.ffmpeg_migrate_streams_output_options_mapping_font_check_box_check_box.isChecked() + ): + self.config["ffmpeg"]["migrate_streams"]["output_options"]["-map"].append( + "1:t?" + ) # if the list is empty, delete the key # otherwise parser will run into an error (key with no value) - if len(self.config['ffmpeg']['migrate_streams']['output_options']['-map']) == 0: - self.config['ffmpeg']['migrate_streams']['output_options'].pop('-map', None) + if len(self.config["ffmpeg"]["migrate_streams"]["output_options"]["-map"]) == 0: + self.config["ffmpeg"]["migrate_streams"]["output_options"].pop("-map", None) - self.config['ffmpeg']['migrate_streams']['output_options']['-pix_fmt'] = self.ffmpeg_migrate_streams_output_options_pixel_format_line_edit.text() + self.config["ffmpeg"]["migrate_streams"]["output_options"][ + "-pix_fmt" + ] = self.ffmpeg_migrate_streams_output_options_pixel_format_line_edit.text() - fps = self.ffmpeg_migrate_streams_output_options_frame_interpolation_spin_box.value() + fps = ( + self.ffmpeg_migrate_streams_output_options_frame_interpolation_spin_box.value() + ) if fps > 0: - if ('-vf' in self.config['ffmpeg']['migrate_streams']['output_options'] and - len(self.config['ffmpeg']['migrate_streams']['output_options']['-vf']) > 0 and - 'minterpolate=' not in self.config['ffmpeg']['migrate_streams']['output_options']['-vf']): - self.config['ffmpeg']['migrate_streams']['output_options']['-vf'] += f',minterpolate=\'fps={fps}\'' + if ( + "-vf" in self.config["ffmpeg"]["migrate_streams"]["output_options"] + and len( + self.config["ffmpeg"]["migrate_streams"]["output_options"]["-vf"] + ) + > 0 + and "minterpolate=" + not in self.config["ffmpeg"]["migrate_streams"]["output_options"]["-vf"] + ): + self.config["ffmpeg"]["migrate_streams"]["output_options"][ + "-vf" + ] += f",minterpolate='fps={fps}'" else: - self.config['ffmpeg']['migrate_streams']['output_options']['-vf'] = f'minterpolate=\'fps={fps}\'' + self.config["ffmpeg"]["migrate_streams"]["output_options"][ + "-vf" + ] = f"minterpolate='fps={fps}'" else: - self.config['ffmpeg']['migrate_streams']['output_options'].pop('-vf', None) + self.config["ffmpeg"]["migrate_streams"]["output_options"].pop("-vf", None) # copy source codec - if self.ffmpeg_migrate_streams_output_options_copy_streams_check_box.isChecked(): - self.config['ffmpeg']['migrate_streams']['output_options']['-c'] = 'copy' + if ( + self.ffmpeg_migrate_streams_output_options_copy_streams_check_box.isChecked() + ): + self.config["ffmpeg"]["migrate_streams"]["output_options"]["-c"] = "copy" else: - self.config['ffmpeg']['migrate_streams']['output_options'].pop('-c', None) + self.config["ffmpeg"]["migrate_streams"]["output_options"].pop("-c", None) # copy known metadata - if self.ffmpeg_migrate_streams_output_options_copy_known_metadata_tags_check_box.isChecked(): - self.config['ffmpeg']['migrate_streams']['output_options']['-map_metadata'] = 0 + if ( + self.ffmpeg_migrate_streams_output_options_copy_known_metadata_tags_check_box.isChecked() + ): + self.config["ffmpeg"]["migrate_streams"]["output_options"][ + "-map_metadata" + ] = 0 else: - self.config['ffmpeg']['migrate_streams']['output_options'].pop('-map_metadata', None) + self.config["ffmpeg"]["migrate_streams"]["output_options"].pop( + "-map_metadata", None + ) # copy arbitrary metadata - if self.ffmpeg_migrate_streams_output_options_copy_arbitrary_metadata_tags_check_box.isChecked(): - self.config['ffmpeg']['migrate_streams']['output_options']['-movflags'] = 'use_metadata_tags' + if ( + self.ffmpeg_migrate_streams_output_options_copy_arbitrary_metadata_tags_check_box.isChecked() + ): + self.config["ffmpeg"]["migrate_streams"]["output_options"][ + "-movflags" + ] = "use_metadata_tags" else: - self.config['ffmpeg']['migrate_streams']['output_options'].pop('-movflags', None) + self.config["ffmpeg"]["migrate_streams"]["output_options"].pop( + "-movflags", None + ) # hardware acceleration if not self.ffmpeg_migrate_streams_hardware_acceleration_check_box.isChecked(): - self.config['ffmpeg']['migrate_streams'].pop('-hwaccel', None) + self.config["ffmpeg"]["migrate_streams"].pop("-hwaccel", None) # Gifski - self.config['gifski']['gifski_path'] = os.path.expandvars(self.gifski_path_line_edit.text()) - self.config['gifski']['quality'] = self.gifski_quality_spin_box.value() - self.config['gifski']['fast'] = self.gifski_fast_check_box.isChecked() - self.config['gifski']['once'] = self.gifski_once_check_box.isChecked() - self.config['gifski']['quiet'] = self.gifski_quiet_check_box.isChecked() + self.config["gifski"]["gifski_path"] = os.path.expandvars( + self.gifski_path_line_edit.text() + ) + self.config["gifski"]["quality"] = self.gifski_quality_spin_box.value() + self.config["gifski"]["fast"] = self.gifski_fast_check_box.isChecked() + self.config["gifski"]["once"] = self.gifski_once_check_box.isChecked() + self.config["gifski"]["quiet"] = self.gifski_quiet_check_box.isChecked() def dragEnterEvent(self, event): if event.mimeData().hasUrls(): @@ -786,7 +1362,9 @@ class Video2XMainWindow(QMainWindow): def dropEvent(self, event): input_paths = [pathlib.Path(u.toLocalFile()) for u in event.mimeData().urls()] for path in input_paths: - if (path.is_file() or path.is_dir()) and not self.input_table_path_exists(path): + if (path.is_file() or path.is_dir()) and not self.input_table_path_exists( + path + ): self.input_table_data.append(path) self.update_output_path() @@ -794,7 +1372,9 @@ class Video2XMainWindow(QMainWindow): def enable_line_edit_file_drop(self, line_edit: QLineEdit): line_edit.dragEnterEvent = self.dragEnterEvent - line_edit.dropEvent = lambda event: line_edit.setText(str(pathlib.Path(event.mimeData().urls()[0].toLocalFile()).absolute())) + line_edit.dropEvent = lambda event: line_edit.setText( + str(pathlib.Path(event.mimeData().urls()[0].toLocalFile()).absolute()) + ) def show_ffprobe_output(self, event): input_paths = [pathlib.Path(u.toLocalFile()) for u in event.mimeData().urls()] @@ -807,7 +1387,7 @@ class Video2XMainWindow(QMainWindow): @staticmethod def read_config(config_file: pathlib.Path) -> dict: - """ read video2x configurations from config file + """read video2x configurations from config file Arguments: config_file {pathlib.Path} -- video2x configuration file pathlib.Path @@ -816,28 +1396,45 @@ class Video2XMainWindow(QMainWindow): dict -- dictionary of video2x configuration """ - with open(config_file, 'r') as config: + with open(config_file, "r") as config: return yaml.load(config, Loader=yaml.FullLoader) def mutually_exclude_scale_ratio_resolution(self): - if self.output_width_spin_box.value() != 0 or self.output_height_spin_box.value() != 0: + if ( + self.output_width_spin_box.value() != 0 + or self.output_height_spin_box.value() != 0 + ): self.scale_ratio_double_spin_box.setDisabled(True) - elif self.output_width_spin_box.value() == 0 and self.output_height_spin_box.value() == 0: + elif ( + self.output_width_spin_box.value() == 0 + and self.output_height_spin_box.value() == 0 + ): self.scale_ratio_double_spin_box.setDisabled(False) def mutually_exclude_frame_interpolation_stream_copy(self): - if self.ffmpeg_migrate_streams_output_options_frame_interpolation_spin_box.value() > 0: - self.ffmpeg_migrate_streams_output_options_copy_streams_check_box.setChecked(False) - self.ffmpeg_migrate_streams_output_options_copy_streams_check_box.setDisabled(True) + if ( + self.ffmpeg_migrate_streams_output_options_frame_interpolation_spin_box.value() + > 0 + ): + self.ffmpeg_migrate_streams_output_options_copy_streams_check_box.setChecked( + False + ) + self.ffmpeg_migrate_streams_output_options_copy_streams_check_box.setDisabled( + True + ) else: - self.ffmpeg_migrate_streams_output_options_copy_streams_check_box.setChecked(True) - self.ffmpeg_migrate_streams_output_options_copy_streams_check_box.setDisabled(False) + self.ffmpeg_migrate_streams_output_options_copy_streams_check_box.setChecked( + True + ) + self.ffmpeg_migrate_streams_output_options_copy_streams_check_box.setDisabled( + False + ) def update_gui_for_driver(self): current_driver = AVAILABLE_DRIVERS[self.driver_combo_box.currentText()] # update preferred processes/threads count - if current_driver == 'anime4kcpp': + if current_driver == "anime4kcpp": self.processes_spin_box.setValue(16) else: self.processes_spin_box.setValue(1) @@ -870,19 +1467,19 @@ class Video2XMainWindow(QMainWindow): def select_file(self, *args, **kwargs) -> pathlib.Path: file_selected = QFileDialog.getOpenFileName(self, *args, **kwargs) - if not isinstance(file_selected, tuple) or file_selected[0] == '': + if not isinstance(file_selected, tuple) or file_selected[0] == "": return None return pathlib.Path(file_selected[0]) def select_folder(self, *args, **kwargs) -> pathlib.Path: folder_selected = QFileDialog.getExistingDirectory(self, *args, **kwargs) - if folder_selected == '': + if folder_selected == "": return None return pathlib.Path(folder_selected) def select_save_file(self, *args, **kwargs) -> pathlib.Path: save_file_selected = QFileDialog.getSaveFileName(self, *args, **kwargs) - if not isinstance(save_file_selected, tuple) or save_file_selected[0] == '': + if not isinstance(save_file_selected, tuple) or save_file_selected[0] == "": return None return pathlib.Path(save_file_selected[0]) @@ -890,51 +1487,55 @@ class Video2XMainWindow(QMainWindow): # if input list is empty # clear output path if len(self.input_table_data) == 0: - self.output_line_edit.setText('') + self.output_line_edit.setText("") # if there are multiple output files # use cwd/output directory for output elif len(self.input_table_data) > 1: - self.output_line_edit.setText(str((CWD / 'output').absolute())) + self.output_line_edit.setText(str((CWD / "output").absolute())) # if there's only one input file # generate output file/directory name automatically elif len(self.input_table_data) == 1: input_path = self.input_table_data[0] # give up if input path doesn't exist or isn't a file or a directory - if not input_path.exists() or not (input_path.is_file() or input_path.is_dir()): + if not input_path.exists() or not ( + input_path.is_file() or input_path.is_dir() + ): return if input_path.is_file(): # generate suffix automatically try: - input_file_mime_type = magic.from_file(str(input_path.absolute()), mime=True) - input_file_type = input_file_mime_type.split('/')[0] - input_file_subtype = input_file_mime_type.split('/')[1] + input_file_mime_type = magic.from_file( + str(input_path.absolute()), mime=True + ) + input_file_type = input_file_mime_type.split("/")[0] + input_file_subtype = input_file_mime_type.split("/")[1] except Exception: input_file_type = input_file_subtype = None # in case python-magic fails to detect file type # try guessing file mime type with mimetypes - if input_file_type not in ['image', 'video']: + if input_file_type not in ["image", "video"]: input_file_mime_type = mimetypes.guess_type(input_path.name)[0] - input_file_type = input_file_mime_type.split('/')[0] - input_file_subtype = input_file_mime_type.split('/')[1] + input_file_type = input_file_mime_type.split("/")[0] + input_file_subtype = input_file_mime_type.split("/")[1] # if input file is an image - if input_file_type == 'image': + if input_file_type == "image": # if file is a gif, use .gif - if input_file_subtype == 'gif': - suffix = '.gif' + if input_file_subtype == "gif": + suffix = ".gif" # otherwise, use .png by default for all images else: suffix = self.image_output_extension_line_edit.text() # if input is video, use .mp4 as output by default - elif input_file_type == 'video': + elif input_file_type == "video": suffix = self.video_output_extension_line_edit.text() # if failed to detect file type @@ -942,86 +1543,102 @@ class Video2XMainWindow(QMainWindow): else: suffix = input_path.suffix - output_path = input_path.parent / self.output_file_name_format_string_line_edit.text().format(original_file_name=input_path.stem, extension=suffix) + output_path = ( + input_path.parent + / self.output_file_name_format_string_line_edit.text().format( + original_file_name=input_path.stem, extension=suffix + ) + ) elif input_path.is_dir(): - output_path = input_path.parent / self.output_file_name_format_string_line_edit.text().format(original_file_name=input_path.stem, extension='') + output_path = ( + input_path.parent + / self.output_file_name_format_string_line_edit.text().format( + original_file_name=input_path.stem, extension="" + ) + ) # try a new name with a different file ID output_path_id = 0 while output_path.exists(): if input_path.is_file(): - output_path = input_path.parent / pathlib.Path(f'{input_path.stem}_output_{output_path_id}{suffix}') + output_path = input_path.parent / pathlib.Path( + f"{input_path.stem}_output_{output_path_id}{suffix}" + ) elif input_path.is_dir(): - output_path = input_path.parent / pathlib.Path(f'{input_path.stem}_output_{output_path_id}') + output_path = input_path.parent / pathlib.Path( + f"{input_path.stem}_output_{output_path_id}" + ) output_path_id += 1 if not output_path.exists(): self.output_line_edit.setText(str(output_path.absolute())) def select_input_file(self): - input_file = self.select_file('Select Input File') - if (input_file is None or self.input_table_path_exists(input_file)): + input_file = self.select_file("Select Input File") + if input_file is None or self.input_table_path_exists(input_file): return self.input_table_data.append(input_file) self.update_output_path() self.update_input_table() def select_input_folder(self): - input_folder = self.select_folder('Select Input Folder') - if (input_folder is None or self.input_table_path_exists(input_folder)): + input_folder = self.select_folder("Select Input Folder") + if input_folder is None or self.input_table_path_exists(input_folder): return self.input_table_data.append(input_folder) self.update_output_path() self.update_input_table() def select_output_file(self): - output_file = self.select_file('Select Output File') + output_file = self.select_file("Select Output File") if output_file is None: return self.output_line_edit.setText(str(output_file.absolute())) def select_output_folder(self): - output_folder = self.select_folder('Select Output Folder') + output_folder = self.select_folder("Select Output Folder") if output_folder is None: return self.output_line_edit.setText(str(output_folder.absolute())) def select_cache_folder(self): - cache_folder = self.select_folder('Select Cache Folder') + cache_folder = self.select_folder("Select Cache Folder") if cache_folder is None: return self.cache_line_edit.setText(str(cache_folder.absolute())) def select_config_file(self): - config_file = self.select_file('Select Config File', filter='(YAML files (*.yaml))') + config_file = self.select_file( + "Select Config File", filter="(YAML files (*.yaml))" + ) if config_file is None: return self.config_line_edit.setText(str(config_file.absolute())) self.load_configurations() def select_driver_binary_path(self, driver_line_edit: QLineEdit): - driver_binary_path = self.select_file('Select Driver Binary File') + driver_binary_path = self.select_file("Select Driver Binary File") if driver_binary_path is None: return driver_line_edit.setText(str(driver_binary_path.absolute())) def show_shortcuts(self): message_box = QMessageBox(self) - message_box.setWindowTitle('Video2X Shortcuts') + message_box.setWindowTitle("Video2X Shortcuts") message_box.setTextFormat(Qt.MarkdownText) - shortcut_information = '''**Ctrl+W**:\tExit application\\ + shortcut_information = """**Ctrl+W**:\tExit application\\ **Ctrl+Q**:\tExit application\\ **Ctrl+I**:\tOpen select input file dialog\\ **Ctrl+O**:\tOpen select output file dialog\\ **Ctrl+Shift+I**:\tOpen select input folder dialog\\ -**Ctrl+Shift+O**:\tOpen select output folder dialog''' +**Ctrl+Shift+O**:\tOpen select output folder dialog""" message_box.setText(shortcut_information) message_box.exec_() def show_about(self): message_box = QMessageBox(self) - message_box.setWindowTitle('About Video2X') + message_box.setWindowTitle("About Video2X") message_box.setIconPixmap(QPixmap(self.video2x_icon_path).scaled(64, 64)) message_box.setTextFormat(Qt.MarkdownText) message_box.setText(LEGAL_INFO) @@ -1029,42 +1646,45 @@ class Video2XMainWindow(QMainWindow): def show_information(self, message: str): message_box = QMessageBox(self) - message_box.setWindowTitle('Information') + message_box.setWindowTitle("Information") message_box.setIcon(QMessageBox.Information) message_box.setText(message) message_box.exec_() def show_warning(self, message: str): message_box = QMessageBox(self) - message_box.setWindowTitle('Warning') + message_box.setWindowTitle("Warning") message_box.setIcon(QMessageBox.Warning) message_box.setText(message) message_box.exec_() def show_error(self, exception: Exception): - def _process_button_press(button_pressed): # if the user pressed the save button, save log file to destination - if button_pressed.text() == 'Save': - log_file_saving_path = self.select_save_file('Select Log File Saving Destination', 'video2x_error.log') + if button_pressed.text() == "Save": + log_file_saving_path = self.select_save_file( + "Select Log File Saving Destination", "video2x_error.log" + ) if log_file_saving_path is not None: - with open(log_file_saving_path, 'w', encoding='utf-8') as log_file: + with open(log_file_saving_path, "w", encoding="utf-8") as log_file: self.log_file.seek(0) log_file.write(self.log_file.read()) # QErrorMessage(self).showMessage(message.replace('\n', '
')) message_box = QMessageBox(self) - message_box.setWindowTitle('Error') + message_box.setWindowTitle("Error") message_box.setIcon(QMessageBox.Critical) message_box.setTextFormat(Qt.MarkdownText) - error_message = '''Upscaler ran into an error:\\ + error_message = """Upscaler ran into an error:\\ {}\\ Check the console output or the log file for details.\\ You can [submit an issue on GitHub](https://github.com/k4yt3x/video2x/issues/new?assignees=K4YT3X&labels=bug&template=bug-report.md&title={}) to report this error.\\ It\'s highly recommended to attach the log file.\\ -You can click \"Save\" to save the log file.''' - message_box.setText(error_message.format(exception, urllib.parse.quote(str(exception)))) +You can click \"Save\" to save the log file.""" + message_box.setText( + error_message.format(exception, urllib.parse.quote(str(exception))) + ) message_box.setStandardButtons(QMessageBox.Save | QMessageBox.Close) message_box.setDefaultButton(QMessageBox.Save) @@ -1074,33 +1694,43 @@ You can click \"Save\" to save the log file.''' def progress_monitor(self, progress_callback: pyqtSignal): # initialize progress bar values - progress_callback.emit((time.time(), 0, 0, 0, 0, 0, [], pathlib.Path(), pathlib.Path())) + progress_callback.emit( + (time.time(), 0, 0, 0, 0, 0, [], pathlib.Path(), pathlib.Path()) + ) # keep querying upscaling process and feed information to callback signal while self.upscaler.running: - progress_callback.emit((self.upscaler.current_processing_starting_time, - self.upscaler.total_frames_upscaled, - self.upscaler.total_frames, - self.upscaler.total_processed, - self.upscaler.total_files, - self.upscaler.current_pass, - self.upscaler.scaling_jobs, - self.upscaler.current_input_file, - self.upscaler.last_frame_upscaled)) + progress_callback.emit( + ( + self.upscaler.current_processing_starting_time, + self.upscaler.total_frames_upscaled, + self.upscaler.total_frames, + self.upscaler.total_processed, + self.upscaler.total_files, + self.upscaler.current_pass, + self.upscaler.scaling_jobs, + self.upscaler.current_input_file, + self.upscaler.last_frame_upscaled, + ) + ) time.sleep(1) # upscale process will stop at 99% # so it's set to 100 manually when all is done - progress_callback.emit((time.time(), - self.upscaler.total_frames, - self.upscaler.total_frames, - self.upscaler.total_files, - self.upscaler.total_files, - len(self.upscaler.scaling_jobs), - self.upscaler.scaling_jobs, - pathlib.Path(), - pathlib.Path())) + progress_callback.emit( + ( + time.time(), + self.upscaler.total_frames, + self.upscaler.total_frames, + self.upscaler.total_files, + self.upscaler.total_files, + len(self.upscaler.scaling_jobs), + self.upscaler.scaling_jobs, + pathlib.Path(), + pathlib.Path(), + ) + ) def set_progress(self, progress_information: tuple): current_processing_starting_time = progress_information[0] @@ -1125,22 +1755,45 @@ You can click \"Save\" to save the log file.''' # set calculated values in GUI self.current_progress_bar.setMaximum(total_frames) self.current_progress_bar.setValue(total_frames_upscaled) - self.frames_label.setText('Frames: {}/{}'.format(total_frames_upscaled, total_frames)) - self.time_elapsed_label.setText('Time Elapsed: {}'.format(time.strftime("%H:%M:%S", time.gmtime(time_elapsed)))) - self.time_remaining_label.setText('Time Remaining: {}'.format(time.strftime("%H:%M:%S", time.gmtime(time_remaining)))) - self.rate_label.setText('Rate (FPS): {}'.format(round(rate, 2))) - self.overall_progress_label.setText('Overall Progress: {}/{}'.format(total_processed, total_files)) + self.frames_label.setText( + "Frames: {}/{}".format(total_frames_upscaled, total_frames) + ) + self.time_elapsed_label.setText( + "Time Elapsed: {}".format( + time.strftime("%H:%M:%S", time.gmtime(time_elapsed)) + ) + ) + self.time_remaining_label.setText( + "Time Remaining: {}".format( + time.strftime("%H:%M:%S", time.gmtime(time_remaining)) + ) + ) + self.rate_label.setText("Rate (FPS): {}".format(round(rate, 2))) + self.overall_progress_label.setText( + "Overall Progress: {}/{}".format(total_processed, total_files) + ) self.overall_progress_bar.setMaximum(total_files) self.overall_progress_bar.setValue(total_processed) - self.currently_processing_label.setText('Currently Processing: {} (pass {}/{})'.format(str(current_input_file.name), current_pass, len(scaling_jobs))) + self.currently_processing_label.setText( + "Currently Processing: {} (pass {}/{})".format( + str(current_input_file.name), current_pass, len(scaling_jobs) + ) + ) # if show frame is checked, show preview image - if self.frame_preview_show_preview_check_box.isChecked() and last_frame_upscaled.is_file(): + if ( + self.frame_preview_show_preview_check_box.isChecked() + and last_frame_upscaled.is_file() + ): last_frame_pixmap = QPixmap(str(last_frame_upscaled.absolute())) # the -2 here behind geometry subtracts frame size from width and height - self.frame_preview_label.setPixmap(last_frame_pixmap.scaled(self.frame_preview_label.width() - 2, - self.frame_preview_label.height() - 2, - Qt.KeepAspectRatio)) + self.frame_preview_label.setPixmap( + last_frame_pixmap.scaled( + self.frame_preview_label.width() - 2, + self.frame_preview_label.height() - 2, + Qt.KeepAspectRatio, + ) + ) # if keep aspect ratio is checked, don't stretch image if self.frame_preview_keep_aspect_ratio_check_box.isChecked(): @@ -1159,14 +1812,18 @@ You can click \"Save\" to save the log file.''' # reset progress display UI elements self.current_progress_bar.setMaximum(100) self.current_progress_bar.setValue(0) - self.frames_label.setText('Frames: {}/{}'.format(0, 0)) - self.time_elapsed_label.setText('Time Elapsed: {}'.format(time.strftime("%H:%M:%S", time.gmtime(0)))) - self.time_remaining_label.setText('Time Remaining: {}'.format(time.strftime("%H:%M:%S", time.gmtime(0)))) - self.rate_label.setText('Rate (FPS): {}'.format(0.0)) - self.overall_progress_label.setText('Overall Progress: {}/{}'.format(0, 0)) + self.frames_label.setText("Frames: {}/{}".format(0, 0)) + self.time_elapsed_label.setText( + "Time Elapsed: {}".format(time.strftime("%H:%M:%S", time.gmtime(0))) + ) + self.time_remaining_label.setText( + "Time Remaining: {}".format(time.strftime("%H:%M:%S", time.gmtime(0))) + ) + self.rate_label.setText("Rate (FPS): {}".format(0.0)) + self.overall_progress_label.setText("Overall Progress: {}/{}".format(0, 0)) self.overall_progress_bar.setMaximum(100) self.overall_progress_bar.setValue(0) - self.currently_processing_label.setText('Currently Processing:') + self.currently_processing_label.setText("Currently Processing:") def start(self): @@ -1177,10 +1834,10 @@ You can click \"Save\" to save the log file.''' # resolve input and output directories from GUI if len(self.input_table_data) == 0: - self.show_warning('Input path unspecified') + self.show_warning("Input path unspecified") return - if self.output_line_edit.text().strip() == '': - self.show_warning('Output path unspecified') + if self.output_line_edit.text().strip() == "": + self.show_warning("Output path unspecified") return if len(self.input_table_data) == 1: @@ -1189,13 +1846,17 @@ You can click \"Save\" to save the log file.''' input_directory = self.input_table_data # resolve output directory - output_directory = pathlib.Path(os.path.expandvars(self.output_line_edit.text())) + output_directory = pathlib.Path( + os.path.expandvars(self.output_line_edit.text()) + ) # load driver settings from GUI self.resolve_driver_settings() # load driver settings for the current driver - self.driver_settings = self.config[AVAILABLE_DRIVERS[self.driver_combo_box.currentText()]] + self.driver_settings = self.config[ + AVAILABLE_DRIVERS[self.driver_combo_box.currentText()] + ] # get scale ratio or resolution if self.scale_ratio_double_spin_box.isEnabled(): @@ -1214,18 +1875,21 @@ You can click \"Save\" to save the log file.''' driver_settings=self.driver_settings, ffmpeg_settings=self.ffmpeg_settings, gifski_settings=self.gifski_settings, - # optional parameters driver=AVAILABLE_DRIVERS[self.driver_combo_box.currentText()], scale_ratio=scale_ratio, scale_width=scale_width, scale_height=scale_height, processes=self.processes_spin_box.value(), - video2x_cache_directory=pathlib.Path(os.path.expandvars(self.cache_line_edit.text())), - extracted_frame_format=self.config['video2x']['extracted_frame_format'].lower(), + video2x_cache_directory=pathlib.Path( + os.path.expandvars(self.cache_line_edit.text()) + ), + extracted_frame_format=self.config["video2x"][ + "extracted_frame_format" + ].lower(), image_output_extension=self.image_output_extension_line_edit.text(), video_output_extension=self.video_output_extension_line_edit.text(), - preserve_frames=bool(self.preserve_frames_check_box.isChecked()) + preserve_frames=bool(self.preserve_frames_check_box.isChecked()), ) # run upscaler @@ -1259,7 +1923,7 @@ You can click \"Save\" to save the log file.''' self.reset_progress_display() def upscale_interrupted(self): - self.show_information('Upscale has been interrupted') + self.show_information("Upscale has been interrupted") self.threadpool.waitForDone(5) self.start_button.setEnabled(True) self.stop_button.setEnabled(False) @@ -1268,7 +1932,11 @@ You can click \"Save\" to save the log file.''' def upscale_successful(self): # if all threads have finished self.threadpool.waitForDone(5) - self.show_information('Upscale finished successfully, taking {} seconds'.format(round((time.time() - self.begin_time), 5))) + self.show_information( + "Upscale finished successfully, taking {} seconds".format( + round((time.time() - self.begin_time), 5) + ) + ) self.start_button.setEnabled(True) self.stop_button.setEnabled(False) self.reset_progress_display() @@ -1278,11 +1946,13 @@ You can click \"Save\" to save the log file.''' try: # if upscaler is running, ask the user for confirmation if self.upscaler.running is True: - confirmation = QMessageBox.question(self, - 'Stopping Confirmation', - 'Are you sure you want to want to stop the upscaling process?', - QMessageBox.Yes, - QMessageBox.No) + confirmation = QMessageBox.question( + self, + "Stopping Confirmation", + "Are you sure you want to want to stop the upscaling process?", + QMessageBox.Yes, + QMessageBox.No, + ) # if the user indeed wants to stop processing if confirmation == QMessageBox.Yes: with contextlib.suppress(AttributeError): @@ -1310,7 +1980,7 @@ You can click \"Save\" to save the log file.''' # this file shouldn't be imported -if __name__ == '__main__': +if __name__ == "__main__": try: app = QApplication(sys.argv) window = Video2XMainWindow() @@ -1321,4 +1991,4 @@ if __name__ == '__main__': # and hold window open using input() except Exception: traceback.print_exc() - input('Press enter to close') + input("Press enter to close") diff --git a/src/wrappers/anime4kcpp.py b/src/wrappers/anime4kcpp.py index 52efd43..1e5d9e4 100755 --- a/src/wrappers/anime4kcpp.py +++ b/src/wrappers/anime4kcpp.py @@ -27,8 +27,7 @@ from avalon_framework import Avalon class WrapperMain: - """ Anime4K CPP wrapper - """ + """Anime4K CPP wrapper""" def __init__(self, driver_settings): self.driver_settings = driver_settings @@ -38,53 +37,55 @@ class WrapperMain: def zero_to_one_float(value): value = float(value) if value < 0.0 or value > 1.0: - raise argparse.ArgumentTypeError(f'{value} is not between 0.0 and 1.0') + raise argparse.ArgumentTypeError(f"{value} is not between 0.0 and 1.0") return value @staticmethod def parse_arguments(arguments): + # fmt: off parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) parser.error = lambda message: (_ for _ in ()).throw(AttributeError(message)) - parser.add_argument('--help', action='help', help='show this help message and exit') - parser.add_argument('-i', '--input', type=str, help=argparse.SUPPRESS) # help='File for loading') - parser.add_argument('-o', '--output', type=str, help=argparse.SUPPRESS) # help='File for outputting') - parser.add_argument('-p', '--passes', type=int, help='Passes for processing') - parser.add_argument('-n', '--pushColorCount', type=int, help='Limit the number of color pushes') - parser.add_argument('-c', '--strengthColor', type=WrapperMain.zero_to_one_float, help='Strength for pushing color,range 0 to 1,higher for thinner') - parser.add_argument('-g', '--strengthGradient', type=WrapperMain.zero_to_one_float, help='Strength for pushing gradient,range 0 to 1,higher for sharper') - parser.add_argument('-z', '--zoomFactor', type=float, help='zoom factor for resizing') - parser.add_argument('-t', '--threads', type=int, help='Threads count for video processing') - parser.add_argument('-f', '--fastMode', action='store_true', help='Faster but maybe low quality') - parser.add_argument('-v', '--videoMode', action='store_true', help='Video process') - parser.add_argument('-s', '--preview', action='store_true', help='Preview image') - parser.add_argument('-b', '--preprocessing', action='store_true', help='Enable pre processing') - parser.add_argument('-a', '--postprocessing', action='store_true', help='Enable post processing') - parser.add_argument('-r', '--preFilters', type=int, help='Enhancement filter, only working when preProcessing is true,there are 5 options by binary:Median blur=0000001, Mean blur=0000010, CAS Sharpening=0000100, Gaussian blur weak=0001000, Gaussian blur=0010000, Bilateral filter=0100000, Bilateral filter faster=1000000, you can freely combine them, eg: Gaussian blur weak + Bilateral filter = 0001000 | 0100000 = 0101000 = 40(D)') - parser.add_argument('-e', '--postFilters', type=int, help='Enhancement filter, only working when postProcessing is true,there are 5 options by binary:Median blur=0000001, Mean blur=0000010, CAS Sharpening=0000100, Gaussian blur weak=0001000, Gaussian blur=0010000, Bilateral filter=0100000, Bilateral filter faster=1000000, you can freely combine them, eg: Gaussian blur weak + Bilateral filter = 0001000 | 0100000 = 0101000 = 40(D), so you can put 40 to enable Gaussian blur weak and Bilateral filter, which also is what I recommend for image that < 1080P, 48 for image that >= 1080P, and for performance I recommend to use 72 for video that < 1080P, 80 for video that >=1080P') - parser.add_argument('-q', '--GPUMode', action='store_true', help='Enable GPU acceleration') - parser.add_argument('-w', '--CNNMode', action='store_true', help='Enable ACNet') - parser.add_argument('-H', '--HDN', action='store_true', help='Enable HDN mode for ACNet') - parser.add_argument('-L', '--HDNLevel', type=int, help='Set HDN level') - parser.add_argument('-l', '--listGPUs', action='store_true', help='list GPUs') - parser.add_argument('-h', '--platformID', type=int, help='Specify the platform ID') - parser.add_argument('-d', '--deviceID', type=int, help='Specify the device ID') - parser.add_argument('-C', '--codec', type=str, help='Specify the codec for encoding from mp4v(recommended in Windows), dxva(for Windows), avc1(H264, recommended in Linux), vp09(very slow), hevc(not support in Windowds), av01(not support in Windowds) (string [=mp4v])') - parser.add_argument('-F', '--forceFps', type=float, help='Set output video fps to the specifying number, 0 to disable') - parser.add_argument('-D', '--disableProgress', action='store_true', help='disable progress display') - parser.add_argument('-W', '--webVideo', type=str, help='process the video from URL') - parser.add_argument('-A', '--alpha', action='store_true', help='preserve the Alpha channel for transparent image') + parser.add_argument("--help", action="help", help="show this help message and exit") + parser.add_argument("-i", "--input", type=str, help=argparse.SUPPRESS) # help="File for loading") + parser.add_argument("-o", "--output", type=str, help=argparse.SUPPRESS) # help="File for outputting") + parser.add_argument("-p", "--passes", type=int, help="Passes for processing") + parser.add_argument("-n", "--pushColorCount", type=int, help="Limit the number of color pushes") + parser.add_argument("-c", "--strengthColor", type=WrapperMain.zero_to_one_float, help="Strength for pushing color,range 0 to 1,higher for thinner") + parser.add_argument("-g", "--strengthGradient", type=WrapperMain.zero_to_one_float, help="Strength for pushing gradient,range 0 to 1,higher for sharper") + parser.add_argument("-z", "--zoomFactor", type=float, help="zoom factor for resizing") + parser.add_argument("-t", "--threads", type=int, help="Threads count for video processing") + parser.add_argument("-f", "--fastMode", action="store_true", help="Faster but maybe low quality") + parser.add_argument("-v", "--videoMode", action="store_true", help="Video process") + parser.add_argument("-s", "--preview", action="store_true", help="Preview image") + parser.add_argument("-b", "--preprocessing", action="store_true", help="Enable pre processing") + parser.add_argument("-a", "--postprocessing", action="store_true", help="Enable post processing") + parser.add_argument("-r", "--preFilters", type=int, help="Enhancement filter, only working when preProcessing is true,there are 5 options by binary:Median blur=0000001, Mean blur=0000010, CAS Sharpening=0000100, Gaussian blur weak=0001000, Gaussian blur=0010000, Bilateral filter=0100000, Bilateral filter faster=1000000, you can freely combine them, eg: Gaussian blur weak + Bilateral filter = 0001000 | 0100000 = 0101000 = 40(D)") + parser.add_argument("-e", "--postFilters", type=int, help="Enhancement filter, only working when postProcessing is true,there are 5 options by binary:Median blur=0000001, Mean blur=0000010, CAS Sharpening=0000100, Gaussian blur weak=0001000, Gaussian blur=0010000, Bilateral filter=0100000, Bilateral filter faster=1000000, you can freely combine them, eg: Gaussian blur weak + Bilateral filter = 0001000 | 0100000 = 0101000 = 40(D), so you can put 40 to enable Gaussian blur weak and Bilateral filter, which also is what I recommend for image that < 1080P, 48 for image that >= 1080P, and for performance I recommend to use 72 for video that < 1080P, 80 for video that >=1080P") + parser.add_argument("-q", "--GPUMode", action="store_true", help="Enable GPU acceleration") + parser.add_argument("-w", "--CNNMode", action="store_true", help="Enable ACNet") + parser.add_argument("-H", "--HDN", action="store_true", help="Enable HDN mode for ACNet") + parser.add_argument("-L", "--HDNLevel", type=int, help="Set HDN level") + parser.add_argument("-l", "--listGPUs", action="store_true", help="list GPUs") + parser.add_argument("-h", "--platformID", type=int, help="Specify the platform ID") + parser.add_argument("-d", "--deviceID", type=int, help="Specify the device ID") + parser.add_argument("-C", "--codec", type=str, help="Specify the codec for encoding from mp4v(recommended in Windows), dxva(for Windows), avc1(H264, recommended in Linux), vp09(very slow), hevc(not support in Windowds), av01(not support in Windowds) (string [=mp4v])") + parser.add_argument("-F", "--forceFps", type=float, help="Set output video fps to the specifying number, 0 to disable") + parser.add_argument("-D", "--disableProgress", action="store_true", help="disable progress display") + parser.add_argument("-W", "--webVideo", type=str, help="process the video from URL") + parser.add_argument("-A", "--alpha", action="store_true", help="preserve the Alpha channel for transparent image") return parser.parse_args(arguments) + # fmt: on def load_configurations(self, upscaler): # self.driver_settings['zoomFactor'] = upscaler.scale_ratio - self.driver_settings['threads'] = upscaler.processes + self.driver_settings["threads"] = upscaler.processes # append FFmpeg path to the end of PATH # Anime4KCPP will then use FFmpeg to migrate audio tracks - os.environ['PATH'] += f';{upscaler.ffmpeg_settings["ffmpeg_path"]}' + os.environ["PATH"] += f';{upscaler.ffmpeg_settings["ffmpeg_path"]}' def set_scale_ratio(self, scale_ratio: float): - self.driver_settings['zoomFactor'] = scale_ratio + self.driver_settings["zoomFactor"] = scale_ratio def upscale(self, input_file, output_file): """This is the core function for WAIFU2X class @@ -98,33 +99,33 @@ class WrapperMain: # change the working directory to the binary's parent directory # so the binary can find shared object files and other files - os.chdir(pathlib.Path(self.driver_settings['path']).parent) + os.chdir(pathlib.Path(self.driver_settings["path"]).parent) # overwrite config file settings - self.driver_settings['input'] = input_file - self.driver_settings['output'] = output_file + self.driver_settings["input"] = input_file + self.driver_settings["output"] = output_file # Anime4KCPP will look for Anime4KCPPKernel.cl under the current working directory # change the CWD to its containing directory so it will find it - if platform.system() == 'Windows': - os.chdir(pathlib.Path(self.driver_settings['path']).parent) + if platform.system() == "Windows": + os.chdir(pathlib.Path(self.driver_settings["path"]).parent) # list to be executed # initialize the list with waifu2x binary path as the first element - execute = [self.driver_settings['path']] + execute = [self.driver_settings["path"]] for key in self.driver_settings.keys(): value = self.driver_settings[key] # null or None means that leave this option out (keep default) - if key == 'path' or value is None or value is False: + if key == "path" or value is None or value is False: continue else: if len(key) == 1: - execute.append(f'-{key}') + execute.append(f"-{key}") else: - execute.append(f'--{key}') + execute.append(f"--{key}") # true means key is an option if value is not True: @@ -132,6 +133,8 @@ class WrapperMain: # return the Popen object of the new process created self.print_lock.acquire() - Avalon.debug_info(f'[upscaler] Subprocess {os.getpid()} executing: {" ".join(execute)}') + Avalon.debug_info( + f'[upscaler] Subprocess {os.getpid()} executing: {" ".join(execute)}' + ) self.print_lock.release() return subprocess.Popen(execute, stdout=sys.stdout, stderr=sys.stderr) diff --git a/src/wrappers/ffmpeg.py b/src/wrappers/ffmpeg.py index e3cefea..58b04fc 100755 --- a/src/wrappers/ffmpeg.py +++ b/src/wrappers/ffmpeg.py @@ -27,20 +27,24 @@ class Ffmpeg: and inserting audio tracks to videos. """ - def __init__(self, ffmpeg_settings, extracted_frame_format='png'): + def __init__(self, ffmpeg_settings, extracted_frame_format="png"): self.ffmpeg_settings = ffmpeg_settings - self.ffmpeg_path = pathlib.Path(self.ffmpeg_settings['ffmpeg_path']) - self.ffmpeg_binary = self.ffmpeg_path / 'ffmpeg' - self.ffmpeg_probe_binary = self.ffmpeg_path / 'ffprobe' + self.ffmpeg_path = pathlib.Path(self.ffmpeg_settings["ffmpeg_path"]) + self.ffmpeg_binary = self.ffmpeg_path / "ffmpeg" + self.ffmpeg_probe_binary = self.ffmpeg_path / "ffprobe" # video metadata self.extracted_frame_format = extracted_frame_format - self.intermediate_file_name = pathlib.Path(self.ffmpeg_settings['intermediate_file_name']) - self.pixel_format = self.ffmpeg_settings['extract_frames']['output_options']['-pix_fmt'] + self.intermediate_file_name = pathlib.Path( + self.ffmpeg_settings["intermediate_file_name"] + ) + self.pixel_format = self.ffmpeg_settings["extract_frames"]["output_options"][ + "-pix_fmt" + ] def get_pixel_formats(self): - """ Get a dictionary of supported pixel formats + """Get a dictionary of supported pixel formats List all supported pixel formats and their corresponding bit depth. @@ -48,12 +52,7 @@ class Ffmpeg: Returns: dictionary -- JSON dict of all pixel formats to bit depth """ - execute = [ - self.ffmpeg_probe_binary, - '-v', - 'quiet', - '-pix_fmts' - ] + execute = [self.ffmpeg_probe_binary, "-v", "quiet", "-pix_fmts"] # turn elements into str execute = [str(e) for e in execute] @@ -64,9 +63,15 @@ class Ffmpeg: pixel_formats = {} # record all pixel formats into dictionary - for line in subprocess.run(execute, check=True, stdout=subprocess.PIPE).stdout.decode().split('\n'): + for line in ( + subprocess.run(execute, check=True, stdout=subprocess.PIPE) + .stdout.decode() + .split("\n") + ): try: - pixel_formats[" ".join(line.split()).split()[1]] = int(" ".join(line.split()).split()[3]) + pixel_formats[" ".join(line.split()).split()[1]] = int( + " ".join(line.split()).split()[3] + ) except (IndexError, ValueError): pass @@ -76,7 +81,7 @@ class Ffmpeg: return pixel_formats def get_number_of_frames(self, input_file: str, video_stream_index: int) -> int: - """ Count the number of frames in a video + """Count the number of frames in a video Args: input_file (str): input file path @@ -88,26 +93,30 @@ class Ffmpeg: execute = [ self.ffmpeg_probe_binary, - '-v', - 'quiet', - '-count_frames', - '-select_streams', - f'v:{video_stream_index}', - '-show_entries', - 'stream=nb_read_frames', - '-of', - 'default=nokey=1:noprint_wrappers=1', - input_file + "-v", + "quiet", + "-count_frames", + "-select_streams", + f"v:{video_stream_index}", + "-show_entries", + "stream=nb_read_frames", + "-of", + "default=nokey=1:noprint_wrappers=1", + input_file, ] # turn elements into str execute = [str(e) for e in execute] Avalon.debug_info(f'Executing: {" ".join(execute)}') - return int(subprocess.run(execute, check=True, stdout=subprocess.PIPE).stdout.decode().strip()) + return int( + subprocess.run(execute, check=True, stdout=subprocess.PIPE) + .stdout.decode() + .strip() + ) def probe_file_info(self, input_video): - """ Gets input video information + """Gets input video information This method reads input video information using ffprobe in dictionary @@ -123,14 +132,14 @@ class Ffmpeg: # since video2x only strictly recignizes this one format execute = [ self.ffmpeg_probe_binary, - '-v', - 'quiet', - '-print_format', - 'json', - '-show_format', - '-show_streams', - '-i', - input_video + "-v", + "quiet", + "-print_format", + "json", + "-show_format", + "-show_streams", + "-i", + input_video, ] # turn elements into str @@ -138,37 +147,38 @@ class Ffmpeg: Avalon.debug_info(f'Executing: {" ".join(execute)}') json_str = subprocess.run(execute, check=True, stdout=subprocess.PIPE).stdout - return json.loads(json_str.decode('utf-8')) + return json.loads(json_str.decode("utf-8")) def extract_frames(self, input_file, extracted_frames): - """ extract frames from video or GIF file - """ - execute = [ - self.ffmpeg_binary - ] + """extract frames from video or GIF file""" + execute = [self.ffmpeg_binary] # load general options - execute.extend(self._read_configuration(phase='extract_frames')) + execute.extend(self._read_configuration(phase="extract_frames")) # load input_options - execute.extend(self._read_configuration(phase='extract_frames', section='input_options')) + execute.extend( + self._read_configuration(phase="extract_frames", section="input_options") + ) # specify input file - execute.extend([ - '-i', - input_file - ]) + execute.extend(["-i", input_file]) # load output options - execute.extend(self._read_configuration(phase='extract_frames', section='output_options')) + execute.extend( + self._read_configuration(phase="extract_frames", section="output_options") + ) # specify output file - execute.extend([ - extracted_frames / f'extracted_%0d.{self.extracted_frame_format}' - # extracted_frames / f'frame_%06d.{self.extracted_frame_format}' - ]) + execute.extend( + [ + extracted_frames + / f"extracted_%0d.{self.extracted_frame_format}" + # extracted_frames / f'frame_%06d.{self.extracted_frame_format}' + ] + ) - return(self._execute(execute)) + return self._execute(execute) def assemble_video(self, framerate, upscaled_frames): """Converts images into videos @@ -182,86 +192,93 @@ class Ffmpeg: """ execute = [ self.ffmpeg_binary, - '-r', + "-r", str(framerate) # '-s', # resolution ] # read other options - execute.extend(self._read_configuration(phase='assemble_video')) + execute.extend(self._read_configuration(phase="assemble_video")) # read input options - execute.extend(self._read_configuration(phase='assemble_video', section='input_options')) + execute.extend( + self._read_configuration(phase="assemble_video", section="input_options") + ) # WORKAROUND FOR WAIFU2X-NCNN-VULKAN # Dev: SAT3LL # rename all .png.png suffixes to .png import re - regex = re.compile(r'\.png\.png$', re.IGNORECASE) + + regex = re.compile(r"\.png\.png$", re.IGNORECASE) for frame_name in upscaled_frames.iterdir(): - (upscaled_frames / frame_name).rename(upscaled_frames / regex.sub('.png', str(frame_name))) + (upscaled_frames / frame_name).rename( + upscaled_frames / regex.sub(".png", str(frame_name)) + ) # END WORKAROUND # append input frames path into command - execute.extend([ - '-i', - upscaled_frames / f'extracted_%d.{self.extracted_frame_format}' - # upscaled_frames / f'%06d.{self.extracted_frame_format}' - ]) + execute.extend( + [ + "-i", + upscaled_frames / f"extracted_%d.{self.extracted_frame_format}" + # upscaled_frames / f'%06d.{self.extracted_frame_format}' + ] + ) # read FFmpeg output options - execute.extend(self._read_configuration(phase='assemble_video', section='output_options')) + execute.extend( + self._read_configuration(phase="assemble_video", section="output_options") + ) # specify output file location - execute.extend([ - upscaled_frames / self.intermediate_file_name - ]) + execute.extend([upscaled_frames / self.intermediate_file_name]) - return(self._execute(execute)) + return self._execute(execute) def migrate_streams(self, input_video, output_video, upscaled_frames): - """ Migrates audio tracks and subtitles from input video to output video + """Migrates audio tracks and subtitles from input video to output video Arguments: input_video {string} -- input video file path output_video {string} -- output video file path upscaled_frames {string} -- directory containing upscaled frames """ - execute = [ - self.ffmpeg_binary - ] + execute = [self.ffmpeg_binary] # load general options - execute.extend(self._read_configuration(phase='migrate_streams')) + execute.extend(self._read_configuration(phase="migrate_streams")) # load input options - execute.extend(self._read_configuration(phase='migrate_streams', section='input_options')) + execute.extend( + self._read_configuration(phase="migrate_streams", section="input_options") + ) # load input file names - execute.extend([ - - # input 1: upscaled intermediate file without sound - '-i', - upscaled_frames / self.intermediate_file_name, - - # input 2: original video with streams to copy over - '-i', - input_video - ]) + execute.extend( + [ + # input 1: upscaled intermediate file without sound + "-i", + upscaled_frames / self.intermediate_file_name, + # input 2: original video with streams to copy over + "-i", + input_video, + ] + ) # load output options - execute.extend(self._read_configuration(phase='migrate_streams', section='output_options')) + execute.extend( + self._read_configuration(phase="migrate_streams", section="output_options") + ) # load output video path - execute.extend([ - output_video - ]) + execute.extend([output_video]) - return(self._execute(execute)) + return self._execute(execute) def _read_configuration(self, phase, section=None): - """ read configuration from JSON + """read configuration from JSON Read the configurations (arguments) from the JSON configuration file and append them to the end of the @@ -290,7 +307,12 @@ class Ffmpeg: value = self.ffmpeg_settings[phase][key] # null or None means that leave this option out (keep default) - if value is None or value is False or isinstance(value, dict) or value == '': + if ( + value is None + or value is False + or isinstance(value, dict) + or value == "" + ): continue # if the value is a list, append the same argument and all values diff --git a/src/wrappers/gifski.py b/src/wrappers/gifski.py index e1c9d1f..4bc2621 100755 --- a/src/wrappers/gifski.py +++ b/src/wrappers/gifski.py @@ -19,30 +19,37 @@ from avalon_framework import Avalon class Gifski: - def __init__(self, gifski_settings): self.gifski_settings = gifski_settings - def make_gif(self, upscaled_frames: pathlib.Path, output_path: pathlib.Path, framerate: float, extracted_frame_format: str, output_width: int, output_height: int) -> subprocess.Popen: + def make_gif( + self, + upscaled_frames: pathlib.Path, + output_path: pathlib.Path, + framerate: float, + extracted_frame_format: str, + output_width: int, + output_height: int, + ) -> subprocess.Popen: execute = [ - self.gifski_settings['gifski_path'], - '-o', + self.gifski_settings["gifski_path"], + "-o", output_path, - '--fps', + "--fps", int(round(framerate, 0)), - '--width', + "--width", output_width, - '--height', - output_height + "--height", + output_height, ] # load configurations from config file execute.extend(self._load_configuration()) # append frames location - execute.extend([upscaled_frames / f'extracted_*.{extracted_frame_format}']) + execute.extend([upscaled_frames / f"extracted_*.{extracted_frame_format}"]) - return(self._execute(execute)) + return self._execute(execute) def _load_configuration(self): @@ -53,13 +60,13 @@ class Gifski: value = self.gifski_settings[key] # null or None means that leave this option out (keep default) - if key == 'gifski_path' or value is None or value is False: + if key == "gifski_path" or value is None or value is False: continue else: if len(key) == 1: - configuration.append(f'-{key}') + configuration.append(f"-{key}") else: - configuration.append(f'--{key}') + configuration.append(f"--{key}") # true means key is an option if value is not True: @@ -70,6 +77,6 @@ class Gifski: # turn all list elements into string to avoid errors execute = [str(e) for e in execute] - Avalon.debug_info(f'Executing: {execute}') + Avalon.debug_info(f"Executing: {execute}") return subprocess.Popen(execute, stdout=sys.stdout, stderr=sys.stderr) diff --git a/src/wrappers/realsr_ncnn_vulkan.py b/src/wrappers/realsr_ncnn_vulkan.py index 43992da..6d0e174 100755 --- a/src/wrappers/realsr_ncnn_vulkan.py +++ b/src/wrappers/realsr_ncnn_vulkan.py @@ -38,28 +38,32 @@ class WrapperMain: @staticmethod def parse_arguments(arguments): + # fmt: off parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) parser.error = lambda message: (_ for _ in ()).throw(AttributeError(message)) - parser.add_argument('--help', action='help', help='show this help message and exit') - parser.add_argument('-v', action='store_true', help='verbose output') - parser.add_argument('-i', type=str, help=argparse.SUPPRESS) # help='input image path (jpg/png) or directory') - parser.add_argument('-o', type=str, help=argparse.SUPPRESS) # help='output image path (png) or directory') - parser.add_argument('-s', type=int, help='upscale ratio') - parser.add_argument('-t', type=int, help='tile size (>=32/0=auto)') - parser.add_argument('-m', type=str, help='realsr model path') - parser.add_argument('-g', type=int, help='gpu device to use') - parser.add_argument('-j', type=str, help='thread count for load/proc/save') - parser.add_argument('-x', action='store_true', help='enable tta mode') - parser.add_argument('-f', type=str, help=argparse.SUPPRESS) # help='output image format (jpg/png/webp, default=ext/png)') + parser.add_argument("--help", action="help", help="show this help message and exit") + parser.add_argument("-v", action="store_true", help="verbose output") + parser.add_argument("-i", type=str, help=argparse.SUPPRESS) # help="input image path (jpg/png) or directory") + parser.add_argument("-o", type=str, help=argparse.SUPPRESS) # help="output image path (png) or directory") + parser.add_argument("-s", type=int, help="upscale ratio") + parser.add_argument("-t", type=int, help="tile size (>=32/0=auto)") + parser.add_argument("-m", type=str, help="realsr model path") + parser.add_argument("-g", type=int, help="gpu device to use") + parser.add_argument("-j", type=str, help="thread count for load/proc/save") + parser.add_argument("-x", action="store_true", help="enable tta mode") + parser.add_argument("-f", type=str, help=argparse.SUPPRESS) # help="output image format (jpg/png/webp, default=ext/png)") return parser.parse_args(arguments) + # fmt: on def load_configurations(self, upscaler): # self.driver_settings['s'] = int(upscaler.scale_ratio) - self.driver_settings['j'] = '{}:{}:{}'.format(upscaler.processes, upscaler.processes, upscaler.processes) - self.driver_settings['f'] = upscaler.extracted_frame_format.lower() + self.driver_settings["j"] = "{}:{}:{}".format( + upscaler.processes, upscaler.processes, upscaler.processes + ) + self.driver_settings["f"] = upscaler.extracted_frame_format.lower() def set_scale_ratio(self, scale_ratio: int): - self.driver_settings['s'] = int(scale_ratio) + self.driver_settings["s"] = int(scale_ratio) def upscale(self, input_directory, output_directory): """This is the core function for RealSR NCNN Vulkan class @@ -72,33 +76,33 @@ class WrapperMain: # change the working directory to the binary's parent directory # so the binary can find shared object files and other files - os.chdir(pathlib.Path(self.driver_settings['path']).parent) + os.chdir(pathlib.Path(self.driver_settings["path"]).parent) # overwrite config file settings - self.driver_settings['i'] = input_directory - self.driver_settings['o'] = output_directory + self.driver_settings["i"] = input_directory + self.driver_settings["o"] = output_directory # by default, realsr-ncnn-vulkan will look for the models under the current working directory # change the working directory to its containing folder if model directory not specified - if self.driver_settings['m'] is None and platform.system() == 'Windows': - os.chdir(pathlib.Path(self.driver_settings['path']).parent) + if self.driver_settings["m"] is None and platform.system() == "Windows": + os.chdir(pathlib.Path(self.driver_settings["path"]).parent) # list to be executed # initialize the list with the binary path as the first element - execute = [self.driver_settings['path']] + execute = [self.driver_settings["path"]] for key in self.driver_settings.keys(): value = self.driver_settings[key] # null or None means that leave this option out (keep default) - if key == 'path' or value is None or value is False: + if key == "path" or value is None or value is False: continue else: if len(key) == 1: - execute.append(f'-{key}') + execute.append(f"-{key}") else: - execute.append(f'--{key}') + execute.append(f"--{key}") # true means key is an option if value is not True: @@ -106,6 +110,8 @@ class WrapperMain: # return the Popen object of the new process created self.print_lock.acquire() - Avalon.debug_info(f'[upscaler] Subprocess {os.getpid()} executing: {" ".join(execute)}') + Avalon.debug_info( + f'[upscaler] Subprocess {os.getpid()} executing: {" ".join(execute)}' + ) self.print_lock.release() return subprocess.Popen(execute, stdout=sys.stdout, stderr=sys.stderr) diff --git a/src/wrappers/srmd_ncnn_vulkan.py b/src/wrappers/srmd_ncnn_vulkan.py index ed1531b..5d4c152 100755 --- a/src/wrappers/srmd_ncnn_vulkan.py +++ b/src/wrappers/srmd_ncnn_vulkan.py @@ -38,29 +38,33 @@ class WrapperMain: @staticmethod def parse_arguments(arguments): + # fmt: off parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) parser.error = lambda message: (_ for _ in ()).throw(AttributeError(message)) - parser.add_argument('--help', action='help', help='show this help message and exit') - parser.add_argument('-v', action='store_true', help='verbose output') - parser.add_argument('-i', type=str, help=argparse.SUPPRESS) # help='input image path (jpg/png) or directory') - parser.add_argument('-o', type=str, help=argparse.SUPPRESS) # help='output image path (png) or directory') - parser.add_argument('-n', type=int, choices=range(-1, 11), help='denoise level') - parser.add_argument('-s', type=int, help='upscale ratio') - parser.add_argument('-t', type=int, help='tile size (>=32)') - parser.add_argument('-m', type=str, help='srmd model path') - parser.add_argument('-g', type=int, help='gpu device to use') - parser.add_argument('-j', type=str, help='thread count for load/proc/save') - parser.add_argument('-x', action='store_true', help='enable tta mode') - parser.add_argument('-f', type=str, help=argparse.SUPPRESS) # help='output image format (jpg/png/webp, default=ext/png)') + parser.add_argument("--help", action="help", help="show this help message and exit") + parser.add_argument("-v", action="store_true", help="verbose output") + parser.add_argument("-i", type=str, help=argparse.SUPPRESS) # help="input image path (jpg/png) or directory") + parser.add_argument("-o", type=str, help=argparse.SUPPRESS) # help="output image path (png) or directory") + parser.add_argument("-n", type=int, choices=range(-1, 11), help="denoise level") + parser.add_argument("-s", type=int, help="upscale ratio") + parser.add_argument("-t", type=int, help="tile size (>=32)") + parser.add_argument("-m", type=str, help="srmd model path") + parser.add_argument("-g", type=int, help="gpu device to use") + parser.add_argument("-j", type=str, help="thread count for load/proc/save") + parser.add_argument("-x", action="store_true", help="enable tta mode") + parser.add_argument("-f", type=str, help=argparse.SUPPRESS) # help="output image format (jpg/png/webp, default=ext/png)") return parser.parse_args(arguments) + # fmt: on def load_configurations(self, upscaler): # self.driver_settings['s'] = int(upscaler.scale_ratio) - self.driver_settings['j'] = '{}:{}:{}'.format(upscaler.processes, upscaler.processes, upscaler.processes) - self.driver_settings['f'] = upscaler.extracted_frame_format.lower() + self.driver_settings["j"] = "{}:{}:{}".format( + upscaler.processes, upscaler.processes, upscaler.processes + ) + self.driver_settings["f"] = upscaler.extracted_frame_format.lower() def set_scale_ratio(self, scale_ratio: int): - self.driver_settings['s'] = int(scale_ratio) + self.driver_settings["s"] = int(scale_ratio) def upscale(self, input_directory, output_directory): """This is the core function for SRMD ncnn Vulkan class @@ -73,33 +77,33 @@ class WrapperMain: # change the working directory to the binary's parent directory # so the binary can find shared object files and other files - os.chdir(pathlib.Path(self.driver_settings['path']).parent) + os.chdir(pathlib.Path(self.driver_settings["path"]).parent) # overwrite config file settings - self.driver_settings['i'] = input_directory - self.driver_settings['o'] = output_directory + self.driver_settings["i"] = input_directory + self.driver_settings["o"] = output_directory # by default, srmd-ncnn-vulkan will look for the models under the current working directory # change the working directory to its containing folder if model directory not specified - if self.driver_settings['m'] is None and platform.system() == 'Windows': - os.chdir(pathlib.Path(self.driver_settings['path']).parent) + if self.driver_settings["m"] is None and platform.system() == "Windows": + os.chdir(pathlib.Path(self.driver_settings["path"]).parent) # list to be executed # initialize the list with the binary path as the first element - execute = [self.driver_settings['path']] + execute = [self.driver_settings["path"]] for key in self.driver_settings.keys(): value = self.driver_settings[key] # null or None means that leave this option out (keep default) - if key == 'path' or value is None or value is False: + if key == "path" or value is None or value is False: continue else: if len(key) == 1: - execute.append(f'-{key}') + execute.append(f"-{key}") else: - execute.append(f'--{key}') + execute.append(f"--{key}") # true means key is an option if value is not True: @@ -107,6 +111,8 @@ class WrapperMain: # return the Popen object of the new process created self.print_lock.acquire() - Avalon.debug_info(f'[upscaler] Subprocess {os.getpid()} executing: {" ".join(execute)}') + Avalon.debug_info( + f'[upscaler] Subprocess {os.getpid()} executing: {" ".join(execute)}' + ) self.print_lock.release() return subprocess.Popen(execute, stdout=sys.stdout, stderr=sys.stderr) diff --git a/src/wrappers/waifu2x_caffe.py b/src/wrappers/waifu2x_caffe.py index 7df3724..66e1277 100755 --- a/src/wrappers/waifu2x_caffe.py +++ b/src/wrappers/waifu2x_caffe.py @@ -37,77 +37,78 @@ class WrapperMain: @staticmethod def parse_arguments(arguments): + # fmt: off parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) parser.error = lambda message: (_ for _ in ()).throw(AttributeError(message)) - parser.add_argument('--help', action='help', help='show this help message and exit') - parser.add_argument('-t', '--tta', type=int, choices=range(2), help='8x slower and slightly high quality') - parser.add_argument('--gpu', type=int, help='gpu device no') - parser.add_argument('-b', '--batch_size', type=int, help='input batch size') - parser.add_argument('--crop_h', type=int, help='input image split size(height)') - parser.add_argument('--crop_w', type=int, help='input image split size(width)') - parser.add_argument('-c', '--crop_size', type=int, help='input image split size') - parser.add_argument('-d', '--output_depth', type=int, help='output image chaneel depth bit') - parser.add_argument('-q', '--output_quality', type=int, help='output image quality') - parser.add_argument('-p', '--process', choices=['cpu', 'gpu', 'cudnn'], help='process mode') - parser.add_argument('--model_dir', type=str, help='path to custom model directory (don\'t append last / )') - parser.add_argument('-h', '--scale_height', type=int, help='custom scale height') - parser.add_argument('-w', '--scale_width', type=int, help='custom scale width') - parser.add_argument('-s', '--scale_ratio', type=float, help='custom scale ratio') - parser.add_argument('-n', '--noise_level', type=int, choices=range(4), help='noise reduction level') - parser.add_argument('-m', '--mode', choices=['noise', 'scale', 'noise_scale', 'auto_scale'], help='image processing mode') - parser.add_argument('-e', '--output_extention', type=str, help='extention to output image file when output_path is (auto) or input_path is folder') - parser.add_argument('-l', '--input_extention_list', type=str, help='extention to input image file when input_path is folder') - parser.add_argument('-o', '--output_path', type=str, help=argparse.SUPPRESS) # help='path to output image file (when input_path is folder, output_path must be folder)') - parser.add_argument('-i', '--input_path', type=str, help=argparse.SUPPRESS) # help='(required) path to input image file') + parser.add_argument("--help", action="help", help="show this help message and exit") + parser.add_argument("-t", "--tta", type=int, choices=range(2), help="8x slower and slightly high quality") + parser.add_argument("--gpu", type=int, help="gpu device no") + parser.add_argument("-b", "--batch_size", type=int, help="input batch size") + parser.add_argument("--crop_h", type=int, help="input image split size(height)") + parser.add_argument("--crop_w", type=int, help="input image split size(width)") + parser.add_argument("-c", "--crop_size", type=int, help="input image split size") + parser.add_argument("-d", "--output_depth", type=int, help="output image chaneel depth bit") + parser.add_argument("-q", "--output_quality", type=int, help="output image quality") + parser.add_argument("-p", "--process", choices=["cpu", "gpu", "cudnn"], help="process mode") + parser.add_argument("--model_dir", type=str, help="path to custom model directory (don\"t append last / )") + parser.add_argument("-h", "--scale_height", type=int, help="custom scale height") + parser.add_argument("-w", "--scale_width", type=int, help="custom scale width") + parser.add_argument("-s", "--scale_ratio", type=float, help="custom scale ratio") + parser.add_argument("-n", "--noise_level", type=int, choices=range(4), help="noise reduction level") + parser.add_argument("-m", "--mode", choices=["noise", "scale", "noise_scale", "auto_scale"], help="image processing mode") + parser.add_argument("-e", "--output_extention", type=str, help="extention to output image file when output_path is (auto) or input_path is folder") + parser.add_argument("-l", "--input_extention_list", type=str, help="extention to input image file when input_path is folder") + parser.add_argument("-o", "--output_path", type=str, help=argparse.SUPPRESS) # help="path to output image file (when input_path is folder, output_path must be folder)") + parser.add_argument("-i", "--input_path", type=str, help=argparse.SUPPRESS) # help="(required) path to input image file") return parser.parse_args(arguments) + # fmt: on def load_configurations(self, upscaler): # use scale width and scale height if specified # self.driver_settings['scale_ratio'] = upscaler.scale_ratio - self.driver_settings['output_extention'] = upscaler.extracted_frame_format + self.driver_settings["output_extention"] = upscaler.extracted_frame_format # bit_depth will be 12 at this point # it will up updated later - self.driver_settings['output_depth'] = 12 + self.driver_settings["output_depth"] = 12 def set_scale_resolution(self, width: int, height: int): - self.driver_settings['scale_width'] = width - self.driver_settings['scale_height'] = height - self.driver_settings['scale_ratio'] = None + self.driver_settings["scale_width"] = width + self.driver_settings["scale_height"] = height + self.driver_settings["scale_ratio"] = None def set_scale_ratio(self, scale_ratio: float): - self.driver_settings['scale_width'] = None - self.driver_settings['scale_height'] = None - self.driver_settings['scale_ratio'] = scale_ratio + self.driver_settings["scale_width"] = None + self.driver_settings["scale_height"] = None + self.driver_settings["scale_ratio"] = scale_ratio def upscale(self, input_directory, output_directory): - """ start upscaling process - """ + """start upscaling process""" # change the working directory to the binary's parent directory # so the binary can find shared object files and other files - os.chdir(pathlib.Path(self.driver_settings['path']).parent) + os.chdir(pathlib.Path(self.driver_settings["path"]).parent) # overwrite config file settings - self.driver_settings['input_path'] = input_directory - self.driver_settings['output_path'] = output_directory + self.driver_settings["input_path"] = input_directory + self.driver_settings["output_path"] = output_directory # list to be executed # initialize the list with waifu2x binary path as the first element - execute = [self.driver_settings['path']] + execute = [self.driver_settings["path"]] for key in self.driver_settings.keys(): value = self.driver_settings[key] # null or None means that leave this option out (keep default) - if key == 'path' or value is None or value is False: + if key == "path" or value is None or value is False: continue else: if len(key) == 1: - execute.append(f'-{key}') + execute.append(f"-{key}") else: - execute.append(f'--{key}') + execute.append(f"--{key}") # true means key is an option if value is not True: @@ -115,6 +116,8 @@ class WrapperMain: # return the Popen object of the new process created self.print_lock.acquire() - Avalon.debug_info(f'[upscaler] Subprocess {os.getpid()} executing: {" ".join(execute)}') + Avalon.debug_info( + f'[upscaler] Subprocess {os.getpid()} executing: {" ".join(execute)}' + ) self.print_lock.release() return subprocess.Popen(execute, stdout=sys.stdout, stderr=sys.stderr) diff --git a/src/wrappers/waifu2x_converter_cpp.py b/src/wrappers/waifu2x_converter_cpp.py index 361edf1..6149d49 100755 --- a/src/wrappers/waifu2x_converter_cpp.py +++ b/src/wrappers/waifu2x_converter_cpp.py @@ -37,45 +37,47 @@ class WrapperMain: @staticmethod def parse_arguments(arguments): + # fmt: off parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) parser.error = lambda message: (_ for _ in ()).throw(AttributeError(message)) - parser.add_argument('--help', action='help', help='show this help message and exit') - parser.add_argument('--list-supported-formats', action='store_true', help='dump currently supported format list') - parser.add_argument('--list-opencv-formats', action='store_true', help='(deprecated. Use --list-supported-formats) dump opencv supported format list') - parser.add_argument('-l', '--list-processor', action='store_true', help='dump processor list') - parser.add_argument('-f', '--output-format', choices=['png', 'jpg'], help='The format used when running in recursive/folder mode\nSee --list-supported-formats for a list of supported formats/extensions.') - parser.add_argument('-c', '--png-compression', type=int, choices=range(10), help='Set PNG compression level (0-9), 9 = Max compression (slowest & smallest)') - parser.add_argument('-q', '--image-quality', type=int, choices=range(-1, 102), help='JPEG & WebP Compression quality (0-101, 0 being smallest size and lowest quality), use 101 for lossless WebP') - parser.add_argument('--block-size', type=int, help='block size') - parser.add_argument('--disable-gpu', action='store_true', help='disable GPU') - parser.add_argument('--force-OpenCL', action='store_true', help='force to use OpenCL on Intel Platform') - parser.add_argument('-p', '--processor', type=int, help='set target processor') - parser.add_argument('-j', '--jobs', type=int, help='number of threads launching at the same time') - parser.add_argument('--model-dir', type=str, help='path to custom model directory (don\'t append last / )') - parser.add_argument('--scale-ratio', type=float, help='custom scale ratio') - parser.add_argument('--noise-level', type=int, choices=range(4), help='noise reduction level') - parser.add_argument('-m', '--mode', choices=['noise', 'scale', 'noise-scale'], help='image processing mode') - parser.add_argument('-v', '--log-level', type=int, choices=range(5), help='Set log level') - parser.add_argument('-s', '--silent', action='store_true', help='Enable silent mode. (same as --log-level 1)') - parser.add_argument('-t', '--tta', type=int, choices=range(2), help='Enable Test-Time Augmentation mode.') - parser.add_argument('-g', '--generate-subdir', type=int, choices=range(2), help='Generate sub folder when recursive directory is enabled.') - parser.add_argument('-a', '--auto-naming', type=int, choices=range(2), help='Add postfix to output name when output path is not specified.\nSet 0 to disable this.') - parser.add_argument('-r', '--recursive-directory', type=int, choices=range(2), help='Search recursively through directories to find more images to process.') - parser.add_argument('-o', '--output', type=str, help=argparse.SUPPRESS) # help='path to output image file or directory (you should use the full path)') - parser.add_argument('-i', '--input', type=str, help=argparse.SUPPRESS) # help='(required) path to input image file or directory (you should use the full path)') - parser.add_argument('--version', action='store_true', help='Displays version information and exits.') + parser.add_argument("--help", action="help", help="show this help message and exit") + parser.add_argument("--list-supported-formats", action="store_true", help="dump currently supported format list") + parser.add_argument("--list-opencv-formats", action="store_true", help="(deprecated. Use --list-supported-formats) dump opencv supported format list") + parser.add_argument("-l", "--list-processor", action="store_true", help="dump processor list") + parser.add_argument("-f", "--output-format", choices=["png", "jpg"], help="The format used when running in recursive/folder mode\nSee --list-supported-formats for a list of supported formats/extensions.") + parser.add_argument("-c", "--png-compression", type=int, choices=range(10), help="Set PNG compression level (0-9), 9 = Max compression (slowest & smallest)") + parser.add_argument("-q", "--image-quality", type=int, choices=range(-1, 102), help="JPEG & WebP Compression quality (0-101, 0 being smallest size and lowest quality), use 101 for lossless WebP") + parser.add_argument("--block-size", type=int, help="block size") + parser.add_argument("--disable-gpu", action="store_true", help="disable GPU") + parser.add_argument("--force-OpenCL", action="store_true", help="force to use OpenCL on Intel Platform") + parser.add_argument("-p", "--processor", type=int, help="set target processor") + parser.add_argument("-j", "--jobs", type=int, help="number of threads launching at the same time") + parser.add_argument("--model-dir", type=str, help="path to custom model directory (don\"t append last / )") + parser.add_argument("--scale-ratio", type=float, help="custom scale ratio") + parser.add_argument("--noise-level", type=int, choices=range(4), help="noise reduction level") + parser.add_argument("-m", "--mode", choices=["noise", "scale", "noise-scale"], help="image processing mode") + parser.add_argument("-v", "--log-level", type=int, choices=range(5), help="Set log level") + parser.add_argument("-s", "--silent", action="store_true", help="Enable silent mode. (same as --log-level 1)") + parser.add_argument("-t", "--tta", type=int, choices=range(2), help="Enable Test-Time Augmentation mode.") + parser.add_argument("-g", "--generate-subdir", type=int, choices=range(2), help="Generate sub folder when recursive directory is enabled.") + parser.add_argument("-a", "--auto-naming", type=int, choices=range(2), help="Add postfix to output name when output path is not specified.\nSet 0 to disable this.") + parser.add_argument("-r", "--recursive-directory", type=int, choices=range(2), help="Search recursively through directories to find more images to process.") + parser.add_argument("-o", "--output", type=str, help=argparse.SUPPRESS) # help="path to output image file or directory (you should use the full path)") + parser.add_argument("-i", "--input", type=str, help=argparse.SUPPRESS) # help="(required) path to input image file or directory (you should use the full path)") + parser.add_argument("--version", action="store_true", help="Displays version information and exits.") return parser.parse_args(arguments) + # fmt: on def load_configurations(self, upscaler): # self.driver_settings['scale-ratio'] = upscaler.scale_ratio - self.driver_settings['jobs'] = upscaler.processes - self.driver_settings['output-format'] = upscaler.extracted_frame_format.lower() + self.driver_settings["jobs"] = upscaler.processes + self.driver_settings["output-format"] = upscaler.extracted_frame_format.lower() def set_scale_ratio(self, scale_ratio: float): - self.driver_settings['scale-ratio'] = scale_ratio + self.driver_settings["scale-ratio"] = scale_ratio def upscale(self, input_directory, output_directory): - """ Waifu2x Converter Driver Upscaler + """Waifu2x Converter Driver Upscaler This method executes the upscaling of extracted frames. Arguments: @@ -87,33 +89,35 @@ class WrapperMain: # change the working directory to the binary's parent directory # so the binary can find shared object files and other files - os.chdir(pathlib.Path(self.driver_settings['path']).parent) + os.chdir(pathlib.Path(self.driver_settings["path"]).parent) # overwrite config file settings - self.driver_settings['input'] = input_directory - self.driver_settings['output'] = output_directory + self.driver_settings["input"] = input_directory + self.driver_settings["output"] = output_directory # models_rgb must be specified manually for waifu2x-converter-cpp # if it's not specified in the arguments, create automatically - if self.driver_settings['model-dir'] is None: - self.driver_settings['model-dir'] = pathlib.Path(self.driver_settings['path']).parent / 'models_rgb' + if self.driver_settings["model-dir"] is None: + self.driver_settings["model-dir"] = ( + pathlib.Path(self.driver_settings["path"]).parent / "models_rgb" + ) # list to be executed # initialize the list with waifu2x binary path as the first element - execute = [self.driver_settings['path']] + execute = [self.driver_settings["path"]] for key in self.driver_settings.keys(): value = self.driver_settings[key] # null or None means that leave this option out (keep default) - if key == 'path' or value is None or value is False: + if key == "path" or value is None or value is False: continue else: if len(key) == 1: - execute.append(f'-{key}') + execute.append(f"-{key}") else: - execute.append(f'--{key}') + execute.append(f"--{key}") # true means key is an option if value is not True: @@ -121,6 +125,8 @@ class WrapperMain: # return the Popen object of the new process created self.print_lock.acquire() - Avalon.debug_info(f'[upscaler] Subprocess {os.getpid()} executing: {" ".join(execute)}') + Avalon.debug_info( + f'[upscaler] Subprocess {os.getpid()} executing: {" ".join(execute)}' + ) self.print_lock.release() return subprocess.Popen(execute, stdout=sys.stdout, stderr=sys.stderr) diff --git a/src/wrappers/waifu2x_ncnn_vulkan.py b/src/wrappers/waifu2x_ncnn_vulkan.py index e9b87fa..9ad9834 100755 --- a/src/wrappers/waifu2x_ncnn_vulkan.py +++ b/src/wrappers/waifu2x_ncnn_vulkan.py @@ -41,32 +41,36 @@ class WrapperMain: @staticmethod def parse_arguments(arguments): + # fmt: off parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) parser.error = lambda message: (_ for _ in ()).throw(AttributeError(message)) - parser.add_argument('--help', action='help', help='show this help message and exit') - parser.add_argument('-v', action='store_true', help='verbose output') - parser.add_argument('-i', type=str, help=argparse.SUPPRESS) # help='input image path (jpg/png/webp) or directory') - parser.add_argument('-o', type=str, help=argparse.SUPPRESS) # help='output image path (jpg/png/webp) or directory') - parser.add_argument('-n', type=int, choices=range(-1, 4), help='denoise level') - parser.add_argument('-s', type=int, help='upscale ratio') - parser.add_argument('-t', type=int, help='tile size (>=32)') - parser.add_argument('-m', type=str, help='waifu2x model path') - parser.add_argument('-g', type=int, help='gpu device to use') - parser.add_argument('-j', type=str, help='thread count for load/proc/save') - parser.add_argument('-x', action='store_true', help='enable tta mode') - parser.add_argument('-f', type=str, help=argparse.SUPPRESS) # help='output image format (jpg/png/webp, default=ext/png)') + parser.add_argument("--help", action="help", help="show this help message and exit") + parser.add_argument("-v", action="store_true", help="verbose output") + parser.add_argument("-i", type=str, help=argparse.SUPPRESS) # help="input image path (jpg/png/webp) or directory") + parser.add_argument("-o", type=str, help=argparse.SUPPRESS) # help="output image path (jpg/png/webp) or directory") + parser.add_argument("-n", type=int, choices=range(-1, 4), help="denoise level") + parser.add_argument("-s", type=int, help="upscale ratio") + parser.add_argument("-t", type=int, help="tile size (>=32)") + parser.add_argument("-m", type=str, help="waifu2x model path") + parser.add_argument("-g", type=int, help="gpu device to use") + parser.add_argument("-j", type=str, help="thread count for load/proc/save") + parser.add_argument("-x", action="store_true", help="enable tta mode") + parser.add_argument("-f", type=str, help=argparse.SUPPRESS) # help="output image format (jpg/png/webp, default=ext/png)") return parser.parse_args(arguments) + # fmt: on def load_configurations(self, upscaler): # self.driver_settings['s'] = int(upscaler.scale_ratio) - self.driver_settings['j'] = '{}:{}:{}'.format(upscaler.processes, upscaler.processes, upscaler.processes) - self.driver_settings['f'] = upscaler.extracted_frame_format.lower() + self.driver_settings["j"] = "{}:{}:{}".format( + upscaler.processes, upscaler.processes, upscaler.processes + ) + self.driver_settings["f"] = upscaler.extracted_frame_format.lower() def set_scale_ratio(self, scale_ratio: int): - self.driver_settings['s'] = int(scale_ratio) + self.driver_settings["s"] = int(scale_ratio) def upscale(self, input_directory, output_directory): - """ This is the core function for waifu2x class + """This is the core function for waifu2x class Arguments: input_directory {string} -- source directory path @@ -76,33 +80,33 @@ class WrapperMain: # change the working directory to the binary's parent directory # so the binary can find shared object files and other files - os.chdir(pathlib.Path(self.driver_settings['path']).parent) + os.chdir(pathlib.Path(self.driver_settings["path"]).parent) # overwrite config file settings - self.driver_settings['i'] = input_directory - self.driver_settings['o'] = output_directory + self.driver_settings["i"] = input_directory + self.driver_settings["o"] = output_directory # by default, waifu2x-ncnn-vulkan will look for the models under the current working directory # change the working directory to its containing folder if model directory not specified - if self.driver_settings['m'] is None and platform.system() == 'Windows': - os.chdir(pathlib.Path(self.driver_settings['path']).parent) + if self.driver_settings["m"] is None and platform.system() == "Windows": + os.chdir(pathlib.Path(self.driver_settings["path"]).parent) # list to be executed # initialize the list with waifu2x binary path as the first element - execute = [self.driver_settings['path']] + execute = [self.driver_settings["path"]] for key in self.driver_settings.keys(): value = self.driver_settings[key] # null or None means that leave this option out (keep default) - if key == 'path' or value is None or value is False: + if key == "path" or value is None or value is False: continue else: if len(key) == 1: - execute.append(f'-{key}') + execute.append(f"-{key}") else: - execute.append(f'--{key}') + execute.append(f"--{key}") # true means key is an option if value is not True: @@ -110,6 +114,8 @@ class WrapperMain: # return the Popen object of the new process created self.print_lock.acquire() - Avalon.debug_info(f'[upscaler] Subprocess {os.getpid()} executing: {" ".join(execute)}') + Avalon.debug_info( + f'[upscaler] Subprocess {os.getpid()} executing: {" ".join(execute)}' + ) self.print_lock.release() return subprocess.Popen(execute, stdout=sys.stdout, stderr=sys.stderr) From bd6690fed1429f4b15acc24c6c3f2e848b82fb8f Mon Sep 17 00:00:00 2001 From: K4YT3X Date: Sun, 20 Dec 2020 20:56:49 -0500 Subject: [PATCH 2/4] updated Patreon badge --- README.md | 52 +--------------------------------------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/README.md b/README.md index 140add9..7f6f484 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ![GitHub All Releases](https://img.shields.io/github/downloads/k4yt3x/video2x/total?style=flat-square) ![GitHub](https://img.shields.io/github/license/k4yt3x/video2x?style=flat-square) ![Platforms](https://img.shields.io/badge/Platforms-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square) -![Become A Patron!](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.herokuapp.com%2Fk4yt3x&style=flat-square) +![Become A Patron!](https://img.shields.io/badge/dynamic/json?color=%23e85b46&label=Patreon&query=data.attributes.patron_count&suffix=%20patrons&url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F4507807&style=flat-square) @@ -232,56 +232,6 @@ Are you interested in how the idea of Video2X was born? Do you want to know the --- -# Full Usage - -## Video2X Options - -### -h, --help - show this help message and exit - -### -i INPUT, --input INPUT - source video file/directory - -### -o OUTPUT, --output OUTPUT - output video file/directory - -### -c CONFIG, --config CONFIG - video2x config file path - -### --log LOG - log file path (default: program_directory\video2x_%Y-%m-%d_%H-%M-%S.log) - -### --disable_logging - disable logging (default: False) - -### -v, --version - display version, lawful information and exit - -## Upscaling Options - -### -d DRIVER, --driver DRIVER - upscaling driver (default: waifu2x_caffe) - -Available options are: - -- waifu2x_caffe -- waifu2x_converter_cpp -- waifu2x_ncnn_vulkan -- srmd_ncnn_vulkan -- realsr_ncnn_vulkan -- anime4kcpp - -### -r RATIO, --ratio RATIO - scaling ratio - -### -p PROCESSES, --processes PROCESSES - number of processes to use for upscaling (default: 1) - -### --preserve_frames - preserve extracted and upscaled frames (default: False) - ---- - ## License Licensed under the GNU General Public License Version 3 (GNU GPL v3) From d67f55c824f1cf957f2dbd62d9aff0a843e3efc4 Mon Sep 17 00:00:00 2001 From: K4YT3X Date: Mon, 21 Dec 2020 02:31:40 +0000 Subject: [PATCH 3/4] updated Video2X banner to be compatible with GitHub's new dark theme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7f6f484..c76a36a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

![GitHub release (latest by date)](https://img.shields.io/github/v/release/k4yt3x/video2x?style=flat-square) From f97ed080e17ac1b643489effe5dc1792fbc982a0 Mon Sep 17 00:00:00 2001 From: K4YT3X Date: Sun, 20 Dec 2020 21:32:10 -0500 Subject: [PATCH 4/4] updated Video2X banner files --- src/images/Video2X Banner.png | Bin 14326 -> 29644 bytes src/images/Video2X Banner.psd | Bin 195222 -> 222725 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/images/Video2X Banner.png b/src/images/Video2X Banner.png index 30b0d62a57f01c3e89ec32d6621b0190f8a3c144..e9c785beb7d61f026ea2bc6832156ae8e77b387c 100644 GIT binary patch literal 29644 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 diff --git a/src/images/Video2X Banner.psd b/src/images/Video2X Banner.psd index fd874ec0c1684bfd4dab05dbba4666ade75cd786..a19f2cd2a7419e3ff9371362a334de8b61327fed 100644 GIT binary patch literal 222725 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 KEOc~L9{&OC^_$!P literal 195222 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ⅈ`8}O`1E!pe)(Lt4(ox9c?+9}t#|>O2$PV` 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 i-L~H