From b39dd35af9186bcbecd5a520f31fcfbe8b6ce0a2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Dec 2025 13:01:49 +0100 Subject: [PATCH 01/35] 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 02/35] 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 03/35] 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 04/35] 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 05/35] 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 06/35] 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 07/35] 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 08/35] 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 09/35] 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 10/35] 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 11/35] 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 12/35] 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 cdac62aae77421ff583a88722cb772ad6672b68d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Dec 2025 11:07:05 +0100 Subject: [PATCH 13/35] 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 14/35] 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 15/35] 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 c52a7e367bf4312fe68b6652db758634ad1a61f0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 Dec 2025 18:35:22 +0100 Subject: [PATCH 16/35] 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 ec510ab14915f8b624c4cf9e607d3a30cbcac82b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Dec 2025 12:16:45 +0100 Subject: [PATCH 17/35] 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 18/35] 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 19/35] 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 20/35] 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 21/35] 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 22/35] 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 23/35] 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 24/35] 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 25/35] 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 26/35] 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 27/35] 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 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 28/35] 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 29/35] 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 30/35] 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 31/35] 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 32/35] 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 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 33/35] 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 34/35] 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 35/35] 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