From cb125a192f0728562d5e76d2b510370d65c4f1f8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 31 Mar 2025 23:14:17 +0200 Subject: [PATCH 01/55] 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 | 203 +++++++--------------------- 1 file changed, 50 insertions(+), 153 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 @@ from typing import Optional 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 + reason = f"has too long value ({len(attr_value)} chars)." + erase_attr_names.append(attr_name) + reasons.append(reason) - 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 + 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) - if erase_attribute: + if logger is not None: + for attr_name, reason in zip(erase_attr_names, reasons): # Set attribute to empty string - logger.info(( - "Removed attribute \"{}\" from metadata because {}." - ).format(attr_name, erase_reason)) - oiio_cmd.extend(["--eraseattrib", attr_name]) - - # 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 - ]) - - logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) - run_subprocess(oiio_cmd, logger=logger) + 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/55] 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/55] 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/55] 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 .profiles_filtering import ( 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 @@ __all__ = [ "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/55] 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 @@ from typing import Optional 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 @@ class ExtractOIIOTranscode(publish.Extractor): 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 @@ class ExtractOIIOTranscode(publish.Extractor): view, display, additional_command_args, - self.log + frames=frames, + parallel_frames=parallel_frames, + logger=self.log ) # cleanup temporary transcoded files @@ -256,17 +267,22 @@ class ExtractOIIOTranscode(publish.Extractor): 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 @@ class ExtractOIIOTranscode(publish.Extractor): "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/55] 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/55] 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 @@ class ExtractOIIOTranscode(publish.Extractor): 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 @@ class ExtractOIIOTranscode(publish.Extractor): 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 @@ class ExtractOIIOTranscode(publish.Extractor): 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/55] 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 @@ class ExtractOIIOTranscode(publish.Extractor): 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/55] 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 @@ class ExtractOIIOTranscode(publish.Extractor): 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 @@ class ExtractOIIOTranscode(publish.Extractor): display, additional_command_args, frames=frames, + frame_padding=frame_padding, parallel_frames=parallel_frames, logger=self.log ) @@ -279,10 +281,7 @@ class ExtractOIIOTranscode(publish.Extractor): """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/55] 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 @@ class ExtractOIIOTranscode(publish.Extractor): 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/55] 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 @@ class ExtractOIIOTranscode(publish.Extractor): 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/55] 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/55] 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/55] 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/55] 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()) From 8fbb8c93c12b75dd29e1ca16e6e5567bf846baec Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jun 2025 14:47:41 +0200 Subject: [PATCH 16/55] Allow more frames patterns --- client/ayon_core/lib/transcoding.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index f249213f2a..3f30f771c1 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -616,9 +616,12 @@ def convert_input_paths_for_ffmpeg( input_info, logger=logger ) + # clique.PATTERNS["frames"] supports only `.1001.exr` not `_1001.exr` so + # we use a customized pattern. + pattern = "[_.](?P(?P0*)\\d+)\\.\\D+\\d?$" input_collections, input_remainder = clique.assemble( input_paths, - patterns=[clique.PATTERNS["frames"]], + patterns=[pattern], assume_padded_when_ambiguous=True, ) input_items = list(input_collections) From 74dc83d14a18c0ff9acc646864efe3b294051fa3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:06:19 +0100 Subject: [PATCH 17/55] skip base classes --- client/ayon_core/pipeline/plugin_discover.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/plugin_discover.py b/client/ayon_core/pipeline/plugin_discover.py index dddd6847ec..896b7966d6 100644 --- a/client/ayon_core/pipeline/plugin_discover.py +++ b/client/ayon_core/pipeline/plugin_discover.py @@ -138,7 +138,14 @@ def discover_plugins( for item in modules: filepath, module = item result.add_module(module) - all_plugins.extend(classes_from_module(base_class, module)) + for cls in classes_from_module(base_class, module): + if cls is base_class: + continue + # Class has defined 'is_base_class = True' + is_base_class = cls.__dict__.get("is_base_class") + if is_base_class is True: + continue + all_plugins.append(cls) if base_class not in ignored_classes: ignored_classes.append(base_class) From 2885ed180527e10a11dc18958f5c36bd87a26fbc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 17:24:37 +0100 Subject: [PATCH 18/55] Added profiles to ExtractThumbnail --- server/settings/publish_plugins.py | 75 +++++++++++++++++++----------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index d7b794cb5b..60098895d8 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -400,24 +400,28 @@ class ExtractThumbnailOIIODefaultsModel(BaseSettingsModel): ) -class ExtractThumbnailModel(BaseSettingsModel): - _isGroup = True - enabled: bool = SettingsField(True) +class ExtractThumbnailProfileModel(BaseSettingsModel): + product_types: list[str] = SettingsField( + default_factory=list, title="Product types" + ) + hosts: list[str] = SettingsField(default_factory=list, title="Host names") + task_types: list[str] = SettingsField( + default_factory=list, title="Task types", enum_resolver=task_types_enum + ) + task_names: list[str] = SettingsField( + default_factory=list, title="Task names" + ) product_names: list[str] = SettingsField( - default_factory=list, - title="Product names" + default_factory=list, title="Product names" ) integrate_thumbnail: bool = SettingsField( - True, - title="Integrate Thumbnail Representation" + True, title="Integrate Thumbnail Representation" ) target_size: ResizeModel = SettingsField( - default_factory=ResizeModel, - title="Target size" + default_factory=ResizeModel, title="Target size" ) background_color: ColorRGBA_uint8 = SettingsField( - (0, 0, 0, 0.0), - title="Background color" + (0, 0, 0, 0.0), title="Background color" ) duration_split: float = SettingsField( 0.5, @@ -434,6 +438,15 @@ class ExtractThumbnailModel(BaseSettingsModel): ) +class ExtractThumbnailModel(BaseSettingsModel): + _isGroup = True + enabled: bool = SettingsField(True) + + profiles: list[ExtractThumbnailProfileModel] = SettingsField( + default_factory=list, title="Profiles" + ) + + def _extract_oiio_transcoding_type(): return [ {"value": "colorspace", "label": "Use Colorspace"}, @@ -1458,22 +1471,30 @@ DEFAULT_PUBLISH_VALUES = { }, "ExtractThumbnail": { "enabled": True, - "product_names": [], - "integrate_thumbnail": True, - "target_size": { - "type": "source" - }, - "duration_split": 0.5, - "oiiotool_defaults": { - "type": "colorspace", - "colorspace": "color_picking" - }, - "ffmpeg_args": { - "input": [ - "-apply_trc gamma22" - ], - "output": [] - } + "profiles": [ + { + "product_types": [], + "hosts": [], + "task_types": [], + "task_names": [], + "product_names": [], + "integrate_thumbnail": True, + "target_size": { + "type": "source" + }, + "duration_split": 0.5, + "oiiotool_defaults": { + "type": "colorspace", + "colorspace": "color_picking" + }, + "ffmpeg_args": { + "input": [ + "-apply_trc gamma22" + ], + "output": [] + } + } + ] }, "ExtractOIIOTranscode": { "enabled": True, From 7f40b6c6a2632e80acc2bcc1263116ecc9373f9f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 17:25:20 +0100 Subject: [PATCH 19/55] Added conversion to profiles to ExtractThumbnail --- server/settings/conversion.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index 757818a9ff..c572fe70a9 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -158,6 +158,35 @@ def _convert_publish_plugins(overrides): _convert_oiio_transcode_0_4_5(overrides["publish"]) +def _convert_extract_thumbnail(overrides): + """ExtractThumbnail config settings did change to profiles.""" + extract_thumbnail_overrides = overrides.get("ExtractThumbnail") or {} + if "profiles" in extract_thumbnail_overrides: + return + + base_value = { + "product_types": [], + "hosts": [], + "task_types": [], + "task_names": [], + "product_names": [], + "integrate_thumbnail": True, + "target_size": {"type": "source"}, + "duration_split": 0.5, + "oiiotool_defaults": { + "type": "colorspace", + "colorspace": "color_picking", + }, + "ffmpeg_args": {"input": ["-apply_trc gamma22"], "output": []}, + } + base_value.update(extract_thumbnail_overrides) + + extract_thumbnail_profiles = extract_thumbnail_overrides.setdefault( + "profiles", [] + ) + extract_thumbnail_profiles.append(base_value) + + def convert_settings_overrides( source_version: str, overrides: dict[str, Any], @@ -166,4 +195,5 @@ def convert_settings_overrides( _convert_imageio_configs_0_4_5(overrides) _convert_imageio_configs_1_6_5(overrides) _convert_publish_plugins(overrides) + _convert_extract_thumbnail(overrides) return overrides From c0ed22c4d7486dc7171605abfe51f9c3d4b634b8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 17:27:54 +0100 Subject: [PATCH 20/55] Added profiles for ExtractThumbnail --- .../plugins/publish/extract_thumbnail.py | 152 +++++++++++++++--- 1 file changed, 132 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index adfb4298b9..8607244d72 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -1,8 +1,10 @@ import copy +from dataclasses import dataclass, field, fields import os +import re import subprocess import tempfile -import re +from typing import Dict, Any, List, Tuple import pyblish.api from ayon_core.lib import ( @@ -15,6 +17,7 @@ from ayon_core.lib import ( path_to_subprocess_arg, run_subprocess, + filter_profiles, ) from ayon_core.lib.transcoding import ( MissingRGBAChannelsError, @@ -26,6 +29,63 @@ from ayon_core.lib.transcoding import ( from ayon_core.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS +@dataclass +class ProfileConfig: + """ + Data class representing the full configuration for selected profile + + Any change of controllable fields in Settings must propagate here! + """ + product_names: List[str] = field(default_factory=list) + + integrate_thumbnail: bool = False + + target_size: Dict[str, Any] = field( + default_factory=lambda: { + "type": "source", + "resize": {"width": 1920, "height": 1080}, + } + ) + + duration_split: float = 0.5 + + oiiotool_defaults: Dict[str, str] = field( + default_factory=lambda: { + "type": "colorspace", + "colorspace": "color_picking" + } + ) + + ffmpeg_args: Dict[str, List[Any]] = field( + default_factory=lambda: {"input": [], "output": []} + ) + + # Background color defined as (R, G, B, A) tuple. + # Note: Use float for alpha channel (0.0 to 1.0). + background_color: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0) + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ProfileConfig": + """ + Creates a ProfileConfig instance from a dictionary, safely ignoring + any keys in the dictionary that are not fields in the dataclass. + + Args: + data (Dict[str, Any]): The dictionary containing configuration data + + Returns: + MediaConfig: A new instance of the dataclass. + """ + # Get all field names defined in the dataclass + field_names = {f.name for f in fields(cls)} + + # Filter the input dictionary to include only keys matching field names + filtered_data = {k: v for k, v in data.items() if k in field_names} + + # Unpack the filtered dictionary into the constructor + return cls(**filtered_data) + + class ExtractThumbnail(pyblish.api.InstancePlugin): """Create jpg thumbnail from sequence using ffmpeg""" @@ -99,6 +159,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): instance.data["representations"].remove(repre) def _main_process(self, instance): + if not self.profiles: + self.log.debug("No profiles present for color transcode") + return + profile_config = self._get_config_from_profile(instance) + if not profile_config: + return + product_name = instance.data["productName"] instance_repres = instance.data.get("representations") if not instance_repres: @@ -138,7 +205,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return True return False - product_names = self.product_names + product_names = profile_config.product_names if product_names: result = validate_string_against_patterns( product_name, product_names @@ -205,8 +272,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # exclude first frame if slate in representation tags if "slate-frame" in repre.get("tags", []): repre_files_thumb = repre_files_thumb[1:] - file_index = int( - float(len(repre_files_thumb)) * self.duration_split) + file_index = int(float(len(repre_files_thumb)) * profile_config.duration_split) # noqa: E501 input_file = repre_files[file_index] full_input_path = os.path.join(src_staging, input_file) @@ -243,13 +309,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # colorspace data if not repre_thumb_created: repre_thumb_created = self._create_thumbnail_ffmpeg( - full_input_path, full_output_path + full_input_path, full_output_path, profile_config ) # Skip representation and try next one if wasn't created if not repre_thumb_created and oiio_supported: repre_thumb_created = self._create_thumbnail_oiio( - full_input_path, full_output_path + full_input_path, full_output_path, profile_config ) if not repre_thumb_created: @@ -277,7 +343,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): new_repre_tags = ["thumbnail"] # for workflows which needs to have thumbnails published as # separate representations `delete` tag should not be added - if not self.integrate_thumbnail: + if not profile_config.integrate_thumbnail: new_repre_tags.append("delete") new_repre = { @@ -399,6 +465,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): src_path, dst_path, colorspace_data, + profile_config ): """Create thumbnail using OIIO tool oiiotool @@ -416,7 +483,9 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): str: path to created thumbnail """ self.log.info("Extracting thumbnail {}".format(dst_path)) - resolution_arg = self._get_resolution_arg("oiiotool", src_path) + resolution_arg = self._get_resolution_arg( + "oiiotool", src_path, profile_config + ) repre_display = colorspace_data.get("display") repre_view = colorspace_data.get("view") @@ -435,12 +504,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ) # if representation doesn't have display and view then use # oiiotool_defaults - elif self.oiiotool_defaults: - oiio_default_type = self.oiiotool_defaults["type"] + elif profile_config.oiiotool_defaults: + oiiotool_defaults = profile_config.oiiotool_defaults + oiio_default_type = oiiotool_defaults["type"] if "colorspace" == oiio_default_type: - oiio_default_colorspace = self.oiiotool_defaults["colorspace"] + oiio_default_colorspace = oiiotool_defaults["colorspace"] else: - display_and_view = self.oiiotool_defaults["display_and_view"] + display_and_view = oiiotool_defaults["display_and_view"] oiio_default_display = display_and_view["display"] oiio_default_view = display_and_view["view"] @@ -467,11 +537,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return True - def _create_thumbnail_oiio(self, src_path, dst_path): + def _create_thumbnail_oiio(self, src_path, dst_path, profile_config): self.log.debug(f"Extracting thumbnail with OIIO: {dst_path}") try: - resolution_arg = self._get_resolution_arg("oiiotool", src_path) + resolution_arg = self._get_resolution_arg( + "oiiotool", src_path, profile_config + ) except RuntimeError: self.log.warning( "Failed to create thumbnail using oiio", exc_info=True @@ -511,9 +583,11 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ) return False - def _create_thumbnail_ffmpeg(self, src_path, dst_path): + def _create_thumbnail_ffmpeg(self, src_path, dst_path, profile_config): try: - resolution_arg = self._get_resolution_arg("ffmpeg", src_path) + resolution_arg = self._get_resolution_arg( + "ffmpeg", src_path, profile_config + ) except RuntimeError: self.log.warning( "Failed to create thumbnail using ffmpeg", exc_info=True @@ -521,7 +595,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return False ffmpeg_path_args = get_ffmpeg_tool_args("ffmpeg") - ffmpeg_args = self.ffmpeg_args or {} + ffmpeg_args = profile_config.ffmpeg_args or {} jpeg_items = [ subprocess.list2cmdline(ffmpeg_path_args) @@ -664,12 +738,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): self, application, input_path, + profile_config ): # get settings - if self.target_size["type"] == "source": + if profile_config.target_size["type"] == "source": return [] - resize = self.target_size["resize"] + resize = profile_config.target_size["resize"] target_width = resize["width"] target_height = resize["height"] @@ -679,6 +754,43 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): input_path, target_width, target_height, - bg_color=self.background_color, + bg_color=profile_config.background_color, log=self.log ) + + def _get_config_from_profile( + self, + instance: pyblish.api.Instance + ) -> ProfileConfig: + """Returns profile if and how repre should be color transcoded.""" + host_name = instance.context.data["hostName"] + product_type = instance.data["productType"] + product_name = instance.data["productName"] + task_data = instance.data["anatomyData"].get("task", {}) + task_name = task_data.get("name") + task_type = task_data.get("type") + filtering_criteria = { + "hosts": host_name, + "product_types": product_type, + "product_names": product_name, + "task_names": task_name, + "task_types": task_type, + } + profile = filter_profiles( + self.profiles, filtering_criteria, + logger=self.log + ) + + if not profile: + self.log.debug( + ( + "Skipped instance. None of profiles in presets are for" + ' Host: "{}" | Product types: "{}" | Product names: "{}"' + ' | Task name "{}" | Task type "{}"' + ).format( + host_name, product_type, product_name, task_name, task_type + ) + ) + return + + return ProfileConfig.from_dict(profile) From c7672fd51127db9364eeda2fa5a63ca41ba69986 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 18:53:30 +0100 Subject: [PATCH 21/55] Fix querying of overrides --- server/settings/conversion.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index c572fe70a9..4f28801ba1 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -160,7 +160,9 @@ def _convert_publish_plugins(overrides): def _convert_extract_thumbnail(overrides): """ExtractThumbnail config settings did change to profiles.""" - extract_thumbnail_overrides = overrides.get("ExtractThumbnail") or {} + extract_thumbnail_overrides = ( + overrides.get("publish", {}).get("ExtractThumbnail") or {} + ) if "profiles" in extract_thumbnail_overrides: return From 56df03848f5b2352b0f33dbc4a7d248f06dffb96 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Dec 2025 11:59:33 +0100 Subject: [PATCH 22/55] Updated logging --- client/ayon_core/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 8607244d72..bc8246e1bd 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -160,7 +160,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): def _main_process(self, instance): if not self.profiles: - self.log.debug("No profiles present for color transcode") + self.log.debug("No profiles present for extract review thumbnail.") return profile_config = self._get_config_from_profile(instance) if not profile_config: From a59b2644968968a91a23e20e93d547753c5144cb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Dec 2025 11:59:50 +0100 Subject: [PATCH 23/55] Updated Settings controlled variables --- .../plugins/publish/extract_thumbnail.py | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index bc8246e1bd..80e4054ecb 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -113,30 +113,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): settings_category = "core" enabled = False - integrate_thumbnail = False - target_size = { - "type": "source", - "resize": { - "width": 1920, - "height": 1080 - } - } - background_color = (0, 0, 0, 0.0) - duration_split = 0.5 - # attribute presets from settings - oiiotool_defaults = { - "type": "colorspace", - "colorspace": "color_picking", - "display_and_view": { - "display": "default", - "view": "sRGB" - } - } - ffmpeg_args = { - "input": [], - "output": [] - } - product_names = [] + profiles = [] def process(self, instance): # run main process From fa6e8b447842e96bbda3ca90c417d2967e3f873a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Dec 2025 12:05:54 +0100 Subject: [PATCH 24/55] Added missed argument --- .../ayon_core/plugins/publish/extract_thumbnail.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 80e4054ecb..022979d09e 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -237,7 +237,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ) file_path = self._create_frame_from_video( video_file_path, - dst_staging + dst_staging, + profile_config ) if file_path: src_staging, input_file = os.path.split(file_path) @@ -612,7 +613,12 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ) return False - def _create_frame_from_video(self, video_file_path, output_dir): + def _create_frame_from_video( + self, + video_file_path, + output_dir, + profile_config + ): """Convert video file to one frame image via ffmpeg""" # create output file path base_name = os.path.basename(video_file_path) @@ -637,7 +643,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): seek_position = 0.0 # Only use timestamp calculation for videos longer than 0.1 seconds if duration > 0.1: - seek_position = duration * self.duration_split + seek_position = duration * profile_config.duration_split # Build command args cmd_args = [] From 32bc4248fc7f421334190f2f6be179db8968033c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Dec 2025 12:11:08 +0100 Subject: [PATCH 25/55] Typing --- client/ayon_core/plugins/publish/extract_thumbnail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 022979d09e..2cee12304a 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -4,7 +4,7 @@ import os import re import subprocess import tempfile -from typing import Dict, Any, List, Tuple +from typing import Dict, Any, List, Tuple, Optional import pyblish.api from ayon_core.lib import ( @@ -744,7 +744,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): def _get_config_from_profile( self, instance: pyblish.api.Instance - ) -> ProfileConfig: + ) -> Optional[ProfileConfig]: """Returns profile if and how repre should be color transcoded.""" host_name = instance.context.data["hostName"] product_type = instance.data["productType"] From 074c43ff68c0770bee03c84238dc6255e4825bd1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:05:56 +0100 Subject: [PATCH 26/55] added is_base_class to create base classes --- client/ayon_core/pipeline/create/creator_plugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 7573589b82..8595ff4ca5 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -651,7 +651,7 @@ class Creator(BaseCreator): Creation requires prepared product name and instance data. """ - + is_base_class = True # GUI Purposes # - default_variants may not be used if `get_default_variants` # is overridden @@ -949,6 +949,8 @@ class Creator(BaseCreator): class HiddenCreator(BaseCreator): + is_base_class = True + @abstractmethod def create(self, instance_data, source_data): pass @@ -959,6 +961,7 @@ class AutoCreator(BaseCreator): Can be used e.g. for `workfile`. """ + is_base_class = True def remove_instances(self, instances): """Skip removal.""" From 89129dfeb4046cb49d47c7ca9928c0eb2993033f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Dec 2025 10:10:51 +0100 Subject: [PATCH 27/55] Renamed hosts to host_names for ExtractThumbnail --- client/ayon_core/plugins/publish/extract_thumbnail.py | 2 +- server/settings/conversion.py | 2 +- server/settings/publish_plugins.py | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 2cee12304a..c7e99a186a 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -753,7 +753,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): task_name = task_data.get("name") task_type = task_data.get("type") filtering_criteria = { - "hosts": host_name, + "host_names": host_name, "product_types": product_type, "product_names": product_name, "task_names": task_name, diff --git a/server/settings/conversion.py b/server/settings/conversion.py index 4f28801ba1..6fd534704c 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -168,7 +168,7 @@ def _convert_extract_thumbnail(overrides): base_value = { "product_types": [], - "hosts": [], + "host_names": [], "task_types": [], "task_names": [], "product_names": [], diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 60098895d8..fd20ccf9c6 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -404,7 +404,9 @@ class ExtractThumbnailProfileModel(BaseSettingsModel): product_types: list[str] = SettingsField( default_factory=list, title="Product types" ) - hosts: list[str] = SettingsField(default_factory=list, title="Host names") + host_names: list[str] = SettingsField( + default_factory=list, title="Host names" + ) task_types: list[str] = SettingsField( default_factory=list, title="Task types", enum_resolver=task_types_enum ) @@ -1474,7 +1476,7 @@ DEFAULT_PUBLISH_VALUES = { "profiles": [ { "product_types": [], - "hosts": [], + "host_names": [], "task_types": [], "task_names": [], "product_names": [], From a4559fe79ee19737974a83e34109ddb2e8040701 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Dec 2025 10:11:36 +0100 Subject: [PATCH 28/55] Changed datatype of rgb --- client/ayon_core/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index c7e99a186a..ff4ee5b431 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -62,7 +62,7 @@ class ProfileConfig: # Background color defined as (R, G, B, A) tuple. # Note: Use float for alpha channel (0.0 to 1.0). - background_color: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0) + background_color: Tuple[int, int, int, float] = (0, 0, 0, 0.0) @classmethod def from_dict(cls, data: Dict[str, Any]) -> "ProfileConfig": From 6cfb22a4b5defdaabfb57551506b390048684f34 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Dec 2025 10:13:00 +0100 Subject: [PATCH 29/55] Formatting change --- client/ayon_core/plugins/publish/extract_thumbnail.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index ff4ee5b431..b9e0a4a5b1 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -250,7 +250,9 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # exclude first frame if slate in representation tags if "slate-frame" in repre.get("tags", []): repre_files_thumb = repre_files_thumb[1:] - file_index = int(float(len(repre_files_thumb)) * profile_config.duration_split) # noqa: E501 + file_index = int( + float(len(repre_files_thumb)) * profile_config.duration_split # noqa: E501 + ) input_file = repre_files[file_index] full_input_path = os.path.join(src_staging, input_file) From d859ea2fc3742d0cb14be03fcc27db6a8f80ee2d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Dec 2025 10:13:57 +0100 Subject: [PATCH 30/55] Explicit key values updates --- server/settings/conversion.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index 6fd534704c..348518f0a3 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -181,7 +181,16 @@ def _convert_extract_thumbnail(overrides): }, "ffmpeg_args": {"input": ["-apply_trc gamma22"], "output": []}, } - base_value.update(extract_thumbnail_overrides) + for key in ( + "product_names", + "integrate_thumbnail", + "target_size", + "duration_split", + "oiiotool_defaults", + "ffmpeg_args", + ): + if key in extract_thumbnail_overrides: + base_value[key] = extract_thumbnail_overrides[key] extract_thumbnail_profiles = extract_thumbnail_overrides.setdefault( "profiles", [] From f1288eb096d30c3788f54c64cf3f022e361a67d0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Dec 2025 10:15:46 +0100 Subject: [PATCH 31/55] Renamed ProfileConfig to ThumbnailDef --- client/ayon_core/plugins/publish/extract_thumbnail.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index b9e0a4a5b1..47d2a9419f 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -30,7 +30,7 @@ from ayon_core.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS @dataclass -class ProfileConfig: +class ThumbnailDef: """ Data class representing the full configuration for selected profile @@ -65,9 +65,9 @@ class ProfileConfig: background_color: Tuple[int, int, int, float] = (0, 0, 0, 0.0) @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "ProfileConfig": + def from_dict(cls, data: Dict[str, Any]) -> "ThumbnailDef": """ - Creates a ProfileConfig instance from a dictionary, safely ignoring + Creates a ThumbnailDef instance from a dictionary, safely ignoring any keys in the dictionary that are not fields in the dataclass. Args: @@ -746,7 +746,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): def _get_config_from_profile( self, instance: pyblish.api.Instance - ) -> Optional[ProfileConfig]: + ) -> Optional[ThumbnailDef]: """Returns profile if and how repre should be color transcoded.""" host_name = instance.context.data["hostName"] product_type = instance.data["productType"] @@ -778,4 +778,4 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ) return - return ProfileConfig.from_dict(profile) + return ThumbnailDef.from_dict(profile) From 14bead732cc73335c557ab6f30fb1e98504debb0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Dec 2025 10:16:54 +0100 Subject: [PATCH 32/55] Removed unnecessary filtering Already done in profile filter --- .../plugins/publish/extract_thumbnail.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 47d2a9419f..23309f4d34 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -36,8 +36,6 @@ class ThumbnailDef: Any change of controllable fields in Settings must propagate here! """ - product_names: List[str] = field(default_factory=list) - integrate_thumbnail: bool = False target_size: Dict[str, Any] = field( @@ -175,24 +173,6 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): self.log.debug("Skipping crypto passes.") return - # We only want to process the produces needed from settings. - def validate_string_against_patterns(input_str, patterns): - for pattern in patterns: - if re.match(pattern, input_str): - return True - return False - - product_names = profile_config.product_names - if product_names: - result = validate_string_against_patterns( - product_name, product_names - ) - if not result: - self.log.debug(( - "Product name \"{}\" did not match settings filters: {}" - ).format(product_name, product_names)) - return - # first check for any explicitly marked representations for thumbnail explicit_repres = self._get_explicit_repres_for_thumbnail(instance) if explicit_repres: From 44251c93c776935abb1b5eb0338e24c0768a77a0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 9 Dec 2025 10:33:12 +0100 Subject: [PATCH 33/55] Ruff --- client/ayon_core/plugins/publish/extract_thumbnail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 23309f4d34..447a24656d 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -1,7 +1,6 @@ import copy from dataclasses import dataclass, field, fields import os -import re import subprocess import tempfile from typing import Dict, Any, List, Tuple, Optional From e3206796a764e2adf74a1097acf7780f91836863 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Dec 2025 22:16:03 +0100 Subject: [PATCH 34/55] Fix #1600: Filter out containers that lack required keys (looking at you `ayon-unreal`!) --- .../publish/collect_scene_loaded_versions.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py index f509ed807a..6d1563a33a 100644 --- a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py +++ b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py @@ -1,3 +1,5 @@ +from __future__ import annotations +from typing import Any import ayon_api import ayon_api.utils @@ -32,6 +34,8 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): self.log.debug("No loaded containers found in scene.") return + containers = self._filter_invalid_containers(containers) + repre_ids = { container["representation"] for container in containers @@ -78,3 +82,29 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): self.log.debug(f"Collected {len(loaded_versions)} loaded versions.") context.data["loadedVersions"] = loaded_versions + + def _filter_invalid_containers( + self, + containers: list[dict[str, Any]] + ) -> list[dict[str, Any]]: + """Filter out invalid containers lacking required keys. + + Skip any invalid containers that lack 'representation' or 'name' + keys to avoid KeyError. + """ + # Only filter by what's required for this plug-in instead of validating + # a full container schema. + required_keys = {"name", "representation"} + valid = [] + for container in containers: + missing = [key for key in required_keys if key not in container] + if missing: + self.log.debug( + "Skipping invalid container, missing required keys:" + " {}. {}".format(", ".join(missing), container) + ) + continue + valid.append(container) + + + return valid From 5e674844b5cb2a5172b12d4008fd31e22497ceeb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Dec 2025 22:19:12 +0100 Subject: [PATCH 35/55] Cosmetics --- .../ayon_core/plugins/publish/collect_scene_loaded_versions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py index 6d1563a33a..2c214cd1a7 100644 --- a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py +++ b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py @@ -106,5 +106,4 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): continue valid.append(container) - return valid From 82427cb004003e415583266fe036bb56fc240481 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 12 Dec 2025 10:21:45 +0100 Subject: [PATCH 36/55] Fix merge conflicts --- client/ayon_core/lib/transcoding.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 9848063133..d462b94153 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1122,9 +1122,6 @@ def convert_colorspace( view=None, display=None, additional_command_args=None, - frames=None, - frame_padding=None, - parallel_frames=False, logger=None, ): """DEPRECATED function use `oiio_color_convert` instead @@ -1176,6 +1173,9 @@ def oiio_color_convert( target_display=None, target_view=None, additional_command_args=None, + frames=None, + frame_padding=None, + parallel_frames=False, logger=None, ): """Transcode source file to other with colormanagement. @@ -1212,10 +1212,10 @@ def oiio_color_convert( frames (Optional[str]): Complex frame range to process. This requires input path and output path to use frame token placeholder like `#` 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. + 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: From 11ecc69b3537b6f3af974c794a18ff37fa066a17 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 12 Dec 2025 10:31:25 +0100 Subject: [PATCH 37/55] Refactor `_input` -> `input_item` --- client/ayon_core/lib/transcoding.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index d462b94153..c37a0a88b7 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -733,7 +733,7 @@ def convert_input_paths_for_ffmpeg( ) input_items = list(input_collections) input_items.extend(input_remainder) - for _input in input_items: + for input_item in input_items: # Prepare subprocess arguments oiio_cmd = get_oiio_tool_args( "oiiotool", @@ -746,21 +746,21 @@ def convert_input_paths_for_ffmpeg( # Convert a sequence of files using a single oiiotool command # using its sequence syntax - if isinstance(_input, clique.Collection): - frames = _input.format("{head}#{tail}").replace(" ", "") + if isinstance(input_item, clique.Collection): + frames = input_item.format("{head}#{tail}").replace(" ", "") oiio_cmd.extend([ - "--framepadding", _input.padding, + "--framepadding", input_item.padding, "--frames", frames, "--parallel-frames" ]) - _input: str = _input.format("{head}#{tail}") - elif not isinstance(_input, str): + input_item: str = input_item.format("{head}#{tail}") + elif not isinstance(input_item, str): raise TypeError( - f"Input is not a string or Collection: {_input}" + f"Input is not a string or Collection: {input_item}" ) oiio_cmd.extend([ - input_arg, _input, + input_arg, input_item, # Tell oiiotool which channels should be put to top stack # (and output) "--ch", channels_arg, @@ -772,7 +772,7 @@ def convert_input_paths_for_ffmpeg( oiio_cmd.extend(["--eraseattrib", attr_name]) # Add last argument - path to output - base_filename = os.path.basename(_input) + base_filename = os.path.basename(input_item) output_path = os.path.join(output_dir, base_filename) oiio_cmd.extend([ "-o", output_path From af901213a2fd31571fe3d06252eb20989fe2b6dc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 12 Dec 2025 10:31:54 +0100 Subject: [PATCH 38/55] Update client/ayon_core/lib/transcoding.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/transcoding.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index c37a0a88b7..e82905fe98 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1261,9 +1261,7 @@ def oiio_color_convert( ]) if parallel_frames: - oiio_cmd.extend([ - "--parallel-frames" - ]) + oiio_cmd.append("--parallel-frames") oiio_cmd.extend([ input_arg, input_path, From f9ca97ec714d261b6b1d1b94fa90f3b057bd57a7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 12 Dec 2025 10:32:54 +0100 Subject: [PATCH 39/55] Perform actual type hint --- 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 c37a0a88b7..965ab5e865 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1228,7 +1228,7 @@ def oiio_color_convert( # 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 + frames: str first_frame = int(re.split("[ x-]", frames, 1)[0]) first_frame = str(first_frame).zfill(frame_padding or 0) for token in ["#", "%d"]: From 2a7316b262cc54c0ee50e05819c64d8ab3b61cc1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 12 Dec 2025 10:40:10 +0100 Subject: [PATCH 40/55] Type hints to the arguments --- client/ayon_core/lib/transcoding.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index cc74964a2f..061a7bd398 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1173,10 +1173,10 @@ def oiio_color_convert( target_display=None, target_view=None, additional_command_args=None, - frames=None, - frame_padding=None, - parallel_frames=False, - logger=None, + frames: Optional[str] = None, + frame_padding: Optional[int] = None, + parallel_frames: bool = False, + logger: Optional[logging.Logger] = None, ): """Transcode source file to other with colormanagement. From 55eb4cccbe267af4f8560604e73a7214b13a276b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 12 Dec 2025 10:44:46 +0100 Subject: [PATCH 41/55] Add soft compatibility requirement to `ayon_third_party` `>=1.3.0` --- package.py | 1 + 1 file changed, 1 insertion(+) diff --git a/package.py b/package.py index 003c41f0f5..857b3f6906 100644 --- a/package.py +++ b/package.py @@ -12,6 +12,7 @@ ayon_server_version = ">=1.8.4,<2.0.0" ayon_launcher_version = ">=1.0.2" ayon_required_addons = {} ayon_compatible_addons = { + "ayon_third_party": ">=1.3.0", "ayon_ocio": ">=1.2.1", "applications": ">=1.1.2", "harmony": ">0.4.0", From 4d8d9078b8e94963edbcfb06585d8a45a78f9e9a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Dec 2025 10:56:39 +0100 Subject: [PATCH 42/55] Returned None IDK why, comment, must be really important though. --- client/ayon_core/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 447a24656d..d855c0d530 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -755,6 +755,6 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): host_name, product_type, product_name, task_name, task_type ) ) - return + return None return ThumbnailDef.from_dict(profile) From 3dbba063cae4a42f13064067b01b4683f10488bf Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Dec 2025 10:57:02 +0100 Subject: [PATCH 43/55] Renamed variables --- .../plugins/publish/extract_thumbnail.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index d855c0d530..242b5e3987 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -136,8 +136,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): if not self.profiles: self.log.debug("No profiles present for extract review thumbnail.") return - profile_config = self._get_config_from_profile(instance) - if not profile_config: + thumbnail_def = self._get_config_from_profile(instance) + if not thumbnail_def: return product_name = instance.data["productName"] @@ -217,7 +217,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): file_path = self._create_frame_from_video( video_file_path, dst_staging, - profile_config + thumbnail_def ) if file_path: src_staging, input_file = os.path.split(file_path) @@ -230,7 +230,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): if "slate-frame" in repre.get("tags", []): repre_files_thumb = repre_files_thumb[1:] file_index = int( - float(len(repre_files_thumb)) * profile_config.duration_split # noqa: E501 + float(len(repre_files_thumb)) * thumbnail_def.duration_split # noqa: E501 ) input_file = repre_files[file_index] @@ -268,13 +268,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # colorspace data if not repre_thumb_created: repre_thumb_created = self._create_thumbnail_ffmpeg( - full_input_path, full_output_path, profile_config + full_input_path, full_output_path, thumbnail_def ) # Skip representation and try next one if wasn't created if not repre_thumb_created and oiio_supported: repre_thumb_created = self._create_thumbnail_oiio( - full_input_path, full_output_path, profile_config + full_input_path, full_output_path, thumbnail_def ) if not repre_thumb_created: @@ -302,7 +302,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): new_repre_tags = ["thumbnail"] # for workflows which needs to have thumbnails published as # separate representations `delete` tag should not be added - if not profile_config.integrate_thumbnail: + if not thumbnail_def.integrate_thumbnail: new_repre_tags.append("delete") new_repre = { @@ -442,7 +442,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): str: path to created thumbnail """ self.log.info("Extracting thumbnail {}".format(dst_path)) - resolution_arg = self._get_resolution_arg( + resolution_arg = self._get_resolution_args( "oiiotool", src_path, profile_config ) @@ -500,7 +500,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): self.log.debug(f"Extracting thumbnail with OIIO: {dst_path}") try: - resolution_arg = self._get_resolution_arg( + resolution_arg = self._get_resolution_args( "oiiotool", src_path, profile_config ) except RuntimeError: @@ -544,7 +544,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): def _create_thumbnail_ffmpeg(self, src_path, dst_path, profile_config): try: - resolution_arg = self._get_resolution_arg( + resolution_arg = self._get_resolution_args( "ffmpeg", src_path, profile_config ) except RuntimeError: @@ -698,7 +698,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ): os.remove(output_thumb_file_path) - def _get_resolution_arg( + def _get_resolution_args( self, application, input_path, From 41fa48dbe726e2fcf24677db44a408c622c486bd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Dec 2025 10:57:19 +0100 Subject: [PATCH 44/55] Formatting change --- client/ayon_core/plugins/publish/extract_thumbnail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 242b5e3987..e51eda0da6 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -741,7 +741,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "task_types": task_type, } profile = filter_profiles( - self.profiles, filtering_criteria, + self.profiles, + filtering_criteria, logger=self.log ) From f03ae1bc156ab8e8ca3aafabbd9a2cdc90bd2380 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Dec 2025 10:59:02 +0100 Subject: [PATCH 45/55] Formatting change --- .../ayon_core/plugins/publish/extract_thumbnail.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index e51eda0da6..7376237f9b 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -748,13 +748,12 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): if not profile: self.log.debug( - ( - "Skipped instance. None of profiles in presets are for" - ' Host: "{}" | Product types: "{}" | Product names: "{}"' - ' | Task name "{}" | Task type "{}"' - ).format( - host_name, product_type, product_name, task_name, task_type - ) + "Skipped instance. None of profiles in presets are for" + f' Host: "{host_name}"' + f' | Product types: "{product_type}"' + f' | Product names: "{product_name}"' + f' | Task name "{task_name}"' + f' | Task type "{task_type}"' ) return None From 7d248880cc6cab7bac604c3b4b0f19436c67c333 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Dec 2025 11:01:12 +0100 Subject: [PATCH 46/55] Changed variable resolution --- server/settings/conversion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index 348518f0a3..45250fc9d9 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -161,9 +161,9 @@ def _convert_publish_plugins(overrides): def _convert_extract_thumbnail(overrides): """ExtractThumbnail config settings did change to profiles.""" extract_thumbnail_overrides = ( - overrides.get("publish", {}).get("ExtractThumbnail") or {} + overrides.get("publish", {}).get("ExtractThumbnail") ) - if "profiles" in extract_thumbnail_overrides: + if extract_thumbnail_overrides is None: return base_value = { From 73297259795e5132e7eed644086d6d415c61c2f8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Dec 2025 11:02:01 +0100 Subject: [PATCH 47/55] Used pop IDK why --- server/settings/conversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index 45250fc9d9..9da765e366 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -190,7 +190,7 @@ def _convert_extract_thumbnail(overrides): "ffmpeg_args", ): if key in extract_thumbnail_overrides: - base_value[key] = extract_thumbnail_overrides[key] + base_value[key] = extract_thumbnail_overrides.pop(key) extract_thumbnail_profiles = extract_thumbnail_overrides.setdefault( "profiles", [] From ea2642ab15692910c41bf1151e1f89cd56d0c722 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:21:06 +0100 Subject: [PATCH 48/55] added resolution and pixel aspect to version --- client/ayon_core/plugins/publish/integrate.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 9f24b35754..b37091e8d4 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -924,8 +924,12 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Include optional data if present in optionals = [ - "frameStart", "frameEnd", "step", - "handleEnd", "handleStart", "sourceHashes" + "frameStart", "frameEnd", + "handleEnd", "handleStart", + "resolutionWidth", "resolutionHeight", + "pixelAspect", + "step", + "sourceHashes" ] for key in optionals: if key in instance.data: From 15b0192d4effe4723a41ffeffe96440346aba5d2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:54:17 +0100 Subject: [PATCH 49/55] move step next to frames Co-authored-by: Roy Nieterau --- client/ayon_core/plugins/publish/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index b37091e8d4..5afdcbdf07 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -926,9 +926,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): optionals = [ "frameStart", "frameEnd", "handleEnd", "handleStart", + "step", "resolutionWidth", "resolutionHeight", "pixelAspect", - "step", "sourceHashes" ] for key in optionals: From d0034b60078ed20f04156d0432d593748690cfe9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:00:51 +0100 Subject: [PATCH 50/55] use 'skip_discovery' instead --- client/ayon_core/pipeline/create/creator_plugins.py | 8 +++++--- client/ayon_core/pipeline/plugin_discover.py | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index b8de1af691..16cd34d9b9 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -147,6 +147,8 @@ class BaseCreator(ABC): create_context (CreateContext): Context which initialized creator. headless (bool): Running in headless mode. """ + skip_discovery = True + # Label shown in UI label = None group_label = None @@ -642,7 +644,7 @@ class Creator(BaseCreator): Creation requires prepared product name and instance data. """ - is_base_class = True + skip_discovery = True # GUI Purposes # - default_variants may not be used if `get_default_variants` # is overridden @@ -931,7 +933,7 @@ class Creator(BaseCreator): class HiddenCreator(BaseCreator): - is_base_class = True + skip_discovery = True @abstractmethod def create(self, instance_data, source_data): @@ -943,7 +945,7 @@ class AutoCreator(BaseCreator): Can be used e.g. for `workfile`. """ - is_base_class = True + skip_discovery = True def remove_instances(self, instances): """Skip removal.""" diff --git a/client/ayon_core/pipeline/plugin_discover.py b/client/ayon_core/pipeline/plugin_discover.py index 896b7966d6..fd907eb22c 100644 --- a/client/ayon_core/pipeline/plugin_discover.py +++ b/client/ayon_core/pipeline/plugin_discover.py @@ -141,9 +141,9 @@ def discover_plugins( for cls in classes_from_module(base_class, module): if cls is base_class: continue - # Class has defined 'is_base_class = True' - is_base_class = cls.__dict__.get("is_base_class") - if is_base_class is True: + # Class has defined 'skip_discovery = True' + skip_discovery = cls.__dict__.get("skip_discovery") + if skip_discovery is True: continue all_plugins.append(cls) From 4faf61dd22d4712405b994684c24f0b0dbeea1c0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:01:04 +0100 Subject: [PATCH 51/55] add logic description --- client/ayon_core/pipeline/create/creator_plugins.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 16cd34d9b9..7b168984ef 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -146,7 +146,13 @@ class BaseCreator(ABC): project_settings (dict[str, Any]): Project settings. create_context (CreateContext): Context which initialized creator. headless (bool): Running in headless mode. + """ + # Attribute 'skip_discovery' is used during discovery phase to skip + # plugins, which can be used to mark base plugins that should not be + # considered as plugins "to use". The discovery logic does NOT use + # the attribute value from parent classes. Each base class has to define + # the attribute again. skip_discovery = True # Label shown in UI From 65791a1d9f0aec602fcf241175d4f2d3f1333d6d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:03:31 +0100 Subject: [PATCH 52/55] added 'skip_discovery' to loader plugin --- client/ayon_core/pipeline/load/plugins.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index ed963110c6..b8cca08802 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -21,6 +21,13 @@ from .utils import get_representation_path_from_context class LoaderPlugin(list): """Load representation into host application""" + # Attribute 'skip_discovery' is used during discovery phase to skip + # plugins, which can be used to mark base plugins that should not be + # considered as plugins "to use". The discovery logic does NOT use + # the attribute value from parent classes. Each base class has to define + # the attribute again. + skip_discovery = True + product_types: set[str] = set() product_base_types: Optional[set[str]] = None representations = set() From 52e4932c97e611561a466bfefa3969cf44e44f28 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Dec 2025 16:39:22 +0100 Subject: [PATCH 53/55] Used renamed class name as variable --- .../plugins/publish/extract_thumbnail.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 7376237f9b..ff3c77b79c 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -424,7 +424,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): src_path, dst_path, colorspace_data, - profile_config + thumbnail_def ): """Create thumbnail using OIIO tool oiiotool @@ -443,7 +443,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): """ self.log.info("Extracting thumbnail {}".format(dst_path)) resolution_arg = self._get_resolution_args( - "oiiotool", src_path, profile_config + "oiiotool", src_path, thumbnail_def ) repre_display = colorspace_data.get("display") @@ -463,8 +463,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ) # if representation doesn't have display and view then use # oiiotool_defaults - elif profile_config.oiiotool_defaults: - oiiotool_defaults = profile_config.oiiotool_defaults + elif thumbnail_def.oiiotool_defaults: + oiiotool_defaults = thumbnail_def.oiiotool_defaults oiio_default_type = oiiotool_defaults["type"] if "colorspace" == oiio_default_type: oiio_default_colorspace = oiiotool_defaults["colorspace"] @@ -496,12 +496,12 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return True - def _create_thumbnail_oiio(self, src_path, dst_path, profile_config): + def _create_thumbnail_oiio(self, src_path, dst_path, thumbnail_def): self.log.debug(f"Extracting thumbnail with OIIO: {dst_path}") try: resolution_arg = self._get_resolution_args( - "oiiotool", src_path, profile_config + "oiiotool", src_path, thumbnail_def ) except RuntimeError: self.log.warning( @@ -542,10 +542,10 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ) return False - def _create_thumbnail_ffmpeg(self, src_path, dst_path, profile_config): + def _create_thumbnail_ffmpeg(self, src_path, dst_path, thumbnail_def): try: resolution_arg = self._get_resolution_args( - "ffmpeg", src_path, profile_config + "ffmpeg", src_path, thumbnail_def ) except RuntimeError: self.log.warning( @@ -554,7 +554,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return False ffmpeg_path_args = get_ffmpeg_tool_args("ffmpeg") - ffmpeg_args = profile_config.ffmpeg_args or {} + ffmpeg_args = thumbnail_def.ffmpeg_args or {} jpeg_items = [ subprocess.list2cmdline(ffmpeg_path_args) @@ -598,7 +598,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): self, video_file_path, output_dir, - profile_config + thumbnail_def ): """Convert video file to one frame image via ffmpeg""" # create output file path @@ -624,7 +624,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): seek_position = 0.0 # Only use timestamp calculation for videos longer than 0.1 seconds if duration > 0.1: - seek_position = duration * profile_config.duration_split + seek_position = duration * thumbnail_def.duration_split # Build command args cmd_args = [] @@ -702,13 +702,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): self, application, input_path, - profile_config + thumbnail_def ): # get settings - if profile_config.target_size["type"] == "source": + if thumbnail_def.target_size["type"] == "source": return [] - resize = profile_config.target_size["resize"] + resize = thumbnail_def.target_size["resize"] target_width = resize["width"] target_height = resize["height"] @@ -718,7 +718,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): input_path, target_width, target_height, - bg_color=profile_config.background_color, + bg_color=thumbnail_def.background_color, log=self.log ) From 6f534f4ff0ca281b360edd8b5b382746a48f0e7b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 12 Dec 2025 22:41:28 +0100 Subject: [PATCH 54/55] Update client/ayon_core/plugins/publish/collect_scene_loaded_versions.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../ayon_core/plugins/publish/collect_scene_loaded_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py index 2c214cd1a7..54eeefc60b 100644 --- a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py +++ b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py @@ -99,7 +99,7 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): for container in containers: missing = [key for key in required_keys if key not in container] if missing: - self.log.debug( + self.log.warning( "Skipping invalid container, missing required keys:" " {}. {}".format(", ".join(missing), container) ) From b39e09142f8b5ac7aca323e717d1fef7667c19c5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Dec 2025 23:21:38 +0100 Subject: [PATCH 55/55] :recycle: change pytest-ayon dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b06f812b27..562bb72035 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ opentimelineio = "^0.17.0" speedcopy = "^2.1" qtpy="^2.4.3" pyside6 = "^6.5.2" -pytest-ayon = { git = "https://github.com/ynput/pytest-ayon.git", branch = "chore/align-dependencies" } +pytest-ayon = { git = "https://github.com/ynput/pytest-ayon.git", branch = "develop" } [tool.codespell] # Ignore words that are not in the dictionary.