From d91b922cadd76a6514f960e49f2d4fb542dd03ad Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 May 2026 21:37:19 +0800 Subject: [PATCH 01/30] clean up and add support for custom frame range in local rendering --- client/ayon_max/api/lib.py | 33 +++++++++++++++++ client/ayon_max/api/lib_renderproducts.py | 35 ++++++++----------- .../plugins/publish/collect_frame_range.py | 9 +++-- .../plugins/publish/collect_render.py | 5 +-- .../plugins/publish/extract_render.py | 15 ++++++-- 5 files changed, 69 insertions(+), 28 deletions(-) diff --git a/client/ayon_max/api/lib.py b/client/ayon_max/api/lib.py index 42fd34561b..0bf8e7eee8 100644 --- a/client/ayon_max/api/lib.py +++ b/client/ayon_max/api/lib.py @@ -1060,3 +1060,36 @@ def build_general_output_filename( ext = match.group("ext") filename = f"{name}_{element}..{ext}" return os.path.join(output_dir, filename) + + +def get_expected_frames(instance: pyblish.api.Instance) -> list[int]: + """Get expected frames from the instance. + + Args: + instance (pyblish.api.Instance): The Pyblish instance. + + Returns: + list[int]: A list containing the expected frames. + """ + def parse_frame_range(frame_str: str) -> list[int]: + """Parse a frame range string into a list of frames. + + Args: + frame_str (str): The frame range string. + + Returns: + list[int]: A list of frames. + """ + frames = [] + for part in frame_str.split(","): + if "-" in part: + start, end = map(int, part.split("-")) + frames.extend(range(start, end + 1)) + else: + frames.append(int(part)) + return frames + + frames = instance.data["custom_frames"] + if instance.data["custom_frames"]: + return parse_frame_range(frames) + return list(range(instance.data["frameStart"], instance.data["frameEnd"] + 1)) diff --git a/client/ayon_max/api/lib_renderproducts.py b/client/ayon_max/api/lib_renderproducts.py index 1844fc4fc8..28719b01d8 100644 --- a/client/ayon_max/api/lib_renderproducts.py +++ b/client/ayon_max/api/lib_renderproducts.py @@ -38,7 +38,7 @@ def __init__(self, project_settings: Dict[str, Any] = None): get_current_project_name() ) - def get_render_products(self) -> Dict[str, list[str]]: + def get_render_products(self, frame_range: list[int]) -> Dict[str, list[str]]: """Get render output file paths for the current scene. Handles both beauty and AOV extraction with shared setup logic. @@ -50,8 +50,7 @@ def get_render_products(self) -> Dict[str, list[str]]: (e.g., "Cryptomatte", "Alpha"). """ extension = self.image_format() - start_frame = int(rt.rendStart) - end_frame = int(rt.rendEnd) + # todo: Support Custom Frames sequences 0,5-10,100-120 # we can add filtering frames list to get expected frames list # instead of using start and end frame @@ -60,7 +59,7 @@ def get_render_products(self) -> Dict[str, list[str]]: render_dict: Dict[str, list[str]] = {} # Always add beauty pass - render_dict["beauty"] = self.get_expected_beauty(start_frame, end_frame, extension) + render_dict["beauty"] = self.get_expected_beauty(frame_range, extension) # Optionally add AOVs renderer = get_current_renderer() @@ -70,8 +69,7 @@ def get_render_products(self) -> Dict[str, list[str]]: for aov_name, aov_filepath in render_elements: aov_expected_files = self.get_expected_files( aov_filepath, - start_frame, - end_frame, + frame_range, aov_name, renderer_name ) @@ -80,7 +78,7 @@ def get_render_products(self) -> Dict[str, list[str]]: return render_dict def get_multiple_render_products( - self, outputs: list[str], cameras: list[str] + self, outputs: list[str], cameras: list[str], frame_range: list[int] ) -> Dict[str, list[str]]: """Get render output file paths for multiple cameras. @@ -105,11 +103,9 @@ def get_multiple_render_products( filename, ext = os.path.splitext(output) filename = filename.replace(".", "") ext = ext.replace(".", "") - start_frame = int(rt.rendStart) - end_frame = int(rt.rendEnd) # Always add beauty pass - beauty_files = self.get_expected_beauty(start_frame, end_frame, ext) + beauty_files = self.get_expected_beauty(frame_range, ext) render_output_frames[f"{camera}_beauty"] = beauty_files # Add AOVs @@ -118,8 +114,7 @@ def get_multiple_render_products( for aov_name, aov_filepath in render_elements: aov_expected_files = self.get_expected_files( aov_filepath, - start_frame, - end_frame, + frame_range, aov_name, renderer_name ) @@ -128,12 +123,13 @@ def get_multiple_render_products( return render_output_frames def get_expected_beauty( - self, start_frame: int, end_frame: int, extension: str + self, frame_range: list[int], extension: str ) -> list[str]: """Get expected beauty render output file paths for each frame. Args: - start_frame (int): The starting frame number. + frame_range (list[int]): The range of frames. + end_frame (int): The ending frame number. extension (str): The file extension for the output files. @@ -151,8 +147,7 @@ def get_expected_beauty( return self.get_expected_files( output_path, - start_frame, - end_frame, + frame_range, "", renderer_name ) @@ -258,8 +253,7 @@ def get_arnold_render_output(self, arnold_renderer: Any, extension: str) -> str: def get_expected_files( self, filepath: str, - start_frame: int, - end_frame: int, + frame_range: list[int], aov_name: str, renderer_name: str, ) -> list[str]: @@ -267,8 +261,7 @@ def get_expected_files( Args: filepath (str): filepath of the render output. - start_frame (int): start frame of the render sequence. - end_frame (int): end frame of the render sequence. + frame_range (list[int]): range of frames of the render sequence. aov_name (str): name of the AOV. renderer_name (str): name of the renderer. @@ -281,7 +274,7 @@ def get_expected_files( name, ext = os.path.splitext(filename) name = name.lstrip(".") aov_name = aov_name.strip() - for frame in range(start_frame, end_frame + 1): + for frame in frame_range: aov_filename = f"{name}.{frame:04d}{ext}" expected_aov = os.path.join(directory, aov_filename) if aov_name and renderer_name.startswith("V_Ray_"): diff --git a/client/ayon_max/plugins/publish/collect_frame_range.py b/client/ayon_max/plugins/publish/collect_frame_range.py index cd62830f6a..50d1789fef 100644 --- a/client/ayon_max/plugins/publish/collect_frame_range.py +++ b/client/ayon_max/plugins/publish/collect_frame_range.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- import pyblish.api from pymxs import runtime as rt +from ayon_max.api.lib import get_expected_frames class CollectFrameRange(pyblish.api.InstancePlugin): """Collect Frame Range.""" - order = pyblish.api.CollectorOrder + 0.011 + order = pyblish.api.CollectorOrder + 0.019 label = "Collect Frame Range" hosts = ['max'] families = ["camera", "maxrender", @@ -16,8 +17,10 @@ class CollectFrameRange(pyblish.api.InstancePlugin): def process(self, instance): if instance.data["productBaseType"] == "maxrender": - instance.data["frameStartHandle"] = int(rt.rendStart) - instance.data["frameEndHandle"] = int(rt.rendEnd) + frame_range = get_expected_frames(instance) + instance.data["frameStartHandle"] = min(frame_range) + instance.data["frameEndHandle"] = max(frame_range) + instance.data["expectedFrameRange"] = frame_range elif instance.data["productBaseType"] in {"tycache", "tyspline"}: operator = instance.data["operator"] diff --git a/client/ayon_max/plugins/publish/collect_render.py b/client/ayon_max/plugins/publish/collect_render.py index 7228722b80..d72dd56c76 100644 --- a/client/ayon_max/plugins/publish/collect_render.py +++ b/client/ayon_max/plugins/publish/collect_render.py @@ -60,7 +60,8 @@ def process(self, instance): renderer_name = str(renderer).split(":")[0] renderproducts = RenderProducts(context.data["project_settings"]) img_format = renderproducts.image_format() - files_by_aov: Dict[str, list[str]] = renderproducts.get_render_products() + expected_frames = instance.data.get("expectedFrameRange", []) + files_by_aov: Dict[str, list[str]] = renderproducts.get_render_products(expected_frames) camera = rt.viewport.GetCamera() @@ -94,7 +95,7 @@ def process(self, instance): instance.data["cameras"] = sel_cam files_by_aov = renderproducts.get_multiple_render_products( - outputs, sel_cam + outputs, sel_cam, expected_frames ) if "expectedFiles" not in instance.data: diff --git a/client/ayon_max/plugins/publish/extract_render.py b/client/ayon_max/plugins/publish/extract_render.py index cc1d4c63f3..0faf6cec92 100644 --- a/client/ayon_max/plugins/publish/extract_render.py +++ b/client/ayon_max/plugins/publish/extract_render.py @@ -34,8 +34,19 @@ def process(self, instance): if camera else rt.viewport.GetCamera() ) - for frame in range(int(rt.rendStart), int(rt.rendEnd) + 1): - rt.render(frame=frame, vfb=False, camera=camera_node) + wasCancelled = rt.Name('wasCancelled') + wasCancelled.value = False + for frame in instance.data.get("expectedFrameRange", []): + rt.render( + frame=frame, + vfb=False, + camera=camera_node, + cancelled=wasCancelled + ) + if wasCancelled.value: + self.log.warning(f"Render cancelled at frame {frame}.") + break + self.log.debug("Local render extraction completed.") else: self.log.debug( From 476d32f39ad29d7278f889cc56c73ed5be720722 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 May 2026 21:54:53 +0800 Subject: [PATCH 02/30] make sure we can get the correct frame range in max --- client/ayon_max/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_max/api/lib.py b/client/ayon_max/api/lib.py index 0bf8e7eee8..23f293f49c 100644 --- a/client/ayon_max/api/lib.py +++ b/client/ayon_max/api/lib.py @@ -1092,4 +1092,4 @@ def parse_frame_range(frame_str: str) -> list[int]: frames = instance.data["custom_frames"] if instance.data["custom_frames"]: return parse_frame_range(frames) - return list(range(instance.data["frameStart"], instance.data["frameEnd"] + 1)) + return list(range(rt.rendStart, rt.rendEnd + 1)) From c1ab81008c15ab0a8bf0a5d7bb3fa421f0d61bf8 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Fri, 8 May 2026 21:55:15 +0800 Subject: [PATCH 03/30] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- client/ayon_max/plugins/publish/extract_render.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_max/plugins/publish/extract_render.py b/client/ayon_max/plugins/publish/extract_render.py index 0faf6cec92..8993fbaf1a 100644 --- a/client/ayon_max/plugins/publish/extract_render.py +++ b/client/ayon_max/plugins/publish/extract_render.py @@ -34,16 +34,16 @@ def process(self, instance): if camera else rt.viewport.GetCamera() ) - wasCancelled = rt.Name('wasCancelled') - wasCancelled.value = False + was_cancelled = rt.Name('wasCancelled') + was_cancelled.value = False for frame in instance.data.get("expectedFrameRange", []): rt.render( frame=frame, vfb=False, camera=camera_node, - cancelled=wasCancelled + cancelled=was_cancelled ) - if wasCancelled.value: + if was_cancelled.value: self.log.warning(f"Render cancelled at frame {frame}.") break From 9fbe3510af0fede41cb46cd4db2c97bed8883a11 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Fri, 8 May 2026 21:56:20 +0800 Subject: [PATCH 04/30] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- client/ayon_max/api/lib.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/client/ayon_max/api/lib.py b/client/ayon_max/api/lib.py index 23f293f49c..85087857e3 100644 --- a/client/ayon_max/api/lib.py +++ b/client/ayon_max/api/lib.py @@ -1082,11 +1082,31 @@ def parse_frame_range(frame_str: str) -> list[int]: """ frames = [] for part in frame_str.split(","): + part = part.strip() + if not part: + continue + if "-" in part: - start, end = map(int, part.split("-")) + start_end = part.split("-", 1) + if len(start_end) != 2 or not start_end[0] or not start_end[1]: + raise ValueError( + f"Invalid frame range segment: '{part}'" + ) + + start, end = map(int, start_end) + if start > end: + raise ValueError( + f"Frame range start must be less than or equal " + f"to end: '{part}'" + ) frames.extend(range(start, end + 1)) else: frames.append(int(part)) + + if not frames: + raise ValueError( + f"No valid frames could be parsed from '{frame_str}'" + ) return frames frames = instance.data["custom_frames"] From 8813e5dfe7e3f721145aa4bd6d46b4f49fa7855e Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Fri, 8 May 2026 21:56:38 +0800 Subject: [PATCH 05/30] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- client/ayon_max/api/lib_renderproducts.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_max/api/lib_renderproducts.py b/client/ayon_max/api/lib_renderproducts.py index 28719b01d8..8ec2ed4d77 100644 --- a/client/ayon_max/api/lib_renderproducts.py +++ b/client/ayon_max/api/lib_renderproducts.py @@ -128,9 +128,8 @@ def get_expected_beauty( """Get expected beauty render output file paths for each frame. Args: - frame_range (list[int]): The range of frames. - - end_frame (int): The ending frame number. + frame_range (list[int]): The frame range to generate expected + output file paths for. extension (str): The file extension for the output files. Returns: From dd97e28c0be3691254e03423dce48297fac3a090 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 May 2026 22:00:09 +0800 Subject: [PATCH 06/30] upload the code suggestion by copilot --- client/ayon_max/api/lib.py | 8 +++++--- client/ayon_max/plugins/publish/extract_render.py | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/ayon_max/api/lib.py b/client/ayon_max/api/lib.py index 85087857e3..13558345fe 100644 --- a/client/ayon_max/api/lib.py +++ b/client/ayon_max/api/lib.py @@ -1109,7 +1109,9 @@ def parse_frame_range(frame_str: str) -> list[int]: ) return frames - frames = instance.data["custom_frames"] - if instance.data["custom_frames"]: + frames = instance.data.get("custom_frames", "") + if frames: return parse_frame_range(frames) - return list(range(rt.rendStart, rt.rendEnd + 1)) + frame_start = int(rt.rendStart) + frame_end = int(rt.rendEnd) + return list(range(frame_start, frame_end + 1)) diff --git a/client/ayon_max/plugins/publish/extract_render.py b/client/ayon_max/plugins/publish/extract_render.py index 8993fbaf1a..d110aaac2b 100644 --- a/client/ayon_max/plugins/publish/extract_render.py +++ b/client/ayon_max/plugins/publish/extract_render.py @@ -36,7 +36,7 @@ def process(self, instance): was_cancelled = rt.Name('wasCancelled') was_cancelled.value = False - for frame in instance.data.get("expectedFrameRange", []): + for frame in instance.data["expectedFrameRange"]: rt.render( frame=frame, vfb=False, @@ -47,7 +47,7 @@ def process(self, instance): self.log.warning(f"Render cancelled at frame {frame}.") break - self.log.debug("Local render extraction completed.") + self.log.debug("Local render extraction completed.") else: self.log.debug( "Local render extraction for multi-camera is already " From ebe08da902628473ef096b7e441f87535bd845f4 Mon Sep 17 00:00:00 2001 From: Kayla Date: Tue, 26 May 2026 17:19:51 +0200 Subject: [PATCH 07/30] wip: rename custom_frames to customFrames --- client/ayon_max/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_max/api/lib.py b/client/ayon_max/api/lib.py index 13558345fe..f379360353 100644 --- a/client/ayon_max/api/lib.py +++ b/client/ayon_max/api/lib.py @@ -1109,7 +1109,7 @@ def parse_frame_range(frame_str: str) -> list[int]: ) return frames - frames = instance.data.get("custom_frames", "") + frames = instance.data.get("customFrames", "") if frames: return parse_frame_range(frames) frame_start = int(rt.rendStart) From 2249e81b8c00906d05c8cd85083696b929ead8b2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Jul 2026 17:39:01 +0800 Subject: [PATCH 08/30] remove unused logic. --- client/ayon_max/plugins/publish/extract_render.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_max/plugins/publish/extract_render.py b/client/ayon_max/plugins/publish/extract_render.py index 6465ac1c01..6e035ed3d2 100644 --- a/client/ayon_max/plugins/publish/extract_render.py +++ b/client/ayon_max/plugins/publish/extract_render.py @@ -37,8 +37,6 @@ def process(self, instance): if camera else rt.viewport.GetCamera() ) - was_cancelled = rt.Name('wasCancelled') - was_cancelled.value = False for frame in instance.data["expectedFrameRange"]: _, cancelled = rt.render( frame=frame, From e6e48d6479c8e80ea7c2bb2483011873242f9033 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Jul 2026 19:04:17 +0800 Subject: [PATCH 09/30] Clean up and implement the existing logic from core --- client/ayon_max/api/lib.py | 42 ++----------------- .../plugins/publish/extract_render.py | 1 - 2 files changed, 3 insertions(+), 40 deletions(-) diff --git a/client/ayon_max/api/lib.py b/client/ayon_max/api/lib.py index f379360353..d662ec828b 100644 --- a/client/ayon_max/api/lib.py +++ b/client/ayon_max/api/lib.py @@ -23,6 +23,8 @@ from ayon_core.pipeline.context_tools import ( get_current_task_entity ) +from ayon_core.pipeline.farm.pyblish_functions import \ + convert_frames_str_to_list from ayon_core.pipeline.create import CreateContext from ayon_core.style import load_stylesheet @@ -1071,47 +1073,9 @@ def get_expected_frames(instance: pyblish.api.Instance) -> list[int]: Returns: list[int]: A list containing the expected frames. """ - def parse_frame_range(frame_str: str) -> list[int]: - """Parse a frame range string into a list of frames. - - Args: - frame_str (str): The frame range string. - - Returns: - list[int]: A list of frames. - """ - frames = [] - for part in frame_str.split(","): - part = part.strip() - if not part: - continue - - if "-" in part: - start_end = part.split("-", 1) - if len(start_end) != 2 or not start_end[0] or not start_end[1]: - raise ValueError( - f"Invalid frame range segment: '{part}'" - ) - - start, end = map(int, start_end) - if start > end: - raise ValueError( - f"Frame range start must be less than or equal " - f"to end: '{part}'" - ) - frames.extend(range(start, end + 1)) - else: - frames.append(int(part)) - - if not frames: - raise ValueError( - f"No valid frames could be parsed from '{frame_str}'" - ) - return frames - frames = instance.data.get("customFrames", "") if frames: - return parse_frame_range(frames) + return convert_frames_str_to_list(frames) frame_start = int(rt.rendStart) frame_end = int(rt.rendEnd) return list(range(frame_start, frame_end + 1)) diff --git a/client/ayon_max/plugins/publish/extract_render.py b/client/ayon_max/plugins/publish/extract_render.py index 6e035ed3d2..d22f1c99e7 100644 --- a/client/ayon_max/plugins/publish/extract_render.py +++ b/client/ayon_max/plugins/publish/extract_render.py @@ -40,7 +40,6 @@ def process(self, instance): for frame in instance.data["expectedFrameRange"]: _, cancelled = rt.render( frame=frame, - vfb=False, camera=camera_node, cancelled=pymxs.byref(None) ) From 492004b34b8f975cf82ede40da7f8f775c388152 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Jul 2026 19:41:24 +0800 Subject: [PATCH 10/30] add hasExplicitFrames and reuseLastVersion data --- client/ayon_max/plugins/publish/collect_render_local.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/ayon_max/plugins/publish/collect_render_local.py b/client/ayon_max/plugins/publish/collect_render_local.py index 78db346939..843726a4b8 100644 --- a/client/ayon_max/plugins/publish/collect_render_local.py +++ b/client/ayon_max/plugins/publish/collect_render_local.py @@ -74,6 +74,11 @@ def process(self, instance): aov_instance.data.update(aov_instance_data) aov_instance.data["families"] = [f"render.{render_target}"] + aov_instance.data["hasExplicitFrames"] = True + aov_instance.data["reuseLastVersion"] = instance.data.get( + "reuse_last_version", False + ) + # Pass on 'review' family if "review" in aov_instance_data["families"]: aov_instance.data["families"].append("review") From 0f38a37ae995384e34963b76c3b8623bf6c52ce0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Jul 2026 20:44:19 +0800 Subject: [PATCH 11/30] revert vfb changes --- client/ayon_max/plugins/publish/extract_render.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_max/plugins/publish/extract_render.py b/client/ayon_max/plugins/publish/extract_render.py index d22f1c99e7..6e035ed3d2 100644 --- a/client/ayon_max/plugins/publish/extract_render.py +++ b/client/ayon_max/plugins/publish/extract_render.py @@ -40,6 +40,7 @@ def process(self, instance): for frame in instance.data["expectedFrameRange"]: _, cancelled = rt.render( frame=frame, + vfb=False, camera=camera_node, cancelled=pymxs.byref(None) ) From 48a13e0bf4e08b636b96169b7bc01947d837a3aa Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:01:45 +0800 Subject: [PATCH 12/30] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- client/ayon_max/plugins/publish/collect_render.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_max/plugins/publish/collect_render.py b/client/ayon_max/plugins/publish/collect_render.py index 2b87798c5c..7ed114e5b9 100644 --- a/client/ayon_max/plugins/publish/collect_render.py +++ b/client/ayon_max/plugins/publish/collect_render.py @@ -61,7 +61,9 @@ def process(self, instance): renderer_name = str(renderer).split(":")[0] renderproducts = RenderProducts(context.data["project_settings"]) img_format = renderproducts.image_format() - expected_frames = instance.data.get("expectedFrameRange", []) + expected_frames = instance.data.get("expectedFrameRange") + if not expected_frames: + expected_frames = list(range(int(rt.rendStart), int(rt.rendEnd) + 1)) files_by_aov: Dict[str, list[str]] = renderproducts.get_render_products(expected_frames) From b00226a8180806009a8869bb900e4d26ae68a8c5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 7 Jul 2026 17:11:07 +0800 Subject: [PATCH 13/30] revert the change by cilpot suggestion & implement the changes to make sure the frame lists are at the correct order. --- client/ayon_max/api/lib.py | 14 +++++++++++--- client/ayon_max/plugins/publish/collect_render.py | 2 -- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/client/ayon_max/api/lib.py b/client/ayon_max/api/lib.py index d662ec828b..28a4b755ab 100644 --- a/client/ayon_max/api/lib.py +++ b/client/ayon_max/api/lib.py @@ -18,6 +18,7 @@ AYON_INSTANCE_ID, AVALON_INSTANCE_ID, ) +from ayon_core.pipeline.publish import PublishError from ayon_core.tools.utils import SimplePopup from ayon_core.settings import get_project_settings from ayon_core.pipeline.context_tools import ( @@ -1073,9 +1074,16 @@ def get_expected_frames(instance: pyblish.api.Instance) -> list[int]: Returns: list[int]: A list containing the expected frames. """ - frames = instance.data.get("customFrames", "") - if frames: - return convert_frames_str_to_list(frames) + frames_str = instance.data.get("customFrames", "") + if frames_str: + frames_list = convert_frames_str_to_list(frames_str.strip()) + frames_list = sorted({int(frame) for frame in frames_list}) + if not frames_list: + raise PublishError( + f"Invalid customFrames value: {frames_str}. " + "Expected a comma-separated list of integers or ranges." + ) + return frames_list frame_start = int(rt.rendStart) frame_end = int(rt.rendEnd) return list(range(frame_start, frame_end + 1)) diff --git a/client/ayon_max/plugins/publish/collect_render.py b/client/ayon_max/plugins/publish/collect_render.py index 7ed114e5b9..707ea684f9 100644 --- a/client/ayon_max/plugins/publish/collect_render.py +++ b/client/ayon_max/plugins/publish/collect_render.py @@ -62,8 +62,6 @@ def process(self, instance): renderproducts = RenderProducts(context.data["project_settings"]) img_format = renderproducts.image_format() expected_frames = instance.data.get("expectedFrameRange") - if not expected_frames: - expected_frames = list(range(int(rt.rendStart), int(rt.rendEnd) + 1)) files_by_aov: Dict[str, list[str]] = renderproducts.get_render_products(expected_frames) From 944d506cacc37eb2f850692b5e5cb5cb7cb1c8c4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 7 Jul 2026 17:12:14 +0800 Subject: [PATCH 14/30] add docstrings. --- client/ayon_max/api/lib_renderproducts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_max/api/lib_renderproducts.py b/client/ayon_max/api/lib_renderproducts.py index e252261026..072a1c2c75 100644 --- a/client/ayon_max/api/lib_renderproducts.py +++ b/client/ayon_max/api/lib_renderproducts.py @@ -89,6 +89,8 @@ def get_multiple_render_products( Args: outputs (list[str]): A list of output file paths. cameras (list[str]): A list of camera names. + frame_range (list[int]): A list of frames to generate expected + output file paths for. Returns: Dict[str, list[str]]: A dictionary containing render output file From ba0d366c48d62be0744fb2de57b2b5a8ea77b36a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 7 Jul 2026 21:13:10 +0800 Subject: [PATCH 15/30] add the colorspace data into the OIIO transcode. --- .../plugins/publish/collect_render.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/client/ayon_max/plugins/publish/collect_render.py b/client/ayon_max/plugins/publish/collect_render.py index 707ea684f9..fd13bb980c 100644 --- a/client/ayon_max/plugins/publish/collect_render.py +++ b/client/ayon_max/plugins/publish/collect_render.py @@ -112,18 +112,6 @@ def process(self, instance): if colorspace_data: instance.data.update(colorspace_data) - colorspace_product = colorspace.ARenderProduct( - instance.data["frameStartHandle"], - instance.data["frameEndHandle"] - ) - colorspace_product.add_colorspace_data( - product_name=str(instance.name), - colorspace=colorspace_data.get("colorspace", "sRGB"), - view=colorspace_data.get("sceneView", "ACES 1.0"), - display=colorspace_data.get("sceneDisplay", "sRGB") - ) - instance.data["renderProducts"] = colorspace_product - instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] @@ -141,7 +129,20 @@ def process(self, instance): ) self._precollect_required_data(instance) - # also need to get the render dir for conversion + # set the colorspace data for each AOV in the instance data + for aov_name in files_by_aov.keys(): + colorspace_product = colorspace.ARenderProduct( + instance.data["frameStartHandle"], + instance.data["frameEndHandle"] + ) + colorspace_product.add_colorspace_data( + product_name=aov_name, + colorspace=colorspace_data.get("colorspace", "sRGB"), + view=colorspace_data.get("sceneView", "ACES 1.0"), + display=colorspace_data.get("sceneDisplay", "sRGB") + ) + instance.data["renderProducts"] = colorspace_product + data = { "folderPath": instance.data["folderPath"], "workfile_name": filename, From 40245600d5e117f4137c1d97da7c8b897ed9747b Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Thu, 16 Jul 2026 01:05:35 +0800 Subject: [PATCH 16/30] Update client/ayon_max/api/lib.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- client/ayon_max/api/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_max/api/lib.py b/client/ayon_max/api/lib.py index 23e7f8948e..969a7fa94a 100644 --- a/client/ayon_max/api/lib.py +++ b/client/ayon_max/api/lib.py @@ -29,8 +29,9 @@ get_current_task_entity ) from ayon_core.pipeline.template_data import get_template_data_with_names -from ayon_core.pipeline.farm.pyblish_functions import \ - convert_frames_str_to_list +from ayon_core.pipeline.farm.pyblish_functions import ( + convert_frames_str_to_list, +) from ayon_core.pipeline.create import CreateContext from ayon_core.style import load_stylesheet From ea91cfdc340b373bdb1ed1d0803bbb12ffc24cb6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 16 Jul 2026 14:15:09 +0800 Subject: [PATCH 17/30] add docstring --- client/ayon_max/api/lib_renderproducts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_max/api/lib_renderproducts.py b/client/ayon_max/api/lib_renderproducts.py index f9aed9a577..d08895dd90 100644 --- a/client/ayon_max/api/lib_renderproducts.py +++ b/client/ayon_max/api/lib_renderproducts.py @@ -44,6 +44,10 @@ def get_render_products(self, frame_range: list[int]) -> Dict[str, list[str]]: Handles both beauty and AOV extraction with shared setup logic. Always includes the beauty pass; optionally includes render elements/AOVs. + Args: + frame_range (list[int]): A list of frames to generate expected + output file paths for. + Returns: Dict[str, list[str]]: A dictionary containing render output file paths. Beauty key is "beauty"; AOV keys are named after the render element From a0e7ad08bf2c2990045dd9fa503f6ab97f54daf3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 16 Jul 2026 14:15:42 +0800 Subject: [PATCH 18/30] add comment for `hasExplicitFrames` and remove unneccessary instance data --- client/ayon_max/plugins/publish/collect_render_local.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_max/plugins/publish/collect_render_local.py b/client/ayon_max/plugins/publish/collect_render_local.py index 50dea7537d..f126a3ba0c 100644 --- a/client/ayon_max/plugins/publish/collect_render_local.py +++ b/client/ayon_max/plugins/publish/collect_render_local.py @@ -78,10 +78,10 @@ def process(self, instance): aov_instance.data.update(aov_instance_data) aov_instance.data["families"] = [f"render.{render_target}"] + # The hasExplicitFrames flag controls whether frame indices are renumbered. + # Setting this to True ensures the published frames keep their original numbering + # and are not shifted during integration with the AOV server. aov_instance.data["hasExplicitFrames"] = True - aov_instance.data["reuseLastVersion"] = instance.data.get( - "reuse_last_version", False - ) # Pass on 'review' family if "review" in aov_instance_data["families"]: From 8a70ebaab617fac37455efd58f960b1ea37a6b12 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Thu, 16 Jul 2026 18:45:18 +0800 Subject: [PATCH 19/30] Update client/ayon_max/plugins/publish/collect_render_local.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- client/ayon_max/plugins/publish/collect_render_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_max/plugins/publish/collect_render_local.py b/client/ayon_max/plugins/publish/collect_render_local.py index f126a3ba0c..cc0164cf55 100644 --- a/client/ayon_max/plugins/publish/collect_render_local.py +++ b/client/ayon_max/plugins/publish/collect_render_local.py @@ -80,7 +80,7 @@ def process(self, instance): # The hasExplicitFrames flag controls whether frame indices are renumbered. # Setting this to True ensures the published frames keep their original numbering - # and are not shifted during integration with the AOV server. + # and are not shifted during integration with the AYON server. aov_instance.data["hasExplicitFrames"] = True # Pass on 'review' family From c31a3d135a281552d4e90a5177e120d610d5e4d7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 30 Jul 2026 21:07:20 +0800 Subject: [PATCH 20/30] rename frame_range to frames --- client/ayon_max/api/lib_renderproducts.py | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/client/ayon_max/api/lib_renderproducts.py b/client/ayon_max/api/lib_renderproducts.py index d08895dd90..5eb112bafa 100644 --- a/client/ayon_max/api/lib_renderproducts.py +++ b/client/ayon_max/api/lib_renderproducts.py @@ -38,14 +38,14 @@ def __init__(self, project_settings: Dict[str, Any] = None): get_current_project_name() ) - def get_render_products(self, frame_range: list[int]) -> Dict[str, list[str]]: + def get_render_products(self, frames: list[int]) -> Dict[str, list[str]]: """Get render output file paths for the current scene. Handles both beauty and AOV extraction with shared setup logic. Always includes the beauty pass; optionally includes render elements/AOVs. Args: - frame_range (list[int]): A list of frames to generate expected + frames (list[int]): A list of frames to generate expected output file paths for. Returns: @@ -63,7 +63,7 @@ def get_render_products(self, frame_range: list[int]) -> Dict[str, list[str]]: render_dict: Dict[str, list[str]] = {} # Always add beauty pass - render_dict["beauty"] = self.get_expected_beauty(frame_range, extension) + render_dict["beauty"] = self.get_expected_beauty(frames, extension) # Optionally add AOVs renderer = get_current_renderer() @@ -73,7 +73,7 @@ def get_render_products(self, frame_range: list[int]) -> Dict[str, list[str]]: for aov_name, aov_filepath in render_elements: aov_expected_files = self.get_expected_files( aov_filepath, - frame_range, + frames, aov_name, renderer_name ) @@ -81,7 +81,7 @@ def get_render_products(self, frame_range: list[int]) -> Dict[str, list[str]]: return render_dict def get_multiple_render_products( - self, outputs: list[str], cameras: list[str], frame_range: list[int] + self, outputs: list[str], cameras: list[str], frames: list[int] ) -> Dict[str, list[str]]: """Get render output file paths for multiple cameras. @@ -92,7 +92,7 @@ def get_multiple_render_products( Args: outputs (list[str]): A list of output file paths. cameras (list[str]): A list of camera names. - frame_range (list[int]): A list of frames to generate expected + frames (list[int]): A list of frames to generate expected output file paths for. Returns: @@ -110,7 +110,7 @@ def get_multiple_render_products( ext = ext.replace(".", "") # Always add beauty pass - beauty_files = self.get_expected_beauty(frame_range, ext) + beauty_files = self.get_expected_beauty(frames, ext) render_output_frames[f"{camera}_beauty"] = beauty_files # Add AOVs @@ -119,7 +119,7 @@ def get_multiple_render_products( for aov_name, aov_filepath in render_elements: aov_expected_files = self.get_expected_files( aov_filepath, - frame_range, + frames, aov_name, renderer_name ) @@ -128,12 +128,12 @@ def get_multiple_render_products( return render_output_frames def get_expected_beauty( - self, frame_range: list[int], extension: str + self, frames: list[int], extension: str ) -> list[str]: """Get expected beauty render output file paths for each frame. Args: - frame_range (list[int]): The frame range to generate expected + frames (list[int]): The frame range to generate expected output file paths for. extension (str): The file extension for the output files. @@ -151,7 +151,7 @@ def get_expected_beauty( return self.get_expected_files( output_path, - frame_range, + frames, "", renderer_name ) @@ -263,7 +263,7 @@ def get_arnold_render_output(self, arnold_renderer: Any, extension: str) -> str: def get_expected_files( self, filepath: str, - frame_range: list[int], + frames: list[int], aov_name: str, renderer_name: str, ) -> list[str]: @@ -271,7 +271,7 @@ def get_expected_files( Args: filepath (str): filepath of the render output. - frame_range (list[int]): range of frames of the render sequence. + frames (list[int]): range of frames of the render sequence. aov_name (str): name of the AOV. renderer_name (str): name of the renderer. @@ -286,7 +286,7 @@ def get_expected_files( name, ext = os.path.splitext(filename) name = name.lstrip(".") aov_name = aov_name.strip() - for frame in frame_range: + for frame in frames: aov_filename = f"{name}.{frame:04d}{ext}" expected_aov = os.path.join(directory, aov_filename) if aov_name and renderer_name.startswith("V_Ray_"): From f5de5af3f55d234d24d4e001a2486823a6137287 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 30 Jul 2026 21:08:33 +0800 Subject: [PATCH 21/30] add comment to explain why ordering move later --- client/ayon_max/plugins/publish/collect_frame_range.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_max/plugins/publish/collect_frame_range.py b/client/ayon_max/plugins/publish/collect_frame_range.py index 50d1789fef..65485190f0 100644 --- a/client/ayon_max/plugins/publish/collect_frame_range.py +++ b/client/ayon_max/plugins/publish/collect_frame_range.py @@ -7,6 +7,8 @@ class CollectFrameRange(pyblish.api.InstancePlugin): """Collect Frame Range.""" + # move the collector order later to ensure + # it run after CollectCustomFrameRange in Core addon order = pyblish.api.CollectorOrder + 0.019 label = "Collect Frame Range" hosts = ['max'] From 87e8ebbb55f929ad7311cfbbf97e858da0f89459 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 30 Jul 2026 21:08:47 +0800 Subject: [PATCH 22/30] clean up --- client/ayon_max/api/lib.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/ayon_max/api/lib.py b/client/ayon_max/api/lib.py index 969a7fa94a..c1a1adca89 100644 --- a/client/ayon_max/api/lib.py +++ b/client/ayon_max/api/lib.py @@ -1171,12 +1171,6 @@ def get_expected_frames(instance: pyblish.api.Instance) -> list[int]: frames_str = instance.data.get("customFrames", "") if frames_str: frames_list = convert_frames_str_to_list(frames_str.strip()) - frames_list = sorted({int(frame) for frame in frames_list}) - if not frames_list: - raise PublishError( - f"Invalid customFrames value: {frames_str}. " - "Expected a comma-separated list of integers or ranges." - ) return frames_list frame_start = int(rt.rendStart) frame_end = int(rt.rendEnd) From 6ddfc05a934de85d5aa08666e36ffaaea64225e3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 30 Jul 2026 21:15:39 +0800 Subject: [PATCH 23/30] clarify the comment on HasExplicitFrames --- client/ayon_max/plugins/publish/collect_render_local.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_max/plugins/publish/collect_render_local.py b/client/ayon_max/plugins/publish/collect_render_local.py index cc0164cf55..ea3351ef10 100644 --- a/client/ayon_max/plugins/publish/collect_render_local.py +++ b/client/ayon_max/plugins/publish/collect_render_local.py @@ -78,9 +78,8 @@ def process(self, instance): aov_instance.data.update(aov_instance_data) aov_instance.data["families"] = [f"render.{render_target}"] - # The hasExplicitFrames flag controls whether frame indices are renumbered. - # Setting this to True ensures the published frames keep their original numbering - # and are not shifted during integration with the AYON server. + # The hasExplicitFrames flag would ensure the preset frame ranges are + # preserved and not renumbered to consecutive frame ranges on publish. aov_instance.data["hasExplicitFrames"] = True # Pass on 'review' family From 8d8e0e98176b49facfe10bd34a62b818615468c3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 30 Jul 2026 21:17:13 +0800 Subject: [PATCH 24/30] ruff fix --- client/ayon_max/api/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_max/api/lib.py b/client/ayon_max/api/lib.py index c1a1adca89..dc0048e5ed 100644 --- a/client/ayon_max/api/lib.py +++ b/client/ayon_max/api/lib.py @@ -22,7 +22,6 @@ AVALON_INSTANCE_ID, ) from ayon_core.lib import StringTemplate, get_version_from_path -from ayon_core.pipeline.publish import PublishError from ayon_core.tools.utils import SimplePopup from ayon_core.settings import get_project_settings from ayon_core.pipeline.context_tools import ( From 5868d9b466763506597ae2c815271ee9c6334613 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 10 Aug 2026 21:27:56 +0800 Subject: [PATCH 25/30] update the repair action for repairing arnold render settings. --- client/ayon_max/plugins/publish/validate_rendersettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_max/plugins/publish/validate_rendersettings.py b/client/ayon_max/plugins/publish/validate_rendersettings.py index cbe9a2d3ee..7b9f856218 100644 --- a/client/ayon_max/plugins/publish/validate_rendersettings.py +++ b/client/ayon_max/plugins/publish/validate_rendersettings.py @@ -496,12 +496,12 @@ def repair_arnold_settings( if not path: path = reset_rendersetting(instance, project_settings) render_dir = os.path.dirname(path) - aov_manager.outputPath = path filename = os.path.basename(path) rt.rendOutputFilename = build_general_output_filename( render_dir, filename, ) + aov_manager.outputPath = os.path.dirname(rt.rendOutputFilename) driver = aov_manager.drivers[0] driver.multipart = get_multipass_setting( renderer_name, From 34a3fa288bb91ad5724ebef2d3c37c7751f25129 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 10 Aug 2026 22:51:28 +0800 Subject: [PATCH 26/30] fix the local rendering failed in Arnold. --- .../ayon_max/plugins/publish/extract_render.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/client/ayon_max/plugins/publish/extract_render.py b/client/ayon_max/plugins/publish/extract_render.py index 6e035ed3d2..9d8aba532d 100644 --- a/client/ayon_max/plugins/publish/extract_render.py +++ b/client/ayon_max/plugins/publish/extract_render.py @@ -1,4 +1,6 @@ from __future__ import annotations + +import os from ayon_core.pipeline import publish from ayon_core.pipeline.publish import KnownPublishError @@ -36,13 +38,19 @@ def process(self, instance): rt.getNodeByName(camera) if camera else rt.viewport.GetCamera() ) - + kwargs = { + "camera_node": camera_node, + "cancelled": pymxs.byref(None), + "vfb": False, + } for frame in instance.data["expectedFrameRange"]: + if instance.data["renderer"].startswith("Arnold"): + filename, ext = os.path.splitext(rt.rendOutputFilename) + new_output = f"{filename}{frame:04d}{ext}" + kwargs["outputFile"] = new_output _, cancelled = rt.render( frame=frame, - vfb=False, - camera=camera_node, - cancelled=pymxs.byref(None) + **kwargs ) if cancelled: raise KnownPublishError(f"Render cancelled at frame {frame}.") From 38b3a687191310bfad066270b43b96f640399d78 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 14 Aug 2026 14:33:30 +0800 Subject: [PATCH 27/30] normalize the path for stagingDir when using local rendering --- client/ayon_max/plugins/publish/collect_render_local.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_max/plugins/publish/collect_render_local.py b/client/ayon_max/plugins/publish/collect_render_local.py index ea3351ef10..03863e5062 100644 --- a/client/ayon_max/plugins/publish/collect_render_local.py +++ b/client/ayon_max/plugins/publish/collect_render_local.py @@ -1,3 +1,5 @@ +import os + import pyblish.api from ayon_core.pipeline.farm.pyblish_functions import ( @@ -71,7 +73,7 @@ def process(self, instance): # like the "stagingDir" for each representation which we will make # absolute again. for repre in aov_instance_data["representations"]: - repre["stagingDir"] = anatomy.fill_root(repre["stagingDir"]) + repre["stagingDir"] = os.path.normpath(anatomy.fill_root(repre["stagingDir"])) aov_instance = context.create_instance( aov_instance_data["productName"] ) From e5a4bab00410391a3a961d4e70c459357bcae744 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 14 Aug 2026 18:56:22 +0800 Subject: [PATCH 28/30] use Pathlib only for expectedFiles. --- client/ayon_max/api/lib.py | 8 +++----- client/ayon_max/api/lib_renderproducts.py | 11 ++++++----- .../ayon_max/plugins/publish/collect_render_local.py | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/client/ayon_max/api/lib.py b/client/ayon_max/api/lib.py index dc0048e5ed..d8550094a9 100644 --- a/client/ayon_max/api/lib.py +++ b/client/ayon_max/api/lib.py @@ -4,7 +4,6 @@ import contextlib import logging import json -from pathlib import Path from functools import partial import pyblish.api import re @@ -306,10 +305,9 @@ def get_default_render_folder( render_data["work"] = work_dir render_folder = project_setting["max"]["RenderSettings"]["default_render_image_folder"] formatted_render_folder = StringTemplate(render_folder).format(render_data) - normalized_render_folder = Path(formatted_render_folder) - if not normalized_render_folder.is_absolute(): - normalized_render_folder = Path(work_dir) / normalized_render_folder - return str(normalized_render_folder) + if not os.path.isabs(formatted_render_folder): + formatted_render_folder = os.path.join(work_dir, formatted_render_folder) + return formatted_render_folder def get_vray_settings(renderer_name: str, renderer: Any) -> Any: diff --git a/client/ayon_max/api/lib_renderproducts.py b/client/ayon_max/api/lib_renderproducts.py index 5eb112bafa..6f3a33f484 100644 --- a/client/ayon_max/api/lib_renderproducts.py +++ b/client/ayon_max/api/lib_renderproducts.py @@ -4,6 +4,7 @@ # https://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_3ds_max_ax_maxscript_commands_ax_renderview_commands_html from __future__ import annotations import os +from pathlib import Path from typing import Dict, Any try: @@ -281,19 +282,19 @@ def get_expected_files( expected_aovs: list[str] = [] if not filepath: return expected_aovs - directory = os.path.dirname(filepath) - filename = os.path.basename(filepath) + file_path = Path(filepath) + directory = file_path.parent + filename = file_path.name name, ext = os.path.splitext(filename) name = name.lstrip(".") aov_name = aov_name.strip() for frame in frames: aov_filename = f"{name}.{frame:04d}{ext}" - expected_aov = os.path.join(directory, aov_filename) if aov_name and renderer_name.startswith("V_Ray_"): aov_filename = f"{name}.{aov_name}.{frame:04d}{ext}" aov_filename = reformat_filename(aov_filename) - expected_aov = os.path.join(directory, aov_filename) - expected_aovs.append(expected_aov) + expected_aov = Path(directory) / aov_filename + expected_aovs.append(expected_aov.as_posix()) return expected_aovs diff --git a/client/ayon_max/plugins/publish/collect_render_local.py b/client/ayon_max/plugins/publish/collect_render_local.py index 03863e5062..afd51493b4 100644 --- a/client/ayon_max/plugins/publish/collect_render_local.py +++ b/client/ayon_max/plugins/publish/collect_render_local.py @@ -73,7 +73,7 @@ def process(self, instance): # like the "stagingDir" for each representation which we will make # absolute again. for repre in aov_instance_data["representations"]: - repre["stagingDir"] = os.path.normpath(anatomy.fill_root(repre["stagingDir"])) + repre["stagingDir"] = anatomy.fill_root(repre["stagingDir"]) aov_instance = context.create_instance( aov_instance_data["productName"] ) From ecc9991a4fef12fea0ab2f0af8ba3492bcc9e02e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 14 Aug 2026 19:00:23 +0800 Subject: [PATCH 29/30] ruff fix --- client/ayon_max/plugins/publish/collect_render_local.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_max/plugins/publish/collect_render_local.py b/client/ayon_max/plugins/publish/collect_render_local.py index afd51493b4..645a893597 100644 --- a/client/ayon_max/plugins/publish/collect_render_local.py +++ b/client/ayon_max/plugins/publish/collect_render_local.py @@ -1,4 +1,3 @@ -import os import pyblish.api From 67e2d2bc03c3fb9216ff87535bf78193018ce859 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 14 Aug 2026 21:29:50 +0800 Subject: [PATCH 30/30] normalized the forward slashes to fix the path issue faced in Arnold --- client/ayon_max/api/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_max/api/lib.py b/client/ayon_max/api/lib.py index d8550094a9..2c545161c3 100644 --- a/client/ayon_max/api/lib.py +++ b/client/ayon_max/api/lib.py @@ -307,7 +307,8 @@ def get_default_render_folder( formatted_render_folder = StringTemplate(render_folder).format(render_data) if not os.path.isabs(formatted_render_folder): formatted_render_folder = os.path.join(work_dir, formatted_render_folder) - return formatted_render_folder + # Normalize to forward slashes for consistency + return formatted_render_folder.replace("\\", "/") def get_vray_settings(renderer_name: str, renderer: Any) -> Any: