From 3d612016089436d0742262e02862586b8410c5b4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Jul 2025 12:04:17 +0200 Subject: [PATCH 01/42] Collect Loaded Scene Versions: Enable for more hosts --- .../plugins/publish/collect_scene_loaded_versions.py | 10 +++++++++- 1 file changed, 9 insertions(+), 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 1abb8e29d2..1c28c28f5b 100644 --- a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py +++ b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py @@ -13,15 +13,23 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): "aftereffects", "blender", "celaction", + "cinema4d", + "flame", "fusion", "harmony", "hiero", "houdini", + "max", "maya", + "motionbuilder", "nuke", "photoshop", + "silhouette", + "substancepainter", + "substancedesigner", "resolve", - "tvpaint" + "tvpaint", + "zbrush", ] def process(self, context): From 2fe89c4b4619d01b53f0bcdbc5ef2c20b5d05c5c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Nov 2025 22:09:25 +0800 Subject: [PATCH 02/42] add substance painter as host and adjust some instance data so that it can be used to review for image product --- client/ayon_core/plugins/publish/extract_review.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 580aa27eef..665d031d5a 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -163,7 +163,8 @@ class ExtractReview(pyblish.api.InstancePlugin): "flame", "unreal", "batchdelivery", - "photoshop" + "photoshop", + "substancepainter", ] settings_category = "core" @@ -571,7 +572,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # review output files "timecode": frame_to_timecode( frame=temp_data.frame_start_handle, - fps=float(instance.data["fps"]) + fps=float(instance.data.get("fps", 25.0)) ) }) @@ -664,8 +665,8 @@ class ExtractReview(pyblish.api.InstancePlugin): with values may be added. """ - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] + frame_start = instance.data.get("frameStart", 1) + frame_end = instance.data.get("frameEnd", 1) # Try to get handles from instance handle_start = instance.data.get("handleStart") @@ -725,7 +726,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ext = os.path.splitext(repre["files"])[1].replace(".", "") return TempData( - fps=float(instance.data["fps"]), + fps=float(instance.data.get("fps", 25.0)), frame_start=frame_start, frame_end=frame_end, handle_start=handle_start, From cfed4afaaf7e22741b463fe77fb2eb3affaf7156 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Nov 2025 15:30:33 +0800 Subject: [PATCH 03/42] use the frame range from context data if it cannot find one --- client/ayon_core/plugins/publish/extract_review.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 665d031d5a..e519a4a97d 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -572,7 +572,9 @@ class ExtractReview(pyblish.api.InstancePlugin): # review output files "timecode": frame_to_timecode( frame=temp_data.frame_start_handle, - fps=float(instance.data.get("fps", 25.0)) + fps=float(instance.data.get( + "fps", instance.context.data["fps"] + )) ) }) @@ -665,8 +667,12 @@ class ExtractReview(pyblish.api.InstancePlugin): with values may be added. """ - frame_start = instance.data.get("frameStart", 1) - frame_end = instance.data.get("frameEnd", 1) + frame_start = instance.data.get( + "frameStart", instance.context.data["frameStart"] + ) + frame_end = instance.data.get( + "frameEnd", instance.context.data["frameEnd"] + ) # Try to get handles from instance handle_start = instance.data.get("handleStart") @@ -726,7 +732,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ext = os.path.splitext(repre["files"])[1].replace(".", "") return TempData( - fps=float(instance.data.get("fps", 25.0)), + fps=float(instance.data.get("fps", instance.context.data["fps"])), frame_start=frame_start, frame_end=frame_end, handle_start=handle_start, From 5ab274aa503cecc07a7289175b5c61f755c0aede Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Nov 2025 17:47:49 +0800 Subject: [PATCH 04/42] restore the instance data and adjust them into textureset collector in substance instead --- client/ayon_core/plugins/publish/extract_review.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index e519a4a97d..3f2a0dcd3e 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -572,9 +572,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # review output files "timecode": frame_to_timecode( frame=temp_data.frame_start_handle, - fps=float(instance.data.get( - "fps", instance.context.data["fps"] - )) + fps=float(instance.data["fps"]) ) }) @@ -667,12 +665,8 @@ class ExtractReview(pyblish.api.InstancePlugin): with values may be added. """ - frame_start = instance.data.get( - "frameStart", instance.context.data["frameStart"] - ) - frame_end = instance.data.get( - "frameEnd", instance.context.data["frameEnd"] - ) + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] # Try to get handles from instance handle_start = instance.data.get("handleStart") @@ -732,7 +726,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ext = os.path.splitext(repre["files"])[1].replace(".", "") return TempData( - fps=float(instance.data.get("fps", instance.context.data["fps"])), + fps=float(instance.data["fps"]), frame_start=frame_start, frame_end=frame_end, handle_start=handle_start, From c0fd2aa8c57b5ef17cfe38245f74f0b889243e51 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Nov 2025 18:10:52 +0800 Subject: [PATCH 05/42] add additional default settings into ExtractReview for substance painter --- server/settings/publish_plugins.py | 99 ++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index ee422a0acf..311b4672cf 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1448,6 +1448,105 @@ DEFAULT_PUBLISH_VALUES = { "fill_missing_frames": "closest_existing" } ] + }, + { + "product_types": [], + "hosts": ["substancepainter"], + "task_types": [], + "outputs": [ + { + "name": "png", + "ext": "png", + "tags": [ + "ftrackreview", + "kitsureview", + "webreview" + ], + "burnins": [], + "ffmpeg_args": { + "video_filters": [], + "audio_filters": [], + "input": [], + "output": [] + }, + "filter": { + "families": [ + "render", + "review", + "ftrack" + ], + "product_names": [], + "custom_tags": [], + "single_frame_filter": "single_frame" + }, + "overscan_crop": "", + # "overscan_color": [0, 0, 0], + "overscan_color": [0, 0, 0, 0.0], + "width": 1920, + "height": 1080, + "scale_pixel_aspect": True, + "bg_color": [0, 0, 0, 0.0], + "letter_box": { + "enabled": False, + "ratio": 0.0, + "fill_color": [0, 0, 0, 1.0], + "line_thickness": 0, + "line_color": [255, 0, 0, 1.0] + }, + "fill_missing_frames": "only_rendered" + }, + { + "name": "h264", + "ext": "mp4", + "tags": [ + "burnin", + "ftrackreview", + "kitsureview", + "webreview" + ], + "burnins": [], + "ffmpeg_args": { + "video_filters": [], + "audio_filters": [], + "input": [ + "-apply_trc gamma22" + ], + "output": [ + "-pix_fmt yuv420p", + "-crf 18", + "-c:a aac", + "-b:a 192k", + "-g 1", + "-movflags faststart" + ] + }, + "filter": { + "families": [ + "render", + "review", + "ftrack" + ], + "product_names": [], + "custom_tags": [], + "single_frame_filter": "multi_frame" + }, + "overscan_crop": "", + # "overscan_color": [0, 0, 0], + "overscan_color": [0, 0, 0, 0.0], + "width": 0, + "height": 0, + "scale_pixel_aspect": True, + "bg_color": [0, 0, 0, 0.0], + "letter_box": { + "enabled": False, + "ratio": 0.0, + "fill_color": [0, 0, 0, 1.0], + "line_thickness": 0, + "line_color": [255, 0, 0, 1.0] + }, + "fill_missing_frames": "only_rendered" + } + ] } ] }, From 67d5422c94584a100a1d3818137c4e16e06465ab Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 8 Nov 2025 22:45:42 +0100 Subject: [PATCH 06/42] If folder name starts with a digit we now prefix it with `_` to avoid invalid USD data to be authored. --- client/ayon_core/pipeline/usdlib.py | 17 +++++++++++++++++ .../publish/extract_usd_layer_contributions.py | 5 +++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/usdlib.py b/client/ayon_core/pipeline/usdlib.py index 2ff98c5e45..095f6fdc57 100644 --- a/client/ayon_core/pipeline/usdlib.py +++ b/client/ayon_core/pipeline/usdlib.py @@ -684,3 +684,20 @@ def get_sdf_format_args(path): """Return SDF_FORMAT_ARGS parsed to `dict`""" _raw_path, data = Sdf.Layer.SplitIdentifier(path) return data + + +def get_standard_default_prim_name(folder_path: str) -> str: + """Return the AYON-specified default prim name for a folder path. + + This is used e.g. for the default prim in AYON USD Contribution workflows. + """ + folder_name: str = folder_path.rsplit("/", 1)[-1] + + # Prim names are not allowed to start with a digit in USD. Authoring them + # would mean generating essentially garbage data and may result in + # unexpected behavior in certain USD or DCC versions, like failure to + # refresh in usdview or crashes in Houdini 21. + if folder_name and folder_name[0].isdigit(): + folder_name = f"_{folder_name}" + + return folder_name 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 9db8c49a02..c73d1aa447 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -25,7 +25,8 @@ try: variant_nested_prim_path, setup_asset_layer, add_ordered_sublayer, - set_layer_defaults + set_layer_defaults, + get_standard_default_prim_name ) except ImportError: pass @@ -652,7 +653,7 @@ class ExtractUSDLayerContribution(publish.Extractor): sdf_layer = Sdf.Layer.OpenAsAnonymous(path) default_prim = sdf_layer.defaultPrim else: - default_prim = folder_path.rsplit("/", 1)[-1] # use folder name + default_prim = get_standard_default_prim_name(folder_path) sdf_layer = Sdf.Layer.CreateAnonymous() set_layer_defaults(sdf_layer, default_prim=default_prim) From 113d01ce9952762a7dafca4960f80d899bdbc395 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 8 Nov 2025 22:54:24 +0100 Subject: [PATCH 07/42] Add setting to always enforce the default prim value --- .../extract_usd_layer_contributions.py | 10 +++++++++ server/settings/publish_plugins.py | 22 ++++++++++++++++--- 2 files changed, 29 insertions(+), 3 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 c73d1aa447..ff1c4fec8a 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -641,6 +641,7 @@ class ExtractUSDLayerContribution(publish.Extractor): settings_category = "core" use_ayon_entity_uri = False + enforce_default_prim = False def process(self, instance): @@ -651,6 +652,15 @@ class ExtractUSDLayerContribution(publish.Extractor): path = get_last_publish(instance) if path and BUILD_INTO_LAST_VERSIONS: sdf_layer = Sdf.Layer.OpenAsAnonymous(path) + + # If enabled in settings, ignore any default prim specified on + # older publish versions and always publish with the AYON + # standard default prim + if self.enforce_default_prim: + sdf_layer.defaultPrim = get_standard_default_prim_name( + folder_path + ) + default_prim = sdf_layer.defaultPrim else: default_prim = get_standard_default_prim_name(folder_path) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index ee422a0acf..60bd933344 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -251,6 +251,19 @@ class AyonEntityURIModel(BaseSettingsModel): ) +class ExtractUSDLayerContributionModel(AyonEntityURIModel): + enforce_default_prim: bool = SettingsField( + title="Always set default prim to folder name.", + description=( + "When enabled ignore any default prim specified on older " + "published versions of a layer and always override it to the " + "AYON standard default prim. When disabled, preserve default prim " + "on the layer and then only the initial version would be setting " + "the AYON standard default prim." + ) + ) + + class PluginStateByHostModelProfile(BaseSettingsModel): _layout = "expanded" # Filtering @@ -1134,9 +1147,11 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=AyonEntityURIModel, title="Extract USD Asset Contribution", ) - ExtractUSDLayerContribution: AyonEntityURIModel = SettingsField( - default_factory=AyonEntityURIModel, - title="Extract USD Layer Contribution", + ExtractUSDLayerContribution: ExtractUSDLayerContributionModel = ( + SettingsField( + default_factory=ExtractUSDLayerContributionModel, + title="Extract USD Layer Contribution", + ) ) PreIntegrateThumbnails: PreIntegrateThumbnailsModel = SettingsField( default_factory=PreIntegrateThumbnailsModel, @@ -1526,6 +1541,7 @@ DEFAULT_PUBLISH_VALUES = { }, "ExtractUSDLayerContribution": { "use_ayon_entity_uri": False, + "enforce_default_prim": False, }, "PreIntegrateThumbnails": { "enabled": True, From e7896c66f3b513ae1f024259943ad4cf3f60fb09 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 9 Nov 2025 11:33:33 +0100 Subject: [PATCH 08/42] Update server/settings/publish_plugins.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- 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 60bd933344..e939a6518b 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -256,7 +256,7 @@ class ExtractUSDLayerContributionModel(AyonEntityURIModel): title="Always set default prim to folder name.", description=( "When enabled ignore any default prim specified on older " - "published versions of a layer and always override it to the " + "published versions of a layer and always override it to the " "AYON standard default prim. When disabled, preserve default prim " "on the layer and then only the initial version would be setting " "the AYON standard default prim." From f4824cdc426c47f5db65d4ac417d1328259d0cb4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 12 Nov 2025 14:41:24 +0100 Subject: [PATCH 09/42] Allow creation of farm instances without colorspace data --- .../pipeline/farm/pyblish_functions.py | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 2193e96cb1..5e632c3599 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -591,22 +591,6 @@ def create_instances_for_aov( # AOV product of its own. log = Logger.get_logger("farm_publishing") - additional_color_data = { - "renderProducts": instance.data["renderProducts"], - "colorspaceConfig": instance.data["colorspaceConfig"], - "display": instance.data["colorspaceDisplay"], - "view": instance.data["colorspaceView"] - } - - # Get templated path from absolute config path. - anatomy = instance.context.data["anatomy"] - colorspace_template = instance.data["colorspaceConfig"] - try: - additional_color_data["colorspaceTemplate"] = remap_source( - colorspace_template, anatomy) - except ValueError as e: - log.warning(e) - additional_color_data["colorspaceTemplate"] = colorspace_template # if there are product to attach to and more than one AOV, # we cannot proceed. @@ -618,6 +602,29 @@ def create_instances_for_aov( "attaching multiple AOVs or renderable cameras to " "product is not supported yet.") + additional_data = { + "renderProducts": instance.data["renderProducts"], + } + + # Collect color management data if present + if "colorspaceConfig" in instance.data: + additional_data.update({ + "colorspaceConfig": instance.data["colorspaceConfig"], + # Display/View are optional + "display": instance.data.get("colorspaceDisplay"), + "view": instance.data.get("colorspaceView") + }) + + # Get templated path from absolute config path. + anatomy = instance.context.data["anatomy"] + colorspace_template = instance.data["colorspaceConfig"] + try: + additional_data["colorspaceTemplate"] = remap_source( + colorspace_template, anatomy) + except ValueError as e: + log.warning(e) + additional_data["colorspaceTemplate"] = colorspace_template + # create instances for every AOV we found in expected files. # NOTE: this is done for every AOV and every render camera (if # there are multiple renderable cameras in scene) @@ -625,7 +632,7 @@ def create_instances_for_aov( instance, skeleton, aov_filter, - additional_color_data, + additional_data, skip_integration_repre_list, do_not_add_review, frames_to_render @@ -936,16 +943,28 @@ def _create_instances_for_aov( "stagingDir": staging_dir, "fps": new_instance.get("fps"), "tags": ["review"] if preview else [], - "colorspaceData": { + } + + if colorspace and additional_data["colorspaceConfig"]: + # Only apply colorspace data if the image has a colorspace + colorspace_data: dict = { "colorspace": colorspace, "config": { "path": additional_data["colorspaceConfig"], "template": additional_data["colorspaceTemplate"] }, - "display": additional_data["display"], - "view": additional_data["view"] } - } + # Display/View are optional + display = additional_data.get("display") + if display: + additional_data["display"] = display + view = additional_data.get("view") + if view: + additional_data["view"] = view + + rep["colorspaceData"] = colorspace_data + else: + log.debug("No colorspace data for representation: {}".format(rep)) # support conversion from tiled to scanline if instance.data.get("convertToScanline"): From 2cdcfa3f22278623c2dcdbb171359484a781f22d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Nov 2025 10:49:55 +0100 Subject: [PATCH 10/42] store host name to version entity data --- client/ayon_core/plugins/publish/integrate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index d18e546392..6182598e14 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -457,6 +457,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): else: version_data[key] = value + host_name = instance.context.data["hostName"] + version_data["host_name"] = host_name + version_entity = new_version_entity( version_number, product_entity["id"], From 0262a8e7630080a5c4d8e5a64a729febb2dee3c5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 13 Nov 2025 16:33:00 +0100 Subject: [PATCH 11/42] Apply suggestion from @BigRoy --- client/ayon_core/pipeline/farm/pyblish_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 5e632c3599..6d116dcece 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -957,10 +957,10 @@ def _create_instances_for_aov( # Display/View are optional display = additional_data.get("display") if display: - additional_data["display"] = display + colorspace_data["display"] = display view = additional_data.get("view") if view: - additional_data["view"] = view + colorspace_data["view"] = view rep["colorspaceData"] = colorspace_data else: From f29470a08ca34d467c1070e9b05ec08b6fcc1f26 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 13 Nov 2025 16:34:08 +0100 Subject: [PATCH 12/42] Apply suggestion from @iLLiCiTiT Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/farm/pyblish_functions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 6d116dcece..265d79b53e 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -607,9 +607,10 @@ def create_instances_for_aov( } # Collect color management data if present - if "colorspaceConfig" in instance.data: + colorspace_config = instance.data.get("colorspaceConfig") + if colorspace_config: additional_data.update({ - "colorspaceConfig": instance.data["colorspaceConfig"], + "colorspaceConfig": colorspace_config, # Display/View are optional "display": instance.data.get("colorspaceDisplay"), "view": instance.data.get("colorspaceView") @@ -617,13 +618,12 @@ def create_instances_for_aov( # Get templated path from absolute config path. anatomy = instance.context.data["anatomy"] - colorspace_template = instance.data["colorspaceConfig"] try: additional_data["colorspaceTemplate"] = remap_source( - colorspace_template, anatomy) + colorspace_config, anatomy) except ValueError as e: log.warning(e) - additional_data["colorspaceTemplate"] = colorspace_template + additional_data["colorspaceTemplate"] = colorspace_config # create instances for every AOV we found in expected files. # NOTE: this is done for every AOV and every render camera (if From aea231d64e6a07258ee106ec7043967630e4e21c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 16 Nov 2025 22:20:57 +0100 Subject: [PATCH 13/42] Also prefix folder name with `_` for initializing the asset layer --- .../plugins/publish/extract_usd_layer_contributions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 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 ff1c4fec8a..4d8a8005f2 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -821,7 +821,7 @@ class ExtractUSDAssetContribution(publish.Extractor): folder_path = instance.data["folderPath"] product_name = instance.data["productName"] self.log.debug(f"Building asset: {folder_path} > {product_name}") - folder_name = folder_path.rsplit("/", 1)[-1] + asset_name = get_standard_default_prim_name(folder_path) # Contribute layers to asset # Use existing asset and add to it, or initialize a new asset layer @@ -840,7 +840,7 @@ class ExtractUSDAssetContribution(publish.Extractor): # the layer as either a default asset or shot structure. init_type = instance.data["contribution_target_product_init"] asset_layer, payload_layer = self.init_layer( - asset_name=folder_name, init_type=init_type + asset_name=asset_name, init_type=init_type ) # Author timeCodesPerSecond and framesPerSecond if the asset layer From b307cc6227db88347d70bdb149076d9c1ae66e41 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 16 Nov 2025 22:40:23 +0100 Subject: [PATCH 14/42] Run plug-in for all hosts --- .../publish/collect_scene_loaded_versions.py | 22 ------------------- 1 file changed, 22 deletions(-) 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 1c28c28f5b..5370b52492 100644 --- a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py +++ b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py @@ -9,28 +9,6 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.0001 label = "Collect Versions Loaded in Scene" - hosts = [ - "aftereffects", - "blender", - "celaction", - "cinema4d", - "flame", - "fusion", - "harmony", - "hiero", - "houdini", - "max", - "maya", - "motionbuilder", - "nuke", - "photoshop", - "silhouette", - "substancepainter", - "substancedesigner", - "resolve", - "tvpaint", - "zbrush", - ] def process(self, context): host = registered_host() From 1c25e357776f0a3a04686ffc96439f6b567635e4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 17 Nov 2025 12:18:07 +0100 Subject: [PATCH 15/42] Fix Context card being clickable in Nuke 14/15 only outside the Context label area. Previously you could only click on the far left or far right side of the context card to be able to select it and access the Context attributes. Cosmetically the removal of the `` doesn't do much to the Context card because it doesn't have a sublabel. --- client/ayon_core/tools/publisher/widgets/card_view_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index ca95b1ff1a..aef3f85e0c 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -211,7 +211,7 @@ class ContextCardWidget(CardWidget): icon_widget = PublishPixmapLabel(None, self) icon_widget.setObjectName("ProductTypeIconLabel") - label_widget = QtWidgets.QLabel(f"{CONTEXT_LABEL}", self) + label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self) icon_layout = QtWidgets.QHBoxLayout() icon_layout.setContentsMargins(5, 5, 5, 5) From 335f9cf21b7ccba2ca6674081c8b9eb6e11b9f3f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 17 Nov 2025 14:39:27 +0100 Subject: [PATCH 16/42] Implement generic ExtractOIIOPostProcess plug-in. This can be used to take any image representation through `oiiotool` to process with settings-defined arguments, to e.g. resize an image, convert all layers to scanline, etc. --- .../publish/extract_oiio_postprocess.py | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 client/ayon_core/plugins/publish/extract_oiio_postprocess.py diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py new file mode 100644 index 0000000000..6163eb98d2 --- /dev/null +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -0,0 +1,322 @@ +import os +import copy +import clique +import pyblish.api + +from ayon_core.pipeline import ( + publish, + get_temp_dir +) +from ayon_core.lib import ( + is_oiio_supported, + get_oiio_tool_args, + run_subprocess +) +from ayon_core.lib.profiles_filtering import filter_profiles + + +class ExtractOIIOPostProcess(publish.Extractor): + """Process representations through `oiiotool` with profile defined + settings so that e.g. color space conversions can be applied or images + could be converted to scanline, resized, etc. regardless of colorspace + data. + """ + + label = "OIIO Post Process" + order = pyblish.api.ExtractorOrder + 0.020 + + settings_category = "core" + + optional = True + + # Supported extensions + supported_exts = {"exr", "jpg", "jpeg", "png", "dpx"} + + # Configurable by Settings + profiles = None + options = None + + def process(self, instance): + if not self.profiles: + self.log.debug("No profiles present for OIIO Post Process") + return + + if "representations" not in instance.data: + self.log.debug("No representations, skipping.") + return + + if not is_oiio_supported(): + self.log.warning("OIIO not supported, no transcoding possible.") + return + + profile, representations = self._get_profile( + instance + ) + if not profile: + return + + profile_output_defs = profile["outputs"] + new_representations = [] + for idx, repre in enumerate(list(instance.data["representations"])): + self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) + if not self._repre_is_valid(repre, profile): + continue + + # Get representation files to convert + if isinstance(repre["files"], list): + repre_files_to_convert = copy.deepcopy(repre["files"]) + else: + repre_files_to_convert = [repre["files"]] + + added_representations = False + added_review = False + + # Process each output definition + for output_def in profile_output_defs: + + # Local copy to avoid accidental mutable changes + files_to_convert = list(repre_files_to_convert) + + output_name = output_def["name"] + new_repre = copy.deepcopy(repre) + + original_staging_dir = new_repre["stagingDir"] + new_staging_dir = get_temp_dir( + project_name=instance.context.data["projectName"], + use_local_temp=True, + ) + new_repre["stagingDir"] = new_staging_dir + + output_extension = output_def["extension"] + output_extension = output_extension.replace('.', '') + self._rename_in_representation(new_repre, + files_to_convert, + output_name, + output_extension) + + sequence_files = self._translate_to_sequence(files_to_convert) + self.log.debug("Files to convert: {}".format(sequence_files)) + for file_name in sequence_files: + if isinstance(file_name, clique.Collection): + # Convert to filepath that can be directly converted + # by oiio like `frame.1001-1025%04d.exr` + file_name: str = file_name.format( + "{head}{range}{padding}{tail}" + ) + + self.log.debug("Transcoding file: `{}`".format(file_name)) + input_path = os.path.join(original_staging_dir, + file_name) + output_path = self._get_output_file_path(input_path, + new_staging_dir, + output_extension) + + # TODO: Support formatting with dynamic keys from the + # representation, like e.g. colorspace config, display, + # view, etc. + input_arguments: list[str] = output_def.get( + "input_arguments", [] + ) + output_arguments: list[str] = output_def.get( + "output_arguments", [] + ) + + # Prepare subprocess arguments + oiio_cmd = get_oiio_tool_args( + "oiiotool", + *input_arguments, + input_path, + *output_arguments, + "-o", + output_path + ) + + self.log.debug( + "Conversion command: {}".format(" ".join(oiio_cmd))) + run_subprocess(oiio_cmd, logger=self.log) + + # cleanup temporary transcoded files + for file_name in new_repre["files"]: + transcoded_file_path = os.path.join(new_staging_dir, + file_name) + instance.context.data["cleanupFullPaths"].append( + transcoded_file_path) + + custom_tags = output_def.get("custom_tags") + if custom_tags: + if new_repre.get("custom_tags") is None: + new_repre["custom_tags"] = [] + new_repre["custom_tags"].extend(custom_tags) + + # Add additional tags from output definition to representation + if new_repre.get("tags") is None: + new_repre["tags"] = [] + for tag in output_def["tags"]: + if tag not in new_repre["tags"]: + new_repre["tags"].append(tag) + + if tag == "review": + added_review = True + + # If there is only 1 file outputted then convert list to + # string, because that'll indicate that it is not a sequence. + if len(new_repre["files"]) == 1: + new_repre["files"] = new_repre["files"][0] + + # If the source representation has "review" tag, but it's not + # part of the output definition tags, then both the + # representations will be transcoded in ExtractReview and + # their outputs will clash in integration. + if "review" in repre.get("tags", []): + added_review = True + + new_representations.append(new_repre) + added_representations = True + + if added_representations: + self._mark_original_repre_for_deletion( + repre, profile, added_review + ) + + tags = repre.get("tags") or [] + if "delete" in tags and "thumbnail" not in tags: + instance.data["representations"].remove(repre) + + instance.data["representations"].extend(new_representations) + + def _rename_in_representation(self, new_repre, files_to_convert, + output_name, output_extension): + """Replace old extension with new one everywhere in representation. + + Args: + new_repre (dict) + files_to_convert (list): of filenames from repre["files"], + standardized to always list + output_name (str): key of output definition from Settings, + if "" token used, keep original repre name + output_extension (str): extension from output definition + """ + if output_name != "passthrough": + new_repre["name"] = output_name + if not output_extension: + return + + new_repre["ext"] = output_extension + new_repre["outputName"] = output_name + + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(file_name) + new_repre["files"] = renamed_files + + def _translate_to_sequence(self, files_to_convert): + """Returns original list or a clique.Collection of a sequence. + + Uses clique to find frame sequence Collection. + If sequence not found, it returns original list. + + Args: + files_to_convert (list): list of file names + Returns: + list[str | clique.Collection]: List of filepaths or a list + of Collections (usually one, unless there are holes) + """ + pattern = [clique.PATTERNS["frames"]] + collections, _ = clique.assemble( + files_to_convert, patterns=pattern, + assume_padded_when_ambiguous=True) + if collections: + if len(collections) > 1: + raise ValueError( + "Too many collections {}".format(collections)) + + collection = collections[0] + # TODO: Technically oiiotool supports holes in the sequence as well + # using the dedicated --frames argument to specify the frames. + # We may want to use that too so conversions of sequences with + # holes will perform faster as well. + # Separate the collection so that we have no holes/gaps per + # collection. + return collection.separate() + + return files_to_convert + + def _get_output_file_path(self, input_path, output_dir, + output_extension): + """Create output file name path.""" + file_name = os.path.basename(input_path) + file_name, input_extension = os.path.splitext(file_name) + if not output_extension: + output_extension = input_extension.replace(".", "") + new_file_name = '{}.{}'.format(file_name, + output_extension) + return os.path.join(output_dir, new_file_name) + + def _get_profile(self, instance): + """Returns profile if it should process this instance.""" + 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 profile + + def _repre_is_valid(self, repre) -> bool: + """Validation if representation should be processed. + + Args: + repre (dict): Representation which should be checked. + + Returns: + bool: False if can't be processed else True. + """ + if repre.get("ext") not in self.supported_exts: + self.log.debug(( + "Representation '{}' has unsupported extension: '{}'. Skipped." + ).format(repre["name"], repre.get("ext"))) + return False + + if not repre.get("files"): + self.log.debug(( + "Representation '{}' has empty files. Skipped." + ).format(repre["name"])) + return False + + return True + + def _mark_original_repre_for_deletion(self, repre, profile, added_review): + """If new transcoded representation created, delete old.""" + if not repre.get("tags"): + repre["tags"] = [] + + delete_original = profile["delete_original"] + + if delete_original: + if "delete" not in repre["tags"]: + repre["tags"].append("delete") + + if added_review and "review" in repre["tags"]: + repre["tags"].remove("review") From a6ecea872efda602b2fe0b157924d893d6611192 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 17 Nov 2025 14:47:25 +0100 Subject: [PATCH 17/42] Add missing changes --- .../publish/extract_oiio_postprocess.py | 4 +- server/settings/publish_plugins.py | 111 ++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 6163eb98d2..2e93c68283 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -49,7 +49,7 @@ class ExtractOIIOPostProcess(publish.Extractor): self.log.warning("OIIO not supported, no transcoding possible.") return - profile, representations = self._get_profile( + profile = self._get_profile( instance ) if not profile: @@ -59,7 +59,7 @@ class ExtractOIIOPostProcess(publish.Extractor): new_representations = [] for idx, repre in enumerate(list(instance.data["representations"])): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - if not self._repre_is_valid(repre, profile): + if not self._repre_is_valid(repre): continue # Get representation files to convert diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index ee422a0acf..173526e13f 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -565,12 +565,115 @@ class ExtractOIIOTranscodeProfileModel(BaseSettingsModel): class ExtractOIIOTranscodeModel(BaseSettingsModel): + """Color conversion transcoding using OIIO for images mostly aimed at + transcoding for reviewables (it'll process and output only RGBA channels). + """ enabled: bool = SettingsField(True) profiles: list[ExtractOIIOTranscodeProfileModel] = SettingsField( default_factory=list, title="Profiles" ) +class ExtractOIIOPostProcessOutputModel(BaseSettingsModel): + _layout = "expanded" + name: str = SettingsField( + "", + title="Name", + description="Output name (no space)", + regex=r"[a-zA-Z0-9_]([a-zA-Z0-9_\.\-]*[a-zA-Z0-9_])?$", + ) + extension: str = SettingsField( + "", + title="Extension", + description=( + "Target extension. If left empty, original" + " extension is used." + ), + ) + input_arguments: list[str] = SettingsField( + default_factory=list, + title="Input arguments", + description="Arguments passed prior to the input file argument.", + ) + output_arguments: list[str] = SettingsField( + default_factory=list, + title="Output arguments", + description="Arguments passed prior to the -o argument.", + ) + tags: list[str] = SettingsField( + default_factory=list, + title="Tags", + description=( + "Additional tags that will be added to the created representation." + "\nAdd *review* tag to create review from the transcoded" + " representation instead of the original." + ) + ) + custom_tags: list[str] = SettingsField( + default_factory=list, + title="Custom Tags", + description=( + "Additional custom tags that will be added" + " to the created representation." + ) + ) + + +class ExtractOIIOPostProcessProfileModel(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" + ) + delete_original: bool = SettingsField( + True, + title="Delete Original Representation", + description=( + "Choose to preserve or remove the original representation.\n" + "Keep in mind that if the transcoded representation includes" + " a `review` tag, it will take precedence over" + " the original for creating reviews." + ), + ) + outputs: list[ExtractOIIOPostProcessOutputModel] = SettingsField( + default_factory=list, + title="Output Definitions", + ) + + @validator("outputs") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +class ExtractOIIOPostProcessModel(BaseSettingsModel): + """Process representation images with `oiiotool` on publish. + + This could be used to convert images to different formats, convert to + scanline images or flatten deep images. + """ + enabled: bool = SettingsField(True) + profiles: list[ExtractOIIOPostProcessProfileModel] = SettingsField( + default_factory=list, title="Profiles" + ) + + # --- [START] Extract Review --- class ExtractReviewFFmpegModel(BaseSettingsModel): video_filters: list[str] = SettingsField( @@ -1122,6 +1225,10 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ExtractOIIOTranscodeModel, title="Extract OIIO Transcode" ) + ExtractOIIOPostProcess: ExtractOIIOPostProcessModel = SettingsField( + default_factory=ExtractOIIOPostProcessModel, + title="Extract OIIO Post Process" + ) ExtractReview: ExtractReviewModel = SettingsField( default_factory=ExtractReviewModel, title="Extract Review" @@ -1347,6 +1454,10 @@ DEFAULT_PUBLISH_VALUES = { "enabled": True, "profiles": [] }, + "ExtractOIIOPostProcess": { + "enabled": True, + "profiles": [] + }, "ExtractReview": { "enabled": True, "profiles": [ From 82128c30c5d8a1e12939ffd3db09002f3c87b9d6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 17 Nov 2025 14:55:23 +0100 Subject: [PATCH 18/42] Disable text interaction instead --- .../ayon_core/tools/publisher/widgets/card_view_widgets.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index aef3f85e0c..a9abd56584 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -211,7 +211,12 @@ class ContextCardWidget(CardWidget): icon_widget = PublishPixmapLabel(None, self) icon_widget.setObjectName("ProductTypeIconLabel") - label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self) + label_widget = QtWidgets.QLabel(f"{CONTEXT_LABEL}", self) + # HTML text will cause that label start catch mouse clicks + # - disabling with changing interaction flag + label_widget.setTextInteractionFlags( + QtCore.Qt.NoTextInteraction + ) icon_layout = QtWidgets.QHBoxLayout() icon_layout.setContentsMargins(5, 5, 5, 5) From 3b86b3612823c4ab4cc75feabaf2843a21602359 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 18 Nov 2025 13:05:19 +0000 Subject: [PATCH 19/42] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index da0cbff11d..ebf7e34a32 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.9+dev" +__version__ = "1.6.10" diff --git a/package.py b/package.py index 99524be8aa..461e09c38a 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.9+dev" +version = "1.6.10" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index f69f4f843a..798405d3a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.9+dev" +version = "1.6.10" description = "" authors = ["Ynput Team "] readme = "README.md" From 90eef3f6b7cf9485f8385b2c94bc1c7929ebdf82 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 18 Nov 2025 13:05:58 +0000 Subject: [PATCH 20/42] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index ebf7e34a32..a220d400d4 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.10" +__version__ = "1.6.10+dev" diff --git a/package.py b/package.py index 461e09c38a..bb9c1b0c7e 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.10" +version = "1.6.10+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 798405d3a8..e61c2708de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.10" +version = "1.6.10+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 55f7ff6a46c2fe49f9398582bc2fba3b7a42c685 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Nov 2025 13:06:52 +0000 Subject: [PATCH 21/42] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e48e4b3b29..78e86f43e4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.6.10 - 1.6.9 - 1.6.8 - 1.6.7 From 2375dda43bb72ac8c5396423a8b76ec35c41878f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 18 Nov 2025 17:10:38 +0200 Subject: [PATCH 22/42] Add `folderpaths` for template profile filtering --- .../pipeline/workfile/workfile_template_builder.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 52e27baa80..9c77d2f7f7 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -832,14 +832,18 @@ class AbstractTemplateBuilder(ABC): host_name = self.host_name task_name = self.current_task_name task_type = self.current_task_type + folder_path = self.current_folder_path build_profiles = self._get_build_profiles() + filter_data = { + "task_types": task_type, + "task_names": task_name, + "folder_paths": folder_path + } profile = filter_profiles( build_profiles, - { - "task_types": task_type, - "task_names": task_name - } + filter_data, + logger=self.log ) if not profile: raise TemplateProfileNotFound(( From 2af5e918a745075c814c9b4d8dec08b3bfe5862b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:11:50 +0100 Subject: [PATCH 23/42] safe guard downloading of image for ayon_url --- client/ayon_core/tools/utils/lib.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/utils/lib.py b/client/ayon_core/tools/utils/lib.py index e087112a04..3308b943f0 100644 --- a/client/ayon_core/tools/utils/lib.py +++ b/client/ayon_core/tools/utils/lib.py @@ -548,11 +548,17 @@ class _IconsCache: elif icon_type == "ayon_url": url = icon_def["url"].lstrip("/") url = f"{ayon_api.get_base_url()}/{url}" - stream = io.BytesIO() - ayon_api.download_file_to_stream(url, stream) - pix = QtGui.QPixmap() - pix.loadFromData(stream.getvalue()) - icon = QtGui.QIcon(pix) + try: + stream = io.BytesIO() + ayon_api.download_file_to_stream(url, stream) + pix = QtGui.QPixmap() + pix.loadFromData(stream.getvalue()) + icon = QtGui.QIcon(pix) + except Exception: + log.warning( + "Failed to download image '%s'", url, exc_info=True + ) + icon = None elif icon_type == "transparent": size = icon_def.get("size") From 17f5788e43e980d308ef7f3442d59a05cbe7a216 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 19 Nov 2025 12:56:01 +0200 Subject: [PATCH 24/42] Template Builder: Add folder types to filter data --- .../pipeline/workfile/workfile_template_builder.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 9c77d2f7f7..25b1c6737b 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -834,12 +834,17 @@ class AbstractTemplateBuilder(ABC): task_type = self.current_task_type folder_path = self.current_folder_path - build_profiles = self._get_build_profiles() filter_data = { "task_types": task_type, "task_names": task_name, "folder_paths": folder_path } + + folder_entity = self.current_folder_entity + if folder_entity: + filter_data.update({"folder_types": folder_entity["folderType"]}) + + build_profiles = self._get_build_profiles() profile = filter_profiles( build_profiles, filter_data, From 6125a7db803dd2bfe878fa0fe255822b9a0655c6 Mon Sep 17 00:00:00 2001 From: Mustafa Zaky Jafar Date: Wed, 19 Nov 2025 13:15:37 +0200 Subject: [PATCH 25/42] Update client/ayon_core/pipeline/workfile/workfile_template_builder.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../pipeline/workfile/workfile_template_builder.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index dfb17b850f..6c0bc4c602 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -841,16 +841,18 @@ class AbstractTemplateBuilder(ABC): task_name = self.current_task_name task_type = self.current_task_type folder_path = self.current_folder_path + folder_type = None + folder_entity = self.current_folder_entity + if folder_entity: + folder_type = folder_entity["folderType"] filter_data = { "task_types": task_type, "task_names": task_name, - "folder_paths": folder_path + "folder_types": folder_type, + "folder_paths": folder_path, } - folder_entity = self.current_folder_entity - if folder_entity: - filter_data.update({"folder_types": folder_entity["folderType"]}) build_profiles = self._get_build_profiles() profile = filter_profiles( From 42da0fb424a0b3c07b2da8b38897851ebc025b61 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 19 Nov 2025 13:17:12 +0200 Subject: [PATCH 26/42] Make Ruff Happy. --- client/ayon_core/pipeline/workfile/workfile_template_builder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 6c0bc4c602..cbe9f82a81 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -853,7 +853,6 @@ class AbstractTemplateBuilder(ABC): "folder_paths": folder_path, } - build_profiles = self._get_build_profiles() profile = filter_profiles( build_profiles, From 17925292670462c518664d7fc2fb41b20c50c9db Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Nov 2025 16:27:42 +0100 Subject: [PATCH 27/42] Safe-guard workfile template builder for versionless products --- client/ayon_core/pipeline/workfile/workfile_template_builder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index cbe9f82a81..2f9e7250c0 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -1687,6 +1687,8 @@ class PlaceholderLoadMixin(object): for version in get_last_versions( project_name, filtered_product_ids, fields={"id"} ).values() + # Version may be none if a product has no versions + if version is not None ) return list(get_representations( project_name, From 626f627f581793749b6b74ccd98f8d733a23ee56 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:59:51 +0100 Subject: [PATCH 28/42] fix product types fetching in loader tool --- .../ayon_core/tools/loader/models/products.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index 7915a75bcf..83a017613d 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Iterable, Optional import arrow import ayon_api +from ayon_api.graphql_queries import project_graphql_query from ayon_api.operations import OperationsSession from ayon_core.lib import NestedCacheItem @@ -202,7 +203,7 @@ class ProductsModel: cache = self._product_type_items_cache[project_name] if not cache.is_valid: icons_mapping = self._get_product_type_icons(project_name) - product_types = ayon_api.get_project_product_types(project_name) + product_types = self._get_project_product_types(project_name) cache.update_data([ ProductTypeItem( product_type["name"], @@ -462,6 +463,24 @@ class ProductsModel: PRODUCTS_MODEL_SENDER ) + def _get_project_product_types(self, project_name: str) -> list[dict]: + """This is a temporary solution for product types fetching. + + There was a bug in ayon_api.get_project(...) which did not use GraphQl + but REST instead. That is fixed in ayon-python-api 1.2.6 that will + be as part of ayon launcher 1.4.3 release. + + """ + if not project_name: + return [] + query = project_graphql_query({"productTypes.name"}) + query.set_variable_value("projectName", project_name) + parsed_data = query.query(ayon_api.get_server_api_connection()) + project = parsed_data["project"] + if project is None: + return [] + return project["productTypes"] + def _get_product_type_icons( self, project_name: Optional[str] ) -> ProductTypeIconMapping: From cebf3be97fd2dc4f7f2192ad08e7001359a41917 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 20 Nov 2025 14:37:22 +0000 Subject: [PATCH 29/42] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index a220d400d4..5c9b1cdce8 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.10+dev" +__version__ = "1.6.11" diff --git a/package.py b/package.py index bb9c1b0c7e..9bdf309bab 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.10+dev" +version = "1.6.11" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index e61c2708de..e30eb1d5d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.10+dev" +version = "1.6.11" description = "" authors = ["Ynput Team "] readme = "README.md" From 1ac26453d5f926a6692ca4127dc6820899d0eecb Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 20 Nov 2025 14:38:01 +0000 Subject: [PATCH 30/42] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 5c9b1cdce8..a3e1a6c939 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.11" +__version__ = "1.6.11+dev" diff --git a/package.py b/package.py index 9bdf309bab..62231060f0 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.11" +version = "1.6.11+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index e30eb1d5d9..d568edefc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.11" +version = "1.6.11+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 5bccc7cf2bcdc51e446cf7794f09d6081ff7255a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 20 Nov 2025 14:39:23 +0000 Subject: [PATCH 31/42] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 78e86f43e4..7fc253b1b8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.6.11 - 1.6.10 - 1.6.9 - 1.6.8 From 73dfff9191e4ba5d159d2d495c71950d60389236 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 22 Nov 2025 13:28:08 +0100 Subject: [PATCH 32/42] Fix call to `get_representation_path_by_names` --- .../plugins/publish/extract_usd_layer_contributions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 4d8a8005f2..ec6b2b9792 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -177,7 +177,10 @@ def get_instance_uri_path( # If for whatever reason we were unable to retrieve from the context # then get the path from an existing database entry - path = get_representation_path_by_names(**query) + path = get_representation_path_by_names( + anatomy=context.data["anatomy"], + **names + ) # Ensure `None` for now is also a string path = str(path) From 70bf746c7acac36d8cf3670843b862cf08d82537 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 22 Nov 2025 13:29:49 +0100 Subject: [PATCH 33/42] Fail clearly if the path can't be resolved instead of setting path to "None" --- .../plugins/publish/extract_usd_layer_contributions.py | 2 ++ 1 file changed, 2 insertions(+) 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 ec6b2b9792..d5c5aca0f2 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -181,6 +181,8 @@ def get_instance_uri_path( anatomy=context.data["anatomy"], **names ) + if not path: + raise RuntimeError(f"Unable to resolve publish path for: {names}") # Ensure `None` for now is also a string path = str(path) From 344f91c983256d5a29641c5e809761ab39b60f64 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 26 Nov 2025 00:38:37 +0100 Subject: [PATCH 34/42] Make settings profiles more granular for OIIO post process --- .../publish/extract_oiio_postprocess.py | 46 +++++++++++++------ server/settings/publish_plugins.py | 10 ++++ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 2e93c68283..610f464989 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -1,3 +1,5 @@ +from __future__ import annotations +from typing import Any, Optional import os import copy import clique @@ -49,19 +51,21 @@ class ExtractOIIOPostProcess(publish.Extractor): self.log.warning("OIIO not supported, no transcoding possible.") return - profile = self._get_profile( - instance - ) - if not profile: - return - - profile_output_defs = profile["outputs"] new_representations = [] for idx, repre in enumerate(list(instance.data["representations"])): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) if not self._repre_is_valid(repre): continue + # We check profile per representation name and extension because + # it's included in the profile check. As such, an instance may have + # a different profile applied per representation. + profile = self._get_profile( + instance + ) + if not profile: + continue + # Get representation files to convert if isinstance(repre["files"], list): repre_files_to_convert = copy.deepcopy(repre["files"]) @@ -72,7 +76,7 @@ class ExtractOIIOPostProcess(publish.Extractor): added_review = False # Process each output definition - for output_def in profile_output_defs: + for output_def in profile["outputs"]: # Local copy to avoid accidental mutable changes files_to_convert = list(repre_files_to_convert) @@ -255,7 +259,7 @@ class ExtractOIIOPostProcess(publish.Extractor): output_extension) return os.path.join(output_dir, new_file_name) - def _get_profile(self, instance): + def _get_profile(self, instance: pyblish.api.Instance, repre: dict) -> Optional[dict[str, Any]]: """Returns profile if it should process this instance.""" host_name = instance.context.data["hostName"] product_type = instance.data["productType"] @@ -263,24 +267,30 @@ class ExtractOIIOPostProcess(publish.Extractor): task_data = instance.data["anatomyData"].get("task", {}) task_name = task_data.get("name") task_type = task_data.get("type") + repre_name: str = repre["name"] + repre_ext: str = repre["ext"] filtering_criteria = { "hosts": host_name, "product_types": product_type, "product_names": product_name, "task_names": task_name, "task_types": task_type, + "representation_names": repre_name, + "representation_exts": repre_ext, } profile = filter_profiles(self.profiles, filtering_criteria, logger=self.log) if not profile: - self.log.debug(( + 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 - )) + f" Host: \"{host_name}\" |" + f" Product types: \"{product_type}\" |" + f" Product names: \"{product_name}\" |" + f" Task name \"{task_name}\" |" + f" Task type \"{task_type}\" |" + f" Representation: \"{repre_name}\" (.{repre_ext})" + ) return profile @@ -305,6 +315,12 @@ class ExtractOIIOPostProcess(publish.Extractor): ).format(repre["name"])) return False + if "delete" in repre.get("tags", []): + self.log.debug(( + "Representation '{}' has 'delete' tag. Skipped." + ).format(repre["name"])) + return False + return True def _mark_original_repre_for_deletion(self, repre, profile, added_review): diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 173526e13f..2c133ddbbf 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -621,6 +621,7 @@ class ExtractOIIOPostProcessOutputModel(BaseSettingsModel): class ExtractOIIOPostProcessProfileModel(BaseSettingsModel): product_types: list[str] = SettingsField( + section="Profile", default_factory=list, title="Product types" ) @@ -641,6 +642,14 @@ class ExtractOIIOPostProcessProfileModel(BaseSettingsModel): default_factory=list, title="Product names" ) + representation_names: list[str] = SettingsField( + default_factory=list, + title="Representation names", + ) + representation_exts: list[str] = SettingsField( + default_factory=list, + title="Representation extensions", + ) delete_original: bool = SettingsField( True, title="Delete Original Representation", @@ -650,6 +659,7 @@ class ExtractOIIOPostProcessProfileModel(BaseSettingsModel): " a `review` tag, it will take precedence over" " the original for creating reviews." ), + section="Conversion Outputs", ) outputs: list[ExtractOIIOPostProcessOutputModel] = SettingsField( default_factory=list, From 4f332766f0ccc9071d6f189abce8ed9fe67e9e6a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 26 Nov 2025 10:22:17 +0100 Subject: [PATCH 35/42] Use `IMAGE_EXTENSIONS` --- client/ayon_core/plugins/publish/extract_oiio_postprocess.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 610f464989..33384e0185 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -14,6 +14,7 @@ from ayon_core.lib import ( get_oiio_tool_args, run_subprocess ) +from ayon_core.lib.transcoding import IMAGE_EXTENSIONS from ayon_core.lib.profiles_filtering import filter_profiles @@ -32,7 +33,7 @@ class ExtractOIIOPostProcess(publish.Extractor): optional = True # Supported extensions - supported_exts = {"exr", "jpg", "jpeg", "png", "dpx"} + supported_exts = {ext.lstrip(".") for ext in IMAGE_EXTENSIONS} # Configurable by Settings profiles = None From 2a5210ccc535e13ae4aec24415a108505982ba2b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 14:10:55 +0100 Subject: [PATCH 36/42] Update client/ayon_core/plugins/publish/extract_oiio_postprocess.py Co-authored-by: Mustafa Zaky Jafar --- client/ayon_core/plugins/publish/extract_oiio_postprocess.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 33384e0185..1130a86fb6 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -62,7 +62,8 @@ class ExtractOIIOPostProcess(publish.Extractor): # it's included in the profile check. As such, an instance may have # a different profile applied per representation. profile = self._get_profile( - instance + instance, + repre ) if not profile: continue From 877a9fdecd61d7935e0bedf62344d2cc14c785d8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 14:38:35 +0100 Subject: [PATCH 37/42] Refactor profile `hosts` -> `host_names` --- client/ayon_core/plugins/publish/extract_oiio_postprocess.py | 2 +- server/settings/publish_plugins.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 1130a86fb6..3228e418b5 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -272,7 +272,7 @@ class ExtractOIIOPostProcess(publish.Extractor): repre_name: str = repre["name"] repre_ext: str = repre["ext"] 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 aa86398582..9b490ab208 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -638,7 +638,7 @@ class ExtractOIIOPostProcessProfileModel(BaseSettingsModel): default_factory=list, title="Product types" ) - hosts: list[str] = SettingsField( + host_names: list[str] = SettingsField( default_factory=list, title="Host names" ) From e2727ad15e7db7c5789ff2a67ffabbbec32f917a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 14:41:31 +0100 Subject: [PATCH 38/42] Cosmetics + type hints --- .../plugins/publish/extract_oiio_postprocess.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 3228e418b5..c7ae4c3910 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -261,7 +261,11 @@ class ExtractOIIOPostProcess(publish.Extractor): output_extension) return os.path.join(output_dir, new_file_name) - def _get_profile(self, instance: pyblish.api.Instance, repre: dict) -> Optional[dict[str, Any]]: + def _get_profile( + self, + instance: pyblish.api.Instance, + repre: dict + ) -> Optional[dict[str, Any]]: """Returns profile if it should process this instance.""" host_name = instance.context.data["hostName"] product_type = instance.data["productType"] @@ -296,7 +300,7 @@ class ExtractOIIOPostProcess(publish.Extractor): return profile - def _repre_is_valid(self, repre) -> bool: + def _repre_is_valid(self, repre: dict) -> bool: """Validation if representation should be processed. Args: @@ -325,7 +329,12 @@ class ExtractOIIOPostProcess(publish.Extractor): return True - def _mark_original_repre_for_deletion(self, repre, profile, added_review): + def _mark_original_repre_for_deletion( + self, + repre: dict, + profile: dict, + added_review: bool + ): """If new transcoded representation created, delete old.""" if not repre.get("tags"): repre["tags"] = [] From c1210b297788cd35190edc300fe8ebccfc5bb2a0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 14:42:34 +0100 Subject: [PATCH 39/42] Also skip early if no representations in the data. --- client/ayon_core/plugins/publish/extract_oiio_postprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index c7ae4c3910..7ea6ba50e3 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -44,7 +44,7 @@ class ExtractOIIOPostProcess(publish.Extractor): self.log.debug("No profiles present for OIIO Post Process") return - if "representations" not in instance.data: + if not instance.data.get("representations"): self.log.debug("No representations, skipping.") return From 596612cc99e6e0238d77ca4278f4e926fc41380f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 15:02:11 +0100 Subject: [PATCH 40/42] Move over product types in settings so order makes more sense --- server/settings/publish_plugins.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 9b490ab208..a5b84354bd 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -633,12 +633,8 @@ class ExtractOIIOPostProcessOutputModel(BaseSettingsModel): class ExtractOIIOPostProcessProfileModel(BaseSettingsModel): - product_types: list[str] = SettingsField( - section="Profile", - default_factory=list, - title="Product types" - ) host_names: list[str] = SettingsField( + section="Profile", default_factory=list, title="Host names" ) @@ -651,6 +647,10 @@ class ExtractOIIOPostProcessProfileModel(BaseSettingsModel): default_factory=list, title="Task names" ) + product_types: list[str] = SettingsField( + default_factory=list, + title="Product types" + ) product_names: list[str] = SettingsField( default_factory=list, title="Product names" From a7e02c19e5c7443638283a82b3f7ecab613edf55 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 15:30:53 +0100 Subject: [PATCH 41/42] Update client/ayon_core/plugins/publish/extract_oiio_postprocess.py Co-authored-by: Mustafa Zaky Jafar --- client/ayon_core/plugins/publish/extract_oiio_postprocess.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 7ea6ba50e3..7f3ba38963 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -40,6 +40,9 @@ class ExtractOIIOPostProcess(publish.Extractor): options = None def process(self, instance): + if instance.data.get("farm"): + self.log.debug("Should be processed on farm, skipping.") + return if not self.profiles: self.log.debug("No profiles present for OIIO Post Process") return From 0b942a062f3255a43a3d2a1a1f830f473396860b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 15:31:39 +0100 Subject: [PATCH 42/42] Cosmetics --- client/ayon_core/plugins/publish/extract_oiio_postprocess.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 7f3ba38963..2b432f2a0a 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -43,6 +43,7 @@ class ExtractOIIOPostProcess(publish.Extractor): if instance.data.get("farm"): self.log.debug("Should be processed on farm, skipping.") return + if not self.profiles: self.log.debug("No profiles present for OIIO Post Process") return