From 0b6b971fbc739d20142127201babe9e6a56bd912 Mon Sep 17 00:00:00 2001 From: Brett Date: Fri, 20 Oct 2023 21:06:16 -0500 Subject: [PATCH] Get detailed formats from engines (#38) * Add get_detected_gpus() and supported_render_engines() to Blender class * Parse help args for Blender * Return dict instead of list * Parse args for FFMPEG * Add API to get renderer args * Only return available renderers * Parse help args for Blender * Return dict instead of list * Parse args for FFMPEG * Rebase off master * Rebase * Change methods from class methods to instance methods * FFMPEG format fetching --- lib/engines/blender_engine.py | 0 lib/engines/ffmpeg_engine.py | 4 +++ src/api_server.py | 9 ++++++ src/engines/base_engine.py | 6 +++- src/engines/blender_engine.py | 49 +++++++++++++++++++++++++++++++++ src/engines/ffmpeg_engine.py | 52 +++++++++++++++++++++++++++++++---- 6 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 lib/engines/blender_engine.py create mode 100644 lib/engines/ffmpeg_engine.py diff --git a/lib/engines/blender_engine.py b/lib/engines/blender_engine.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/engines/ffmpeg_engine.py b/lib/engines/ffmpeg_engine.py new file mode 100644 index 0000000..8d61033 --- /dev/null +++ b/lib/engines/ffmpeg_engine.py @@ -0,0 +1,4 @@ + + +if __name__ == "__main__": + print(FFMPEG.get_frame_count('/Users/brett/Desktop/Big_Fire_02.mov')) \ No newline at end of file diff --git a/src/api_server.py b/src/api_server.py index b1474be..4f69dc3 100755 --- a/src/api_server.py +++ b/src/api_server.py @@ -561,6 +561,15 @@ def delete_engine_download(): return "Success" if delete_result else ("Error deleting requested engine", 500) +@server.get('/api/renderer//args') +def get_renderer_args(renderer): + try: + renderer_engine_class = RenderWorkerFactory.class_for_name(renderer).engine() + return renderer_engine_class.get_arguments() + except LookupError: + return f"Cannot find renderer '{renderer}'", 400 + + @server.route('/upload') def upload_file_page(): return render_template('upload.html', supported_renderers=RenderWorkerFactory.supported_renderers()) diff --git a/src/engines/base_engine.py b/src/engines/base_engine.py index 606d524..9d31b98 100644 --- a/src/engines/base_engine.py +++ b/src/engines/base_engine.py @@ -47,4 +47,8 @@ class BaseRenderEngine(object): @classmethod def get_output_formats(cls): - raise NotImplementedError(f"get_output_formats not implemented for {cls.__name__}") \ No newline at end of file + raise NotImplementedError(f"get_output_formats not implemented for {cls.__name__}") + + @classmethod + def get_arguments(cls): + pass diff --git a/src/engines/blender_engine.py b/src/engines/blender_engine.py index c8bfae9..0868ecf 100644 --- a/src/engines/blender_engine.py +++ b/src/engines/blender_engine.py @@ -96,3 +96,52 @@ class Blender(BaseRenderEngine): except Exception as e: logger.error(f'Error packing .blend file: {e}') return None + + def get_arguments(self): + help_text = subprocess.check_output([self.renderer_path(), '-h']).decode('utf-8') + lines = help_text.splitlines() + + options = {} + current_category = None + current_option = None + + for line in lines: + line = line.strip() + # Check if line starts with - or --, indicating a new option + if line.endswith('Options:'): + current_category = line.split('Options')[0].strip() + options[current_category] = {} + elif line.startswith("-") or line.startswith("/"): + parts = line.split(' or ') + flag = parts[-1] # Choose the verbose option + has_argument = '<' in flag and '>' in flag + flag = flag.split(' <')[0] # Remove argument placeholder + current_option = flag.strip("--") or flag + options[current_category][current_option] = { + 'flag': flag, 'description': '', 'takes_argument': has_argument + } + elif line == "" and current_option is not None: + current_option = None + elif current_option is not None: + d = options[current_category][current_option]['description'] + d = d + (' ' if d else '') + line + options[current_category][current_option]['description'] = d + + return options + + def get_detected_gpus(self): + engine_output = subprocess.run([self.renderer_path(), '-E', 'help'], timeout=SUBPROCESS_TIMEOUT, + capture_output=True).stdout.decode('utf-8') + gpu_names = re.findall(r"DETECTED GPU: (.+)", engine_output) + return gpu_names + + def supported_render_engines(self): + engine_output = subprocess.run([self.renderer_path(), '-E', 'help'], timeout=SUBPROCESS_TIMEOUT, + capture_output=True).stdout.decode('utf-8').strip() + render_engines = [x.strip() for x in engine_output.split('Blender Engine Listing:')[-1].strip().splitlines()] + return render_engines + + +if __name__ == "__main__": + x = Blender.get_detected_gpus() + print(x) diff --git a/src/engines/ffmpeg_engine.py b/src/engines/ffmpeg_engine.py index eb46c2b..8dec66e 100644 --- a/src/engines/ffmpeg_engine.py +++ b/src/engines/ffmpeg_engine.py @@ -26,12 +26,28 @@ class FFMPEG(BaseRenderEngine): encoders = [m.groupdict() for m in re.finditer(pattern, raw_stdout)] return encoders + def get_formats(self): + encoders = [x['name'] for x in self.get_all_formats() if 'E' in x['type']] + return encoders + def get_all_formats(self): - raw_stdout = subprocess.check_output([self.renderer_path(), '-formats'], stderr=subprocess.DEVNULL, + formats_raw = subprocess.check_output([self.renderer_path(), '-formats'], stderr=subprocess.DEVNULL, timeout=SUBPROCESS_TIMEOUT).decode('utf-8') - pattern = '(?P[DE]{1,2})\s+(?P\S{2,})\s+(?P.*)' - formats = [m.groupdict() for m in re.finditer(pattern, raw_stdout)] - return formats + pattern = '(?P[DE]{1,2})\s+(?P\S{2,})\s+(?P.*)' + all_formats = [m.groupdict() for m in re.finditer(pattern, formats_raw)] + return all_formats + + def extension_for_format(self, ffmpeg_format): + # Extract the common extension using regex + muxer_flag = 'muxer' if 'E' in ffmpeg_format['type'] else 'demuxer' + format_detail_raw = subprocess.check_output( + [self.renderer_path(), '-hide_banner', '-h', f"{muxer_flag}={ffmpeg_format['id']}"]).decode('utf-8') + pattern = r"Common extensions: (\w+)" + common_extensions = re.findall(pattern, format_detail_raw) + found_extensions = [] + if common_extensions: + found_extensions = common_extensions[0].split(',') + return found_extensions def get_output_formats(self): return [x for x in self.get_all_formats() if 'E' in x['type'].upper()] @@ -45,6 +61,32 @@ class FFMPEG(BaseRenderEngine): frame_number = int(match[-1]) return frame_number + def get_arguments(self): + help_text = subprocess.check_output([self.renderer_path(), '-h', 'long'], stderr=subprocess.STDOUT).decode('utf-8') + lines = help_text.splitlines() + + options = {} + current_category = None + + for line in lines: + line = line.strip() + + if line.endswith(":") and "options" in line.lower(): + current_category = line.split('options')[0].strip() + options[current_category] = {} + elif line: # Ignore blank lines + split_line = line.split(' ', 2) + flag = split_line[0] + argument = split_line[1] if len(split_line) > 1 else '' + description = split_line[2] if len(split_line) > 2 else '' + + if current_category: + name = flag.strip('-') + options[current_category][name] = { + 'flag': flag, 'description': description.strip(), 'takes_argument': bool(argument) + } + return options + if __name__ == "__main__": - print(FFMPEG.get_frame_count('/Users/brett/Desktop/Big_Fire_02.mov')) \ No newline at end of file + print(FFMPEG().get_all_formats()) \ No newline at end of file