From cb125a192f0728562d5e76d2b510370d65c4f1f8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 31 Mar 2025 23:14:17 +0200 Subject: [PATCH 01/74] 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/74] 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/74] 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/74] 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/74] 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/74] 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/74] 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/74] 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/74] 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/74] 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/74] 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/74] 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/74] 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/74] 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/74] 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/74] 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 07650130c601d9cbb5d3370bc5faaff54333bfbd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:11:24 +0200 Subject: [PATCH 17/74] initial support to use folder in product name template --- .../ayon_core/pipeline/create/product_name.py | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index ecffa4a340..58cf251f9d 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -1,14 +1,19 @@ +import warnings + import ayon_api from ayon_core.lib import ( StringTemplate, filter_profiles, prepare_template_data, + Logger, ) from ayon_core.settings import get_project_settings from .constants import DEFAULT_PRODUCT_TEMPLATE from .exceptions import TaskNotSetError, TemplateFillError +log = Logger.get_logger(__name__) + def get_product_name_template( project_name, @@ -81,6 +86,8 @@ def get_product_name( project_settings=None, product_type_filter=None, project_entity=None, + folder_entity=None, + task_entity=None, ): """Calculate product name based on passed context and AYON settings. @@ -98,8 +105,8 @@ def get_product_name( Args: project_name (str): Project name. - task_name (Union[str, None]): Task name. - task_type (Union[str, None]): Task type. + task_name (Union[str, None]): Task name. Deprecated use 'task_entity'. + task_type (Union[str, None]): Task type. Deprecated use 'task_entity'. host_name (str): Host name. product_type (str): Product type. variant (str): In most of the cases it is user input during creation. @@ -115,6 +122,8 @@ def get_product_name( not passed. project_entity (Optional[Dict[str, Any]]): Project entity used when task short name is required by template. + folder_entity (Optional[Dict[str, Any]]): Folder entity. + task_entity (Optional[Dict[str, Any]]): Task entity. Returns: str: Product name. @@ -139,17 +148,36 @@ def get_product_name( ) # Simple check of task name existence for template with {task} in # - missing task should be possible only in Standalone publisher - if not task_name and "{task" in template.lower(): + if task_name and not task_entity: + warnings.warn( + "Used deprecated 'task' argument. Please use" + " 'task_entity' instead.", + DeprecationWarning, + stacklevel=2 + ) + + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] + + template_low = template.lower() + if not task_name and "{task" in template_low: raise TaskNotSetError() task_value = { "name": task_name, "type": task_type, } - if "{task}" in template.lower(): + if "{task}" in template_low: task_value = task_name + # NOTE this is message for TDs and Admins -> not really for users + # TODO validate this in settings and not allow it + log.warning( + "Found deprecated task key '{task}' in product name template." + " Please use '{task[name]}' instead." + ) - elif "{task[short]}" in template.lower(): + elif "{task[short]}" in template_low: if project_entity is None: project_entity = ayon_api.get_project(project_name) task_types_by_name = { @@ -167,6 +195,12 @@ def get_product_name( "type": product_type } } + if folder_entity: + fill_pairs["folder"] = { + "name": folder_entity["name"], + "type": folder_entity["folderType"], + } + if dynamic_data: # Dynamic data may override default values for key, value in dynamic_data.items(): From fc7ca39f39465f5c70be3ac7a3f38b75c2e2967b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:13:19 +0200 Subject: [PATCH 18/74] move comment to correct place --- client/ayon_core/pipeline/create/product_name.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 58cf251f9d..d2d161a789 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -146,8 +146,6 @@ def get_product_name( default_template=default_template, project_settings=project_settings ) - # Simple check of task name existence for template with {task} in - # - missing task should be possible only in Standalone publisher if task_name and not task_entity: warnings.warn( "Used deprecated 'task' argument. Please use" @@ -161,6 +159,7 @@ def get_product_name( task_type = task_entity["taskType"] template_low = template.lower() + # Simple check of task name existence for template with {task[name]} in if not task_name and "{task" in template_low: raise TaskNotSetError() From f7e9f6e7c9f7914d623bdbb874b73f4818f0e7bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:17:51 +0200 Subject: [PATCH 19/74] use kwargs in default implementation --- client/ayon_core/pipeline/create/creator_plugins.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 7573589b82..56fa431090 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -566,14 +566,16 @@ class BaseCreator(ABC): return get_product_name( project_name, - task_name, - task_type, - host_name, - self.product_type, - variant, + folder_entity=folder_entity, + task_entity=task_entity, + host_name=host_name, + product_type=self.product_type, + variant=variant, dynamic_data=dynamic_data, project_settings=self.project_settings, project_entity=project_entity, + task_name=task_name, + task_type=task_type, ) def get_instance_attr_defs(self): From 348e11f9680bd7c754ac04854c9d162471f48bca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:40:12 +0200 Subject: [PATCH 20/74] wrap get_product_name function --- .../ayon_core/pipeline/create/product_name.py | 309 +++++++++++++----- 1 file changed, 235 insertions(+), 74 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index d2d161a789..1b22ff4523 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -1,4 +1,8 @@ +from __future__ import annotations + import warnings +from functools import wraps +from typing import Optional, Any import ayon_api from ayon_core.lib import ( @@ -6,7 +10,9 @@ from ayon_core.lib import ( filter_profiles, prepare_template_data, Logger, + is_func_signature_supported, ) +from ayon_core.lib.path_templates import TemplateResult from ayon_core.settings import get_project_settings from .constants import DEFAULT_PRODUCT_TEMPLATE @@ -74,68 +80,27 @@ def get_product_name_template( return template -def get_product_name( - project_name, - task_name, - task_type, - host_name, - product_type, - variant, - default_template=None, - dynamic_data=None, - project_settings=None, - product_type_filter=None, - project_entity=None, - folder_entity=None, - task_entity=None, -): - """Calculate product name based on passed context and AYON settings. - - Subst name templates are defined in `project_settings/global/tools/creator - /product_name_profiles` where are profiles with host name, product type, - task name and task type filters. If context does not match any profile - then `DEFAULT_PRODUCT_TEMPLATE` is used as default template. - - That's main reason why so many arguments are required to calculate product - name. - - Todos: - Find better filtering options to avoid requirement of - argument 'family_filter'. - - Args: - project_name (str): Project name. - task_name (Union[str, None]): Task name. Deprecated use 'task_entity'. - task_type (Union[str, None]): Task type. Deprecated use 'task_entity'. - host_name (str): Host name. - product_type (str): Product type. - variant (str): In most of the cases it is user input during creation. - default_template (Optional[str]): Default template if any profile does - not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE' - is used if is not passed. - dynamic_data (Optional[Dict[str, Any]]): Dynamic data specific for - a creator which creates instance. - project_settings (Optional[Union[Dict[str, Any]]]): Prepared settings - for project. Settings are queried if not passed. - product_type_filter (Optional[str]): Use different product type for - product template filtering. Value of `product_type` is used when - not passed. - project_entity (Optional[Dict[str, Any]]): Project entity used when - task short name is required by template. - folder_entity (Optional[Dict[str, Any]]): Folder entity. - task_entity (Optional[Dict[str, Any]]): Task entity. - - Returns: - str: Product name. - - Raises: - TaskNotSetError: If template requires task which is not provided. - TemplateFillError: If filled template contains placeholder key which - is not collected. - - """ +def _get_product_name_old( + project_name: str, + task_name: Optional[str], + task_type: Optional[str], + host_name: str, + product_type: str, + variant: str, + default_template: Optional[str] = None, + dynamic_data: Optional[dict[str, Any]] = None, + project_settings: Optional[dict[str, Any]] = None, + product_type_filter: Optional[str] = None, + project_entity: Optional[dict[str, Any]] = None, +) -> TemplateResult: + warnings.warn( + "Used deprecated 'task_name' and 'task_type' arguments." + " Please use new signature with 'folder_entity' and 'task_entity'.", + DeprecationWarning, + stacklevel=2 + ) if not product_type: - return "" + return StringTemplate("").format({}) template = get_product_name_template( project_name, @@ -146,17 +111,6 @@ def get_product_name( default_template=default_template, project_settings=project_settings ) - if task_name and not task_entity: - warnings.warn( - "Used deprecated 'task' argument. Please use" - " 'task_entity' instead.", - DeprecationWarning, - stacklevel=2 - ) - - if task_entity: - task_name = task_entity["name"] - task_type = task_entity["taskType"] template_low = template.lower() # Simple check of task name existence for template with {task[name]} in @@ -194,6 +148,106 @@ def get_product_name( "type": product_type } } + + if dynamic_data: + # Dynamic data may override default values + for key, value in dynamic_data.items(): + fill_pairs[key] = value + + try: + return StringTemplate.format_strict_template( + template=template, + data=prepare_template_data(fill_pairs) + ) + except KeyError as exp: + raise TemplateFillError( + "Value for {} key is missing in template '{}'." + " Available values are {}".format(str(exp), template, fill_pairs) + ) + + +def _get_product_name( + project_name: str, + folder_entity: dict[str, Any], + task_entity: Optional[dict[str, Any]], + host_name: str, + product_type: str, + variant: str, + *, + default_template: Optional[str] = None, + dynamic_data: Optional[dict[str, Any]] = None, + project_settings: Optional[dict[str, Any]] = None, + product_type_filter: Optional[str] = None, + project_entity: Optional[dict[str, Any]] = None, + # Ignore unused kwargs passed to 'get_product_name' + task_name: Optional[str] = None, + task_type: Optional[str] = None, +) -> TemplateResult: + """Future replacement of 'get_product_name' function.""" + # Future warning when 'task_name' and 'task_type' are deprecated + # if task_name is None: + # warnings.warn( + # "Still using deprecated 'task_name' argument. Please use" + # " 'task_entity' only.", + # DeprecationWarning, + # stacklevel=2 + # ) + + if not product_type: + return StringTemplate("").format({}) + + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] + + template = get_product_name_template( + project_name, + product_type_filter or product_type, + task_name, + task_type, + host_name, + default_template=default_template, + project_settings=project_settings + ) + + template_low = template.lower() + # Simple check of task name existence for template with {task[name]} in + if not task_name and "{task" in template_low: + raise TaskNotSetError() + + task_value = { + "name": task_name, + "type": task_type, + } + if "{task}" in template_low: + task_value = task_name + # NOTE this is message for TDs and Admins -> not really for users + # TODO validate this in settings and not allow it + log.warning( + "Found deprecated task key '{task}' in product name template." + " Please use '{task[name]}' instead." + ) + + elif "{task[short]}" in template_low: + if project_entity is None: + project_entity = ayon_api.get_project(project_name) + task_types_by_name = { + task["name"]: task for task in + project_entity["taskTypes"] + } + task_short = task_types_by_name.get(task_type, {}).get("shortName") + task_value["short"] = task_short + + fill_pairs = { + "variant": variant, + # TODO We should stop support 'family' key. + "family": product_type, + "task": task_value, + "product": { + "type": product_type + } + } if folder_entity: fill_pairs["folder"] = { "name": folder_entity["name"], @@ -212,6 +266,113 @@ def get_product_name( ) except KeyError as exp: raise TemplateFillError( - "Value for {} key is missing in template '{}'." - " Available values are {}".format(str(exp), template, fill_pairs) + f"Value for {exp} key is missing in template '{template}'." + f" Available values are {fill_pairs}" ) + + +def _get_product_name_decorator(func): + """Helper to decide which variant of 'get_product_name' to use. + + The old version expected 'task_name' and 'task_type' arguments. The new + version expects 'folder_entity' and 'task_entity' arguments instead. + """ + @wraps(_get_product_name) + def inner(*args, **kwargs): + # --- + # Decide which variant of the function is used based on + # passed arguments. + # --- + + # Entities in key-word arguments mean that the new function is used + if "folder_entity" in kwargs or "task_entity" in kwargs: + return func(*args, **kwargs) + + # Using more than 6 positional arguments is not allowed + # in the new function + if len(args) > 6: + return func(*args, **kwargs) + + if len(args) > 1: + arg_2 = args[1] + # Second argument is dictionary -> folder entity + if isinstance(arg_2, dict): + return func(*args, **kwargs) + + if is_func_signature_supported(func, *args, **kwargs): + return func(*args, **kwargs) + return _get_product_name_old(*args, **kwargs) + + return inner + + +def get_product_name( + project_name: str, + folder_entity: dict[str, Any], + task_entity: Optional[dict[str, Any]], + host_name: str, + product_type: str, + variant: str, + *, + default_template: Optional[str] = None, + dynamic_data: Optional[dict[str, Any]] = None, + project_settings: Optional[dict[str, Any]] = None, + product_type_filter: Optional[str] = None, + project_entity: Optional[dict[str, Any]] = None, +) -> TemplateResult: + """Calculate product name based on passed context and AYON settings. + + Subst name templates are defined in `project_settings/global/tools/creator + /product_name_profiles` where are profiles with host name, product type, + task name and task type filters. If context does not match any profile + then `DEFAULT_PRODUCT_TEMPLATE` is used as default template. + + That's main reason why so many arguments are required to calculate product + name. + + Todos: + Find better filtering options to avoid requirement of + argument 'family_filter'. + + Args: + project_name (str): Project name. + folder_entity (Optional[Dict[str, Any]]): Folder entity. + task_entity (Optional[Dict[str, Any]]): Task entity. + host_name (str): Host name. + product_type (str): Product type. + variant (str): In most of the cases it is user input during creation. + default_template (Optional[str]): Default template if any profile does + not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE' + is used if is not passed. + dynamic_data (Optional[Dict[str, Any]]): Dynamic data specific for + a creator which creates instance. + project_settings (Optional[Union[Dict[str, Any]]]): Prepared settings + for project. Settings are queried if not passed. + product_type_filter (Optional[str]): Use different product type for + product template filtering. Value of `product_type` is used when + not passed. + project_entity (Optional[Dict[str, Any]]): Project entity used when + task short name is required by template. + + Returns: + TemplateResult: Product name. + + Raises: + TaskNotSetError: If template requires task which is not provided. + TemplateFillError: If filled template contains placeholder key which + is not collected. + + """ + return _get_product_name( + project_name, + folder_entity, + task_entity, + host_name, + product_type, + variant, + default_template=default_template, + dynamic_data=dynamic_data, + project_settings=project_settings, + product_type_filter=product_type_filter, + project_entity=project_entity, + ) From 31b023b0fac2452af6bd3bc78d977d03ec802441 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:47:14 +0200 Subject: [PATCH 21/74] use only new signature --- client/ayon_core/pipeline/create/creator_plugins.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 56fa431090..931b33afd4 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -546,11 +546,6 @@ class BaseCreator(ABC): if host_name is None: host_name = self.create_context.host_name - task_name = task_type = None - if task_entity: - task_name = task_entity["name"] - task_type = task_entity["taskType"] - dynamic_data = self.get_dynamic_data( project_name, folder_entity, @@ -574,8 +569,6 @@ class BaseCreator(ABC): dynamic_data=dynamic_data, project_settings=self.project_settings, project_entity=project_entity, - task_name=task_name, - task_type=task_type, ) def get_instance_attr_defs(self): From 16b45846094c5616f90412d7c4130f3767839d59 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:50:57 +0200 Subject: [PATCH 22/74] mark the function with an attribute to know if entities are expected in arguments --- client/ayon_core/pipeline/create/product_name.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 1b22ff4523..f4ec4199d5 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -277,6 +277,11 @@ def _get_product_name_decorator(func): The old version expected 'task_name' and 'task_type' arguments. The new version expects 'folder_entity' and 'task_entity' arguments instead. """ + # Add attribute to function to identify it as the new function + # so other addons can easily identify it. + # >>> geattr(get_product_name, "use_entities", False) + func.use_entities = True + @wraps(_get_product_name) def inner(*args, **kwargs): # --- From a35b179ed1122f435bb5c83b509c0f957f2a4bcf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:59:46 +0200 Subject: [PATCH 23/74] remove the private variant of the function --- .../ayon_core/pipeline/create/product_name.py | 209 +++++++----------- 1 file changed, 84 insertions(+), 125 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index f4ec4199d5..687d152e89 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -166,7 +166,48 @@ def _get_product_name_old( ) -def _get_product_name( +def _get_product_name_decorator(func): + """Helper to decide which variant of 'get_product_name' to use. + + The old version expected 'task_name' and 'task_type' arguments. The new + version expects 'folder_entity' and 'task_entity' arguments instead. + """ + # Add attribute to function to identify it as the new function + # so other addons can easily identify it. + # >>> geattr(get_product_name, "use_entities", False) + func.use_entities = True + + @wraps(func) + def inner(*args, **kwargs): + # --- + # Decide which variant of the function is used based on + # passed arguments. + # --- + + # Entities in key-word arguments mean that the new function is used + if "folder_entity" in kwargs or "task_entity" in kwargs: + return func(*args, **kwargs) + + # Using more than 6 positional arguments is not allowed + # in the new function + if len(args) > 6: + return _get_product_name_old(*args, **kwargs) + + if len(args) > 1: + arg_2 = args[1] + # The second argument is a string -> task name + if isinstance(arg_2, str): + return _get_product_name_old(*args, **kwargs) + + if is_func_signature_supported(func, *args, **kwargs): + return func(*args, **kwargs) + return _get_product_name_old(*args, **kwargs) + + return inner + + +@_get_product_name_decorator +def get_product_name( project_name: str, folder_entity: dict[str, Any], task_entity: Optional[dict[str, Any]], @@ -179,20 +220,50 @@ def _get_product_name( project_settings: Optional[dict[str, Any]] = None, product_type_filter: Optional[str] = None, project_entity: Optional[dict[str, Any]] = None, - # Ignore unused kwargs passed to 'get_product_name' - task_name: Optional[str] = None, - task_type: Optional[str] = None, ) -> TemplateResult: - """Future replacement of 'get_product_name' function.""" - # Future warning when 'task_name' and 'task_type' are deprecated - # if task_name is None: - # warnings.warn( - # "Still using deprecated 'task_name' argument. Please use" - # " 'task_entity' only.", - # DeprecationWarning, - # stacklevel=2 - # ) + """Calculate product name based on passed context and AYON settings. + Subst name templates are defined in `project_settings/global/tools/creator + /product_name_profiles` where are profiles with host name, product type, + task name and task type filters. If context does not match any profile + then `DEFAULT_PRODUCT_TEMPLATE` is used as default template. + + That's main reason why so many arguments are required to calculate product + name. + + Todos: + Find better filtering options to avoid requirement of + argument 'family_filter'. + + Args: + project_name (str): Project name. + folder_entity (Optional[Dict[str, Any]]): Folder entity. + task_entity (Optional[Dict[str, Any]]): Task entity. + host_name (str): Host name. + product_type (str): Product type. + variant (str): In most of the cases it is user input during creation. + default_template (Optional[str]): Default template if any profile does + not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE' + is used if is not passed. + dynamic_data (Optional[Dict[str, Any]]): Dynamic data specific for + a creator which creates instance. + project_settings (Optional[Union[Dict[str, Any]]]): Prepared settings + for project. Settings are queried if not passed. + product_type_filter (Optional[str]): Use different product type for + product template filtering. Value of `product_type` is used when + not passed. + project_entity (Optional[Dict[str, Any]]): Project entity used when + task short name is required by template. + + Returns: + TemplateResult: Product name. + + Raises: + TaskNotSetError: If template requires task which is not provided. + TemplateFillError: If filled template contains placeholder key which + is not collected. + + """ if not product_type: return StringTemplate("").format({}) @@ -269,115 +340,3 @@ def _get_product_name( f"Value for {exp} key is missing in template '{template}'." f" Available values are {fill_pairs}" ) - - -def _get_product_name_decorator(func): - """Helper to decide which variant of 'get_product_name' to use. - - The old version expected 'task_name' and 'task_type' arguments. The new - version expects 'folder_entity' and 'task_entity' arguments instead. - """ - # Add attribute to function to identify it as the new function - # so other addons can easily identify it. - # >>> geattr(get_product_name, "use_entities", False) - func.use_entities = True - - @wraps(_get_product_name) - def inner(*args, **kwargs): - # --- - # Decide which variant of the function is used based on - # passed arguments. - # --- - - # Entities in key-word arguments mean that the new function is used - if "folder_entity" in kwargs or "task_entity" in kwargs: - return func(*args, **kwargs) - - # Using more than 6 positional arguments is not allowed - # in the new function - if len(args) > 6: - return func(*args, **kwargs) - - if len(args) > 1: - arg_2 = args[1] - # Second argument is dictionary -> folder entity - if isinstance(arg_2, dict): - return func(*args, **kwargs) - - if is_func_signature_supported(func, *args, **kwargs): - return func(*args, **kwargs) - return _get_product_name_old(*args, **kwargs) - - return inner - - -def get_product_name( - project_name: str, - folder_entity: dict[str, Any], - task_entity: Optional[dict[str, Any]], - host_name: str, - product_type: str, - variant: str, - *, - default_template: Optional[str] = None, - dynamic_data: Optional[dict[str, Any]] = None, - project_settings: Optional[dict[str, Any]] = None, - product_type_filter: Optional[str] = None, - project_entity: Optional[dict[str, Any]] = None, -) -> TemplateResult: - """Calculate product name based on passed context and AYON settings. - - Subst name templates are defined in `project_settings/global/tools/creator - /product_name_profiles` where are profiles with host name, product type, - task name and task type filters. If context does not match any profile - then `DEFAULT_PRODUCT_TEMPLATE` is used as default template. - - That's main reason why so many arguments are required to calculate product - name. - - Todos: - Find better filtering options to avoid requirement of - argument 'family_filter'. - - Args: - project_name (str): Project name. - folder_entity (Optional[Dict[str, Any]]): Folder entity. - task_entity (Optional[Dict[str, Any]]): Task entity. - host_name (str): Host name. - product_type (str): Product type. - variant (str): In most of the cases it is user input during creation. - default_template (Optional[str]): Default template if any profile does - not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE' - is used if is not passed. - dynamic_data (Optional[Dict[str, Any]]): Dynamic data specific for - a creator which creates instance. - project_settings (Optional[Union[Dict[str, Any]]]): Prepared settings - for project. Settings are queried if not passed. - product_type_filter (Optional[str]): Use different product type for - product template filtering. Value of `product_type` is used when - not passed. - project_entity (Optional[Dict[str, Any]]): Project entity used when - task short name is required by template. - - Returns: - TemplateResult: Product name. - - Raises: - TaskNotSetError: If template requires task which is not provided. - TemplateFillError: If filled template contains placeholder key which - is not collected. - - """ - return _get_product_name( - project_name, - folder_entity, - task_entity, - host_name, - product_type, - variant, - default_template=default_template, - dynamic_data=dynamic_data, - project_settings=project_settings, - product_type_filter=product_type_filter, - project_entity=project_entity, - ) From 5fd5b73e913eb45b2810b3ba7d63531d20758362 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:05:19 +0200 Subject: [PATCH 24/74] fix type hints --- client/ayon_core/pipeline/create/product_name.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 687d152e89..ede3141537 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -39,10 +39,10 @@ def get_product_name_template( host_name (str): Name of host in which the product name is calculated. task_name (str): Name of task in which context the product is created. task_type (str): Type of task in which context the product is created. - default_template (Union[str, None]): Default template which is used if + default_template (Optional[str]): Default template which is used if settings won't find any matching possibility. Constant 'DEFAULT_PRODUCT_TEMPLATE' is used if not defined. - project_settings (Union[Dict[str, Any], None]): Prepared settings for + project_settings (Optional[dict[str, Any]]): Prepared settings for project. Settings are queried if not passed. """ @@ -237,22 +237,22 @@ def get_product_name( Args: project_name (str): Project name. - folder_entity (Optional[Dict[str, Any]]): Folder entity. - task_entity (Optional[Dict[str, Any]]): Task entity. + folder_entity (Optional[dict[str, Any]]): Folder entity. + task_entity (Optional[dict[str, Any]]): Task entity. host_name (str): Host name. product_type (str): Product type. variant (str): In most of the cases it is user input during creation. default_template (Optional[str]): Default template if any profile does not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE' is used if is not passed. - dynamic_data (Optional[Dict[str, Any]]): Dynamic data specific for + dynamic_data (Optional[dict[str, Any]]): Dynamic data specific for a creator which creates instance. - project_settings (Optional[Union[Dict[str, Any]]]): Prepared settings + project_settings (Optional[dict[str, Any]]): Prepared settings for project. Settings are queried if not passed. product_type_filter (Optional[str]): Use different product type for product template filtering. Value of `product_type` is used when not passed. - project_entity (Optional[Dict[str, Any]]): Project entity used when + project_entity (Optional[dict[str, Any]]): Project entity used when task short name is required by template. Returns: From 882c0bcc6aed066026e67bfe1b4c211038c08576 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 20 Oct 2025 14:58:26 +0200 Subject: [PATCH 25/74] rename decorator and add more information to the example --- client/ayon_core/pipeline/create/product_name.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index ede3141537..45b77d1a95 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -166,11 +166,21 @@ def _get_product_name_old( ) -def _get_product_name_decorator(func): +def _backwards_compatibility_product_name(func): """Helper to decide which variant of 'get_product_name' to use. The old version expected 'task_name' and 'task_type' arguments. The new version expects 'folder_entity' and 'task_entity' arguments instead. + + The function is also marked with an attribute 'version' so other addons + can check if the function is using the new signature or is using + the old signature. That should allow addons to adapt to new signature. + >>> if getattr(get_product_name, "use_entities", None): + >>> # New signature is used + >>> path = get_product_name(project_name, folder_entity, ...) + >>> else: + >>> # Old signature is used + >>> path = get_product_name(project_name, taks_name, ...) """ # Add attribute to function to identify it as the new function # so other addons can easily identify it. @@ -206,7 +216,7 @@ def _get_product_name_decorator(func): return inner -@_get_product_name_decorator +@_backwards_compatibility_product_name def get_product_name( project_name: str, folder_entity: dict[str, Any], From d7433f84d796abb04d0a0aed5fa3eee134ecaf02 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 20 Oct 2025 14:58:34 +0200 Subject: [PATCH 26/74] use setattr --- client/ayon_core/pipeline/create/product_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 45b77d1a95..ee07f939bc 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -185,7 +185,7 @@ def _backwards_compatibility_product_name(func): # Add attribute to function to identify it as the new function # so other addons can easily identify it. # >>> geattr(get_product_name, "use_entities", False) - func.use_entities = True + setattr(func, "use_entities", True) @wraps(func) def inner(*args, **kwargs): From d6431a49908f3bc5bd14b39f2c0c18ce6f7e3137 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:17:13 +0100 Subject: [PATCH 27/74] added overload functionality --- .../ayon_core/pipeline/create/product_name.py | 117 +++++++++++++++++- 1 file changed, 112 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index ee07f939bc..a85b12f0df 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -2,7 +2,7 @@ from __future__ import annotations import warnings from functools import wraps -from typing import Optional, Any +from typing import Optional, Any, overload import ayon_api from ayon_core.lib import ( @@ -216,7 +216,7 @@ def _backwards_compatibility_product_name(func): return inner -@_backwards_compatibility_product_name +@overload def get_product_name( project_name: str, folder_entity: dict[str, Any], @@ -241,9 +241,116 @@ def get_product_name( That's main reason why so many arguments are required to calculate product name. - Todos: - Find better filtering options to avoid requirement of - argument 'family_filter'. + Args: + project_name (str): Project name. + folder_entity (Optional[dict[str, Any]]): Folder entity. + task_entity (Optional[dict[str, Any]]): Task entity. + host_name (str): Host name. + product_type (str): Product type. + variant (str): In most of the cases it is user input during creation. + default_template (Optional[str]): Default template if any profile does + not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE' + is used if is not passed. + dynamic_data (Optional[dict[str, Any]]): Dynamic data specific for + a creator which creates instance. + project_settings (Optional[dict[str, Any]]): Prepared settings + for project. Settings are queried if not passed. + product_type_filter (Optional[str]): Use different product type for + product template filtering. Value of `product_type` is used when + not passed. + project_entity (Optional[dict[str, Any]]): Project entity used when + task short name is required by template. + + Returns: + TemplateResult: Product name. + + Raises: + TaskNotSetError: If template requires task which is not provided. + TemplateFillError: If filled template contains placeholder key which + is not collected. + + """ + + +@overload +def get_product_name( + project_name, + task_name, + task_type, + host_name, + product_type, + variant, + default_template=None, + dynamic_data=None, + project_settings=None, + product_type_filter=None, + project_entity=None, +) -> TemplateResult: + """Calculate product name based on passed context and AYON settings. + + Product name templates are defined in `project_settings/global/tools + /creator/product_name_profiles` where are profiles with host name, + product type, task name and task type filters. If context does not match + any profile then `DEFAULT_PRODUCT_TEMPLATE` is used as default template. + + That's main reason why so many arguments are required to calculate product + name. + + Deprecated: + This function is using deprecate signature that does not support + folder entity data to be used. + + Args: + project_name (str): Project name. + task_name (Optional[str]): Task name. + task_type (Optional[str]): Task type. + host_name (str): Host name. + product_type (str): Product type. + variant (str): In most of the cases it is user input during creation. + default_template (Optional[str]): Default template if any profile does + not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE' + is used if is not passed. + dynamic_data (Optional[Dict[str, Any]]): Dynamic data specific for + a creator which creates instance. + project_settings (Optional[Union[Dict[str, Any]]]): Prepared settings + for project. Settings are queried if not passed. + product_type_filter (Optional[str]): Use different product type for + product template filtering. Value of `product_type` is used when + not passed. + project_entity (Optional[Dict[str, Any]]): Project entity used when + task short name is required by template. + + Returns: + TemplateResult: Product name. + + """ + pass + + +@_backwards_compatibility_product_name +def get_product_name( + project_name: str, + folder_entity: dict[str, Any], + task_entity: Optional[dict[str, Any]], + host_name: str, + product_type: str, + variant: str, + *, + default_template: Optional[str] = None, + dynamic_data: Optional[dict[str, Any]] = None, + project_settings: Optional[dict[str, Any]] = None, + product_type_filter: Optional[str] = None, + project_entity: Optional[dict[str, Any]] = None, +) -> TemplateResult: + """Calculate product name based on passed context and AYON settings. + + Product name templates are defined in `project_settings/global/tools/creator + /product_name_profiles` where are profiles with host name, product type, + task name and task type filters. If context does not match any profile + then `DEFAULT_PRODUCT_TEMPLATE` is used as default template. + + That's main reason why so many arguments are required to calculate product + name. Args: project_name (str): Project name. 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 28/74] 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 29/74] 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 30/74] 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 31/74] 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 32/74] 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 33/74] 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 34/74] 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 35/74] 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 36/74] 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 37/74] 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 cf28f96eda987207bdaa6161d2cfd19d46aad264 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:49:11 +0100 Subject: [PATCH 38/74] fix formatting in docstring --- client/ayon_core/pipeline/create/product_name.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index b0bb2d3430..89ae7ef85b 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -367,13 +367,15 @@ def get_product_name( ) -> TemplateResult: """Calculate product name based on passed context and AYON settings. - Product name templates are defined in `project_settings/global/tools/creator - /product_name_profiles` where are profiles with host name, product type, - task name and task type filters. If context does not match any profile - then `DEFAULT_PRODUCT_TEMPLATE` is used as default template. + Product name templates are defined in `project_settings/global/tools + /creator/product_name_profiles` where are profiles with host name, + product base type, product type, task name and task type filters. + + If context does not match any profile then `DEFAULT_PRODUCT_TEMPLATE` + is used as default template. That's main reason why so many arguments are required to calculate product - name. + name. Args: project_name (str): Project name. From 89129dfeb4046cb49d47c7ca9928c0eb2993033f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Dec 2025 10:10:51 +0100 Subject: [PATCH 39/74] 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 40/74] 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 41/74] 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 42/74] 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 43/74] 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 44/74] 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 45/74] 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 f0e603fe7c73d18910e3a0fb2e708a60e9b284ac Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Dec 2025 00:30:16 +0100 Subject: [PATCH 46/74] Do not invert source display/view if it already matches target display/view --- client/ayon_core/lib/transcoding.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index b4a3e77f5a..1f9005d92b 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1234,17 +1234,21 @@ def oiio_color_convert( if source_view and source_display: color_convert_args = None ocio_display_args = None - oiio_cmd.extend([ - "--ociodisplay:inverse=1:subimages=0", - source_display, - source_view, - ]) + + if source_display != target_display or source_view != target_view: + # Undo source display/view if we have a source display/view + # that does not match the target display/view + oiio_cmd.extend([ + "--ociodisplay:inverse=1:subimages=0", + source_display, + source_view, + ]) if target_colorspace: # This is a two-step conversion process since there's no direct # display/view to colorspace command # This could be a config parameter or determined from OCIO config - # Use temporarty role space 'scene_linear' + # Use temporary role space 'scene_linear' color_convert_args = ("scene_linear", target_colorspace) elif source_display != target_display or source_view != target_view: # Complete display/view pair conversion From b1be956994acd10e1c0c52103e79a33187ac06e7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Dec 2025 00:38:49 +0100 Subject: [PATCH 47/74] Also invert if target_colorspace, which means - always invert source display/view if we have any target colorspace or a display/view that differs from the source display/view --- client/ayon_core/lib/transcoding.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 1f9005d92b..f1c1cd7aa6 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1234,16 +1234,6 @@ def oiio_color_convert( if source_view and source_display: color_convert_args = None ocio_display_args = None - - if source_display != target_display or source_view != target_view: - # Undo source display/view if we have a source display/view - # that does not match the target display/view - oiio_cmd.extend([ - "--ociodisplay:inverse=1:subimages=0", - source_display, - source_view, - ]) - if target_colorspace: # This is a two-step conversion process since there's no direct # display/view to colorspace command @@ -1260,6 +1250,15 @@ def oiio_color_convert( " No color conversion needed." ) + if color_convert_args or ocio_display_args: + # Invert source display/view so that we can go from there to the + # target colorspace or display/view + oiio_cmd.extend([ + "--ociodisplay:inverse=1:subimages=0", + source_display, + source_view, + ]) + if color_convert_args: # Use colorconvert for colorspace target oiio_cmd.extend([ From d4e5f96b3b49d7a0e2bf2041d67657e24005a392 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:46:48 +0100 Subject: [PATCH 48/74] upodate overload function --- .../ayon_core/pipeline/create/product_name.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 89ae7ef85b..2b1255c2b3 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -243,15 +243,16 @@ def get_product_name( project_name: str, folder_entity: dict[str, Any], task_entity: Optional[dict[str, Any]], - host_name: str, + product_base_type: str, product_type: str, + host_name: str, variant: str, *, - default_template: Optional[str] = None, dynamic_data: Optional[dict[str, Any]] = None, project_settings: Optional[dict[str, Any]] = None, - product_type_filter: Optional[str] = None, project_entity: Optional[dict[str, Any]] = None, + default_template: Optional[str] = None, + product_base_type_filter: Optional[str] = None, ) -> TemplateResult: """Calculate product name based on passed context and AYON settings. @@ -268,20 +269,21 @@ def get_product_name( folder_entity (Optional[dict[str, Any]]): Folder entity. task_entity (Optional[dict[str, Any]]): Task entity. host_name (str): Host name. + product_base_type (str): Product base type. product_type (str): Product type. variant (str): In most of the cases it is user input during creation. - default_template (Optional[str]): Default template if any profile does - not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE' - is used if is not passed. dynamic_data (Optional[dict[str, Any]]): Dynamic data specific for a creator which creates instance. project_settings (Optional[dict[str, Any]]): Prepared settings for project. Settings are queried if not passed. - product_type_filter (Optional[str]): Use different product type for - product template filtering. Value of `product_type` is used when - not passed. project_entity (Optional[dict[str, Any]]): Project entity used when task short name is required by template. + default_template (Optional[str]): Default template if any profile does + not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE' + is used if is not passed. + product_base_type_filter (Optional[str]): Use different product base + type for product template filtering. Value of + `product_base_type_filter` is used when not passed. Returns: TemplateResult: Product name. From bceb645a80da8a2f671d42e2a8d6b5feaea42b5a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:47:14 +0100 Subject: [PATCH 49/74] fix typo Co-authored-by: Roy Nieterau --- client/ayon_core/pipeline/create/product_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 2b1255c2b3..a0bfc18eba 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -321,7 +321,7 @@ def get_product_name( name. Deprecated: - This function is using deprecate signature that does not support + This function is using deprecated signature that does not support folder entity data to be used. Args: From 17b09d608bfc33b6bfa7904adfb9abbf3d7b3df8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:03:30 +0100 Subject: [PATCH 50/74] unify indentation --- client/ayon_core/pipeline/create/product_name.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index a0bfc18eba..d32de54774 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -370,8 +370,8 @@ def get_product_name( """Calculate product name based on passed context and AYON settings. Product name templates are defined in `project_settings/global/tools - /creator/product_name_profiles` where are profiles with host name, - product base type, product type, task name and task type filters. + /creator/product_name_profiles` where are profiles with host name, + product base type, product type, task name and task type filters. If context does not match any profile then `DEFAULT_PRODUCT_TEMPLATE` is used as default template. From aff0ecf4362982640cc60c3921885f9314970fe8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Dec 2025 20:29:10 +0100 Subject: [PATCH 51/74] Fix #1598: Do not fallback to current task name --- client/ayon_core/tools/publisher/widgets/create_widget.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index d98bc95eb2..f2afdfffd9 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -328,9 +328,6 @@ class CreateWidget(QtWidgets.QWidget): folder_path = self._context_widget.get_selected_folder_path() if folder_path: task_name = self._context_widget.get_selected_task_name() - - if not task_name: - task_name = self.get_current_task_name() return task_name def _set_context_enabled(self, enabled): From e3206796a764e2adf74a1097acf7780f91836863 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Dec 2025 22:16:03 +0100 Subject: [PATCH 52/74] 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 53/74] 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 b6709f98590841cc80a616635c7f9c4f77f1ed1f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 11 Dec 2025 15:36:45 +0100 Subject: [PATCH 54/74] Also remove fallback for current folder in `_get_folder_path` --- client/ayon_core/tools/publisher/widgets/create_widget.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index f2afdfffd9..db93632471 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -310,9 +310,6 @@ class CreateWidget(QtWidgets.QWidget): folder_path = None if self._context_change_is_enabled(): folder_path = self._context_widget.get_selected_folder_path() - - if folder_path is None: - folder_path = self.get_current_folder_path() return folder_path or None def _get_folder_id(self): From 82427cb004003e415583266fe036bb56fc240481 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 12 Dec 2025 10:21:45 +0100 Subject: [PATCH 55/74] 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 56/74] 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 57/74] 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 58/74] 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 59/74] 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 60/74] 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 61/74] 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 62/74] 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 63/74] 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 64/74] 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 65/74] 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 66/74] 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 67/74] 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 68/74] 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 69/74] 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 70/74] 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 71/74] 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 72/74] 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 73/74] 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 74/74] :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.