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 01/72] 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 b39dd35af9186bcbecd5a520f31fcfbe8b6ce0a2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 13:01:49 +0100 Subject: [PATCH 02/72] Removed webpublisher from regular extract_thumbnail Must use regular one as customer uses `review` as product type. Adding `review` to generic plugin might have unforeseen consequences. --- 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 adfb4298b9..2a43c12af3 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -48,7 +48,6 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "unreal", "houdini", "batchdelivery", - "webpublisher", ] settings_category = "core" enabled = False From 3edb0148cd682f01f9dfef387cc27889b29db458 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 13:02:23 +0100 Subject: [PATCH 03/72] Added setting to match more from source to regular extract thumbnail --- server/settings/publish_plugins.py | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index d7b794cb5b..0bf8e9c7de 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -469,6 +469,43 @@ class UseDisplayViewModel(BaseSettingsModel): ) +class ExtractThumbnailFromSourceProfileModel(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" + ) + + integrate_thumbnail: bool = SettingsField( + True, title="Integrate Thumbnail Representation" + ) + target_size: ResizeModel = SettingsField( + default_factory=ResizeModel, title="Target size" + ) + background_color: ColorRGBA_uint8 = SettingsField( + (0, 0, 0, 0.0), title="Background color" + ) + ffmpeg_args: ExtractThumbnailFFmpegModel = SettingsField( + default_factory=ExtractThumbnailFFmpegModel + ) + + +class ExtractThumbnailFromSourceModel(BaseSettingsModel): + """Thumbnail extraction from source files using ffmpeg and oiiotool.""" + enabled: bool = SettingsField(True) + profiles: list[ExtractThumbnailFromSourceProfileModel] = SettingsField( + default_factory=list, title="Profiles" + ) + + class ExtractOIIOTranscodeOutputModel(BaseSettingsModel): _layout = "expanded" name: str = SettingsField( @@ -1244,6 +1281,17 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ExtractThumbnailModel, title="Extract Thumbnail" ) + ExtractThumbnailFromSource: ExtractThumbnailFromSourceModel = SettingsField( + default_factory=ExtractThumbnailFromSourceModel, + title="Extract Thumbnail (from source)", + description=( + "Extract thumbnails from explicit file set in " + "instance.data['thumbnailSource'] using ffmpeg " + "and oiiotool." + "Used when host does not provide thumbnail, but artist could set " + "custom thumbnail source file. (TrayPublisher, Webpublisher)" + ) + ) ExtractOIIOTranscode: ExtractOIIOTranscodeModel = SettingsField( default_factory=ExtractOIIOTranscodeModel, title="Extract OIIO Transcode" @@ -1475,6 +1523,30 @@ DEFAULT_PUBLISH_VALUES = { "output": [] } }, + "ExtractThumbnailFromSource": { + "enabled": True, + "profiles": [ + { + "product_types": [], + "hosts": [], + "task_types": [], + "task_names": [], + "product_names": [], + "integrate_thumbnail": True, + "target_size": { + "type": "source", + "resize": { + "width": 1920, + "height": 1080 + } + }, + "ffmpeg_args": { + "input": [], + "output": [] + } + } + ] + }, "ExtractOIIOTranscode": { "enabled": True, "profiles": [] From 08c03e980b14fcfbabe86a57d6f22bc9b59f647e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 13:03:18 +0100 Subject: [PATCH 04/72] Moved order to trigger later No need to trigger so early, but must be triggered before regular one to limit double creation of thumbnail. --- .../plugins/publish/extract_thumbnail_from_source.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 59a62b1d7b..e3eda7dd61 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -33,7 +33,10 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): label = "Extract Thumbnail (from source)" # Before 'ExtractThumbnail' in global plugins - order = pyblish.api.ExtractorOrder - 0.00001 + order = pyblish.api.ExtractorOrder + 0.48 + + # Settings + profiles = None def process(self, instance): self._create_context_thumbnail(instance.context) From ad83f76318dc74ff2e81f53cd924ddb8a0f03a61 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 13:08:11 +0100 Subject: [PATCH 05/72] Introduced dataclass for config of selected profile It makes it nicer than using dictionary --- .../publish/extract_thumbnail_from_source.py | 164 ++++++++++++++++-- 1 file changed, 149 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index e3eda7dd61..8b1c50072e 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -13,7 +13,9 @@ Todos: """ import os +from dataclasses import dataclass, field, fields import tempfile +from typing import Dict, Any, List, Tuple import pyblish.api from ayon_core.lib import ( @@ -22,9 +24,57 @@ from ayon_core.lib import ( is_oiio_supported, run_subprocess, + get_rescaled_command_arguments, + filter_profiles, ) +@dataclass +class ProfileConfig: + """ + Data class representing the full configuration for selected profile + + Any change of controllable fields in Settings must propagate here! + """ + integrate_thumbnail: bool = False + + target_size: Dict[str, Any] = field( + default_factory=lambda: { + "type": "source", + "resize": {"width": 1920, "height": 1080}, + } + ) + + 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 that match 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 ExtractThumbnailFromSource(pyblish.api.InstancePlugin): """Create jpg thumbnail for instance based on 'thumbnailSource'. @@ -56,7 +106,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): return dst_filepath = self._create_thumbnail( - instance.context, thumbnail_source + instance.context, thumbnail_source, profile_config ) if not dst_filepath: return @@ -79,7 +129,12 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): instance.data["representations"].append(new_repre) instance.data["thumbnailPath"] = dst_filepath - def _create_thumbnail(self, context, thumbnail_source): + def _create_thumbnail( + self, + context: pyblish.api.Context, + thumbnail_source: str, + profile_config: ProfileConfig + ) -> str: if not thumbnail_source: self.log.debug("Thumbnail source not filled. Skipping.") return @@ -112,7 +167,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): # If the input can read by OIIO then use OIIO method for # conversion otherwise use ffmpeg thumbnail_created = self.create_thumbnail_oiio( - thumbnail_source, full_output_path + thumbnail_source, full_output_path, profile_config ) # Try to use FFMPEG if OIIO is not supported or for cases when @@ -125,7 +180,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): ) thumbnail_created = self.create_thumbnail_ffmpeg( - thumbnail_source, full_output_path + thumbnail_source, full_output_path, profile_config ) # Skip representation and try next one if wasn't created @@ -146,13 +201,15 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): return True return False - def create_thumbnail_oiio(self, src_path, dst_path): + def create_thumbnail_oiio( + self, + src_path: str, + dst_path: str, + profile_config: ProfileConfig + ) -> bool: self.log.debug("Outputting thumbnail with OIIO: {}".format(dst_path)) - oiio_cmd = get_oiio_tool_args( - "oiiotool", - "-a", src_path, - "--ch", "R,G,B", - "-o", dst_path + resolution_arg = self._get_resolution_arg( + "oiiotool", src_path, profile_config ) self.log.debug("Running: {}".format(" ".join(oiio_cmd))) try: @@ -165,7 +222,16 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): ) return False - def create_thumbnail_ffmpeg(self, src_path, dst_path): + def create_thumbnail_ffmpeg( + self, + src_path: str, + dst_path: str, + profile_config: ProfileConfig + ) -> bool: + resolution_arg = self._get_resolution_arg( + "ffmpeg", src_path, profile_config + ) + max_int = str(2147483647) ffmpeg_cmd = get_ffmpeg_tool_args( "ffmpeg", @@ -188,10 +254,78 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): ) return False - def _create_context_thumbnail(self, context): - if "thumbnailPath" in context.data: + def _create_context_thumbnail( + self, + context: pyblish.api.Context, + profile: ProfileConfig + ) -> str: + hasContextThumbnail = "thumbnailPath" in context.data + if hasContextThumbnail: return thumbnail_source = context.data.get("thumbnailSource") - thumbnail_path = self._create_thumbnail(context, thumbnail_source) - context.data["thumbnailPath"] = thumbnail_path + thumbnail_path = self._create_thumbnail( + context, thumbnail_source, profile + ) + return thumbnail_path + + 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) + + def _get_resolution_arg( + self, + application, + input_path, + profile + ): + # get settings + if profile.target_size["type"] == "source": + return [] + + resize = profile.target_size["resize"] + target_width = resize["width"] + target_height = resize["height"] + + # form arg string per application + return get_rescaled_command_arguments( + application, + input_path, + target_width, + target_height, + bg_color=profile.background_color, + log=self.log, + ) From 0ab00dbb4e3e34fec56a4dd219a6075fa51c5d43 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 13:08:36 +0100 Subject: [PATCH 06/72] Check for existing profile --- .../plugins/publish/extract_thumbnail_from_source.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 8b1c50072e..eaa1f356e1 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -89,7 +89,12 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): profiles = None def process(self, instance): - self._create_context_thumbnail(instance.context) + 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"] self.log.debug( From daa9effd040851d44e4098006eae633127c91fcd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 13:08:57 +0100 Subject: [PATCH 07/72] Reorganized position of context thumbnail --- .../plugins/publish/extract_thumbnail_from_source.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index eaa1f356e1..24f8b498b6 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -96,10 +96,12 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): if not profile_config: return - product_name = instance.data["productName"] - self.log.debug( - "Processing instance with product name {}".format(product_name) + context_thumbnail_path = self._create_context_thumbnail( + instance.context, profile_config ) + if context_thumbnail_path: + instance.context.data["thumbnailPath"] = context_thumbnail_path + thumbnail_source = instance.data.get("thumbnailSource") if not thumbnail_source: self.log.debug("Thumbnail source not filled. Skipping.") From 2928c62d2b5b5dfa55ede74e06e9ae37221afac1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 13:09:20 +0100 Subject: [PATCH 08/72] Added flag for integrate representation --- .../ayon_core/plugins/publish/extract_thumbnail_from_source.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 24f8b498b6..139b3366d8 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -129,6 +129,9 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): "outputName": "thumbnail", } + if not profile_config.integrate_thumbnail: + new_repre["tags"].append("delete") + # adding representation self.log.debug( "Adding thumbnail representation: {}".format(new_repre) From bfca3175d6c3e35a8f60af25666304c65e396c04 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 13:09:40 +0100 Subject: [PATCH 09/72] Typing --- .../ayon_core/plugins/publish/extract_thumbnail_from_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 139b3366d8..7d0154ed7c 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -199,7 +199,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): self.log.warning("Thumbnail has not been created.") - def _instance_has_thumbnail(self, instance): + def _instance_has_thumbnail(self, instance: pyblish.api.Instance) -> bool: if "representations" not in instance.data: self.log.warning( "Instance does not have 'representations' key filled" From d9344239dd3c7fccedb2f5c2b16db97ff37405bc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 13:11:36 +0100 Subject: [PATCH 10/72] Fixes for resizing to actually work Resizing argument must be before output arguments --- .../publish/extract_thumbnail_from_source.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 7d0154ed7c..0f532d721c 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -221,6 +221,15 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): resolution_arg = self._get_resolution_arg( "oiiotool", src_path, profile_config ) + oiio_cmd = get_oiio_tool_args("oiiotool", "-a", src_path) + if resolution_arg: + # resize must be before -o + oiio_cmd.extend(resolution_arg) + else: + # resize provides own -ch, must be only one + oiio_cmd.extend(["--ch", "R,G,B"]) + + oiio_cmd.extend(["-o", dst_path]) self.log.debug("Running: {}".format(" ".join(oiio_cmd))) try: run_subprocess(oiio_cmd, logger=self.log) @@ -250,9 +259,17 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): "-probesize", max_int, "-i", src_path, "-frames:v", "1", - dst_path ) + ffmpeg_cmd.extend(profile_config.ffmpeg_args.get("input") or []) + + if resolution_arg: + ffmpeg_cmd.extend(resolution_arg) + + # possible resize must be before output args + ffmpeg_cmd.extend(profile_config.ffmpeg_args.get("output") or []) + ffmpeg_cmd.append(dst_path) + self.log.debug("Running: {}".format(" ".join(ffmpeg_cmd))) try: run_subprocess(ffmpeg_cmd, logger=self.log) From a426baf1a118b26950390e38cee19fc0b513f0e9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 13:30:46 +0100 Subject: [PATCH 11/72] Typing --- .../publish/extract_thumbnail_from_source.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 0f532d721c..4295489bea 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -15,7 +15,7 @@ Todos: import os from dataclasses import dataclass, field, fields 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 ( @@ -88,7 +88,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): # Settings profiles = None - def process(self, instance): + def process(self, instance: pyblish.api.Instance): if not self.profiles: self.log.debug("No profiles present for color transcode") return @@ -144,7 +144,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): context: pyblish.api.Context, thumbnail_source: str, profile_config: ProfileConfig - ) -> str: + ) -> Optional[str]: if not thumbnail_source: self.log.debug("Thumbnail source not filled. Skipping.") return @@ -285,7 +285,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): self, context: pyblish.api.Context, profile: ProfileConfig - ) -> str: + ) -> Optional[str]: hasContextThumbnail = "thumbnailPath" in context.data if hasContextThumbnail: return @@ -335,10 +335,10 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): def _get_resolution_arg( self, - application, - input_path, - profile - ): + application: str, + input_path: str, + profile: ProfileConfig + ) -> List[str]: # get settings if profile.target_size["type"] == "source": return [] From a187a7fc5608253995d117e87be76d4c58779a72 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 13:32:14 +0100 Subject: [PATCH 12/72] Ruff --- .../ayon_core/plugins/publish/extract_thumbnail_from_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 4295489bea..abfbfc70e6 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -68,7 +68,7 @@ class ProfileConfig: # 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 that match field names + # 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 From dabeb0d5522f7d72e3803995c60b972f6746bf51 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 14:04:51 +0100 Subject: [PATCH 13/72] Ruff --- server/settings/publish_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 0bf8e9c7de..dcaa47a351 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1281,7 +1281,7 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ExtractThumbnailModel, title="Extract Thumbnail" ) - ExtractThumbnailFromSource: ExtractThumbnailFromSourceModel = SettingsField( + ExtractThumbnailFromSource: ExtractThumbnailFromSourceModel = SettingsField( # noqa: E501 default_factory=ExtractThumbnailFromSourceModel, title="Extract Thumbnail (from source)", description=( From 2885ed180527e10a11dc18958f5c36bd87a26fbc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 17:24:37 +0100 Subject: [PATCH 14/72] 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 15/72] 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 16/72] 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 17/72] 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 18/72] 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 19/72] 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 20/72] 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 21/72] 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 22/72] added is_base_class to create base classes --- client/ayon_core/pipeline/create/creator_plugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 7573589b82..8595ff4ca5 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -651,7 +651,7 @@ class Creator(BaseCreator): Creation requires prepared product name and instance data. """ - + is_base_class = True # GUI Purposes # - default_variants may not be used if `get_default_variants` # is overridden @@ -949,6 +949,8 @@ class Creator(BaseCreator): class HiddenCreator(BaseCreator): + is_base_class = True + @abstractmethod def create(self, instance_data, source_data): pass @@ -959,6 +961,7 @@ class AutoCreator(BaseCreator): Can be used e.g. for `workfile`. """ + is_base_class = True def remove_instances(self, instances): """Skip removal.""" From 89129dfeb4046cb49d47c7ca9928c0eb2993033f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Dec 2025 10:10:51 +0100 Subject: [PATCH 23/72] 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 24/72] 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 25/72] 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 26/72] 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 27/72] 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 28/72] 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 cdac62aae77421ff583a88722cb772ad6672b68d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Dec 2025 11:07:05 +0100 Subject: [PATCH 29/72] Renamed hosts to host_names for ExtractThumbnailFromSource --- .../publish/extract_thumbnail_from_source.py | 2 +- server/settings/publish_plugins.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index abfbfc70e6..cb96ff4aef 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -308,7 +308,7 @@ class ExtractThumbnailFromSource(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/publish_plugins.py b/server/settings/publish_plugins.py index dcaa47a351..5e4359b7bc 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -470,19 +470,21 @@ class UseDisplayViewModel(BaseSettingsModel): class ExtractThumbnailFromSourceProfileModel(BaseSettingsModel): + host_names: list[str] = SettingsField( + default_factory=list, title="Host names" + ) + product_names: list[str] = SettingsField( + default_factory=list, title="Product names" + ) 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" - ) integrate_thumbnail: bool = SettingsField( True, title="Integrate Thumbnail Representation" @@ -1527,11 +1529,11 @@ DEFAULT_PUBLISH_VALUES = { "enabled": True, "profiles": [ { + "product_names": [], "product_types": [], - "hosts": [], + "host_names": [], "task_types": [], "task_names": [], - "product_names": [], "integrate_thumbnail": True, "target_size": { "type": "source", From ad0cbad6636681512de1d70509e5e67b78f92e96 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Dec 2025 11:07:50 +0100 Subject: [PATCH 30/72] Changed data types of rgb --- .../ayon_core/plugins/publish/extract_thumbnail_from_source.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index cb96ff4aef..603bca1aee 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -50,8 +50,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 00102dae85890a0e876c28055a9e11ae65fd7dc3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Dec 2025 11:09:12 +0100 Subject: [PATCH 31/72] Renamed ProfileConfig --- .../publish/extract_thumbnail_from_source.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 603bca1aee..33850a4324 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -30,7 +30,7 @@ from ayon_core.lib import ( @dataclass -class ProfileConfig: +class ThumbnailDefinition: """ Data class representing the full configuration for selected profile @@ -53,10 +53,11 @@ 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]) -> "ThumbnailDefinition": """ - Creates a ProfileConfig instance from a dictionary, safely ignoring - any keys in the dictionary that are not fields in the dataclass. + Creates a ThumbnailDefinition 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 @@ -142,7 +143,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): self, context: pyblish.api.Context, thumbnail_source: str, - profile_config: ProfileConfig + profile_config: ThumbnailDefinition ) -> Optional[str]: if not thumbnail_source: self.log.debug("Thumbnail source not filled. Skipping.") @@ -214,7 +215,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): self, src_path: str, dst_path: str, - profile_config: ProfileConfig + profile_config: ThumbnailDefinition ) -> bool: self.log.debug("Outputting thumbnail with OIIO: {}".format(dst_path)) resolution_arg = self._get_resolution_arg( @@ -244,7 +245,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): self, src_path: str, dst_path: str, - profile_config: ProfileConfig + profile_config: ThumbnailDefinition ) -> bool: resolution_arg = self._get_resolution_arg( "ffmpeg", src_path, profile_config @@ -283,7 +284,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): def _create_context_thumbnail( self, context: pyblish.api.Context, - profile: ProfileConfig + profile: ThumbnailDefinition ) -> Optional[str]: hasContextThumbnail = "thumbnailPath" in context.data if hasContextThumbnail: @@ -298,7 +299,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): def _get_config_from_profile( self, instance: pyblish.api.Instance - ) -> ProfileConfig: + ) -> ThumbnailDefinition: """Returns profile if and how repre should be color transcoded.""" host_name = instance.context.data["hostName"] product_type = instance.data["productType"] @@ -330,13 +331,13 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): ) return - return ProfileConfig.from_dict(profile) + return ThumbnailDefinition.from_dict(profile) def _get_resolution_arg( self, application: str, input_path: str, - profile: ProfileConfig + profile: ThumbnailDefinition ) -> List[str]: # get settings if profile.target_size["type"] == "source": From 44251c93c776935abb1b5eb0338e24c0768a77a0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 9 Dec 2025 10:33:12 +0100 Subject: [PATCH 32/72] 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 3d321b48960722a54baa74c7c833d739ffd9eda2 Mon Sep 17 00:00:00 2001 From: Vincent Ullmann Date: Tue, 9 Dec 2025 15:21:23 +0000 Subject: [PATCH 33/72] add verbose-flag to get_oiio_info_for_input and changed oiio_color_convert to use verbose=False --- client/ayon_core/lib/transcoding.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index b4a3e77f5a..feb31a46e1 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -131,16 +131,28 @@ def get_transcode_temp_directory(): ) -def get_oiio_info_for_input(filepath, logger=None, subimages=False): +def get_oiio_info_for_input( + filepath, + logger=None, + subimages=False, + verbose=True, +): """Call oiiotool to get information about input and return stdout. + Args: + filepath (str): Path to file. + logger (logging.Logger): Logger used for logging. + subimages (bool): include info about subimages in the output. + verbose (bool): get the full metadata about each input image. + Stdout should contain xml format string. """ args = get_oiio_tool_args( "oiiotool", "--info", - "-v" ) + if verbose: + args.append("-v") if subimages: args.append("-a") @@ -1178,7 +1190,11 @@ def oiio_color_convert( if logger is None: logger = logging.getLogger(__name__) - input_info = get_oiio_info_for_input(input_path, logger=logger) + input_info = get_oiio_info_for_input( + input_path, + logger=logger, + verbose=False, + ) # Collect channels to export input_arg, channels_arg = get_oiio_input_and_channel_args(input_info) From 97a8b13a4e5c7687190d4699caae7483d2fecff8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Dec 2025 12:56:42 +0100 Subject: [PATCH 34/72] Allow specifying a strength ordering offset for each contribution to a single department layer --- client/ayon_core/pipeline/usdlib.py | 24 ++++++++------- .../extract_usd_layer_contributions.py | 29 ++++++++++++++----- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/pipeline/usdlib.py b/client/ayon_core/pipeline/usdlib.py index 095f6fdc57..6b9d19fd35 100644 --- a/client/ayon_core/pipeline/usdlib.py +++ b/client/ayon_core/pipeline/usdlib.py @@ -299,7 +299,6 @@ def add_ordered_sublayer(layer, contribution_path, layer_id, order=None, sdf format args metadata if enabled) """ - # Add the order with the contribution path so that for future # contributions we can again use it to magically fit into the # ordering. We put this in the path because sublayer paths do @@ -317,20 +316,25 @@ def add_ordered_sublayer(layer, contribution_path, layer_id, order=None, # If the layer was already in the layers, then replace it for index, existing_path in enumerate(layer.subLayerPaths): args = get_sdf_format_args(existing_path) - existing_layer = args.get("layer_id") - if existing_layer == layer_id: + existing_layer_id = args.get("layer_id") + if existing_layer_id == layer_id: + existing_layer = layer.subLayerPaths[index] + existing_order = args.get("order") + existing_order = int(existing_order) if existing_order else None + if order is not None and order != existing_order: + # We need to move the layer, so we will remove this index + # and then re-insert it below at the right order + log.debug(f"Removing existing layer: {existing_layer}") + del layer.subLayerPaths[index] + break + # Put it in the same position where it was before when swapping # it with the original, also take over its order metadata - order = args.get("order") - if order is not None: - order = int(order) - else: - order = None contribution_path = _format_path(contribution_path, - order=order, + order=existing_order, layer_id=layer_id) log.debug( - f"Replacing existing layer: {layer.subLayerPaths[index]} " + f"Replacing existing layer: {existing_layer} " f"-> {contribution_path}" ) layer.subLayerPaths[index] = contribution_path diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 2c4cc5aac2..12e0931e89 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -16,7 +16,7 @@ from ayon_core.lib import ( UISeparatorDef, UILabelDef, EnumDef, - filter_profiles + filter_profiles, NumberDef ) try: from ayon_core.pipeline.usdlib import ( @@ -275,7 +275,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, # the contributions so that we can design a system where custom # contributions outside the predefined orders are possible to be # managed. So that if a particular asset requires an extra contribution - # level, you can add itdirectly from the publisher at that particular + # level, you can add it directly from the publisher at that particular # order. Future publishes will then see the existing contribution and will # persist adding it to future bootstraps at that order contribution_layers: Dict[str, int] = { @@ -334,10 +334,12 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, attr_values[key] = attr_values[key].format(**data) # Define contribution - order = self.contribution_layers.get( + order: int = self.contribution_layers.get( attr_values["contribution_layer"], 0 ) + # Allow offsetting the order to the contribution to department layer + order_offset: int = attr_values.get("contribution_order_offset", 0) if attr_values["contribution_apply_as_variant"]: contribution = VariantContribution( instance=instance, @@ -346,14 +348,14 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, variant_set_name=attr_values["contribution_variant_set_name"], variant_name=attr_values["contribution_variant"], variant_is_default=attr_values["contribution_variant_is_default"], # noqa: E501 - order=order + order=order + order_offset ) else: contribution = SublayerContribution( instance=instance, layer_id=attr_values["contribution_layer"], target_product=attr_values["contribution_target_product"], - order=order + order=order + order_offset ) asset_product = contribution.target_product @@ -370,7 +372,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, contribution ) layer_instance.data["usd_layer_id"] = contribution.layer_id - layer_instance.data["usd_layer_order"] = contribution.order + layer_instance.data["usd_layer_order"] = order layer_instance.data["productGroup"] = ( instance.data.get("productGroup") or "USD Layer" @@ -561,6 +563,19 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, items=list(cls.contribution_layers.keys()), default=default_contribution_layer, visible=visible), + # TODO: We may want to make the visibility of this optional + # based on studio preference, to avoid complexity when not needed + NumberDef("contribution_order_offset", + label="Strength order offset", + tooltip=( + "The contribution to the department layer will be " + "made with this offset applied. A higher number means " + "a stronger opinion." + ), + default=0, + minimum=-99999, + maximum=99999, + visible=visible), BoolDef("contribution_apply_as_variant", label="Add as variant", tooltip=( @@ -729,7 +744,7 @@ class ExtractUSDLayerContribution(publish.Extractor): layer=sdf_layer, contribution_path=path, layer_id=product_name, - order=None, # unordered + order=contribution.order, add_sdf_arguments_metadata=True ) else: From ced9eadd3d3d182df5341cd3f60897058dfdf9b3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Dec 2025 16:31:05 +0100 Subject: [PATCH 35/72] Use the instance attribute `Strength order` as the in-layer order completely so that it's the exact value, not an offset to the department layer order --- .../extract_usd_layer_contributions.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 12e0931e89..01162fc481 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -334,12 +334,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, attr_values[key] = attr_values[key].format(**data) # Define contribution - order: int = self.contribution_layers.get( - attr_values["contribution_layer"], 0 - ) - - # Allow offsetting the order to the contribution to department layer - order_offset: int = attr_values.get("contribution_order_offset", 0) + in_layer_order: int = attr_values.get("contribution_in_layer_order", 0) if attr_values["contribution_apply_as_variant"]: contribution = VariantContribution( instance=instance, @@ -348,18 +343,21 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, variant_set_name=attr_values["contribution_variant_set_name"], variant_name=attr_values["contribution_variant"], variant_is_default=attr_values["contribution_variant_is_default"], # noqa: E501 - order=order + order_offset + order=in_layer_order ) else: contribution = SublayerContribution( instance=instance, layer_id=attr_values["contribution_layer"], target_product=attr_values["contribution_target_product"], - order=order + order_offset + order=in_layer_order ) asset_product = contribution.target_product layer_product = "{}_{}".format(asset_product, contribution.layer_id) + layer_order: int = self.contribution_layers.get( + attr_values["contribution_layer"], 0 + ) # Layer contribution instance layer_instance = self.get_or_create_instance( @@ -372,7 +370,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, contribution ) layer_instance.data["usd_layer_id"] = contribution.layer_id - layer_instance.data["usd_layer_order"] = order + layer_instance.data["usd_layer_order"] = layer_order layer_instance.data["productGroup"] = ( instance.data.get("productGroup") or "USD Layer" @@ -565,10 +563,10 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, visible=visible), # TODO: We may want to make the visibility of this optional # based on studio preference, to avoid complexity when not needed - NumberDef("contribution_order_offset", - label="Strength order offset", + NumberDef("contribution_in_layer_order", + label="Strength order", tooltip=( - "The contribution to the department layer will be " + "The contribution inside the department layer will be " "made with this offset applied. A higher number means " "a stronger opinion." ), From c52a7e367bf4312fe68b6652db758634ad1a61f0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 Dec 2025 18:35:22 +0100 Subject: [PATCH 36/72] Simplified ExtractThumbnailFrom source Removed profiles Changed defaults for smaller resolution --- .../publish/extract_thumbnail_from_source.py | 125 ++---------------- server/settings/publish_plugins.py | 59 ++------- 2 files changed, 23 insertions(+), 161 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 33850a4324..702244a45f 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -29,52 +29,6 @@ from ayon_core.lib import ( ) -@dataclass -class ThumbnailDefinition: - """ - Data class representing the full configuration for selected profile - - Any change of controllable fields in Settings must propagate here! - """ - integrate_thumbnail: bool = False - - target_size: Dict[str, Any] = field( - default_factory=lambda: { - "type": "source", - "resize": {"width": 1920, "height": 1080}, - } - ) - - ffmpeg_args: Dict[str, List[Any]] = field( - default_factory=lambda: {"input": [], "output": []} - ) - - # Background color defined as (R, G, B, A) tuple. - background_color: Tuple[int, int, int, float] = (0, 0, 0, 0.0) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "ThumbnailDefinition": - """ - Creates a ThumbnailDefinition 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 ExtractThumbnailFromSource(pyblish.api.InstancePlugin): """Create jpg thumbnail for instance based on 'thumbnailSource'. @@ -86,18 +40,13 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): order = pyblish.api.ExtractorOrder + 0.48 # Settings - profiles = None + target_size = {"type": "source", "resize": {"width": 1920, "height": 1080}} + background_color = (0, 0, 0, 0.0) + def process(self, instance: pyblish.api.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 - context_thumbnail_path = self._create_context_thumbnail( - instance.context, profile_config + instance.context ) if context_thumbnail_path: instance.context.data["thumbnailPath"] = context_thumbnail_path @@ -113,7 +62,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): return dst_filepath = self._create_thumbnail( - instance.context, thumbnail_source, profile_config + instance.context, thumbnail_source ) if not dst_filepath: return @@ -129,8 +78,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): "outputName": "thumbnail", } - if not profile_config.integrate_thumbnail: - new_repre["tags"].append("delete") + new_repre["tags"].append("delete") # adding representation self.log.debug( @@ -143,7 +91,6 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): self, context: pyblish.api.Context, thumbnail_source: str, - profile_config: ThumbnailDefinition ) -> Optional[str]: if not thumbnail_source: self.log.debug("Thumbnail source not filled. Skipping.") @@ -177,7 +124,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): # If the input can read by OIIO then use OIIO method for # conversion otherwise use ffmpeg thumbnail_created = self.create_thumbnail_oiio( - thumbnail_source, full_output_path, profile_config + thumbnail_source, full_output_path ) # Try to use FFMPEG if OIIO is not supported or for cases when @@ -190,7 +137,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): ) thumbnail_created = self.create_thumbnail_ffmpeg( - thumbnail_source, full_output_path, profile_config + thumbnail_source, full_output_path ) # Skip representation and try next one if wasn't created @@ -215,11 +162,10 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): self, src_path: str, dst_path: str, - profile_config: ThumbnailDefinition ) -> bool: self.log.debug("Outputting thumbnail with OIIO: {}".format(dst_path)) resolution_arg = self._get_resolution_arg( - "oiiotool", src_path, profile_config + "oiiotool", src_path ) oiio_cmd = get_oiio_tool_args("oiiotool", "-a", src_path) if resolution_arg: @@ -245,10 +191,9 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): self, src_path: str, dst_path: str, - profile_config: ThumbnailDefinition ) -> bool: resolution_arg = self._get_resolution_arg( - "ffmpeg", src_path, profile_config + "ffmpeg", src_path ) max_int = str(2147483647) @@ -261,13 +206,10 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): "-frames:v", "1", ) - ffmpeg_cmd.extend(profile_config.ffmpeg_args.get("input") or []) - if resolution_arg: ffmpeg_cmd.extend(resolution_arg) # possible resize must be before output args - ffmpeg_cmd.extend(profile_config.ffmpeg_args.get("output") or []) ffmpeg_cmd.append(dst_path) self.log.debug("Running: {}".format(" ".join(ffmpeg_cmd))) @@ -284,7 +226,6 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): def _create_context_thumbnail( self, context: pyblish.api.Context, - profile: ThumbnailDefinition ) -> Optional[str]: hasContextThumbnail = "thumbnailPath" in context.data if hasContextThumbnail: @@ -292,58 +233,20 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): thumbnail_source = context.data.get("thumbnailSource") thumbnail_path = self._create_thumbnail( - context, thumbnail_source, profile + context, thumbnail_source ) return thumbnail_path - def _get_config_from_profile( - self, - instance: pyblish.api.Instance - ) -> ThumbnailDefinition: - """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 = { - "host_names": 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 ThumbnailDefinition.from_dict(profile) - def _get_resolution_arg( self, application: str, input_path: str, - profile: ThumbnailDefinition ) -> List[str]: # get settings - if profile.target_size["type"] == "source": + if self.target_size["type"] == "source": return [] - resize = profile.target_size["resize"] + resize = self.target_size["resize"] target_width = resize["width"] target_height = resize["height"] @@ -353,6 +256,6 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): input_path, target_width, target_height, - bg_color=profile.background_color, + bg_color=self.background_color, log=self.log, ) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 5e4359b7bc..76fadbf9ca 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -469,43 +469,16 @@ class UseDisplayViewModel(BaseSettingsModel): ) -class ExtractThumbnailFromSourceProfileModel(BaseSettingsModel): - host_names: list[str] = SettingsField( - default_factory=list, title="Host names" - ) - product_names: list[str] = SettingsField( - default_factory=list, title="Product names" - ) - product_types: list[str] = SettingsField( - default_factory=list, title="Product types" - ) - 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" - ) +class ExtractThumbnailFromSourceModel(BaseSettingsModel): + """Thumbnail extraction from source files using ffmpeg and oiiotool.""" + enabled: bool = SettingsField(True) - integrate_thumbnail: bool = SettingsField( - True, title="Integrate Thumbnail Representation" - ) target_size: ResizeModel = SettingsField( default_factory=ResizeModel, title="Target size" ) background_color: ColorRGBA_uint8 = SettingsField( (0, 0, 0, 0.0), title="Background color" ) - ffmpeg_args: ExtractThumbnailFFmpegModel = SettingsField( - default_factory=ExtractThumbnailFFmpegModel - ) - - -class ExtractThumbnailFromSourceModel(BaseSettingsModel): - """Thumbnail extraction from source files using ffmpeg and oiiotool.""" - enabled: bool = SettingsField(True) - profiles: list[ExtractThumbnailFromSourceProfileModel] = SettingsField( - default_factory=list, title="Profiles" - ) class ExtractOIIOTranscodeOutputModel(BaseSettingsModel): @@ -1527,27 +1500,13 @@ DEFAULT_PUBLISH_VALUES = { }, "ExtractThumbnailFromSource": { "enabled": True, - "profiles": [ - { - "product_names": [], - "product_types": [], - "host_names": [], - "task_types": [], - "task_names": [], - "integrate_thumbnail": True, - "target_size": { - "type": "source", - "resize": { - "width": 1920, - "height": 1080 - } - }, - "ffmpeg_args": { - "input": [], - "output": [] - } + "target_size": { + "type": "resize", + "resize": { + "width": 300, + "height": 170 } - ] + }, }, "ExtractOIIOTranscode": { "enabled": True, From e3206796a764e2adf74a1097acf7780f91836863 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Dec 2025 22:16:03 +0100 Subject: [PATCH 37/72] 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 38/72] 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 ec510ab14915f8b624c4cf9e607d3a30cbcac82b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Dec 2025 12:16:45 +0100 Subject: [PATCH 39/72] Formatting change Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/publish/extract_thumbnail_from_source.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 702244a45f..2e1d437f5d 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -40,7 +40,10 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): order = pyblish.api.ExtractorOrder + 0.48 # Settings - target_size = {"type": "source", "resize": {"width": 1920, "height": 1080}} + target_size = { + "type": "resize", + "resize": {"width": 1920, "height": 1080} + } background_color = (0, 0, 0, 0.0) From 7a5d6ae77e8a72188b3d5faa0bc861bbc1df255f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Dec 2025 12:19:24 +0100 Subject: [PATCH 40/72] Fix imports --- .../plugins/publish/extract_thumbnail_from_source.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 2e1d437f5d..3c74721922 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -13,9 +13,8 @@ Todos: """ import os -from dataclasses import dataclass, field, fields import tempfile -from typing import Dict, Any, List, Tuple, Optional +from typing import List, Optional import pyblish.api from ayon_core.lib import ( @@ -25,7 +24,6 @@ from ayon_core.lib import ( run_subprocess, get_rescaled_command_arguments, - filter_profiles, ) From 55c74196ab51231fecd2d803ccc42904ddb9af30 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Dec 2025 12:20:24 +0100 Subject: [PATCH 41/72] Formatting changes --- .../ayon_core/plugins/publish/extract_thumbnail_from_source.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 3c74721922..a2118d908b 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -44,7 +44,6 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): } background_color = (0, 0, 0, 0.0) - def process(self, instance: pyblish.api.Instance): context_thumbnail_path = self._create_context_thumbnail( instance.context From 505021344b97192aaa7fe703ddb7fde70ef2ed42 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Dec 2025 12:24:40 +0100 Subject: [PATCH 42/72] Fix logic for context thumbnail creation --- .../plugins/publish/extract_thumbnail_from_source.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index a2118d908b..1f00d96a70 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -45,11 +45,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): background_color = (0, 0, 0, 0.0) def process(self, instance: pyblish.api.Instance): - context_thumbnail_path = self._create_context_thumbnail( - instance.context - ) - if context_thumbnail_path: - instance.context.data["thumbnailPath"] = context_thumbnail_path + self._create_context_thumbnail(instance.context) thumbnail_source = instance.data.get("thumbnailSource") if not thumbnail_source: @@ -226,16 +222,15 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): def _create_context_thumbnail( self, context: pyblish.api.Context, - ) -> Optional[str]: + ): hasContextThumbnail = "thumbnailPath" in context.data if hasContextThumbnail: return thumbnail_source = context.data.get("thumbnailSource") - thumbnail_path = self._create_thumbnail( + context.data["thumbnailPath"] = self._create_thumbnail( context, thumbnail_source ) - return thumbnail_path def _get_resolution_arg( self, From bbff0562684243eb1a96e26be6c51be27fd3f806 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Dec 2025 18:02:04 +0100 Subject: [PATCH 43/72] Added psd to ExtractReview ffmpeg and oiiotool seem to handle it fine. --- client/ayon_core/plugins/publish/extract_review.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 54aa52c3c3..dda69470cf 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -169,7 +169,9 @@ class ExtractReview(pyblish.api.InstancePlugin): settings_category = "core" # Supported extensions - image_exts = {"exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif"} + image_exts = { + "exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif", "psd" + } video_exts = {"mov", "mp4"} supported_exts = image_exts | video_exts From 9f6840a18d2ae4dc3c5607a36226166c98111857 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Dec 2025 18:12:17 +0100 Subject: [PATCH 44/72] Formatting change Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../ayon_core/plugins/publish/extract_thumbnail_from_source.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 1f00d96a70..1889566012 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -223,8 +223,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): self, context: pyblish.api.Context, ): - hasContextThumbnail = "thumbnailPath" in context.data - if hasContextThumbnail: + if "thumbnailPath" in context.data: return thumbnail_source = context.data.get("thumbnailSource") From c1f36199c283bda2da15175c88b5364de3610d65 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Dec 2025 18:20:07 +0100 Subject: [PATCH 45/72] Renamed method --- .../plugins/publish/extract_thumbnail_from_source.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 1f00d96a70..819dbab567 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -160,7 +160,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): dst_path: str, ) -> bool: self.log.debug("Outputting thumbnail with OIIO: {}".format(dst_path)) - resolution_arg = self._get_resolution_arg( + resolution_arg = self._get_resolution_args( "oiiotool", src_path ) oiio_cmd = get_oiio_tool_args("oiiotool", "-a", src_path) @@ -188,7 +188,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): src_path: str, dst_path: str, ) -> bool: - resolution_arg = self._get_resolution_arg( + resolution_arg = self._get_resolution_args( "ffmpeg", src_path ) @@ -232,7 +232,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): context, thumbnail_source ) - def _get_resolution_arg( + def _get_resolution_args( self, application: str, input_path: str, From 061e9c501552fd33c624286909cc710239c739d1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Dec 2025 18:23:17 +0100 Subject: [PATCH 46/72] Renamed variable --- .../plugins/publish/extract_thumbnail_from_source.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 819dbab567..f91621e328 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -160,13 +160,13 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): dst_path: str, ) -> bool: self.log.debug("Outputting thumbnail with OIIO: {}".format(dst_path)) - resolution_arg = self._get_resolution_args( + resolution_args = self._get_resolution_args( "oiiotool", src_path ) oiio_cmd = get_oiio_tool_args("oiiotool", "-a", src_path) - if resolution_arg: + if resolution_args: # resize must be before -o - oiio_cmd.extend(resolution_arg) + oiio_cmd.extend(resolution_args) else: # resize provides own -ch, must be only one oiio_cmd.extend(["--ch", "R,G,B"]) @@ -188,7 +188,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): src_path: str, dst_path: str, ) -> bool: - resolution_arg = self._get_resolution_args( + resolution_args = self._get_resolution_args( "ffmpeg", src_path ) @@ -202,8 +202,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): "-frames:v", "1", ) - if resolution_arg: - ffmpeg_cmd.extend(resolution_arg) + ffmpeg_cmd.extend(resolution_args) # possible resize must be before output args ffmpeg_cmd.append(dst_path) From 738d9cf8d87a6e33ace402ec55a9ef59a0894485 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Dec 2025 18:24:09 +0100 Subject: [PATCH 47/72] Updated docstring --- server/settings/publish_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 76fadbf9ca..0b49ce30f0 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1264,7 +1264,7 @@ class PublishPuginsModel(BaseSettingsModel): "instance.data['thumbnailSource'] using ffmpeg " "and oiiotool." "Used when host does not provide thumbnail, but artist could set " - "custom thumbnail source file. (TrayPublisher, Webpublisher)" + "custom thumbnail source file." ) ) ExtractOIIOTranscode: ExtractOIIOTranscodeModel = SettingsField( From e6eaf872722cd7c84cc2a19295a55959811541ff Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Dec 2025 18:25:16 +0100 Subject: [PATCH 48/72] Updated titles --- .../ayon_core/plugins/publish/extract_thumbnail_from_source.py | 2 +- server/settings/publish_plugins.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index f91621e328..9be157d7f0 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -33,7 +33,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): Thumbnail source must be a single image or video filepath. """ - label = "Extract Thumbnail (from source)" + label = "Extract Thumbnail from source" # Before 'ExtractThumbnail' in global plugins order = pyblish.api.ExtractorOrder + 0.48 diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 0b49ce30f0..9fcb104be7 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1258,7 +1258,7 @@ class PublishPuginsModel(BaseSettingsModel): ) ExtractThumbnailFromSource: ExtractThumbnailFromSourceModel = SettingsField( # noqa: E501 default_factory=ExtractThumbnailFromSourceModel, - title="Extract Thumbnail (from source)", + title="Extract Thumbnail from source", description=( "Extract thumbnails from explicit file set in " "instance.data['thumbnailSource'] using ffmpeg " From 8865e7a2b413c9a8661f09c8449297a819485cee Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Dec 2025 18:32:11 +0100 Subject: [PATCH 49/72] Reverting change of order Deemed unnecessary (by Kuba) --- .../ayon_core/plugins/publish/extract_thumbnail_from_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index 3d34d84bed..913bf818a4 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -35,7 +35,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): label = "Extract Thumbnail from source" # Before 'ExtractThumbnail' in global plugins - order = pyblish.api.ExtractorOrder + 0.48 + order = pyblish.api.ExtractorOrder - 0.00001 # Settings target_size = { From 4d8d9078b8e94963edbcfb06585d8a45a78f9e9a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Dec 2025 10:56:39 +0100 Subject: [PATCH 50/72] 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 51/72] 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 52/72] 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 53/72] 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 54/72] 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 55/72] 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 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 56/72] 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 57/72] 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 58/72] 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 59/72] 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 60/72] 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 61/72] :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. From 3dacfec4ecfe9cbda62a21981b95e8fe45172a3c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:24:49 +0100 Subject: [PATCH 62/72] allow to change registry name in controller --- .../tools/console_interpreter/abstract.py | 14 ++++++++------ .../tools/console_interpreter/control.py | 17 ++++++++++------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/tools/console_interpreter/abstract.py b/client/ayon_core/tools/console_interpreter/abstract.py index a945e6e498..953365d18c 100644 --- a/client/ayon_core/tools/console_interpreter/abstract.py +++ b/client/ayon_core/tools/console_interpreter/abstract.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from abc import ABC, abstractmethod from dataclasses import dataclass, field -from typing import List, Dict, Optional +from typing import Optional @dataclass @@ -13,8 +15,8 @@ class TabItem: class InterpreterConfig: width: Optional[int] height: Optional[int] - splitter_sizes: List[int] = field(default_factory=list) - tabs: List[TabItem] = field(default_factory=list) + splitter_sizes: list[int] = field(default_factory=list) + tabs: list[TabItem] = field(default_factory=list) class AbstractInterpreterController(ABC): @@ -27,7 +29,7 @@ class AbstractInterpreterController(ABC): self, width: int, height: int, - splitter_sizes: List[int], - tabs: List[Dict[str, str]], - ): + splitter_sizes: list[int], + tabs: list[dict[str, str]], + ) -> None: pass diff --git a/client/ayon_core/tools/console_interpreter/control.py b/client/ayon_core/tools/console_interpreter/control.py index b931b6252c..4c5a4b3419 100644 --- a/client/ayon_core/tools/console_interpreter/control.py +++ b/client/ayon_core/tools/console_interpreter/control.py @@ -1,4 +1,5 @@ -from typing import List, Dict +from __future__ import annotations +from typing import Optional from ayon_core.lib import JSONSettingRegistry from ayon_core.lib.local_settings import get_launcher_local_dir @@ -11,13 +12,15 @@ from .abstract import ( class InterpreterController(AbstractInterpreterController): - def __init__(self): + def __init__(self, name: Optional[str] = None) -> None: + if name is None: + name = "python_interpreter_tool" self._registry = JSONSettingRegistry( - "python_interpreter_tool", + name, get_launcher_local_dir(), ) - def get_config(self): + def get_config(self) -> InterpreterConfig: width = None height = None splitter_sizes = [] @@ -54,9 +57,9 @@ class InterpreterController(AbstractInterpreterController): self, width: int, height: int, - splitter_sizes: List[int], - tabs: List[Dict[str, str]], - ): + splitter_sizes: list[int], + tabs: list[dict[str, str]], + ) -> None: self._registry.set_item("width", width) self._registry.set_item("height", height) self._registry.set_item("splitter_sizes", splitter_sizes) From e3fa6e446ec7e1b6ec362bd803569121655eccde Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 15 Dec 2025 12:10:50 +0100 Subject: [PATCH 63/72] Updated docstring Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/publish_plugins.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 9fcb104be7..3d7c0d04ca 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1263,8 +1263,7 @@ class PublishPuginsModel(BaseSettingsModel): "Extract thumbnails from explicit file set in " "instance.data['thumbnailSource'] using ffmpeg " "and oiiotool." - "Used when host does not provide thumbnail, but artist could set " - "custom thumbnail source file." + "Used when artist provided thumbnail source." ) ) ExtractOIIOTranscode: ExtractOIIOTranscodeModel = SettingsField( From e3b94654f8aa838743cef0a3c2f77ceb5d5b6758 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 15 Dec 2025 12:11:07 +0100 Subject: [PATCH 64/72] Updated docstrings Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/publish_plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 3d7c0d04ca..ffd25079d1 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1261,8 +1261,8 @@ class PublishPuginsModel(BaseSettingsModel): title="Extract Thumbnail from source", description=( "Extract thumbnails from explicit file set in " - "instance.data['thumbnailSource'] using ffmpeg " - "and oiiotool." + "instance.data['thumbnailSource'] using oiiotool" + " or ffmpeg." "Used when artist provided thumbnail source." ) ) From 3c0dd4335ede5c5a42506651e02c7d3b90e0bc4b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:24:22 +0100 Subject: [PATCH 65/72] override full stdout and stderr --- .../tools/console_interpreter/ui/utils.py | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/client/ayon_core/tools/console_interpreter/ui/utils.py b/client/ayon_core/tools/console_interpreter/ui/utils.py index 427483215d..b57f22a886 100644 --- a/client/ayon_core/tools/console_interpreter/ui/utils.py +++ b/client/ayon_core/tools/console_interpreter/ui/utils.py @@ -3,40 +3,41 @@ import sys import collections +class _CustomSTD: + def __init__(self, orig_std, write_callback): + self.orig_std = orig_std + self._valid_orig = bool(orig_std) + self._write_callback = write_callback + + def __getattr__(self, attr): + return getattr(self.orig_std, attr) + + def __setattr__(self, key, value): + if key in ("orig_std", "_valid_orig", "_write_callback"): + super().__setattr__(key, value) + else: + setattr(self.orig_std, key, value) + + def write(self, text): + if self._valid_orig: + self.orig_std.write(text) + self._write_callback(text) + + class StdOEWrap: def __init__(self): - self._origin_stdout_write = None - self._origin_stderr_write = None - self._listening = False self.lines = collections.deque() - - if not sys.stdout: - sys.stdout = open(os.devnull, "w") - - if not sys.stderr: - sys.stderr = open(os.devnull, "w") - - if self._origin_stdout_write is None: - self._origin_stdout_write = sys.stdout.write - - if self._origin_stderr_write is None: - self._origin_stderr_write = sys.stderr.write - self._listening = True - sys.stdout.write = self._stdout_listener - sys.stderr.write = self._stderr_listener + + self._stdout_wrap = _CustomSTD(sys.stdout, self._listener) + self._stderr_wrap = _CustomSTD(sys.stderr, self._listener) + + sys.stdout = self._stdout_wrap + sys.stderr = self._stderr_wrap def stop_listen(self): self._listening = False - def _stdout_listener(self, text): + def _listener(self, text): if self._listening: self.lines.append(text) - if self._origin_stdout_write is not None: - self._origin_stdout_write(text) - - def _stderr_listener(self, text): - if self._listening: - self.lines.append(text) - if self._origin_stderr_write is not None: - self._origin_stderr_write(text) From dbdc4c590b13d6878e761ee9c2d15154d89464ad Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:38:17 +0100 Subject: [PATCH 66/72] remove unused import --- client/ayon_core/tools/console_interpreter/ui/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/console_interpreter/ui/utils.py b/client/ayon_core/tools/console_interpreter/ui/utils.py index b57f22a886..c073b784ef 100644 --- a/client/ayon_core/tools/console_interpreter/ui/utils.py +++ b/client/ayon_core/tools/console_interpreter/ui/utils.py @@ -1,4 +1,3 @@ -import os import sys import collections From bd2e26ea50453fd26b51b7c43c77fa17c5953d28 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:03:27 +0100 Subject: [PATCH 67/72] use 'verbose=False' at other places --- client/ayon_core/lib/transcoding.py | 13 ++++++++++--- .../ayon_core/plugins/publish/extract_thumbnail.py | 6 +++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index feb31a46e1..c712472998 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -582,7 +582,7 @@ def get_review_layer_name(src_filepath): return None # Load info about file from oiio tool - input_info = get_oiio_info_for_input(src_filepath) + input_info = get_oiio_info_for_input(src_filepath, verbose=False) if not input_info: return None @@ -1389,7 +1389,11 @@ def get_rescaled_command_arguments( command_args.extend(["-vf", "{0},{1}".format(scale, pad)]) elif application == "oiiotool": - input_info = get_oiio_info_for_input(input_path, logger=log) + input_info = get_oiio_info_for_input( + input_path, + logger=log, + verbose=False, + ) # Collect channels to export _, channels_arg = get_oiio_input_and_channel_args( input_info, alpha_default=1.0) @@ -1529,10 +1533,13 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None): """Get input and channel arguments for oiiotool. Args: oiio_input_info (dict): Information about input from oiio tool. - Should be output of function `get_oiio_info_for_input`. + Should be output of function 'get_oiio_info_for_input' (can be + called with 'verbose=False'). alpha_default (float, optional): Default value for alpha channel. + Returns: tuple[str, str]: Tuple of input and channel arguments. + """ channel_names = oiio_input_info["channelnames"] review_channels = get_convert_rgb_channels(channel_names) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index adfb4298b9..2cc1c53d57 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -478,7 +478,11 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ) return False - input_info = get_oiio_info_for_input(src_path, logger=self.log) + input_info = get_oiio_info_for_input( + src_path, + logger=self.log, + verbose=False, + ) try: input_arg, channels_arg = get_oiio_input_and_channel_args( input_info From a2387d185619cf1007e629db99278b32581c604d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:09:27 +0100 Subject: [PATCH 68/72] require kwargs --- 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 ed30a58f98..fcd68fafc7 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -136,6 +136,7 @@ def get_transcode_temp_directory(): def get_oiio_info_for_input( filepath: str, + *, subimages: bool = False, verbose: bool = True, logger: logging.Logger = None, From 0cfc9598750992c80e30897e07c636ececa27cbf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:25:51 +0100 Subject: [PATCH 69/72] swap order of kwargs --- client/ayon_core/lib/transcoding.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index fcd68fafc7..8e9ed90d1a 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -145,9 +145,9 @@ def get_oiio_info_for_input( Args: filepath (str): Path to file. - logger (logging.Logger): Logger used for logging. subimages (bool): include info about subimages in the output. verbose (bool): get the full metadata about each input image. + logger (logging.Logger): Logger used for logging. Stdout should contain xml format string. """ @@ -1252,8 +1252,8 @@ def oiio_color_convert( input_info = get_oiio_info_for_input( first_input_path, - logger=logger, verbose=False, + logger=logger, ) # Collect channels to export From e2c9cacdd32c9c3da39c066602fa42f4cf8b3da3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:02:57 +0100 Subject: [PATCH 70/72] remove deprecated 'extractenvironments' --- client/ayon_core/cli.py | 47 ----------------------------------------- 1 file changed, 47 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 85c254e7eb..3474cdd38e 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -90,53 +90,6 @@ def addon(ctx): pass -@main_cli.command() -@click.pass_context -@click.argument("output_json_path") -@click.option("--project", help="Project name", default=None) -@click.option("--asset", help="Folder path", default=None) -@click.option("--task", help="Task name", default=None) -@click.option("--app", help="Application name", default=None) -@click.option( - "--envgroup", help="Environment group (e.g. \"farm\")", default=None -) -def extractenvironments( - ctx, output_json_path, project, asset, task, app, envgroup -): - """Extract environment variables for entered context to a json file. - - Entered output filepath will be created if does not exists. - - All context options must be passed otherwise only AYON's global - environments will be extracted. - - Context options are "project", "asset", "task", "app" - - Deprecated: - This function is deprecated and will be removed in future. Please use - 'addon applications extractenvironments ...' instead. - """ - warnings.warn( - ( - "Command 'extractenvironments' is deprecated and will be" - " removed in future. Please use" - " 'addon applications extractenvironments ...' instead." - ), - DeprecationWarning - ) - - addons_manager = ctx.obj["addons_manager"] - applications_addon = addons_manager.get_enabled_addon("applications") - if applications_addon is None: - raise RuntimeError( - "Applications addon is not available or enabled." - ) - - # Please ignore the fact this is using private method - applications_addon._cli_extract_environments( - output_json_path, project, asset, task, app, envgroup - ) - @main_cli.command() @click.pass_context From 0b141009767f376cfd2dcc6c88a47400fb61929c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:08:44 +0100 Subject: [PATCH 71/72] remove line --- client/ayon_core/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 3474cdd38e..0ff927f959 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -90,7 +90,6 @@ def addon(ctx): pass - @main_cli.command() @click.pass_context @click.argument("path", required=True) From d80fc97604087fa65e975658d7407292d0db00f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:09:38 +0100 Subject: [PATCH 72/72] remove unused import --- client/ayon_core/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 0ff927f959..4135aa2e31 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -6,7 +6,6 @@ import logging import code import traceback from pathlib import Path -import warnings import click