From 148ce21a9ac25a446a3ce2233501d288b396d34b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Nov 2024 09:09:12 +0100 Subject: [PATCH 01/20] Allow ExtractOIIOTranscode to pass with a warning if it can't find the RGBA channels in source media instead of raising error --- client/ayon_core/lib/transcoding.py | 7 ++++- .../publish/extract_color_transcode.py | 30 ++++++++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index e9750864ac..7fe2c84789 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -67,6 +67,11 @@ VIDEO_EXTENSIONS = { } +class UnknownRGBAChannelsError(ValueError): + """Raised when we can't find RGB channels for conversion in input media.""" + pass + + def get_transcode_temp_directory(): """Creates temporary folder for transcoding. @@ -1427,7 +1432,7 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None): review_channels = get_convert_rgb_channels(channel_names) if review_channels is None: - raise ValueError( + raise UnknownRGBAChannelsError( "Couldn't find channels that can be used for conversion." ) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 3e54d324e3..b259a3230a 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -10,6 +10,7 @@ from ayon_core.lib import ( ) from ayon_core.lib.transcoding import ( + UnknownRGBAChannelsError, convert_colorspace, get_transcode_temp_directory, ) @@ -160,17 +161,24 @@ class ExtractOIIOTranscode(publish.Extractor): output_path = self._get_output_file_path(input_path, new_staging_dir, output_extension) - convert_colorspace( - input_path, - output_path, - config_path, - source_colorspace, - target_colorspace, - view, - display, - additional_command_args, - self.log - ) + try: + convert_colorspace( + input_path, + output_path, + config_path, + source_colorspace, + target_colorspace, + view, + display, + additional_command_args, + self.log + ) + except UnknownRGBAChannelsError: + self.log.error( + "Skipping OIIO Transcode. Unknown RGBA channels" + f" for colorspace conversion in file: {input_path}" + ) + continue # cleanup temporary transcoded files for file_name in new_repre["files"]: From d072da86d1abada39af3e024c6010efc5554cd52 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Nov 2024 09:11:22 +0100 Subject: [PATCH 02/20] Stop processing directly on unknown RGBA channel for a representation --- .../plugins/publish/extract_color_transcode.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index b259a3230a..0287eca300 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -153,8 +153,7 @@ class ExtractOIIOTranscode(publish.Extractor): additional_command_args = (output_def["oiiotool_args"] ["additional_command_args"]) - files_to_convert = self._translate_to_sequence( - files_to_convert) + unknown_rgba_channels = False for file_name in files_to_convert: input_path = os.path.join(original_staging_dir, file_name) @@ -174,11 +173,16 @@ class ExtractOIIOTranscode(publish.Extractor): self.log ) except UnknownRGBAChannelsError: + unknown_rgba_channels = True self.log.error( "Skipping OIIO Transcode. Unknown RGBA channels" f" for colorspace conversion in file: {input_path}" ) - continue + break + + if unknown_rgba_channels: + # Stop processing this representation + break # cleanup temporary transcoded files for file_name in new_repre["files"]: From 363824d58946796d148b0216a5464928a91b3b1d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Nov 2024 09:12:19 +0100 Subject: [PATCH 03/20] Move logic to make it clearer that we always process the same input files of the source representation but used for multiple output profiles --- .../plugins/publish/extract_color_transcode.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 0287eca300..0d88f3073f 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -100,7 +100,19 @@ class ExtractOIIOTranscode(publish.Extractor): self.log.warning("Config file doesn't exist, skipping") 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"]] + repre_files_to_convert = self._translate_to_sequence( + repre_files_to_convert) + + # 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) @@ -108,11 +120,6 @@ class ExtractOIIOTranscode(publish.Extractor): new_staging_dir = get_transcode_temp_directory() new_repre["stagingDir"] = new_staging_dir - if isinstance(new_repre["files"], list): - files_to_convert = copy.deepcopy(new_repre["files"]) - else: - files_to_convert = [new_repre["files"]] - output_extension = output_def["extension"] output_extension = output_extension.replace('.', '') self._rename_in_representation(new_repre, @@ -121,7 +128,6 @@ class ExtractOIIOTranscode(publish.Extractor): output_extension) transcoding_type = output_def["transcoding_type"] - target_colorspace = view = display = None # NOTE: we use colorspace_data as the fallback values for # the target colorspace. From 2d7bd487bac24b28c78e5bf67a19c4f69c6e3db7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 13 May 2025 15:40:40 +0200 Subject: [PATCH 04/20] Allow review/transcoding of more channels, like "Y", "XYZ", "AR", "AG", "AB" --- client/ayon_core/lib/transcoding.py | 73 ++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 1fda014bd8..b236cd101b 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -345,6 +345,9 @@ def get_review_info_by_layer_name(channel_names): ... ] + This tries to find suitable outputs good for review purposes, by + searching for channel names like 'red', 'green', 'blue' or 'R', 'G', 'B', + Args: channel_names (list[str]): List of channel names. @@ -353,7 +356,6 @@ def get_review_info_by_layer_name(channel_names): """ layer_names_order = [] - rgba_by_layer_name = collections.defaultdict(dict) channels_by_layer_name = collections.defaultdict(dict) for channel_name in channel_names: @@ -362,42 +364,76 @@ def get_review_info_by_layer_name(channel_names): if "." in channel_name: layer_name, last_part = channel_name.rsplit(".", 1) - channels_by_layer_name[layer_name][channel_name] = last_part if last_part.lower() not in { - "r", "red", - "g", "green", - "b", "blue", - "a", "alpha" + # Detect RGBA channels + "r", "g", "b", "a", + # Allow detecting of x, y and z channels, and normal channels + "x", "y", "z", "n", + # red, green and blue alpha/opacity, for colored mattes + "ar", "ag", "ab" }: continue if layer_name not in layer_names_order: layer_names_order.append(layer_name) - # R, G, B or A + + # R, G, B, A or X, Y, Z, N, AR, AG, AB channel = last_part[0].upper() - rgba_by_layer_name[layer_name][channel] = channel_name + channels_by_layer_name[layer_name][channel] = channel_name # Put empty layer to the beginning of the list # - if input has R, G, B, A channels they should be used for review - if "" in layer_names_order: - layer_names_order.remove("") - layer_names_order.insert(0, "") + def _sort(_layer_name: str) -> int: + # Prioritize "" layer name + # Prioritize layers with RGB channels + order = 0 + if _layer_name == "": + order -= 10 + + channels = channels_by_layer_name[_layer_name] + if all(channel in channels for channel in "RGB"): + order -= 1 + return order + layer_names_order.sort(key=_sort) output = [] for layer_name in layer_names_order: - rgba_layer_info = rgba_by_layer_name[layer_name] - red = rgba_layer_info.get("R") - green = rgba_layer_info.get("G") - blue = rgba_layer_info.get("B") - if not red or not green or not blue: + channel_info = channels_by_layer_name[layer_name] + + # RGB channels + if all(channel in channel_info for channel in "RGB"): + rgb = "R", "G", "B" + + # XYZ channels (position pass) + elif all(channel in channel_info for channel in "XYZ"): + rgb = "X", "Y", "Z" + + # Colored mattes (as defined in OpenEXR Channel Name standards) + elif all(channel in channel_info for channel in ("AR", "AG", "AB")): + rgb = "AR", "AG", "AB" + + # Luminance channel (as defined in OpenEXR Channel Name standards) + elif "Y" in channel_info: + rgb = "Y", "Y", "Y" + + # Has only Z channel (Z-depth layer) + elif "Z" in channel_info: + rgb = "Z", "Z", "Z" + + else: + # No reviewable channels found continue + + red = channel_info[rgb[0]] + green = channel_info[rgb[1]] + blue = channel_info[rgb[2]] output.append({ "name": layer_name, "review_channels": { "R": red, "G": green, "B": blue, - "A": rgba_layer_info.get("A"), + "A": channel_info.get("A"), } }) return output @@ -1428,7 +1464,8 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None): if review_channels is None: raise ValueError( - "Couldn't find channels that can be used for conversion." + "Couldn't find channels that can be used for conversion " + f"among channels: {channel_names}." ) red, green, blue, alpha = review_channels From afbf2c8848b6045dd7d712323237a7d823922d5f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 17 May 2025 15:40:54 +0200 Subject: [PATCH 05/20] Refactor `UnknownRGBAChannelsError` -> `MissingRGBAChannelsError` --- client/ayon_core/lib/transcoding.py | 4 ++-- client/ayon_core/plugins/publish/extract_color_transcode.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 8c96bde07e..07b6d8a039 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -67,7 +67,7 @@ VIDEO_EXTENSIONS = { } -class UnknownRGBAChannelsError(ValueError): +class MissingRGBAChannelsError(ValueError): """Raised when we can't find RGB channels for conversion in input media.""" pass @@ -1337,7 +1337,7 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None): review_channels = get_convert_rgb_channels(channel_names) if review_channels is None: - raise UnknownRGBAChannelsError( + raise MissingRGBAChannelsError( "Couldn't find channels that can be used for conversion " f"among channels: {channel_names}." ) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 0475b63f82..a4b6fe7fc2 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -11,7 +11,7 @@ from ayon_core.lib import ( is_oiio_supported, ) from ayon_core.lib.transcoding import ( - UnknownRGBAChannelsError, + MissingRGBAChannelsError, convert_colorspace, ) @@ -186,7 +186,7 @@ class ExtractOIIOTranscode(publish.Extractor): additional_command_args, self.log ) - except UnknownRGBAChannelsError: + except MissingRGBAChannelsError: unknown_rgba_channels = True self.log.error( "Skipping OIIO Transcode. Unknown RGBA channels" From 44dc1ea99e46c108bfbc9f4dd63c9eff7a945731 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 17 May 2025 15:43:41 +0200 Subject: [PATCH 06/20] Include message of the original raised error --- client/ayon_core/plugins/publish/extract_color_transcode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index a4b6fe7fc2..09f7e6f201 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -186,8 +186,9 @@ class ExtractOIIOTranscode(publish.Extractor): additional_command_args, self.log ) - except MissingRGBAChannelsError: + except MissingRGBAChannelsError as exc: unknown_rgba_channels = True + self.log.error(exc) self.log.error( "Skipping OIIO Transcode. Unknown RGBA channels" f" for colorspace conversion in file: {input_path}" From 7fa192229c207483c3bf30bf55d479a1d2aae8b3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 17 May 2025 15:47:50 +0200 Subject: [PATCH 07/20] Improve docstring --- client/ayon_core/lib/transcoding.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 07b6d8a039..e665486d7b 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -351,7 +351,8 @@ def get_review_info_by_layer_name(channel_names): ] This tries to find suitable outputs good for review purposes, by - searching for channel names like 'red', 'green', 'blue' or 'R', 'G', 'B', + searching for channel names like RGBA, but also XYZ, Z, N, AR, AG, AB + channels. Args: channel_names (list[str]): List of channel names. From 72895df6ae121b013c63071e080e2609cc22b7a0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 11:16:52 +0200 Subject: [PATCH 08/20] Match variable name more with captured exception --- client/ayon_core/plugins/publish/extract_color_transcode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 09f7e6f201..45832bd1e5 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -166,7 +166,7 @@ class ExtractOIIOTranscode(publish.Extractor): files_to_convert = self._translate_to_sequence( files_to_convert) self.log.debug("Files to convert: {}".format(files_to_convert)) - unknown_rgba_channels = False + missing_rgba_review_channels = False for file_name in files_to_convert: self.log.debug("Transcoding file: `{}`".format(file_name)) input_path = os.path.join(original_staging_dir, @@ -187,7 +187,7 @@ class ExtractOIIOTranscode(publish.Extractor): self.log ) except MissingRGBAChannelsError as exc: - unknown_rgba_channels = True + missing_rgba_review_channels = True self.log.error(exc) self.log.error( "Skipping OIIO Transcode. Unknown RGBA channels" @@ -195,7 +195,7 @@ class ExtractOIIOTranscode(publish.Extractor): ) break - if unknown_rgba_channels: + if missing_rgba_review_channels: # Stop processing this representation break From b8ea018b43b7256676362f699d6f66df848cb710 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 11:18:37 +0200 Subject: [PATCH 09/20] Clarify exception --- client/ayon_core/lib/transcoding.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index e665486d7b..57ae6cb0bc 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -68,7 +68,12 @@ VIDEO_EXTENSIONS = { class MissingRGBAChannelsError(ValueError): - """Raised when we can't find RGB channels for conversion in input media.""" + """Raised when we can't find channels to use as RGBA for conversion in + input media. + + This may be other channels than solely RGBA, like Z-channel. The error is + raised when no matching 'reviewable' channel was found. + """ pass From 526e5bfabb2af995863ef652fb34635002f37641 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 12:01:14 +0200 Subject: [PATCH 10/20] Add unittest --- .../client/ayon_core/lib/test_transcoding.py | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 tests/client/ayon_core/lib/test_transcoding.py diff --git a/tests/client/ayon_core/lib/test_transcoding.py b/tests/client/ayon_core/lib/test_transcoding.py new file mode 100644 index 0000000000..f637e8bd9e --- /dev/null +++ b/tests/client/ayon_core/lib/test_transcoding.py @@ -0,0 +1,135 @@ +import unittest + +from ayon_core.lib.transcoding import ( + get_review_info_by_layer_name +) + + +class GetReviewInfoByLayerName(unittest.TestCase): + """Test responses from `get_review_info_by_layer_name`""" + def test_rgba_channels(self): + + # RGB is supported + info = get_review_info_by_layer_name(["R", "G", "B"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "R", + "G": "G", + "B": "B", + "A": None, + } + }]) + + # rgb is supported + info = get_review_info_by_layer_name(["r", "g", "b"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "r", + "G": "g", + "B": "b", + "A": None, + } + }]) + + # diffuse.[RGB] is supported + info = get_review_info_by_layer_name( + ["diffuse.R", "diffuse.G", "diffuse.B"] + ) + self.assertEqual(info, [{ + "name": "diffuse", + "review_channels": { + "R": "diffuse.R", + "G": "diffuse.G", + "B": "diffuse.B", + "A": None, + } + }]) + + info = get_review_info_by_layer_name(["R", "G", "B", "A"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "R", + "G": "G", + "B": "B", + "A": "A", + } + }]) + + def test_z_channel(self): + + info = get_review_info_by_layer_name(["Z"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "Z", + "G": "Z", + "B": "Z", + "A": None, + } + }]) + + info = get_review_info_by_layer_name(["Z", "A"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "Z", + "G": "Z", + "B": "Z", + "A": "A", + } + }]) + + + def test_unknown_channels(self): + info = get_review_info_by_layer_name(["hello", "world"]) + self.assertEqual(info, []) + + def test_rgba_priority(self): + """Ensure main layer, and RGB channels are prioritized + + If both Z and RGB channels are present for a layer name, then RGB + should be prioritized and the Z channel should be ignored. + + Also, the alpha channel from another "layer name" is not used. Note + how the diffuse response does not take A channel from the main layer. + + """ + + info = get_review_info_by_layer_name([ + "Z", + "diffuse.R", "diffuse.G", "diffuse.B", + "R", "G", "B", "A", + "specular.R", "specular.G", "specular.B", "specular.A", + ]) + self.assertEqual(info, [ + { + "name": "", + "review_channels": { + "R": "R", + "G": "G", + "B": "B", + "A": "A", + }, + }, + { + "name": "diffuse", + "review_channels": { + "R": "diffuse.R", + "G": "diffuse.G", + "B": "diffuse.B", + "A": None, + }, + }, + { + "name": "specular", + "review_channels": { + "R": "specular.R", + "G": "specular.G", + "B": "specular.B", + "A": "specular.A", + }, + }, + ]) From 591767152131c6be76db170f10a745a65e79fd5d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 12:04:01 +0200 Subject: [PATCH 11/20] Add AR, AG, AB test case and fix behavior --- client/ayon_core/lib/transcoding.py | 2 +- .../client/ayon_core/lib/test_transcoding.py | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 57ae6cb0bc..895e220729 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -389,7 +389,7 @@ def get_review_info_by_layer_name(channel_names): layer_names_order.append(layer_name) # R, G, B, A or X, Y, Z, N, AR, AG, AB - channel = last_part[0].upper() + channel = last_part.upper() channels_by_layer_name[layer_name][channel] = channel_name # Put empty layer to the beginning of the list diff --git a/tests/client/ayon_core/lib/test_transcoding.py b/tests/client/ayon_core/lib/test_transcoding.py index f637e8bd9e..b9959e2958 100644 --- a/tests/client/ayon_core/lib/test_transcoding.py +++ b/tests/client/ayon_core/lib/test_transcoding.py @@ -82,6 +82,29 @@ class GetReviewInfoByLayerName(unittest.TestCase): } }]) + def test_ar_ag_ab_channels(self): + + info = get_review_info_by_layer_name(["AR", "AG", "AB"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "AR", + "G": "AG", + "B": "AB", + "A": None, + } + }]) + + info = get_review_info_by_layer_name(["AR", "AG", "AB", "A"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "AR", + "G": "AG", + "B": "AB", + "A": "A", + } + }]) def test_unknown_channels(self): info = get_review_info_by_layer_name(["hello", "world"]) From 00921e7806ab345037030fc8bb5ebd426840ce53 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 12:05:28 +0200 Subject: [PATCH 12/20] Update client/ayon_core/lib/transcoding.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/ayon_core/lib/transcoding.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 895e220729..a22b8aaf64 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -74,7 +74,6 @@ class MissingRGBAChannelsError(ValueError): This may be other channels than solely RGBA, like Z-channel. The error is raised when no matching 'reviewable' channel was found. """ - pass def get_transcode_temp_directory(): From 0c23ecc70d1dd2c2ea8e39c7f752b2cc406f379a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 20:52:08 +0200 Subject: [PATCH 13/20] Add support for red, green, blue and alpha --- client/ayon_core/lib/transcoding.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index a22b8aaf64..fc0d6bf8cd 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -377,6 +377,8 @@ def get_review_info_by_layer_name(channel_names): if last_part.lower() not in { # Detect RGBA channels "r", "g", "b", "a", + # Support fully written out rgba channel names + "red", "green", "blue", "alpha", # Allow detecting of x, y and z channels, and normal channels "x", "y", "z", "n", # red, green and blue alpha/opacity, for colored mattes @@ -410,10 +412,20 @@ def get_review_info_by_layer_name(channel_names): for layer_name in layer_names_order: channel_info = channels_by_layer_name[layer_name] + alpha = channel_info.get("A") + # RGB channels if all(channel in channel_info for channel in "RGB"): rgb = "R", "G", "B" + # RGB channels using fully written out channel names + elif all( + channel in channel_info + for channel in ("RED", "GREEN", "BLUE") + ): + rgb = "RED", "GREEN", "BLUE" + alpha = channel_info.get("ALPHA") + # XYZ channels (position pass) elif all(channel in channel_info for channel in "XYZ"): rgb = "X", "Y", "Z" @@ -443,7 +455,7 @@ def get_review_info_by_layer_name(channel_names): "R": red, "G": green, "B": blue, - "A": channel_info.get("A"), + "A": alpha, } }) return output From 55bfd79cf3cdfd4225b5e7ffdb9b660e3dd23592 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 20:55:21 +0200 Subject: [PATCH 14/20] Check against `.upper()` instead of `.lower()` to match strings more with how they are compared later in the code (improve style consistency) --- client/ayon_core/lib/transcoding.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index fc0d6bf8cd..643a056563 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -374,23 +374,23 @@ def get_review_info_by_layer_name(channel_names): if "." in channel_name: layer_name, last_part = channel_name.rsplit(".", 1) - if last_part.lower() not in { + # R, G, B, A or X, Y, Z, N, AR, AG, AB, RED, GREEN, BLUE, ALPHA + channel = last_part.upper() + if channel not in { # Detect RGBA channels - "r", "g", "b", "a", + "R", "G", "B", "A", # Support fully written out rgba channel names - "red", "green", "blue", "alpha", + "RED", "GREEN", "BLUE", "ALPHA", # Allow detecting of x, y and z channels, and normal channels - "x", "y", "z", "n", + "X", "Y", "Z", "N", # red, green and blue alpha/opacity, for colored mattes - "ar", "ag", "ab" + "AR", "AG", "AB" }: continue if layer_name not in layer_names_order: layer_names_order.append(layer_name) - # R, G, B, A or X, Y, Z, N, AR, AG, AB - channel = last_part.upper() channels_by_layer_name[layer_name][channel] = channel_name # Put empty layer to the beginning of the list From cb81a57dddf6024318af16dfbde4a7e63cf7546a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Oct 2025 22:31:49 +0100 Subject: [PATCH 15/20] Allow visualizing Alpha only layers for review as a color matte --- client/ayon_core/lib/transcoding.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 1762881846..38033d084f 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -487,6 +487,11 @@ def get_review_info_by_layer_name(channel_names): elif "Z" in channel_info: rgb = "Z", "Z", "Z" + # Has only A channel (Alpha layer) + elif "A" in channel_info: + rgb = "A", "A", "A" + alpha = None + else: # No reviewable channels found continue From 66c6bdd96025f94ddb39fbcd5b442bd9d9f736c2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Oct 2025 22:57:26 +0100 Subject: [PATCH 16/20] Do not fail on thumbnail creation if it can't resolve a reviewable channel with OIIO --- .../ayon_core/plugins/publish/extract_thumbnail.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index b5885178d0..2a43c12af3 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -17,6 +17,7 @@ from ayon_core.lib import ( run_subprocess, ) from ayon_core.lib.transcoding import ( + MissingRGBAChannelsError, oiio_color_convert, get_oiio_input_and_channel_args, get_oiio_info_for_input, @@ -477,7 +478,16 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return False input_info = get_oiio_info_for_input(src_path, logger=self.log) - input_arg, channels_arg = get_oiio_input_and_channel_args(input_info) + try: + input_arg, channels_arg = get_oiio_input_and_channel_args( + input_info + ) + except MissingRGBAChannelsError: + self.log.debug( + "Unable to find relevant reviewable channel for thumbnail " + "creation" + ) + return False oiio_cmd = get_oiio_tool_args( "oiiotool", input_arg, src_path, From 87ba72eb002635d0cd1c69d8739db97a25a4150e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Nov 2025 09:44:20 +0100 Subject: [PATCH 17/20] Typos --- client/ayon_core/plugins/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 580aa27eef..56863921c0 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -361,14 +361,14 @@ class ExtractReview(pyblish.api.InstancePlugin): if not filtered_output_defs: self.log.debug(( "Repre: {} - All output definitions were filtered" - " out by single frame filter. Skipping" + " out by single frame filter. Skipped." ).format(repre["name"])) continue # Skip if file is not set if first_input_path is None: self.log.warning(( - "Representation \"{}\" have empty files. Skipped." + "Representation \"{}\" has empty files. Skipped." ).format(repre["name"])) continue From 7648d6cc817ad993edd489a8fa8ca66f2d5a01c4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Nov 2025 09:50:50 +0100 Subject: [PATCH 18/20] Fix passing on correct filename to the representation instead of including the `1001-1025%04d` pattern used solely for OIIO sequence conversion --- .../publish/extract_color_transcode.py | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index f80ca7e150..2aefd15fd9 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -117,8 +117,6 @@ class ExtractOIIOTranscode(publish.Extractor): repre_files_to_convert = copy.deepcopy(repre["files"]) else: repre_files_to_convert = [repre["files"]] - repre_files_to_convert = self._translate_to_sequence( - repre_files_to_convert) # Process each output definition for output_def in profile_output_defs: @@ -176,11 +174,17 @@ class ExtractOIIOTranscode(publish.Extractor): additional_command_args = (output_def["oiiotool_args"] ["additional_command_args"]) - files_to_convert = self._translate_to_sequence( - files_to_convert) - self.log.debug("Files to convert: {}".format(files_to_convert)) + sequence_files = self._translate_to_sequence(files_to_convert) + self.log.debug("Files to convert: {}".format(sequence_files)) missing_rgba_review_channels = False - for file_name in files_to_convert: + 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) @@ -238,11 +242,11 @@ class ExtractOIIOTranscode(publish.Extractor): added_review = True # If there is only 1 file outputted then convert list to - # string, cause that'll indicate that its not a sequence. + # 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 its not + # 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. @@ -292,8 +296,7 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["files"] = renamed_files def _translate_to_sequence(self, files_to_convert): - """Returns original list or list with filename formatted in single - sequence format. + """Returns original list or a clique.Collection of a sequence. Uses clique to find frame sequence, in this case it merges all frames into sequence format (FRAMESTART-FRAMEEND#) and returns it. @@ -302,32 +305,26 @@ class ExtractOIIOTranscode(publish.Extractor): Args: files_to_convert (list): list of file names Returns: - (list) of [file.1001-1010#.exr] or [fileA.exr, fileB.exr] + 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] - frames = list(collection.indexes) - if collection.holes().indexes: - return files_to_convert - - # Get the padding from the collection - # This is the number of digits used in the frame numbers - padding = collection.padding - - frame_str = "{}-{}%0{}d".format(frames[0], frames[-1], padding) - file_name = "{}{}{}".format(collection.head, frame_str, - collection.tail) - - files_to_convert = [file_name] + # 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 From 76dfbaeb68e23bebc0a263af6172ccea34b2e781 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Nov 2025 09:55:18 +0100 Subject: [PATCH 19/20] Update docstring --- client/ayon_core/plugins/publish/extract_color_transcode.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 2aefd15fd9..1a2c85e597 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -298,9 +298,8 @@ class ExtractOIIOTranscode(publish.Extractor): def _translate_to_sequence(self, files_to_convert): """Returns original list or a clique.Collection of a sequence. - Uses clique to find frame sequence, in this case it merges all frames - into sequence format (FRAMESTART-FRAMEEND#) and returns it. - If sequence not found, it returns original list + Uses clique to find frame sequence Collection. + If sequence not found, it returns original list. Args: files_to_convert (list): list of file names From 84db5d396517c74fd9634b3e0d5a8df0938acf52 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 5 Nov 2025 23:00:22 +0100 Subject: [PATCH 20/20] Update client/ayon_core/lib/transcoding.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/transcoding.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 38033d084f..22396a5324 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -441,16 +441,16 @@ def get_review_info_by_layer_name(channel_names): def _sort(_layer_name: str) -> int: # Prioritize "" layer name # Prioritize layers with RGB channels - order = 0 if _layer_name == "rgba": - order -= 11 + return 0 + if _layer_name == "": - order -= 10 + return 1 channels = channels_by_layer_name[_layer_name] if all(channel in channels for channel in "RGB"): - order -= 1 - return order + return 2 + return 10 layer_names_order.sort(key=_sort) output = []