From cb125a192f0728562d5e76d2b510370d65c4f1f8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 31 Mar 2025 23:14:17 +0200 Subject: [PATCH 01/15] Optimize oiio tool conversion for ffmpeg. - Prepare attributes to remove list just once. - Process sequences as a single `oiiotool` call --- client/ayon_core/lib/transcoding.py | 205 +++++++--------------------- 1 file changed, 51 insertions(+), 154 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 1fda014bd8..39995083c0 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -10,6 +10,8 @@ import xml.etree.ElementTree +import clique + from .execute import run_subprocess from .vendor_bin_utils import ( get_ffmpeg_tool_args, @@ -526,135 +528,36 @@ def should_convert_for_ffmpeg(src_filepath): return False -# Deprecated since 2022 4 20 -# - Reason - Doesn't convert sequences right way: Can't handle gaps, reuse -# first frame for all frames and changes filenames when input -# is sequence. -# - use 'convert_input_paths_for_ffmpeg' instead -def convert_for_ffmpeg( - first_input_path, - output_dir, - input_frame_start=None, - input_frame_end=None, - logger=None -): - """Convert source file to format supported in ffmpeg. - - Currently can convert only exrs. - - Args: - first_input_path (str): Path to first file of a sequence or a single - file path for non-sequential input. - output_dir (str): Path to directory where output will be rendered. - Must not be same as input's directory. - input_frame_start (int): Frame start of input. - input_frame_end (int): Frame end of input. - logger (logging.Logger): Logger used for logging. - - Raises: - ValueError: If input filepath has extension not supported by function. - Currently is supported only ".exr" extension. - """ - if logger is None: - logger = logging.getLogger(__name__) - - logger.warning(( - "DEPRECATED: 'ayon_core.lib.transcoding.convert_for_ffmpeg' is" - " deprecated function of conversion for FFMpeg. Please replace usage" - " with 'ayon_core.lib.transcoding.convert_input_paths_for_ffmpeg'" - )) - - ext = os.path.splitext(first_input_path)[1].lower() - if ext != ".exr": - raise ValueError(( - "Function 'convert_for_ffmpeg' currently support only" - " \".exr\" extension. Got \"{}\"." - ).format(ext)) - - is_sequence = False - if input_frame_start is not None and input_frame_end is not None: - is_sequence = int(input_frame_end) != int(input_frame_start) - - input_info = get_oiio_info_for_input(first_input_path, logger=logger) - - # Change compression only if source compression is "dwaa" or "dwab" - # - they're not supported in ffmpeg - compression = input_info["attribs"].get("compression") - if compression in ("dwaa", "dwab"): - compression = "none" - - # Prepare subprocess arguments - oiio_cmd = get_oiio_tool_args( - "oiiotool", - # Don't add any additional attributes - "--nosoftwareattrib", - ) - # Add input compression if available - if compression: - oiio_cmd.extend(["--compression", compression]) - - # Collect channels to export - input_arg, channels_arg = get_oiio_input_and_channel_args(input_info) - - oiio_cmd.extend([ - input_arg, first_input_path, - # Tell oiiotool which channels should be put to top stack (and output) - "--ch", channels_arg, - # Use first subimage - "--subimage", "0" - ]) - - # Add frame definitions to arguments - if is_sequence: - oiio_cmd.extend([ - "--frames", "{}-{}".format(input_frame_start, input_frame_end) - ]) - +def _get_attributes_to_erase(input_info: dict, logger: logging.Logger = None) -> list[str]: + """FFMPEG does not support some attributes in metadata.""" + erase_attr_names = [] + reasons = [] for attr_name, attr_value in input_info["attribs"].items(): if not isinstance(attr_value, str): continue # Remove attributes that have string value longer than allowed length # for ffmpeg or when contain prohibited symbols - erase_reason = "Missing reason" - erase_attribute = False if len(attr_value) > MAX_FFMPEG_STRING_LEN: - erase_reason = "has too long value ({} chars).".format( - len(attr_value) - ) - erase_attribute = True - - if not erase_attribute: - for char in NOT_ALLOWED_FFMPEG_CHARS: - if char in attr_value: - erase_attribute = True - erase_reason = ( - "contains unsupported character \"{}\"." - ).format(char) - break - - if erase_attribute: - # Set attribute to empty string - logger.info(( - "Removed attribute \"{}\" from metadata because {}." - ).format(attr_name, erase_reason)) - oiio_cmd.extend(["--eraseattrib", attr_name]) + reason = f"has too long value ({len(attr_value)} chars)." + erase_attr_names.append(attr_name) + reasons.append(reason) - # Add last argument - path to output - if is_sequence: - ext = os.path.splitext(first_input_path)[1] - base_filename = "tmp.%{:0>2}d{}".format( - len(str(input_frame_end)), ext - ) - else: - base_filename = os.path.basename(first_input_path) - output_path = os.path.join(output_dir, base_filename) - oiio_cmd.extend([ - "-o", output_path - ]) + for char in NOT_ALLOWED_FFMPEG_CHARS: + if char not in attr_value: + continue + reason = f"contains unsupported character \"{char}\"." + erase_attr_names.append(attr_name) + reasons.append(reason) - logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) - run_subprocess(oiio_cmd, logger=logger) + if logger is not None: + for attr_name, reason in zip(erase_attr_names, reasons): + # Set attribute to empty string + logger.info( + f"Removed attribute \"{attr_name}\" from metadata" + f" because {reason}." + ) + return erase_attr_names def convert_input_paths_for_ffmpeg( @@ -664,7 +567,7 @@ def convert_input_paths_for_ffmpeg( ): """Convert source file to format supported in ffmpeg. - Currently can convert only exrs. The input filepaths should be files + Currently, can convert only exrs. The input filepaths should be files with same type. Information about input is loaded only from first found file. @@ -682,7 +585,7 @@ def convert_input_paths_for_ffmpeg( Raises: ValueError: If input filepath has extension not supported by function. - Currently is supported only ".exr" extension. + Currently, only ".exr" extension is supported. """ if logger is None: logger = logging.getLogger(__name__) @@ -707,7 +610,19 @@ def convert_input_paths_for_ffmpeg( # Collect channels to export input_arg, channels_arg = get_oiio_input_and_channel_args(input_info) - for input_path in input_paths: + # Find which attributes to strip + erase_attributes: list[str] = _get_attributes_to_erase( + input_info, logger=logger + ) + + input_collections, input_remainders = clique.assemble( + input_paths, + patterns=[clique.PATTERNS["frames"]], + assume_padded_when_ambiguous=True, + ) + process_inputs = list(input_collections) + process_inputs.extend(input_remainders) + for _input in process_inputs: # Prepare subprocess arguments oiio_cmd = get_oiio_tool_args( "oiiotool", @@ -718,8 +633,17 @@ def convert_input_paths_for_ffmpeg( if compression: oiio_cmd.extend(["--compression", compression]) + # Convert a sequence of files using a single oiiotool command + # using its sequence syntax + if isinstance(_input, clique.Collection): + oiio_cmd.extend([ + "--framepadding", _input.padding, + "--frames", _input.format("{ranges}"), + ]) + _input: str = _input.format("{head}#{tail}") + oiio_cmd.extend([ - input_arg, input_path, + input_arg, _input, # Tell oiiotool which channels should be put to top stack # (and output) "--ch", channels_arg, @@ -727,38 +651,11 @@ def convert_input_paths_for_ffmpeg( "--subimage", "0" ]) - for attr_name, attr_value in input_info["attribs"].items(): - if not isinstance(attr_value, str): - continue - - # Remove attributes that have string value longer than allowed - # length for ffmpeg or when containing prohibited symbols - erase_reason = "Missing reason" - erase_attribute = False - if len(attr_value) > MAX_FFMPEG_STRING_LEN: - erase_reason = "has too long value ({} chars).".format( - len(attr_value) - ) - erase_attribute = True - - if not erase_attribute: - for char in NOT_ALLOWED_FFMPEG_CHARS: - if char in attr_value: - erase_attribute = True - erase_reason = ( - "contains unsupported character \"{}\"." - ).format(char) - break - - if erase_attribute: - # Set attribute to empty string - logger.info(( - "Removed attribute \"{}\" from metadata because {}." - ).format(attr_name, erase_reason)) - oiio_cmd.extend(["--eraseattrib", attr_name]) + for attr_name in erase_attributes: + oiio_cmd.extend(["--eraseattrib", attr_name]) # Add last argument - path to output - base_filename = os.path.basename(input_path) + base_filename = os.path.basename(_input) output_path = os.path.join(output_dir, base_filename) oiio_cmd.extend([ "-o", output_path From c7c2a4a7eccc543d0262701f9b868a73bc76766e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 1 Apr 2025 09:07:02 +0200 Subject: [PATCH 02/15] Cleanup --- client/ayon_core/lib/transcoding.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 39995083c0..2238b24c3b 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -528,10 +528,11 @@ def should_convert_for_ffmpeg(src_filepath): return False -def _get_attributes_to_erase(input_info: dict, logger: logging.Logger = None) -> list[str]: +def _get_attributes_to_erase( + input_info: dict, logger: logging.Logger +) -> list[str]: """FFMPEG does not support some attributes in metadata.""" - erase_attr_names = [] - reasons = [] + erase_attrs: dict[str, str] = {} # Attr name to reason mapping for attr_name, attr_value in input_info["attribs"].items(): if not isinstance(attr_value, str): continue @@ -540,24 +541,23 @@ def _get_attributes_to_erase(input_info: dict, logger: logging.Logger = None) -> # for ffmpeg or when contain prohibited symbols if len(attr_value) > MAX_FFMPEG_STRING_LEN: reason = f"has too long value ({len(attr_value)} chars)." - erase_attr_names.append(attr_name) - reasons.append(reason) + erase_attrs[attr_name] = reason + continue for char in NOT_ALLOWED_FFMPEG_CHARS: if char not in attr_value: continue reason = f"contains unsupported character \"{char}\"." - erase_attr_names.append(attr_name) - reasons.append(reason) + erase_attrs[attr_name] = reason + break if logger is not None: - for attr_name, reason in zip(erase_attr_names, reasons): - # Set attribute to empty string + for attr_name, reason in erase_attrs.items(): logger.info( f"Removed attribute \"{attr_name}\" from metadata" f" because {reason}." ) - return erase_attr_names + return list(erase_attrs.keys()) def convert_input_paths_for_ffmpeg( @@ -615,14 +615,14 @@ def convert_input_paths_for_ffmpeg( input_info, logger=logger ) - input_collections, input_remainders = clique.assemble( + input_collections, input_remainder = clique.assemble( input_paths, patterns=[clique.PATTERNS["frames"]], assume_padded_when_ambiguous=True, ) - process_inputs = list(input_collections) - process_inputs.extend(input_remainders) - for _input in process_inputs: + input_items = list(input_collections) + input_items.extend(input_remainder) + for _input in input_items: # Prepare subprocess arguments oiio_cmd = get_oiio_tool_args( "oiiotool", From b43969da1c020af1fc209804d39363ef4d3c30c1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 1 Apr 2025 09:08:12 +0200 Subject: [PATCH 03/15] add `from __future__ import annotations` --- client/ayon_core/lib/transcoding.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 2238b24c3b..f249213f2a 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1,3 +1,4 @@ +from __future__ import annotations import os import re import logging From 04c14cab7a9b5109d35d44ed9f441f15b0e7da1c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 1 Apr 2025 09:09:50 +0200 Subject: [PATCH 04/15] Remove deprecated function import --- client/ayon_core/lib/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index 92c3966e77..8d8cc6af49 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -98,7 +98,6 @@ from .transcoding import ( get_transcode_temp_directory, should_convert_for_ffmpeg, - convert_for_ffmpeg, convert_input_paths_for_ffmpeg, get_ffprobe_data, get_ffprobe_streams, @@ -198,7 +197,6 @@ "get_transcode_temp_directory", "should_convert_for_ffmpeg", - "convert_for_ffmpeg", "convert_input_paths_for_ffmpeg", "get_ffprobe_data", "get_ffprobe_streams", From 98e0ec105156d096f9dde519016760d68ae68e6d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Apr 2025 19:44:39 +0200 Subject: [PATCH 05/15] Improve parallelization for ExtractReview and ExtractOIIOTranscode - Support ExtractReview convert to FFMPEG in one `oiiotool` call for sequences - Support sequences with holes in both plug-ins by using dedicated `--frames` argument to `oiiotool` for more complex frame patterns. - Add `--parallel-frames` argument to `oiiotool` to allow parallelizing more of the OIIO tool process, improving throughput. Note: This requires OIIO 2.5.2.0 or higher. See https://github.com/AcademySoftwareFoundation/OpenImageIO/commit/f40f9800c83e2c596c127777bea1e468564fbb10 --- client/ayon_core/lib/transcoding.py | 57 ++++++++++++++++++- .../publish/extract_color_transcode.py | 48 ++++++++++------ 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 1fda014bd8..948bea3685 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -10,6 +10,8 @@ import xml.etree.ElementTree +import clique + from .execute import run_subprocess from .vendor_bin_utils import ( get_ffmpeg_tool_args, @@ -707,7 +709,29 @@ def convert_input_paths_for_ffmpeg( # Collect channels to export input_arg, channels_arg = get_oiio_input_and_channel_args(input_info) - for input_path in input_paths: + # Process input files + # If a sequence of files is detected we process it in one go + # with the dedicated --frames argument for faster processing + collections, remainder = clique.assemble( + input_paths, patterns=clique.PATTERNS["frame"]) + process_queue = collections + remainder + + for input_item in process_queue: + if isinstance(input_item, clique.Collection): + # Support sequences with holes by supplying dedicated `--frames` + # Create `frames` string like "1001-1002,1004,1010-1012 + frames: str = input_item.format("{ranges}").replace(" ", "") + # Create `filename` string like "file.%04d.exr" + input_path = input_item.format("{head}{padding}{tail}") + elif isinstance(input_item, str): + # Single filepath + frames = None + input_path = input_item + else: + raise TypeError( + f"Input is not a string or Collection: {input_item}" + ) + # Prepare subprocess arguments oiio_cmd = get_oiio_tool_args( "oiiotool", @@ -718,6 +742,14 @@ def convert_input_paths_for_ffmpeg( if compression: oiio_cmd.extend(["--compression", compression]) + if frames: + oiio_cmd.extend([ + "--frames", frames, + # TODO: Handle potential toggle for parallel frames + # to support older OIIO releases. + "--parallel-frames" + ]) + oiio_cmd.extend([ input_arg, input_path, # Tell oiiotool which channels should be put to top stack @@ -1106,6 +1138,8 @@ def convert_colorspace( view=None, display=None, additional_command_args=None, + frames=None, + parallel_frames=False, logger=None, ): """Convert source file from one color space to another. @@ -1114,7 +1148,7 @@ def convert_colorspace( input_path (str): Path that should be converted. It is expected that contains single file or image sequence of same type (sequence in format 'file.FRAMESTART-FRAMEEND#.ext', see oiio docs, - eg `big.1-3#.tif`) + eg `big.1-3#.tif` or `big.%04d.ext` with `frames` argument) output_path (str): Path to output filename. (must follow format of 'input_path', eg. single file or sequence in 'file.FRAMESTART-FRAMEEND#.ext', `output.1-3#.tif`) @@ -1128,6 +1162,11 @@ def convert_colorspace( both 'view' and 'display' must be filled (if 'target_colorspace') additional_command_args (list): arguments for oiiotool (like binary depth for .dpx) + frames (Optional[str]): Complex frame range to process. This requires + input path and output path to use frame token placeholder like + e.g. file.%04d.exr + parallel_frames (bool): If True, process frames in parallel inside + the `oiiotool` process. Only supported in OIIO 2.5.20.0+. logger (logging.Logger): Logger used for logging. Raises: ValueError: if misconfigured @@ -1145,9 +1184,21 @@ def convert_colorspace( "oiiotool", # Don't add any additional attributes "--nosoftwareattrib", - "--colorconfig", config_path + "--colorconfig", config_path, ) + if frames: + # If `frames` is specified, then process the input and output + # as if it's a sequence of frames (must contain `%04d` as frame + # token placeholder in filepaths) + oiio_cmd.extend([ + "--frames", frames, + ]) + if parallel_frames: + oiio_cmd.extend([ + "--parallel-frames" + ]) + oiio_cmd.extend([ input_arg, input_path, # Tell oiiotool which channels should be put to top stack diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 1f2c2a89af..25ac747302 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -159,9 +159,18 @@ def process(self, instance): files_to_convert) self.log.debug("Files to convert: {}".format(files_to_convert)) for file_name in files_to_convert: + # Handle special case for sequences where we specify + # the --frames argument to oiiotool + frames = None + parallel_frames = False + if isinstance(file_name, tuple): + file_name, frames = file_name + # TODO: Handle potential toggle for parallel frames + # to support older OIIO releases. + parallel_frames = True + self.log.debug("Transcoding file: `{}`".format(file_name)) - input_path = os.path.join(original_staging_dir, - file_name) + input_path = os.path.join(original_staging_dir, file_name) output_path = self._get_output_file_path(input_path, new_staging_dir, output_extension) @@ -175,7 +184,9 @@ def process(self, instance): view, display, additional_command_args, - self.log + frames=frames, + parallel_frames=parallel_frames, + logger=self.log ) # cleanup temporary transcoded files @@ -256,17 +267,22 @@ def _rename_in_representation(self, new_repre, files_to_convert, new_repre["files"] = renamed_files def _translate_to_sequence(self, files_to_convert): - """Returns original list or list with filename formatted in single - sequence format. + """Returns original individual filepaths or list of a single two-tuple + representating sequence filename with its frames. Uses clique to find frame sequence, in this case it merges all frames - into sequence format (FRAMESTART-FRAMEEND#) and returns it. - If sequence not found, it returns original list + into sequence format (`%04d`) together with all its frames to support + both regular sequences and sequences with holes. + + If sequence not detected in input filenames, it returns original list. Args: - files_to_convert (list): list of file names + files_to_convert (list[str]): list of file names Returns: - (list) of [file.1001-1010#.exr] or [fileA.exr, fileB.exr] + list[Union[str, tuple[str, str]]: List of + or filepaths ['fileA.exr', 'fileB.exr'] + or sequence with frames [('file.%04d.exr', '1001-1002,1004')] + """ pattern = [clique.PATTERNS["frames"]] collections, _ = clique.assemble( @@ -279,15 +295,13 @@ def _translate_to_sequence(self, files_to_convert): "Too many collections {}".format(collections)) collection = collections[0] - frames = list(collection.indexes) - if collection.holes(): - return files_to_convert - - frame_str = "{}-{}#".format(frames[0], frames[-1]) - file_name = "{}{}{}".format(collection.head, frame_str, - collection.tail) - files_to_convert = [file_name] + # Support sequences with holes by supplying dedicated `--frames` + # Create `frames` string like "1001-1002,1004,1010-1012 + frames: str = collection.format("{ranges}").replace(" ", "") + # Create `filename` string like "file.%04d.exr" + filename = collection.format("{head}{padding}{tail}") + return [(filename, frames)] return files_to_convert From 0aa0673b5769d2b2ca4474d0bf1b1a8b94daeb3b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Apr 2025 19:50:59 +0200 Subject: [PATCH 06/15] Use correct variable --- client/ayon_core/lib/transcoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index fe42429851..806d7481f2 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -640,7 +640,7 @@ def convert_input_paths_for_ffmpeg( frames = _input.format("{head}#{tail}").replace(" ", "") oiio_cmd.extend([ "--framepadding", _input.padding, - "--frames", _input.format("{ranges}"), + "--frames", frames, "--parallel-frames" ]) _input: str = _input.format("{head}#{tail}") From 01174c9b1181f046f25f1bcb00f6641e4d60c024 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 25 Apr 2025 09:10:44 +0200 Subject: [PATCH 07/15] Provide more sensible return type for `_translate_to_sequence` --- .../publish/extract_color_transcode.py | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 25ac747302..52b2af6128 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -159,15 +159,21 @@ def process(self, instance): files_to_convert) self.log.debug("Files to convert: {}".format(files_to_convert)) for file_name in files_to_convert: - # Handle special case for sequences where we specify - # the --frames argument to oiiotool - frames = None - parallel_frames = False - if isinstance(file_name, tuple): - file_name, frames = file_name - # TODO: Handle potential toggle for parallel frames - # to support older OIIO releases. + if isinstance(file_name, clique.Collection): + # Support sequences with holes by supplying + # dedicated `--frames` argument to `oiiotool` + # Create `filename` string like "file.%04d.exr" + file_name = file_name.format("{head}{padding}{tail}") + # Create `frames` string like "1001-1002,1004,1010-1012 + frames: str = file_name.format("{ranges}").replace( + " ", "") parallel_frames = True + elif isinstance(file_name, str): + # Single file + frames = None + parallel_frames = False + else: + raise TypeError("Unsupported files to ") self.log.debug("Transcoding file: `{}`".format(file_name)) input_path = os.path.join(original_staging_dir, file_name) @@ -279,9 +285,9 @@ def _translate_to_sequence(self, files_to_convert): Args: files_to_convert (list[str]): list of file names Returns: - list[Union[str, tuple[str, str]]: List of - or filepaths ['fileA.exr', 'fileB.exr'] - or sequence with frames [('file.%04d.exr', '1001-1002,1004')] + list[str | clique.Collection]: List of + filepaths ['fileA.exr', 'fileB.exr'] + or clique.Collection for a sequence. """ pattern = [clique.PATTERNS["frames"]] @@ -294,14 +300,7 @@ def _translate_to_sequence(self, files_to_convert): raise ValueError( "Too many collections {}".format(collections)) - collection = collections[0] - - # Support sequences with holes by supplying dedicated `--frames` - # Create `frames` string like "1001-1002,1004,1010-1012 - frames: str = collection.format("{ranges}").replace(" ", "") - # Create `filename` string like "file.%04d.exr" - filename = collection.format("{head}{padding}{tail}") - return [(filename, frames)] + return collections return files_to_convert From 849a999744853e6390021759822f0fbb932ff58c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 25 Apr 2025 09:11:44 +0200 Subject: [PATCH 08/15] Fix TypeError message --- client/ayon_core/plugins/publish/extract_color_transcode.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 52b2af6128..8988db59ec 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -173,7 +173,10 @@ def process(self, instance): frames = None parallel_frames = False else: - raise TypeError("Unsupported files to ") + raise TypeError( + f"Unsupported file name type: {type(file_name)}." + " Expected str or clique.Collection." + ) self.log.debug("Transcoding file: `{}`".format(file_name)) input_path = os.path.join(original_staging_dir, file_name) From ea5f1c81d61e049f306ef555f539fe24188f31a9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 25 Apr 2025 12:21:34 +0200 Subject: [PATCH 09/15] Fix passing sequence to `oiiotool` --- client/ayon_core/lib/transcoding.py | 13 +++++++++++-- .../plugins/publish/extract_color_transcode.py | 15 +++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 806d7481f2..41835fc8e4 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1011,6 +1011,7 @@ def convert_colorspace( display=None, additional_command_args=None, frames=None, + frame_padding=None, parallel_frames=False, logger=None, ): @@ -1020,7 +1021,7 @@ def convert_colorspace( input_path (str): Path that should be converted. It is expected that contains single file or image sequence of same type (sequence in format 'file.FRAMESTART-FRAMEEND#.ext', see oiio docs, - eg `big.1-3#.tif` or `big.%04d.ext` with `frames` argument) + eg `big.1-3#.tif` or `big.1-3%d.ext` with `frames` argument) output_path (str): Path to output filename. (must follow format of 'input_path', eg. single file or sequence in 'file.FRAMESTART-FRAMEEND#.ext', `output.1-3#.tif`) @@ -1036,9 +1037,11 @@ def convert_colorspace( depth for .dpx) frames (Optional[str]): Complex frame range to process. This requires input path and output path to use frame token placeholder like - e.g. file.%04d.exr + `#` or `%d`, e.g. file.#.exr parallel_frames (bool): If True, process frames in parallel inside the `oiiotool` process. Only supported in OIIO 2.5.20.0+. + frame_padding (Optional[int]): Frame padding to use for the input and + output when using a sequence filepath. logger (logging.Logger): Logger used for logging. Raises: ValueError: if misconfigured @@ -1066,6 +1069,12 @@ def convert_colorspace( oiio_cmd.extend([ "--frames", frames, ]) + + if frame_padding: + oiio_cmd.extend([ + "--framepadding", frame_padding, + ]) + if parallel_frames: oiio_cmd.extend([ "--parallel-frames" diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 8988db59ec..9d315052a2 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -162,15 +162,16 @@ def process(self, instance): if isinstance(file_name, clique.Collection): # Support sequences with holes by supplying # dedicated `--frames` argument to `oiiotool` - # Create `filename` string like "file.%04d.exr" - file_name = file_name.format("{head}{padding}{tail}") + # Create `filename` string like "file.#.exr" # Create `frames` string like "1001-1002,1004,1010-1012 - frames: str = file_name.format("{ranges}").replace( - " ", "") + file_name = file_name.format("{head}#{tail}") + frames = file_name.format("{ranges}").replace(" ", "") + frame_padding = file_name.padding parallel_frames = True elif isinstance(file_name, str): # Single file frames = None + frame_padding = None parallel_frames = False else: raise TypeError( @@ -194,6 +195,7 @@ def process(self, instance): display, additional_command_args, frames=frames, + frame_padding=frame_padding, parallel_frames=parallel_frames, logger=self.log ) @@ -279,10 +281,7 @@ def _translate_to_sequence(self, files_to_convert): """Returns original individual filepaths or list of a single two-tuple representating sequence filename with its frames. - Uses clique to find frame sequence, in this case it merges all frames - into sequence format (`%04d`) together with all its frames to support - both regular sequences and sequences with holes. - + Uses clique to find frame sequence, and return the collections instead. If sequence not detected in input filenames, it returns original list. Args: From 7bf2bfd6b16368f4aeb1fd5bd797a869d460c071 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 25 Apr 2025 12:22:22 +0200 Subject: [PATCH 10/15] Improve docstring --- client/ayon_core/plugins/publish/extract_color_transcode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 9d315052a2..13678610aa 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -278,8 +278,7 @@ def _rename_in_representation(self, new_repre, files_to_convert, new_repre["files"] = renamed_files def _translate_to_sequence(self, files_to_convert): - """Returns original individual filepaths or list of a single two-tuple - representating sequence filename with its frames. + """Returns original individual filepaths or list of clique.Collection. Uses clique to find frame sequence, and return the collections instead. If sequence not detected in input filenames, it returns original list. From 422febf4419bcdf165b8c81297c383e9ca171096 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 25 Apr 2025 12:26:50 +0200 Subject: [PATCH 11/15] Fix variable usage --- client/ayon_core/plugins/publish/extract_color_transcode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 13678610aa..c549ff8a63 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -162,11 +162,11 @@ def process(self, instance): if isinstance(file_name, clique.Collection): # Support sequences with holes by supplying # dedicated `--frames` argument to `oiiotool` - # Create `filename` string like "file.#.exr" # Create `frames` string like "1001-1002,1004,1010-1012 - file_name = file_name.format("{head}#{tail}") + # Create `filename` string like "file.#.exr" frames = file_name.format("{ranges}").replace(" ", "") frame_padding = file_name.padding + file_name = file_name.format("{head}#{tail}") parallel_frames = True elif isinstance(file_name, str): # Single file From 537dac603358bea4b8b07806f6ce2854919aeab5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 25 Apr 2025 12:47:20 +0200 Subject: [PATCH 12/15] Fix `get_oiio_info_for_input` call for sequences in `convert_colorspace` --- client/ayon_core/lib/transcoding.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 41835fc8e4..06ea353aa2 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1049,7 +1049,16 @@ def convert_colorspace( if logger is None: logger = logging.getLogger(__name__) - input_info = get_oiio_info_for_input(input_path, logger=logger) + # Get oiioinfo only from first image, otherwise file can't be found + first_input_path = input_path + if frames: + assert isinstance(frames, str) # for type hints + first_frame = int(frames.split(" x-,")[0]) + first_frame = str(first_frame).zfill(frame_padding or 0) + for token in ["#", "%d"]: + first_input_path = first_input_path.replace(token, first_frame) + + input_info = get_oiio_info_for_input(first_input_path, logger=logger) # Collect channels to export input_arg, channels_arg = get_oiio_input_and_channel_args(input_info) From ec9c6c510a8d0fdb1143f2708141736a50d06dec Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 25 Apr 2025 14:14:27 +0200 Subject: [PATCH 13/15] Split on any of the characters as intended, instead of on literal ` x-` --- client/ayon_core/lib/transcoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 06ea353aa2..7073ba6b89 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1053,7 +1053,7 @@ def convert_colorspace( first_input_path = input_path if frames: assert isinstance(frames, str) # for type hints - first_frame = int(frames.split(" x-,")[0]) + first_frame = int(re.split("[ x-]", frames, 1)[0]) first_frame = str(first_frame).zfill(frame_padding or 0) for token in ["#", "%d"]: first_input_path = first_input_path.replace(token, first_frame) From 3248faff40225ecd884609e05bcaeafbd6c7a825 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 25 Apr 2025 14:57:40 +0200 Subject: [PATCH 14/15] Fix `int` -> `str` frame padding argument to subprocess --- client/ayon_core/lib/transcoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 7073ba6b89..82038ed543 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1081,7 +1081,7 @@ def convert_colorspace( if frame_padding: oiio_cmd.extend([ - "--framepadding", frame_padding, + "--framepadding", str(frame_padding), ]) if parallel_frames: From 204625b5c855d98f3eaf8f8d4336b3a524b12b56 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 May 2025 23:55:29 +0200 Subject: [PATCH 15/15] Update client/ayon_core/lib/transcoding.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/transcoding.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 82038ed543..e60d9d75c8 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -552,12 +552,11 @@ def _get_attributes_to_erase( erase_attrs[attr_name] = reason break - if logger is not None: - for attr_name, reason in erase_attrs.items(): - logger.info( - f"Removed attribute \"{attr_name}\" from metadata" - f" because {reason}." - ) + for attr_name, reason in erase_attrs.items(): + logger.info( + f"Removed attribute \"{attr_name}\" from metadata" + f" because {reason}." + ) return list(erase_attrs.keys())