From bc11ce7aae511946d2db673598d549f2b0946cb9 Mon Sep 17 00:00:00 2001 From: Kyrch Date: Wed, 22 Nov 2023 13:29:36 -0300 Subject: [PATCH] feat: prompt bitrate values for cbr mode (#33) --- README.md | 6 ++- batch_encoder/__main__.py | 2 + batch_encoder/_encode_webm.py | 43 ++++++++++++-------- batch_encoder/_encoding_config.py | 12 +++++- batch_encoder/_interface.py | 66 +++++++++++++++++++------------ setup.py | 2 +- 6 files changed, 85 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 85314f3..2a90142 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Ideally we are iterating over a combination of filters and settings, picking the ### Usage - python -m batch_encoder [-h] [--generate | -g] [--execute | -e] [--custom | -c] [--file [FILE]] [--configfile [CONFIGFILE]] --loglevel [{debug,info,error}] + python -m batch_encoder [-h] [--generate | -g] [--execute | -e] [--custom | -c] [--file [FILE]] [--configfile [CONFIGFILE]] [--inputfile [INPUTFILES]] --loglevel [{debug,info,error}] **Mode** @@ -90,6 +90,10 @@ Available bitrate control modes are: `CRFs` is a comma-separated listing of ordered CRF values to use with `VBR` and/or `CQ` bitrate control modes. +`CBRBitrates` is comma-separated listing of ordered bitrate values to use with `CBR`. + +`CBRMaxBitrates` is comma-separated listing of ordered maximum bitrate values to use with `CBR`. + `Threads` is the number of threads used to encode. Default is 4. `LimitSizeEnable` is a flag for including the `-fs` argument to terminate an encode when it exceeds the allowed size. Default is True. diff --git a/batch_encoder/__main__.py b/batch_encoder/__main__.py index 40498d9..c10e05b 100644 --- a/batch_encoder/__main__.py +++ b/batch_encoder/__main__.py @@ -59,6 +59,8 @@ def main(): config['Encoding'] = {EncodingConfig.config_allowed_filetypes: EncodingConfig.default_allowed_filetypes, EncodingConfig.config_encoding_modes: EncodingConfig.default_encoding_modes, EncodingConfig.config_crfs: EncodingConfig.default_crfs, + EncodingConfig.config_cbr_bitrates: EncodingConfig.default_cbr_bitrates, + EncodingConfig.config_cbr_max_bitrates: EncodingConfig.default_cbr_max_bitrates, EncodingConfig.config_threads: EncodingConfig.default_threads, EncodingConfig.config_limit_size_enable: EncodingConfig.default_limit_size_enable, EncodingConfig.config_alternate_source_files: EncodingConfig.default_alternate_source_files, diff --git a/batch_encoder/_encode_webm.py b/batch_encoder/_encode_webm.py index 4341326..edacd91 100644 --- a/batch_encoder/_encode_webm.py +++ b/batch_encoder/_encode_webm.py @@ -112,25 +112,25 @@ def preview_seek(self, webm_filename=''): f'-vcodec copy -c:a aac -b:a 128k -sn -f mp4 {webm_filename}.mp4' # First-pass encode - def get_first_pass(self, encoding_mode, crf=None, threads=4): + def get_first_pass(self, encoding_mode, crf=None, cbr_bitrate=None, cbr_max_bitrate=None, threads=4): return f'ffmpeg {self.seek.get_seek_string()} ' \ f'-pass 1 -passlogfile {self.seek.output_name} ' \ f'-map 0:v:{self.source_file.selected_video_stream} ' \ f'-map 0:a:{self.source_file.selected_audio_stream} ' \ f'-c:v libvpx-vp9 ' \ - f'{encoding_mode.first_pass_rate_control(self.cbr_bitrate, self.cbr_max_bitrate, crf)} ' \ + f'{encoding_mode.first_pass_rate_control(cbr_bitrate, cbr_max_bitrate, crf)} ' \ f'-cpu-used 4 -g {self.g} -threads {threads} -tile-columns 6 -frame-parallel 0 -auto-alt-ref 1 ' \ f'-lag-in-frames 25 -row-mt 1 -pix_fmt yuv420p {self.colorspace.get_args()} -an -sn -f webm -y NUL' # Second-pass encode - def get_second_pass(self, encoding_mode, crf=None, threads=4, video_filters='', limit_size_enable=True, webm_filename=''): + def get_second_pass(self, encoding_mode, crf=None, cbr_bitrate=None, cbr_max_bitrate=None, threads=4, video_filters='', limit_size_enable=True, webm_filename=''): limit_size = '-fs ' + EncodeWebM.get_limit_file_size(self, video_filters=video_filters) + ' ' if limit_size_enable else '' return f'ffmpeg {self.seek.get_seek_string()} ' \ f'-pass 2 -passlogfile {self.seek.output_name} ' \ f'-map 0:v:{self.source_file.selected_video_stream} ' \ f'-map 0:a:{self.source_file.selected_audio_stream} ' \ f'-c:v libvpx-vp9 ' \ - f'{encoding_mode.second_pass_rate_control(self.cbr_bitrate, self.cbr_max_bitrate, crf)} ' \ + f'{encoding_mode.second_pass_rate_control(cbr_bitrate, cbr_max_bitrate, crf)} ' \ f'-cpu-used 0 -g {self.g} -threads {threads} {self.get_audio_filters()}{video_filters} -tile-columns 6 ' \ f'-frame-parallel 0 -auto-alt-ref 1 -lag-in-frames 25 -row-mt 1 -pix_fmt yuv420p ' \ f'{self.colorspace.get_args()} ' \ @@ -160,14 +160,17 @@ def get_video_filters(config_filter=None): return ' -vf ' + ','.join(video_filters) # Build unique WebM filename for encodes - def get_webm_filename(self, crf=None, cbr_bitrate=None, filter_name=None): + def get_webm_filename(self, crf=None, cbr_bitrate=None, cbr_max_bitrate=None, filter_name=None): webm_filename = self.seek.output_name if crf is not None: webm_filename += f'-{crf}' if cbr_bitrate is not None: - webm_filename += f'-{self.cbr_bitrate}' + webm_filename += f'-{cbr_bitrate}' + + if cbr_max_bitrate is not None: + webm_filename += f'-{cbr_max_bitrate}' if filter_name is not None: webm_filename += f'-{filter_name}' @@ -193,16 +196,24 @@ def get_commands(self, encoding_config): for encoding_mode in encoding_config.encoding_modes: if BitrateMode.CBR.name == encoding_mode.upper(): - file_commands.append(self.get_first_pass(BitrateMode.CBR, threads=encoding_config.threads)) - for filter_name, filter_value in encoding_config.video_filters: - file_commands.append(self.get_second_pass(BitrateMode.CBR, - threads=encoding_config.threads, - video_filters=EncodeWebM.get_video_filters( - config_filter=filter_value), - limit_size_enable=encoding_config.limit_size_enable, - webm_filename=self.get_webm_filename( - cbr_bitrate=self.cbr_bitrate, - filter_name=filter_name))) + cbr_bitrates = encoding_config.cbr_bitrates if encoding_config.cbr_bitrates is not None else [self.cbr_bitrate] + cbr_max_bitrates = encoding_config.cbr_max_bitrates if encoding_config.cbr_max_bitrates is not None else [self.cbr_max_bitrate] + + for cbr_bitrate in cbr_bitrates: + for cbr_max_bitrate in cbr_max_bitrates: + file_commands.append(self.get_first_pass(BitrateMode.CBR, cbr_bitrate=cbr_bitrate, cbr_max_bitrate=cbr_max_bitrate, threads=encoding_config.threads)) + for filter_name, filter_value in encoding_config.video_filters: + file_commands.append(self.get_second_pass(BitrateMode.CBR, + cbr_bitrate=cbr_bitrate, + cbr_max_bitrate=cbr_max_bitrate, + threads=encoding_config.threads, + video_filters=EncodeWebM.get_video_filters( + config_filter=filter_value), + limit_size_enable=encoding_config.limit_size_enable, + webm_filename=self.get_webm_filename( + cbr_bitrate=cbr_bitrate, + cbr_max_bitrate=cbr_max_bitrate, + filter_name=filter_name))) elif BitrateMode.VBR.name == encoding_mode.upper(): for crf in encoding_config.crfs: file_commands.append(self.get_first_pass(BitrateMode.VBR, crf=crf, threads=encoding_config.threads)) diff --git a/batch_encoder/_encoding_config.py b/batch_encoder/_encoding_config.py index c947c5f..dca34b5 100644 --- a/batch_encoder/_encoding_config.py +++ b/batch_encoder/_encoding_config.py @@ -6,6 +6,8 @@ class EncodingConfig: config_allowed_filetypes = 'AllowedFileTypes' config_encoding_modes = 'EncodingModes' config_crfs = 'CRFs' + config_cbr_bitrates = 'CBRBitrates' + config_cbr_max_bitrates = 'CBRMaxBitrates' config_threads = 'Threads' config_limit_size_enable = 'LimitSizeEnable' config_alternate_source_files = 'AlternateSourceFiles' @@ -20,6 +22,8 @@ class EncodingConfig: default_allowed_filetypes = '.avi,.m2ts,.mkv,.mp4,.wmv' default_encoding_modes = f'{BitrateMode.VBR.name},{BitrateMode.CBR.name}' default_crfs = '12,15,18,21,24' + default_cbr_bitrates = '5600' + default_cbr_max_bitrates = '6400' default_threads = '4' default_limit_size_enable = True default_alternate_source_files = False @@ -30,11 +34,13 @@ class EncodingConfig: 'heavydenoise': 'hqdn3d=1.5:1.5:6:6', 'unsharp': 'unsharp'} - def __init__(self, allowed_filetypes, encoding_modes, crfs, threads, limit_size_enable, alternate_source_files, create_preview, include_unfiltered, video_filters, default_video_stream, + def __init__(self, allowed_filetypes, encoding_modes, crfs, cbr_bitrates, cbr_max_bitrates, threads, limit_size_enable, alternate_source_files, create_preview, include_unfiltered, video_filters, default_video_stream, default_audio_stream): self.allowed_filetypes = allowed_filetypes self.encoding_modes = encoding_modes self.crfs = crfs + self.cbr_bitrates = cbr_bitrates + self.cbr_max_bitrates = cbr_max_bitrates self.threads = threads self.limit_size_enable = limit_size_enable self.alternate_source_files = alternate_source_files @@ -51,6 +57,8 @@ def from_config(cls, config): encoding_modes = config['Encoding'].get(EncodingConfig.config_encoding_modes, EncodingConfig.default_encoding_modes).split(',') crfs = config['Encoding'].get(EncodingConfig.config_crfs, EncodingConfig.default_crfs).split(',') + cbr_bitrates = config['Encoding'].get(EncodingConfig.config_cbr_bitrates, EncodingConfig.default_cbr_bitrates).split(',') + cbr_max_bitrates = config['Encoding'].get(EncodingConfig.config_cbr_max_bitrates, EncodingConfig.default_cbr_max_bitrates).split(',') threads = config['Encoding'].get(EncodingConfig.config_threads, EncodingConfig.default_threads) limit_size_enable = config.getboolean('Encoding', EncodingConfig.config_limit_size_enable, fallback=EncodingConfig.default_limit_size_enable) @@ -63,7 +71,7 @@ def from_config(cls, config): default_video_stream = config['Encoding'].get(EncodingConfig.config_default_video_stream) default_audio_stream = config['Encoding'].get(EncodingConfig.config_default_audio_stream) - return cls(allowed_filetypes, encoding_modes, crfs, threads, limit_size_enable, alternate_source_files, create_preview, include_unfiltered, video_filters, default_video_stream, + return cls(allowed_filetypes, encoding_modes, crfs, cbr_bitrates, cbr_max_bitrates, threads, limit_size_enable, alternate_source_files, create_preview, include_unfiltered, video_filters, default_video_stream, default_audio_stream) def get_default_stream(self, stream_type): diff --git a/batch_encoder/_interface.py b/batch_encoder/_interface.py index 16f81a5..a523be0 100644 --- a/batch_encoder/_interface.py +++ b/batch_encoder/_interface.py @@ -1,3 +1,5 @@ +from ._bitrate_mode import BitrateMode + import inquirer import logging import re @@ -9,13 +11,12 @@ class Interface: # Validations validate_time = lambda _, x: all(Interface.time_pattern.match(y) for y in x.split(',')) - validate_crfs = lambda _, x: all(y.strip().isdigit() for y in x.split(',')) - validate_encoding_modes = lambda _, x: all(y.strip().upper() in ['VBR', 'CBR', 'CQ'] for y in x.split(',')) + validate_encoding_modes = lambda _, x: all(y.strip().upper() in [BitrateMode.VBR.name, BitrateMode.CBR.name, BitrateMode.CQ.name] for y in x.split(',')) + validate_digits = lambda _, x: all(y.strip().isdigit() for y in x.split(',')) or len(x.strip()) == 0 # Prompt the user for text questions def prompt_text(message, validate=lambda _, x: x): - question = [inquirer.Text('text', message=message, validate=validate)] - answer = inquirer.prompt(question) + answer = inquirer.prompt([inquirer.Text('text', message=message, validate=validate)]) if answer is None: return 'NoName' @@ -26,8 +27,7 @@ def prompt_text(message, validate=lambda _, x: x): # Prompt the user for time questions def prompt_time(message, validate=validate_time): - question = [inquirer.Text('time', message=message, validate=validate)] - answer = inquirer.prompt(question) + answer = inquirer.prompt([inquirer.Text('time', message=message, validate=validate)]) if answer is None: return '' @@ -39,8 +39,7 @@ def prompt_time(message, validate=validate_time): # Prompt the user for our mode options to run to the user def choose_mode(): modes = [('Generate commands', 1), ('Execute commands', 2), ('Generate and execute commands', 3)] - question = [inquirer.List('mode', message='Mode (Enter)', choices=modes)] - answer = inquirer.prompt(question) + answer = inquirer.prompt([inquirer.List('mode', message='Mode (Enter)', choices=modes)]) if answer is None: sys.exit() @@ -51,8 +50,7 @@ def choose_mode(): # Prompt the user for source files to choose def choose_source_files(source_files): - question = [inquirer.Checkbox('source_files', message='Source Files (Space to select)', choices=source_files)] - answer = inquirer.prompt(question) + answer = inquirer.prompt([inquirer.Checkbox('source_files', message='Source Files (Space to select)', choices=source_files)]) if answer is None: sys.exit() @@ -83,8 +81,7 @@ def audio_filters_options(output_name): while not audio_filters['Exit']: print(f'\n\033[92mOutput Name: {output_name}\033[0m') - question = [inquirer.List('audio_filters', message='Audio Filters (Enter)', choices=list(audio_filters.keys()))] - answer = inquirer.prompt(question) + answer = inquirer.prompt([inquirer.List('audio_filters', message='Audio Filters (Enter)', choices=list(audio_filters.keys()))]) if answer is None: audio_filters['Exit'] = True @@ -146,9 +143,10 @@ def video_filters(encoding_config): if encoding_config.include_unfiltered: encoding_config.video_filters.append((None, 'No Filters')) - question = [inquirer.Checkbox('video_filters', message='Select Video Filters (Space to select)', - choices=video_filters_options.keys(), default=[tp[1] for tp in encoding_config.video_filters])] - answer = inquirer.prompt(question) + answer = inquirer.prompt([ + inquirer.Checkbox('video_filters', message='Select Video Filters (Space to select)', + choices=video_filters_options.keys(), default=[tp[1] for tp in encoding_config.video_filters]) + ]) if answer is None: return encoding_config @@ -170,32 +168,48 @@ def video_filters(encoding_config): def custom_options(encoding_config): create_preview = encoding_config.create_preview limit_size_enable = encoding_config.limit_size_enable - crfs = encoding_config.crfs encoding_modes = encoding_config.encoding_modes + crfs = encoding_config.crfs + cbr_bitrates = encoding_config.cbr_bitrates + cbr_max_bitrates = encoding_config.cbr_max_bitrates - questions = [ - inquirer.Confirm('create_preview', message=f'Create Preview?', default=create_preview), - inquirer.Confirm('limit_size_enable', message=f'Limit Size Enable?', default=limit_size_enable), - inquirer.Text('crfs', message='CRFs', default=','.join(crfs), validate=Interface.validate_crfs), - inquirer.Text('encoding_modes', message='Encoding Modes', default=','.join(encoding_modes), validate=Interface.validate_encoding_modes) - ] - - answer = inquirer.prompt(questions) + answer = inquirer.prompt([ + inquirer.Confirm('create_preview', message='Create Preview?', default=create_preview), + inquirer.Confirm('limit_size_enable', message='Limit Size Enable?', default=limit_size_enable), + inquirer.Text('encoding_modes', message='Encoding Modes', default=','.join(encoding_modes), validate=Interface.validate_encoding_modes), + ]) if answer is None: return encoding_config + + encoding_mode_questions = [] + for encoding_mode in answer['encoding_modes'].split(','): + if encoding_mode == BitrateMode.VBR.name or encoding_mode == BitrateMode.CQ.name: + encoding_mode_questions.append(inquirer.Text('crfs', message='CRFs', default=','.join(crfs), validate=Interface.validate_digits)) + if encoding_mode == BitrateMode.CBR.name: + encoding_mode_questions.append(inquirer.Text('cbr_bitrates', message='Bit Rates', default=','.join(cbr_bitrates), validate=Interface.validate_digits)) + encoding_mode_questions.append(inquirer.Text('cbr_max_bitrates', message='Max Bit Rates', default=','.join(cbr_max_bitrates), validate=Interface.validate_digits)) + + answer_em = inquirer.prompt(encoding_mode_questions) encoding_config.create_preview = answer['create_preview'] encoding_config.limit_size_enable = answer['limit_size_enable'] - encoding_config.crfs = answer['crfs'].split(',') encoding_config.encoding_modes = answer['encoding_modes'].split(',') + if 'crfs' in answer_em: + encoding_config.crfs = answer_em['crfs'].split(',') + if 'cbr_bitrates' in answer_em and 'cbr_max_bitrates' in answer_em: + encoding_config.cbr_bitrates = [x + 'k' for x in answer_em['cbr_bitrates'].split(',')] if len(answer_em['cbr_bitrates'].strip()) != 0 else None + encoding_config.cbr_max_bitrates = [x + 'k' for x in answer_em['cbr_max_bitrates'].split(',')] if len(answer_em['cbr_max_bitrates'].strip()) != 0 else None + logging.debug( f'[Interface.custom_options] ' f'encoding_config.create_preview: \'{encoding_config.create_preview}\', ' f'encoding_config.limit_size_enable: \'{encoding_config.create_preview}\', ' + f'encoding_config.encoding_modes: \'{encoding_config.encoding_modes}\', ' f'encoding_config.crfs: \'{encoding_config.crfs}\', ' - f'encoding_config.encoding_modes: \'{encoding_config.encoding_modes}\'' + f'encoding_config.cbr_bitrates: \'{encoding_config.cbr_bitrates}\', ' + f'encoding_config.cbr_max_bitrates: \'{encoding_config.cbr_max_bitrates}\'' ) return encoding_config \ No newline at end of file diff --git a/setup.py b/setup.py index 8d6778f..a2e22ec 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='animethemes-batch-encoder', - version='2.1', + version='2.2', author='AnimeThemes', author_email='admin@animethemes.moe', url='https://github.com/AnimeThemes/animethemes-batch-encoder',