From d7a2c57fd836ae6e26bc0f7e02f3b46a81a70dd8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 31 Jul 2024 13:54:41 +0200 Subject: [PATCH 01/85] Refactor OCIO config handling, introduce fallback mechanism - Added function to extract config path from profile data - Updated global config retrieval to use new function - Introduced fallback mechanism for missing product entities --- client/ayon_core/pipeline/colorspace.py | 67 +++++++++++++++++----- server/settings/conversion.py | 38 +++++++++++++ server/settings/main.py | 75 ++++++++++++++++++++++--- 3 files changed, 156 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 099616ff4a..1986e2d407 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -699,6 +699,33 @@ def get_ocio_config_views(config_path): ) +def _get_config_path_from_profile_data( + data, data_type, template_data) -> dict: + """Get config path from profile data. + + Args: + data (dict[str, Any]): Profile data. + data_type (str): Profile type. + template_data (dict[str, Any]): Template data. + + Returns: + dict[str, str]: Config data with path and template. + """ + template = data[data_type] + result = StringTemplate.format_strict_template( + template, template_data + ) + normalized_path = str(result.normalized()) + if not os.path.exists(normalized_path): + log.warning(f"Path was not found '{normalized_path}'.") + return None + + return { + "path": normalized_path, + "template": template + } + + def _get_global_config_data( project_name, host_name, @@ -717,7 +744,7 @@ def _get_global_config_data( 2. Custom path to ocio config. 3. Path to 'ocioconfig' representation on product. Name of product can be defined in settings. Product name can be regex but exact match is - always preferred. + always preferred. Fallback can be defined in case no product is found. None is returned when no profile is found, when path @@ -755,19 +782,8 @@ def _get_global_config_data( profile_type = profile["type"] if profile_type in ("builtin_path", "custom_path"): - template = profile[profile_type] - result = StringTemplate.format_strict_template( - template, template_data - ) - normalized_path = str(result.normalized()) - if not os.path.exists(normalized_path): - log.warning(f"Path was not found '{normalized_path}'.") - return None - - return { - "path": normalized_path, - "template": template - } + return _get_config_path_from_profile_data( + profile, profile_type, template_data) # TODO decide if this is the right name for representation repre_name = "ocioconfig" @@ -778,7 +794,21 @@ def _get_global_config_data( return None folder_path = folder_info["path"] - product_name = profile["product_name"] + # Backward compatibility for old projects + # TODO remove in future 0.4.4 onwards + product_name = profile.get("product_name") + # TODO: this should be required after backward compatibility is removed + fallback_data = None + published_product_data = profile.get("published_product") + + if product_name is None and published_product_data is None: + log.warning("Product name or published product is missing.") + return None + + if published_product_data: + product_name = published_product_data["product_name"] + fallback_data = published_product_data["fallback"] + if folder_id is None: folder_entity = ayon_api.get_folder_by_path( project_name, folder_path, fields={"id"} @@ -798,6 +828,13 @@ def _get_global_config_data( ) } if not product_entities_by_name: + # TODO: make this required in future 0.4.4 onwards + if fallback_data: + # in case no product was found we need to use fallback + fallback_type = fallback_data["type"] + return _get_config_path_from_profile_data( + fallback_data, fallback_type, template_data + ) log.debug( f"No product entities were found for folder '{folder_path}' with" f" product name filter '{product_name}'." diff --git a/server/settings/conversion.py b/server/settings/conversion.py index f513738603..b29c827cae 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -4,6 +4,43 @@ from .publish_plugins import DEFAULT_PUBLISH_VALUES +def _convert_imageio_configs_0_4_3(overrides): + """Imageio config settings did change to profiles since 0.4.3.""" + imageio_overrides = overrides.get("imageio") or {} + + # make sure settings are already converted to profiles + if ( + "ocio_config_profiles" not in imageio_overrides + ): + return + + ocio_config_profiles = imageio_overrides["ocio_config_profiles"] + + for inx, profile in enumerate(ocio_config_profiles): + if profile["type"] != "product_name": + continue + + # create new profile + new_profile = { + "type": "published_product", + "published_product": { + "product_name": profile["product_name"], + "fallback": { + "type": "builtin_path", + "builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio", + }, + }, + "host_names": profile["host_names"], + "task_names": profile["task_names"], + "task_types": profile["task_types"], + "custom_path": profile["custom_path"], + "builtin_path": profile["builtin_path"], + } + + # replace old profile with new profile + ocio_config_profiles[inx] = new_profile + + def _convert_imageio_configs_0_3_1(overrides): """Imageio config settings did change to profiles since 0.3.1. .""" imageio_overrides = overrides.get("imageio") or {} @@ -82,5 +119,6 @@ def convert_settings_overrides( overrides: dict[str, Any], ) -> dict[str, Any]: _convert_imageio_configs_0_3_1(overrides) + _convert_imageio_configs_0_4_3(overrides) _conver_publish_plugins(overrides) return overrides diff --git a/server/settings/main.py b/server/settings/main.py index 0972ccdfb9..09c9bf0065 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -58,7 +58,14 @@ def _ocio_config_profile_types(): return [ {"value": "builtin_path", "label": "AYON built-in OCIO config"}, {"value": "custom_path", "label": "Path to OCIO config"}, - {"value": "product_name", "label": "Published product"}, + {"value": "published_product", "label": "Published product"}, + ] + + +def _fallback_ocio_config_profile_types(): + return [ + {"value": "builtin_path", "label": "AYON built-in OCIO config"}, + {"value": "custom_path", "label": "Path to OCIO config"}, ] @@ -76,6 +83,49 @@ def _ocio_built_in_paths(): ] +class FallbackProductModel(BaseSettingsModel): + _layout = "expanded" + type: str = SettingsField( + title="Fallback config type", + enum_resolver=_fallback_ocio_config_profile_types, + conditionalEnum=True, + default="builtin_path", + description=( + "Type of config which needs to be used in case published " + "product is not found." + ), + ) + builtin_path: str = SettingsField( + "ACES 1.2", + title="Built-in OCIO config", + enum_resolver=_ocio_built_in_paths, + description=( + "AYON ocio addon distributed OCIO config. " + "Activated addon in bundle is required: 'ayon_ocio' >= 1.1.1" + ), + ) + custom_path: str = SettingsField( + "", + title="OCIO config path", + description="Path to OCIO config. Anatomy formatting is supported.", + ) + + +class PublishedProductModel(BaseSettingsModel): + _layout = "expanded" + product_name: str = SettingsField( + "", + title="Product name", + description=( + "Context related published product name to get OCIO config from. " + "Partial match is supported via use of regex expression." + ), + ) + fallback: FallbackProductModel = SettingsField( + default_factory=FallbackProductModel, + ) + + class CoreImageIOConfigProfilesModel(BaseSettingsModel): _layout = "expanded" host_names: list[str] = SettingsField( @@ -102,19 +152,19 @@ class CoreImageIOConfigProfilesModel(BaseSettingsModel): "ACES 1.2", title="Built-in OCIO config", enum_resolver=_ocio_built_in_paths, + description=( + "AYON ocio addon distributed OCIO config. " + "Activated addon in bundle is required: 'ayon_ocio' >= 1.1.1" + ), ) custom_path: str = SettingsField( "", title="OCIO config path", description="Path to OCIO config. Anatomy formatting is supported.", ) - product_name: str = SettingsField( - "", - title="Product name", - description=( - "Published product name to get OCIO config from. " - "Partial match is supported." - ), + published_product: PublishedProductModel = SettingsField( + default_factory=PublishedProductModel, + title="Published product", ) @@ -294,7 +344,14 @@ def validate_json(cls, value): "type": "builtin_path", "builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio", "custom_path": "", - "product_name": "", + "published_product": { + "product_name": "", + "fallback": { + "type": "builtin_path", + "builtin_path": "ACES 1.2", + "custom_path": "" + } + } } ], "file_rules": { From 9375b8bee2985f251e5aad1b6b4309caa1c4c5d1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 31 Jul 2024 13:57:47 +0200 Subject: [PATCH 02/85] Update version references for future removal and conversion functions in colorspace and conversion modules. - Update version references to 0.4.5 for future removal in colorspace module. - Update function name and version reference to 0.4.4 for imageio config conversion in the conversion module. --- client/ayon_core/pipeline/colorspace.py | 4 ++-- server/settings/conversion.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 1986e2d407..106d43d55a 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -795,7 +795,7 @@ def _get_global_config_data( folder_path = folder_info["path"] # Backward compatibility for old projects - # TODO remove in future 0.4.4 onwards + # TODO remove in future 0.4.5 onwards product_name = profile.get("product_name") # TODO: this should be required after backward compatibility is removed fallback_data = None @@ -828,7 +828,7 @@ def _get_global_config_data( ) } if not product_entities_by_name: - # TODO: make this required in future 0.4.4 onwards + # TODO: make this required in future 0.4.5 onwards if fallback_data: # in case no product was found we need to use fallback fallback_type = fallback_data["type"] diff --git a/server/settings/conversion.py b/server/settings/conversion.py index b29c827cae..d99483d21f 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -4,8 +4,8 @@ from .publish_plugins import DEFAULT_PUBLISH_VALUES -def _convert_imageio_configs_0_4_3(overrides): - """Imageio config settings did change to profiles since 0.4.3.""" +def _convert_imageio_configs_0_4_4(overrides): + """Imageio config settings did change to profiles since 0.4.4.""" imageio_overrides = overrides.get("imageio") or {} # make sure settings are already converted to profiles @@ -119,6 +119,6 @@ def convert_settings_overrides( overrides: dict[str, Any], ) -> dict[str, Any]: _convert_imageio_configs_0_3_1(overrides) - _convert_imageio_configs_0_4_3(overrides) + _convert_imageio_configs_0_4_4(overrides) _conver_publish_plugins(overrides) return overrides From 1e026d8fcb10e053615b1795edc5157600935e32 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 1 Aug 2024 15:50:45 +0200 Subject: [PATCH 03/85] Refactor config data retrieval logic in colorspace module - Removed redundant folder info handling - Added fallback mechanism for missing folder info --- client/ayon_core/pipeline/colorspace.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 106d43d55a..57a36286db 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -788,12 +788,6 @@ def _get_global_config_data( # TODO decide if this is the right name for representation repre_name = "ocioconfig" - folder_info = template_data.get("folder") - if not folder_info: - log.warning("Folder info is missing.") - return None - folder_path = folder_info["path"] - # Backward compatibility for old projects # TODO remove in future 0.4.5 onwards product_name = profile.get("product_name") @@ -809,6 +803,23 @@ def _get_global_config_data( product_name = published_product_data["product_name"] fallback_data = published_product_data["fallback"] + folder_info = template_data.get("folder") + if not folder_info: + log.warning("Folder info is missing.") + + # TODO: this fallback should be required after backward compatibility + # is removed + if fallback_data: + log.info("Using fallback data for ocio config path.") + # in case no product was found we need to use fallback + fallback_type = fallback_data["type"] + return _get_config_path_from_profile_data( + fallback_data, fallback_type, template_data + ) + return None + + folder_path = folder_info["path"] + if folder_id is None: folder_entity = ayon_api.get_folder_by_path( project_name, folder_path, fields={"id"} @@ -827,6 +838,7 @@ def _get_global_config_data( fields={"id", "name"} ) } + if not product_entities_by_name: # TODO: make this required in future 0.4.5 onwards if fallback_data: @@ -874,6 +886,7 @@ def _get_global_config_data( path = get_representation_path_with_anatomy(repre_entity, anatomy) template = repre_entity["attrib"]["template"] + return { "path": path, "template": template, From f9e0b05bf2dac3f596a9c53f32368219072970dd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 9 Aug 2024 13:27:05 +0200 Subject: [PATCH 04/85] Refactor fallback handling in colorspace module Improve handling of fallback data for OCIO config path. Simplified logic and error messages for better clarity. --- client/ayon_core/pipeline/colorspace.py | 53 +++++++++---------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 57a36286db..82025fabaf 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -788,35 +788,27 @@ def _get_global_config_data( # TODO decide if this is the right name for representation repre_name = "ocioconfig" - # Backward compatibility for old projects - # TODO remove in future 0.4.5 onwards - product_name = profile.get("product_name") - # TODO: this should be required after backward compatibility is removed - fallback_data = None - published_product_data = profile.get("published_product") - - if product_name is None and published_product_data is None: - log.warning("Product name or published product is missing.") + published_product_data = profile["published_product"] + product_name = published_product_data["product_name"] + fallback_data = published_product_data["fallback"] + + if product_name == "": + log.error( + "Colorspace OCIO config path cannot be set. " + "Profile is set to published product but `Product name` is empty." + ) return None - if published_product_data: - product_name = published_product_data["product_name"] - fallback_data = published_product_data["fallback"] - folder_info = template_data.get("folder") if not folder_info: log.warning("Folder info is missing.") - # TODO: this fallback should be required after backward compatibility - # is removed - if fallback_data: - log.info("Using fallback data for ocio config path.") - # in case no product was found we need to use fallback - fallback_type = fallback_data["type"] - return _get_config_path_from_profile_data( - fallback_data, fallback_type, template_data - ) - return None + log.info("Using fallback data for ocio config path.") + # in case no product was found we need to use fallback + fallback_type = fallback_data["type"] + return _get_config_path_from_profile_data( + fallback_data, fallback_type, template_data + ) folder_path = folder_info["path"] @@ -840,18 +832,11 @@ def _get_global_config_data( } if not product_entities_by_name: - # TODO: make this required in future 0.4.5 onwards - if fallback_data: - # in case no product was found we need to use fallback - fallback_type = fallback_data["type"] - return _get_config_path_from_profile_data( - fallback_data, fallback_type, template_data - ) - log.debug( - f"No product entities were found for folder '{folder_path}' with" - f" product name filter '{product_name}'." + # in case no product was found we need to use fallback + fallback_type = fallback_data["type"] + return _get_config_path_from_profile_data( + fallback_data, fallback_type, template_data ) - return None # Try to use exact match first, otherwise use first available product product_entity = product_entities_by_name.get(product_name) From d51a04c8ac2b43f2b181709c728e7bda895e7658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 9 Aug 2024 13:31:25 +0200 Subject: [PATCH 05/85] Update client/ayon_core/pipeline/colorspace.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/colorspace.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 82025fabaf..867c3ec22a 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -700,18 +700,19 @@ def get_ocio_config_views(config_path): def _get_config_path_from_profile_data( - data, data_type, template_data) -> dict: + profile, profile_type, template_data +): """Get config path from profile data. Args: - data (dict[str, Any]): Profile data. - data_type (str): Profile type. + profile(dict[str, Any]): Profile data. + profile_type (str): Profile type. template_data (dict[str, Any]): Template data. Returns: dict[str, str]: Config data with path and template. """ - template = data[data_type] + template = profile[profile_type] result = StringTemplate.format_strict_template( template, template_data ) From f87057506a63c12fcc9835e4a093f60da9e1c52c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 9 Aug 2024 13:34:46 +0200 Subject: [PATCH 06/85] adding space --- client/ayon_core/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 867c3ec22a..1e6f98f272 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -705,7 +705,7 @@ def _get_config_path_from_profile_data( """Get config path from profile data. Args: - profile(dict[str, Any]): Profile data. + profile (dict[str, Any]): Profile data. profile_type (str): Profile type. template_data (dict[str, Any]): Template data. From 49448fc3ffb13d04281e9bb691ab443f0ed2dbd2 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 5 Sep 2024 16:08:03 +0300 Subject: [PATCH 07/85] fix running builder on start if scene is the default scene --- .../workfile/workfile_template_builder.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 7b15dff049..1fce07a722 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -533,29 +533,20 @@ def build_template( if create_first_version: created_version_workfile = self.create_first_workfile_version() - # if first version is created, import template - # and populate placeholders + # Build the template if ( create_first_version and workfile_creation_enabled - and created_version_workfile + and (created_version_workfile or self.host.get_current_workfile() is None) ): self.import_template(template_path) self.populate_scene_placeholders( level_limit, keep_placeholders) - # save workfile after template is populated + # save workfile if a workfile was created. + if created_version_workfile: self.save_workfile(created_version_workfile) - # ignore process if first workfile is enabled - # but a version is already created - if workfile_creation_enabled: - return - - self.import_template(template_path) - self.populate_scene_placeholders( - level_limit, keep_placeholders) - def rebuild_template(self): """Go through existing placeholders in scene and update them. From e4881e2fec7599863e311fd9124db89bc35fd2b8 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 5 Sep 2024 22:04:55 +0300 Subject: [PATCH 08/85] refactor and simplify `build_template` method --- .../workfile/workfile_template_builder.py | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 1fce07a722..283408452e 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -512,39 +512,28 @@ def build_template( process if version is created """ - if any( - value is None - for value in [ - template_path, - keep_placeholders, - create_first_version, - ] - ): - template_preset = self.get_template_preset() - if template_path is None: - template_path = template_preset["path"] + # Get default values if not provided + if template_path is None or keep_placeholders is None or create_first_version is None: + preset = self.get_template_preset() + template_path = template_path or preset["path"] if keep_placeholders is None: - keep_placeholders = template_preset["keep_placeholder"] + keep_placeholders = preset["keep_placeholder"] if create_first_version is None: - create_first_version = template_preset["create_first_version"] + create_first_version = preset["create_first_version"] # check if first version is created - created_version_workfile = False - if create_first_version: - created_version_workfile = self.create_first_workfile_version() + created_version_workfile = create_first_version and self.create_first_workfile_version() # Build the template if ( - create_first_version - and workfile_creation_enabled - and (created_version_workfile or self.host.get_current_workfile() is None) + not self.host.get_current_workfile() or created_version_workfile ): self.import_template(template_path) self.populate_scene_placeholders( level_limit, keep_placeholders) # save workfile if a workfile was created. - if created_version_workfile: + if workfile_creation_enabled and created_version_workfile: self.save_workfile(created_version_workfile) def rebuild_template(self): From 274585ced6e7a7c1c4582e0c28ddf0346b4a9039 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 6 Sep 2024 11:41:21 +0300 Subject: [PATCH 09/85] Optimize the code --- .../workfile/workfile_template_builder.py | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 283408452e..abee71f8ec 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -521,19 +521,26 @@ def build_template( if create_first_version is None: create_first_version = preset["create_first_version"] - # check if first version is created + # Create, open and return the first workfile version. + # This only works if there are no existent files and + # create_first_version is enabled. created_version_workfile = create_first_version and self.create_first_workfile_version() - # Build the template - if ( - not self.host.get_current_workfile() or created_version_workfile - ): + # # Abort the process if workfile_creation_enabled. + # if workfile_creation_enabled: + # return + + # Build the template if the current scene is empty + # or if we have created new file. + # which basically avoids any + if not self.host.get_current_workfile() or created_version_workfile: + self.log.info(f"Building the workfile template: {template_path}") self.import_template(template_path) self.populate_scene_placeholders( level_limit, keep_placeholders) # save workfile if a workfile was created. - if workfile_creation_enabled and created_version_workfile: + if created_version_workfile: self.save_workfile(created_version_workfile) def rebuild_template(self): @@ -833,11 +840,10 @@ def get_template_preset(self): keep_placeholder = True if not path: - raise TemplateLoadFailed(( - "Template path is not set.\n" - "Path need to be set in {}\\Template Workfile Build " - "Settings\\Profiles" - ).format(host_name.title())) + self.log.info( + "Template path is not set." + ) + return # Try fill path with environments and anatomy roots anatomy = Anatomy(project_name) From df203db3d7fa5c9e2d8877f550372c96b8496616 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Sep 2024 17:16:28 +0200 Subject: [PATCH 10/85] Refactor the `build_template` function for readability --- .../workfile/workfile_template_builder.py | 68 +++++++++++-------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index abee71f8ec..c535173038 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -15,6 +15,7 @@ import re import collections import copy +import warnings from abc import ABC, abstractmethod from ayon_api import ( @@ -489,7 +490,7 @@ def build_template( level_limit=None, keep_placeholders=None, create_first_version=None, - workfile_creation_enabled=False + workfile_creation_enabled=None ): """Main callback for building workfile from template path. @@ -507,41 +508,49 @@ def build_template( hosts to decide if they want to remove placeholder after it is used. create_first_version (bool): create first version of a workfile - workfile_creation_enabled (bool): If True, it might create - first version but ignore - process if version is created + workfile_creation_enabled (Any): Deprecated unused variable. """ + + if workfile_creation_enabled is not None: + warnings.warn( + ( + "Using deprecated argument `workfile_creation_enabled`. " + "It has been removed because it remained unused." + ), + category=DeprecationWarning + ) + # Get default values if not provided - if template_path is None or keep_placeholders is None or create_first_version is None: + if ( + template_path is None + or keep_placeholders is None + or create_first_version is None + ): preset = self.get_template_preset() - template_path = template_path or preset["path"] + template_path: str = template_path or preset["path"] if keep_placeholders is None: - keep_placeholders = preset["keep_placeholder"] + keep_placeholders: bool = preset["keep_placeholder"] if create_first_version is None: - create_first_version = preset["create_first_version"] - - # Create, open and return the first workfile version. - # This only works if there are no existent files and - # create_first_version is enabled. - created_version_workfile = create_first_version and self.create_first_workfile_version() - - # # Abort the process if workfile_creation_enabled. - # if workfile_creation_enabled: - # return + create_first_version: bool = preset["create_first_version"] - # Build the template if the current scene is empty - # or if we have created new file. - # which basically avoids any - if not self.host.get_current_workfile() or created_version_workfile: + # Build the template if current workfile is a new unsaved file + # (that's detected by checking if it returns any current filepath) + if not self.host.get_current_workfile(): self.log.info(f"Building the workfile template: {template_path}") self.import_template(template_path) self.populate_scene_placeholders( level_limit, keep_placeholders) - # save workfile if a workfile was created. - if created_version_workfile: - self.save_workfile(created_version_workfile) + if not create_first_version: + # Do not save whatsoever + return + + # If there is no existing workfile, save the first version + workfile_path = self.get_first_workfile_version_to_create() + if workfile_path: + self.log.info("Saving first workfile: %s", workfile_path) + self.save_workfile(workfile_path) def rebuild_template(self): """Go through existing placeholders in scene and update them. @@ -595,7 +604,7 @@ def import_template(self, template_path): pass - def create_first_workfile_version(self): + def get_first_workfile_version_to_create(self): """ Create first version of workfile. @@ -605,7 +614,12 @@ def create_first_workfile_version(self): Args: template_path (str): Fullpath for current task and host's template file. + + Return: + Optional[str]: Filepath to save to, if we should save. """ + # AYON_LAST_WORKFILE will be set to the last existing workfile OR + # if none exist it will be set to the first version. last_workfile_path = os.environ.get("AYON_LAST_WORKFILE") self.log.info("__ last_workfile_path: {}".format(last_workfile_path)) if os.path.exists(last_workfile_path): @@ -613,10 +627,6 @@ def create_first_workfile_version(self): self.log.info("Workfile already exists, skipping creation.") return False - # Create first version - self.log.info("Creating first version of workfile.") - self.save_workfile(last_workfile_path) - # Confirm creation of first version return last_workfile_path From a653ed64e45a20f35f449e8d84a180d5651e4a78 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Sep 2024 17:31:24 +0200 Subject: [PATCH 11/85] :tada: `workfile_creation_enabled` argument was actually useful after all --- .../workfile/workfile_template_builder.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index c535173038..9986e8ddae 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -15,7 +15,6 @@ import re import collections import copy -import warnings from abc import ABC, abstractmethod from ayon_api import ( @@ -508,19 +507,12 @@ def build_template( hosts to decide if they want to remove placeholder after it is used. create_first_version (bool): create first version of a workfile - workfile_creation_enabled (Any): Deprecated unused variable. + workfile_creation_enabled (Any): Whether we are creating a new + workfile. If False, we assume we just want to build the + template in our current scene. """ - if workfile_creation_enabled is not None: - warnings.warn( - ( - "Using deprecated argument `workfile_creation_enabled`. " - "It has been removed because it remained unused." - ), - category=DeprecationWarning - ) - # Get default values if not provided if ( template_path is None @@ -542,6 +534,12 @@ def build_template( self.populate_scene_placeholders( level_limit, keep_placeholders) + if not workfile_creation_enabled: + # Do not consider saving a first workfile version, if this + # is not set to be a "workfile creation". Then we assume + # only the template should get build. + return + if not create_first_version: # Do not save whatsoever return From b470ff214d2c05dbd367e9bf6438caa768aaf39c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Sep 2024 17:33:55 +0200 Subject: [PATCH 12/85] Elaborate `create_first_version` argument to differentiate from `workfile_creation_enabled` --- .../ayon_core/pipeline/workfile/workfile_template_builder.py | 5 ++++- 1 file changed, 4 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 9986e8ddae..c28b156e46 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -506,7 +506,10 @@ def build_template( keep_placeholders (bool): Add flag to placeholder data for hosts to decide if they want to remove placeholder after it is used. - create_first_version (bool): create first version of a workfile + create_first_version (bool): Create first version of a workfile. + When set to True, this option initiates the saving of the + workfile for an initial version. It will skip saving if + a version already exists. workfile_creation_enabled (Any): Whether we are creating a new workfile. If False, we assume we just want to build the template in our current scene. From 0ca2e1044f0efdd3e25cde6ce391b2cd4ab6d8f7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Sep 2024 17:35:43 +0200 Subject: [PATCH 13/85] Fix argument signature --- .../ayon_core/pipeline/workfile/workfile_template_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index c28b156e46..e2c06db328 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -489,7 +489,7 @@ def build_template( level_limit=None, keep_placeholders=None, create_first_version=None, - workfile_creation_enabled=None + workfile_creation_enabled=False ): """Main callback for building workfile from template path. @@ -510,7 +510,7 @@ def build_template( When set to True, this option initiates the saving of the workfile for an initial version. It will skip saving if a version already exists. - workfile_creation_enabled (Any): Whether we are creating a new + workfile_creation_enabled (bool): Whether we are creating a new workfile. If False, we assume we just want to build the template in our current scene. From 0faaadab51b0e7b74c483a32e406f90b49f45636 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Sep 2024 17:47:25 +0200 Subject: [PATCH 14/85] Fix case of explicit load import and improve docstring --- .../workfile/workfile_template_builder.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index b24a4ab653..8bd9c00773 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -511,8 +511,10 @@ def build_template( workfile for an initial version. It will skip saving if a version already exists. workfile_creation_enabled (bool): Whether we are creating a new - workfile. If False, we assume we just want to build the - template in our current scene. + workfile. If False, we assume we explicitly want to build the + template in our current scene. Otherwise we only build if the + current file is not an existing saved workfile but a "new" + file. """ @@ -531,7 +533,14 @@ def build_template( # Build the template if current workfile is a new unsaved file # (that's detected by checking if it returns any current filepath) - if not self.host.get_current_workfile(): + if ( + # If not a workfile creation, an explicit load template + # was requested, so we always want to build the template + not workfile_creation_enabled + # Or if workfile creation, but we're not in an active file + # we still need to build the "new workfile template" + or not self.host.get_current_workfile() + ): self.log.info(f"Building the workfile template: {template_path}") self.import_template(template_path) self.populate_scene_placeholders( From a27157f46d90b8f08fc858583dc4dd85ffc15940 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Sep 2024 15:58:20 +0200 Subject: [PATCH 15/85] Please @fabiaserra's eyes with better indentation --- .../pipeline/workfile/workfile_template_builder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 8bd9c00773..aaa8a951c2 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -520,9 +520,9 @@ def build_template( # Get default values if not provided if ( - template_path is None - or keep_placeholders is None - or create_first_version is None + template_path is None + or keep_placeholders is None + or create_first_version is None ): preset = self.get_template_preset() template_path: str = template_path or preset["path"] From 3df68ef2a5b1e6c8bda457b96ebfe153480e1bbe Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Sep 2024 15:59:32 +0200 Subject: [PATCH 16/85] `workfile_creation_enabled` docstring, first explain the `True` case. --- .../pipeline/workfile/workfile_template_builder.py | 9 +++++---- 1 file changed, 5 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 aaa8a951c2..9537096794 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -511,10 +511,11 @@ def build_template( workfile for an initial version. It will skip saving if a version already exists. workfile_creation_enabled (bool): Whether we are creating a new - workfile. If False, we assume we explicitly want to build the - template in our current scene. Otherwise we only build if the - current file is not an existing saved workfile but a "new" - file. + workfile. When True, we only build if the current file is not + an existing saved workfile but a "new" file. When False, the + default value, we assume we explicitly want to build the + template in our current scene regardless of current scene + state. """ From e7f7eaba680a779323f1a4cd1b28d68d8c346593 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Sep 2024 16:01:57 +0200 Subject: [PATCH 17/85] Elaborate more --- .../workfile/workfile_template_builder.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 9537096794..86cb4b3f86 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -510,12 +510,15 @@ def build_template( When set to True, this option initiates the saving of the workfile for an initial version. It will skip saving if a version already exists. - workfile_creation_enabled (bool): Whether we are creating a new - workfile. When True, we only build if the current file is not - an existing saved workfile but a "new" file. When False, the - default value, we assume we explicitly want to build the - template in our current scene regardless of current scene - state. + workfile_creation_enabled (bool): Whether the call is part of + creating a new workfile. + When True, we only build if the current file is not + an existing saved workfile but a "new" file. Basically when + enabled we assume the user tries to load it only into a + "New File" (unsaved empty workfile). + When False, the default value, we assume we explicitly want to + build the template in our current scene regardless of current + scene state. """ From 522b205a92625eb5c2188f8f6e0d5b6a7bb6e72b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Sep 2024 16:07:17 +0200 Subject: [PATCH 18/85] Refactor logic with better function names --- .../workfile/workfile_template_builder.py | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 86cb4b3f86..7ea69b3f8f 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -561,10 +561,14 @@ def build_template( return # If there is no existing workfile, save the first version - workfile_path = self.get_first_workfile_version_to_create() - if workfile_path: + workfile_path = self.get_workfile_path() + if not os.path.exists(workfile_path): self.log.info("Saving first workfile: %s", workfile_path) self.save_workfile(workfile_path) + else: + self.log.info( + "A workfile already exists. Skipping save of workfile as " + "initial version.") def rebuild_template(self): """Go through existing placeholders in scene and update them. @@ -618,30 +622,16 @@ def import_template(self, template_path): pass - def get_first_workfile_version_to_create(self): - """ - Create first version of workfile. - - Should load the content of template into scene so - 'populate_scene_placeholders' can be started. - - Args: - template_path (str): Fullpath for current task and - host's template file. + def get_workfile_path(self): + """Return last known workfile path or the first workfile path create. Return: - Optional[str]: Filepath to save to, if we should save. + str: Last workfile path, or first version to create if none exist. """ # AYON_LAST_WORKFILE will be set to the last existing workfile OR # if none exist it will be set to the first version. last_workfile_path = os.environ.get("AYON_LAST_WORKFILE") self.log.info("__ last_workfile_path: {}".format(last_workfile_path)) - if os.path.exists(last_workfile_path): - # ignore in case workfile existence - self.log.info("Workfile already exists, skipping creation.") - return False - - # Confirm creation of first version return last_workfile_path def save_workfile(self, workfile_path): From d65b84cf2fea40c615fdf04a14831175e1e37c59 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Sep 2024 16:09:21 +0200 Subject: [PATCH 19/85] Tweak comment to describe both the cases in the if statement --- .../ayon_core/pipeline/workfile/workfile_template_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 7ea69b3f8f..8495529924 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -535,8 +535,8 @@ def build_template( if create_first_version is None: create_first_version: bool = preset["create_first_version"] - # Build the template if current workfile is a new unsaved file - # (that's detected by checking if it returns any current filepath) + # Build the template if we are explicitly requesting it or if it's + # an unsaved "new file". if ( # If not a workfile creation, an explicit load template # was requested, so we always want to build the template From cfcce2138347a0928e42021384960a87797bc567 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Sep 2024 16:14:25 +0200 Subject: [PATCH 20/85] Separate into variables for readability --- .../pipeline/workfile/workfile_template_builder.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 8495529924..673ca7922e 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -537,14 +537,9 @@ def build_template( # Build the template if we are explicitly requesting it or if it's # an unsaved "new file". - if ( - # If not a workfile creation, an explicit load template - # was requested, so we always want to build the template - not workfile_creation_enabled - # Or if workfile creation, but we're not in an active file - # we still need to build the "new workfile template" - or not self.host.get_current_workfile() - ): + is_new_file = not self.host.get_current_workfile() + explicit_build_requested = not workfile_creation_enabled + if is_new_file or explicit_build_requested: self.log.info(f"Building the workfile template: {template_path}") self.import_template(template_path) self.populate_scene_placeholders( From 057a5fffcf96a6c7dc48efb845e21c47de935d6a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Sep 2024 17:23:26 +0200 Subject: [PATCH 21/85] Simplify logic --- .../pipeline/workfile/workfile_template_builder.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 673ca7922e..026eacc19f 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -545,14 +545,9 @@ def build_template( self.populate_scene_placeholders( level_limit, keep_placeholders) - if not workfile_creation_enabled: - # Do not consider saving a first workfile version, if this - # is not set to be a "workfile creation". Then we assume - # only the template should get build. - return - - if not create_first_version: - # Do not save whatsoever + # Do not consider saving a first workfile version, if this is not set + # to be a "workfile creation" or `create_first_version` is disabled. + if explicit_build_requested or not create_first_version: return # If there is no existing workfile, save the first version From 2a2b9563123818969bf9910bf19c826a7493d378 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Sep 2024 10:46:50 +0200 Subject: [PATCH 22/85] do not log into console during publishing --- .../tools/publisher/models/publish.py | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index a60ef69fac..c22f2d263c 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -1,6 +1,7 @@ import uuid import copy import inspect +import logging import traceback import collections from functools import partial @@ -23,6 +24,14 @@ PLUGIN_ORDER_OFFSET = 0.5 +class MessageHandler(logging.Handler): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.records = [] + + def emit(self, record): + self.records.append(record) + class PublishReportMaker: """Report for single publishing process. @@ -869,6 +878,9 @@ def reset(self): # - pop the key after first collector using it would be safest option? self._publish_context.data["create_context"] = create_context publish_plugins = create_context.publish_plugins + for plugin in publish_plugins: + plugin.log.propagate = False + self._publish_plugins = publish_plugins self._publish_plugins_proxy = PublishPluginsProxy( publish_plugins @@ -1219,9 +1231,18 @@ def _process_and_continue( plugin: pyblish.api.Plugin, instance: pyblish.api.Instance ): - result = pyblish.plugin.process( - plugin, self._publish_context, instance - ) + handler = MessageHandler() + root = logging.getLogger() + plugin.log.addHandler(handler) + root.addHandler(handler) + try: + result = pyblish.plugin.process( + plugin, self._publish_context, instance + ) + result["records"] = handler.records + finally: + plugin.log.removeHandler(handler) + root.removeHandler(handler) exception = result.get("error") if exception: From 6099d4a5ae00b5c7a0ea20672eb61e8fe5b021ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:03:44 +0200 Subject: [PATCH 23/85] move propagate to publish iterator --- client/ayon_core/tools/publisher/models/publish.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index c22f2d263c..2041faab54 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -878,9 +878,6 @@ def reset(self): # - pop the key after first collector using it would be safest option? self._publish_context.data["create_context"] = create_context publish_plugins = create_context.publish_plugins - for plugin in publish_plugins: - plugin.log.propagate = False - self._publish_plugins = publish_plugins self._publish_plugins_proxy = PublishPluginsProxy( publish_plugins @@ -1233,6 +1230,8 @@ def _process_and_continue( ): handler = MessageHandler() root = logging.getLogger() + + plugin.log.propagate = False plugin.log.addHandler(handler) root.addHandler(handler) try: @@ -1241,6 +1240,7 @@ def _process_and_continue( ) result["records"] = handler.records finally: + plugin.log.propagate = True plugin.log.removeHandler(handler) root.removeHandler(handler) From 4c809f9278364057252e11a833d5e1f524a13c8a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:09:47 +0200 Subject: [PATCH 24/85] calculate message at the time of emit --- client/ayon_core/tools/publisher/models/publish.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index 2041faab54..4c21a6d5b5 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -30,6 +30,7 @@ def __init__(self, *args, **kwargs): self.records = [] def emit(self, record): + record.msg = record.getMessage() self.records.append(record) From ab498dd7e1edd5f4db0003dcb314aa016dfa889a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Sep 2024 15:03:14 +0200 Subject: [PATCH 25/85] Update client/ayon_core/pipeline/workfile/workfile_template_builder.py Co-authored-by: Mustafa Jafar --- .../pipeline/workfile/workfile_template_builder.py | 9 +++++---- 1 file changed, 5 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 a908e76222..17debdb2e8 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -844,10 +844,11 @@ def get_template_preset(self): keep_placeholder = True if not path: - self.log.info( - "Template path is not set." - ) - return + raise TemplateLoadFailed(( + "Template path is not set.\n" + "Path need to be set in {}\\Template Workfile Build " + "Settings\\Profiles" + ).format(host_name.title())) # Try to fill path with environments and anatomy roots anatomy = Anatomy(project_name) From f5c26e3d6e54e2941331ccb0cb23361ab3a8edb6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:18:06 +0200 Subject: [PATCH 26/85] call getMessage in try block --- client/ayon_core/pipeline/context_tools.py | 5 ++++- client/ayon_core/tools/publisher/models/publish.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index 8b72405048..5fb48cd79b 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -132,7 +132,10 @@ def install_host(host): def modified_emit(obj, record): """Method replacing `emit` in Pyblish's MessageHandler.""" - record.msg = record.getMessage() + try: + record.msg = record.getMessage() + except Exception: + record.msg = str(record.msg) obj.records.append(record) MessageHandler.emit = modified_emit diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index 4c21a6d5b5..e7e765c4d2 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -30,7 +30,10 @@ def __init__(self, *args, **kwargs): self.records = [] def emit(self, record): - record.msg = record.getMessage() + try: + record.msg = record.getMessage() + except Exception: + record.msg = str(record.msg) self.records.append(record) From d01cde7051527c85738b13f1c3769d821e8b0f3a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:01:42 +0200 Subject: [PATCH 27/85] reverse disabled and hidden to positive names --- client/ayon_core/lib/attribute_definitions.py | 80 ++++++++++++++----- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 894b012d59..a882bee0d9 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -90,6 +90,26 @@ def __call__(cls, *args, **kwargs): return obj +def _convert_reversed_attr( + main_value, depr_value, main_label, depr_label, default +): + if main_value is not None and depr_value is not None: + if main_value == depr_value: + print( + f"God invalid '{main_label}' and '{depr_label}' arguments." + f" Using '{main_label}' value." + ) + elif depr_value is not None: + print( + f"Using deprecated argument '{depr_label}'" + f" please use '{main_label}' instead." + ) + main_value = not depr_value + elif main_value is None: + main_value = default + return main_value + + class AbstractAttrDef(metaclass=AbstractAttrDefMeta): """Abstraction of attribute definition. @@ -106,12 +126,14 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Args: key (str): Under which key will be attribute value stored. default (Any): Default value of an attribute. - label (str): Attribute label. - tooltip (str): Attribute tooltip. - is_label_horizontal (bool): UI specific argument. Specify if label is - next to value input or ahead. - hidden (bool): Will be item hidden (for UI purposes). - disabled (bool): Item will be visible but disabled (for UI purposes). + label (Optional[str]): Attribute label. + tooltip (Optional[str]): Attribute tooltip. + is_label_horizontal (Optional[bool]): UI specific argument. Specify + if label is next to value input or ahead. + visible (Optional[bool]): Item is shown to user (for UI purposes). + enabled (Optional[bool]): Item is enabled (for UI purposes). + hidden (Optional[bool]): DEPRECATED: Use 'visible' instead. + disabled (Optional[bool]): DEPRECATED: Use 'enabled' instead. """ type_attributes = [] @@ -125,22 +147,28 @@ def __init__( label=None, tooltip=None, is_label_horizontal=None, - hidden=False, - disabled=False + visible=None, + enabled=None, + hidden=None, + disabled=None, ): if is_label_horizontal is None: is_label_horizontal = True - if hidden is None: - hidden = False + enabled = _convert_reversed_attr( + enabled, disabled, "enabled", "disabled", True + ) + visible = _convert_reversed_attr( + visible, hidden, "visible", "hidden", True + ) self.key = key self.label = label self.tooltip = tooltip self.default = default self.is_label_horizontal = is_label_horizontal - self.hidden = hidden - self.disabled = disabled + self.visible = visible + self.enabled = enabled self._id = uuid.uuid4().hex self.__init__class__ = AbstractAttrDef @@ -149,14 +177,30 @@ def __init__( def id(self): return self._id + @property + def hidden(self): + return not self.visible + + @hidden.setter + def hidden(self, value): + self.visible = not value + + @property + def disabled(self): + return not self.enabled + + @disabled.setter + def disabled(self, value): + self.enabled = not value + def __eq__(self, other): if not isinstance(other, self.__class__): return False return ( self.key == other.key - and self.hidden == other.hidden and self.default == other.default - and self.disabled == other.disabled + and self.visible == other.visible + and self.enabled == other.enabled ) def __ne__(self, other): @@ -198,8 +242,8 @@ def serialize(self): "tooltip": self.tooltip, "default": self.default, "is_label_horizontal": self.is_label_horizontal, - "hidden": self.hidden, - "disabled": self.disabled + "visible": self.visible, + "enabled": self.enabled } for attr in self.type_attributes: data[attr] = getattr(self, attr) @@ -279,8 +323,8 @@ class HiddenDef(AbstractAttrDef): def __init__(self, key, default=None, **kwargs): kwargs["default"] = default - kwargs["hidden"] = True - super(HiddenDef, self).__init__(key, **kwargs) + kwargs["visible"] = False + super().__init__(key, **kwargs) def convert_value(self, value): return value From f174941e1d11fc2d04b85d1de69db8441ff9d6c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:01:54 +0200 Subject: [PATCH 28/85] use new attribute names --- client/ayon_core/tools/attribute_defs/widgets.py | 6 +++--- client/ayon_core/tools/publisher/widgets/widgets.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 5ead3f46a6..026aea00ad 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -28,10 +28,10 @@ def create_widget_for_attr_def(attr_def, parent=None): widget = _create_widget_for_attr_def(attr_def, parent) - if attr_def.hidden: + if not attr_def.visible: widget.setVisible(False) - if attr_def.disabled: + if not attr_def.enabled: widget.setEnabled(False) return widget @@ -135,7 +135,7 @@ def add_attr_defs(self, attr_defs): widget = create_widget_for_attr_def(attr_def, self) self._widgets.append(widget) - if attr_def.hidden: + if not attr_def.visible: continue expand_cols = 2 diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 83a2d9e6c1..b0f32dfcfc 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -1446,7 +1446,7 @@ def set_current_instances(self, instances): self._attr_def_id_to_instances[attr_def.id] = attr_instances self._attr_def_id_to_attr_def[attr_def.id] = attr_def - if attr_def.hidden: + if not attr_def.visible: continue expand_cols = 2 @@ -1585,7 +1585,7 @@ def set_current_instances(self, instances, context_selected): widget = create_widget_for_attr_def( attr_def, content_widget ) - hidden_widget = attr_def.hidden + hidden_widget = not attr_def.visible # Hide unknown values of publish plugins # - The keys in most of cases does not represent what would # label represent From 37697bc6ce62f320da88b3bea1f933e424714acd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:06:40 +0200 Subject: [PATCH 29/85] change print to warning --- client/ayon_core/lib/attribute_definitions.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index a882bee0d9..cffa424798 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -4,6 +4,7 @@ import uuid import json import copy +import warnings from abc import ABCMeta, abstractmethod import clique @@ -100,9 +101,13 @@ def _convert_reversed_attr( f" Using '{main_label}' value." ) elif depr_value is not None: - print( - f"Using deprecated argument '{depr_label}'" - f" please use '{main_label}' instead." + warnings.warn( + ( + "DEPRECATION WARNING: Using deprecated argument" + f" '{depr_label}' please use '{main_label}' instead." + ), + DeprecationWarning, + stacklevel=4, ) main_value = not depr_value elif main_value is None: From b6392a3e4201ca3f2dfc75a7a25bb1b3834783be Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:20:15 +0200 Subject: [PATCH 30/85] fix comment Co-authored-by: Roy Nieterau --- client/ayon_core/lib/attribute_definitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index cffa424798..639778b16d 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -97,7 +97,7 @@ def _convert_reversed_attr( if main_value is not None and depr_value is not None: if main_value == depr_value: print( - f"God invalid '{main_label}' and '{depr_label}' arguments." + f"Got invalid '{main_label}' and '{depr_label}' arguments." f" Using '{main_label}' value." ) elif depr_value is not None: From 57266dc7b660226fc6fddddecb4b1b9e89bfde1b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:59:48 +0200 Subject: [PATCH 31/85] use single object of handler --- .../tools/publisher/models/publish.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index e7e765c4d2..6848e27bc4 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -29,6 +29,9 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.records = [] + def clear_records(self): + self.records = [] + def emit(self, record): try: record.msg = record.getMessage() @@ -858,6 +861,8 @@ def __init__(self, controller: AbstractPublisherBackend): self._default_iterator() ) + self._log_handler: MessageHandler = MessageHandler() + def reset(self): create_context = self._controller.get_create_context() self._publish_up_validation = False @@ -1232,21 +1237,20 @@ def _process_and_continue( plugin: pyblish.api.Plugin, instance: pyblish.api.Instance ): - handler = MessageHandler() root = logging.getLogger() - + self._log_handler.clear_records() plugin.log.propagate = False - plugin.log.addHandler(handler) - root.addHandler(handler) + plugin.log.addHandler(self._log_handler) + root.addHandler(self._log_handler) try: result = pyblish.plugin.process( plugin, self._publish_context, instance ) - result["records"] = handler.records + result["records"] = self._log_handler.records finally: plugin.log.propagate = True - plugin.log.removeHandler(handler) - root.removeHandler(handler) + plugin.log.removeHandler(self._log_handler) + root.removeHandler(self._log_handler) exception = result.get("error") if exception: From 14244e184c932831b4efbecec37d6444087e13a5 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 17 Sep 2024 18:29:59 -0400 Subject: [PATCH 32/85] Fix wrong retimed detection on image sequence clip. --- client/ayon_core/pipeline/editorial.py | 42 ++++++++++++++++---------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 84bffbe1ec..2934e3073b 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -179,6 +179,24 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): media_in = available_range.start_time.value media_out = available_range.end_time_inclusive().value + # Ensure image sequence media_ref source and + # available range are absolute. + media_ref = otio_clip.media_reference + if ( + hasattr(otio.schema, "ImageSequenceReference") and + isinstance(media_ref, otio.schema.ImageSequenceReference) and + media_in != media_ref.start_frame + ): + media_in = media_ref.start_frame + media_out += media_ref.start_frame + source_range_start = otio.opentime.RationalTime( + media_in + source_range.start_time.value, + ) + source_range = otio.opentime.TimeRange( + start_time=source_range_start, + duration=source_range.duration + ) + # modifiers time_scalar = 1. offset_in = 0 @@ -224,38 +242,30 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): offset_in *= time_scalar offset_out *= time_scalar - # filip offset if reversed speed + # flip offset if reversed speed if time_scalar < 0: - _offset_in = offset_out - _offset_out = offset_in - offset_in = _offset_in - offset_out = _offset_out + offset_in, offset_out = offset_out, offset_in # scale handles handle_start *= abs(time_scalar) handle_end *= abs(time_scalar) - # filip handles if reversed speed + # flip handles if reversed speed if time_scalar < 0: - _handle_start = handle_end - _handle_end = handle_start - handle_start = _handle_start - handle_end = _handle_end + handle_start, handle_end = handle_end, handle_start source_in = source_range.start_time.value - media_in_trimmed = ( - media_in + source_in + offset_in) - media_out_trimmed = ( - media_in + source_in + ( + media_in_trimmed = (source_in + offset_in) + media_out_trimmed = ( media_in_trimmed + ( ((source_range.duration.value - 1) * abs( time_scalar)) + offset_out)) # calculate available handles if (media_in_trimmed - media_in) < handle_start: - handle_start = (media_in_trimmed - media_in) + handle_start = max(0, media_in_trimmed - media_in) if (media_out - media_out_trimmed) < handle_end: - handle_end = (media_out - media_out_trimmed) + handle_end = max(0, media_out - media_out_trimmed) # create version data version_data = { From 318ce6b32122afaf65701e505e842550ed769a23 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 17 Sep 2024 18:41:57 -0400 Subject: [PATCH 33/85] Fix lint. --- client/ayon_core/pipeline/editorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 2934e3073b..83035616c6 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -179,7 +179,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): media_in = available_range.start_time.value media_out = available_range.end_time_inclusive().value - # Ensure image sequence media_ref source and + # Ensure image sequence media_ref source and # available range are absolute. media_ref = otio_clip.media_reference if ( From ac226a360aa7f75fc89f38676ecda916af8f966d Mon Sep 17 00:00:00 2001 From: Robin De Lillo Date: Wed, 18 Sep 2024 08:14:59 -0400 Subject: [PATCH 34/85] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/editorial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 83035616c6..d69ca8714a 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -256,8 +256,8 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): source_in = source_range.start_time.value - media_in_trimmed = (source_in + offset_in) - media_out_trimmed = ( media_in_trimmed + ( + media_in_trimmed = source_in + offset_in + media_out_trimmed = (media_in_trimmed + ( ((source_range.duration.value - 1) * abs( time_scalar)) + offset_out)) From 0836fb810e70220ac1a1dd38443de6c065d24740 Mon Sep 17 00:00:00 2001 From: Robin De Lillo Date: Wed, 18 Sep 2024 08:15:13 -0400 Subject: [PATCH 35/85] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/editorial.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index d69ca8714a..ad56eb247f 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -183,9 +183,9 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # available range are absolute. media_ref = otio_clip.media_reference if ( - hasattr(otio.schema, "ImageSequenceReference") and - isinstance(media_ref, otio.schema.ImageSequenceReference) and - media_in != media_ref.start_frame + hasattr(otio.schema, "ImageSequenceReference") + and isinstance(media_ref, otio.schema.ImageSequenceReference) + and media_in != media_ref.start_frame ): media_in = media_ref.start_frame media_out += media_ref.start_frame From efc31f01443bf3b9747d72e77ac2fa3b1c5737d8 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 18 Sep 2024 15:45:45 -0400 Subject: [PATCH 36/85] Consolidate pipeline.editorial.get_media_range_with_retimes --- client/ayon_core/pipeline/editorial.py | 106 ++++++++++++------ .../publish/extract_otio_trimming_video.py | 7 +- 2 files changed, 74 insertions(+), 39 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 83035616c6..bc02ae9b00 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -176,26 +176,36 @@ def _sequence_resize(source, length): def get_media_range_with_retimes(otio_clip, handle_start, handle_end): source_range = otio_clip.source_range available_range = otio_clip.available_range() - media_in = available_range.start_time.value - media_out = available_range.end_time_inclusive().value - # Ensure image sequence media_ref source and - # available range are absolute. - media_ref = otio_clip.media_reference - if ( - hasattr(otio.schema, "ImageSequenceReference") and - isinstance(media_ref, otio.schema.ImageSequenceReference) and - media_in != media_ref.start_frame - ): - media_in = media_ref.start_frame - media_out += media_ref.start_frame - source_range_start = otio.opentime.RationalTime( - media_in + source_range.start_time.value, - ) - source_range = otio.opentime.TimeRange( - start_time=source_range_start, - duration=source_range.duration - ) + source_range_rate = source_range.start_time.rate + available_range_rate = available_range.start_time.rate + + # Conform source range bounds to available range rate + # .e.g. embedded TC of (3600 sec/ 1h), duration 100 frames + # + # available |----------------------------------------| 24fps + # 86400 86500 + # + # + # 90010 90060 + # src |-----|______duration 2s___|----| 25fps + # 90000 + # + # + # 86409.6 86466.8 + # conformed |-------|_____duration _2.38s____|-------| 24fps + # 86400 + # + # Note that 24fps is slower than 25fps hence extended duration + # to preserve media range + + # Compute new source range based on available rate + conformed_src_in = source_range.start_time.rescaled_to(available_range_rate) + conformed_src_duration = source_range.duration.rescaled_to(available_range_rate) + conformed_source_range = otio.opentime.TimeRange( + start_time=conformed_src_in, + duration=conformed_src_duration + ) # modifiers time_scalar = 1. @@ -242,47 +252,75 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): offset_in *= time_scalar offset_out *= time_scalar - # flip offset if reversed speed - if time_scalar < 0: - offset_in, offset_out = offset_out, offset_in - # scale handles handle_start *= abs(time_scalar) handle_end *= abs(time_scalar) - # flip handles if reversed speed + # flip offset and handles if reversed speed if time_scalar < 0: + offset_in, offset_out = offset_out, offset_in handle_start, handle_end = handle_end, handle_start - source_in = source_range.start_time.value + # compute retimed range + media_in_trimmed = conformed_source_range.start_time.value + offset_in + media_out_trimmed = media_in_trimmed + ( + (conformed_source_range.duration.value * abs( + time_scalar) + offset_out) - 1) - media_in_trimmed = (source_in + offset_in) - media_out_trimmed = ( media_in_trimmed + ( - ((source_range.duration.value - 1) * abs( - time_scalar)) + offset_out)) + media_in = available_range.start_time.value + media_out = available_range.end_time_inclusive().value + + # If media source is an image sequence, returned + # mediaIn/mediaOut have to correspond + # to frame numbers from source sequence. + media_ref = otio_clip.media_reference + is_input_sequence = ( + hasattr(otio.schema, "ImageSequenceReference") and + isinstance(media_ref, otio.schema.ImageSequenceReference) + ) + + if is_input_sequence: + # preserve discreet frame numbers + media_in_trimmed = otio.opentime.RationalTime.from_frames( + media_in_trimmed - media_in + media_ref.start_frame, + rate=available_range_rate, + ).to_frames() + media_out_trimmed = otio.opentime.RationalTime.from_frames( + media_out_trimmed - media_in + media_ref.start_frame, + rate=available_range_rate, + ).to_frames() - # calculate available handles + media_in = media_ref.start_frame + media_out = media_in + available_range.duration.to_frames() - 1 + + # adjust available handles if needed if (media_in_trimmed - media_in) < handle_start: handle_start = max(0, media_in_trimmed - media_in) if (media_out - media_out_trimmed) < handle_end: handle_end = max(0, media_out - media_out_trimmed) + # FFmpeg extraction ignores embedded timecode + # so substract to get a (mediaIn-mediaOut) range from 0. + if not is_input_sequence: + media_in_trimmed -= media_in + media_out_trimmed -= media_in + # create version data version_data = { "versionData": { "retime": True, "speed": time_scalar, "timewarps": time_warp_nodes, - "handleStart": int(round(handle_start)), - "handleEnd": int(round(handle_end)) + "handleStart": int(handle_start), + "handleEnd": int(handle_end) } } returning_dict = { "mediaIn": media_in_trimmed, "mediaOut": media_out_trimmed, - "handleStart": int(round(handle_start)), - "handleEnd": int(round(handle_end)), + "handleStart": int(handle_start), + "handleEnd": int(handle_end), "speed": time_scalar } diff --git a/client/ayon_core/plugins/publish/extract_otio_trimming_video.py b/client/ayon_core/plugins/publish/extract_otio_trimming_video.py index 9736c30b73..0c77602681 100644 --- a/client/ayon_core/plugins/publish/extract_otio_trimming_video.py +++ b/client/ayon_core/plugins/publish/extract_otio_trimming_video.py @@ -84,11 +84,8 @@ def _ffmpeg_trim_seqment(self, input_file_path, otio_range): command = get_ffmpeg_tool_args("ffmpeg") video_path = input_file_path - frame_start = otio_range.start_time.value - input_fps = otio_range.start_time.rate - frame_duration = otio_range.duration.value - 1 - sec_start = frames_to_seconds(frame_start, input_fps) - sec_duration = frames_to_seconds(frame_duration, input_fps) + sec_start = otio_range.start_time.to_seconds() + sec_duration = otio_range.duration.to_seconds() # form command for rendering gap files command.extend([ From ef6693f8a0c53b418765a0b88f131fdd911d1e3b Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 18 Sep 2024 16:28:45 -0400 Subject: [PATCH 37/85] Add unit tests. --- .../resources/img_seq_embedded_tc.json | 363 +++++++++++++++++ .../resources/img_seq_no_handles.json | 363 +++++++++++++++++ .../resources/img_seq_with_handles.json | 363 +++++++++++++++++ .../resources/movie_with_handles.json | 358 +++++++++++++++++ .../editorial/resources/qt_embedded_tc.json | 356 +++++++++++++++++ .../editorial/resources/qt_retimed_speed.json | 365 ++++++++++++++++++ 6 files changed, 2168 insertions(+) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_embedded_tc.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_no_handles.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_with_handles.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/movie_with_handles.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/qt_embedded_tc.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/qt_retimed_speed.json diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_embedded_tc.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_embedded_tc.json new file mode 100644 index 0000000000..a7c3ee00cf --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_embedded_tc.json @@ -0,0 +1,363 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "Resolve_OTIO": {} + }, + "name": "output.[1000-1100].exr", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 74.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 91046.625 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Transform", + "Enabled": true, + "Name": "Transform", + "Parameters": [], + "Type": 2 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Cropping", + "Enabled": true, + "Name": "Cropping", + "Parameters": [], + "Type": 3 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Dynamic Zoom", + "Enabled": false, + "Name": "Dynamic Zoom", + "Parameters": [ + { + "Default Parameter Value": [ + 0.0, + 0.0 + ], + "Key Frames": { + "-6": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + "994": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + } + }, + "Parameter ID": "dynamicZoomCenter", + "Parameter Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + { + "Default Parameter Value": 1.0, + "Key Frames": { + "-6": { + "Value": 0.8, + "Variant Type": "Double" + }, + "994": { + "Value": 1.0, + "Variant Type": "Double" + } + }, + "Parameter ID": "dynamicZoomScale", + "Parameter Value": 1.0, + "Variant Type": "Double", + "maxValue": 100.0, + "minValue": 0.01 + } + ], + "Type": 59 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Composite", + "Enabled": true, + "Name": "Composite", + "Parameters": [], + "Type": 1 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Lens Correction", + "Enabled": true, + "Name": "Lens Correction", + "Parameters": [], + "Type": 43 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Retime and Scaling", + "Enabled": true, + "Name": "Retime and Scaling", + "Parameters": [], + "Type": 22 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Video Faders", + "Enabled": true, + "Name": "Video Faders", + "Parameters": [], + "Type": 36 + } + }, + "name": "", + "effect_name": "Resolve Effect" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "Resolve_OTIO": { + "Keywords": [], + "Note": "{\"resolve_sub_products\": {\"io.ayon.creators.resolve.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.shot\", \"variant\": \"Main\", \"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"f2918dd7-a30b-4b7d-8ac1-7d5f400058bf\", \"clip_source_resolution\": {\"width\": \"956\", \"height\": \"720\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/sq01\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"ade94deb-f104-47dc-b8e9-04943f900914\", \"reviewTrack\": null, \"label\": \"/shots/sq01/Video_1sq01sh010 shot\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"5109899f-d744-4ed3-8547-8585ef9b703b\", \"creator_attributes\": {\"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1075, \"clipIn\": 655, \"clipOut\": 729, \"clipDuration\": 74, \"sourceIn\": 6, \"sourceOut\": 80, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"f2918dd7-a30b-4b7d-8ac1-7d5f400058bf\", \"clip_source_resolution\": {\"width\": \"956\", \"height\": \"720\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/sq01\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"ade94deb-f104-47dc-b8e9-04943f900914\", \"reviewTrack\": null, \"parent_instance_id\": \"5109899f-d744-4ed3-8547-8585ef9b703b\", \"label\": \"/shots/sq01/Video_1sq01sh010 plate\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"85c729e9-0503-4c3a-8d7f-be0920f047d8\", \"creator_attributes\": {\"parentInstance\": \"/shots/sq01/Video_1sq01sh010 shot\", \"vSyncOn\": false, \"vSyncTrack\": \"Video 1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"f2918dd7-a30b-4b7d-8ac1-7d5f400058bf\", \"publish\": true}" + }, + "clip_index": "f2918dd7-a30b-4b7d-8ac1-7d5f400058bf", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "f2918dd7-a30b-4b7d-8ac1-7d5f400058bf", + "clip_source_resolution": { + "height": "720", + "pixelAspect": 1.0, + "width": "956" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/sq01/Video_1sq01sh010 shot", + "vSyncOn": false, + "vSyncTrack": "Video 1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/sq01/Video_1sq01sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq01", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq01", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "85c729e9-0503-4c3a-8d7f-be0920f047d8", + "label": "/shots/sq01/Video_1sq01sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "5109899f-d744-4ed3-8547-8585ef9b703b", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq01", + "folder_type": "sequence" + } + ], + "productName": "plateVideo_1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "sq01", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "ade94deb-f104-47dc-b8e9-04943f900914", + "variant": "Video_1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "f2918dd7-a30b-4b7d-8ac1-7d5f400058bf", + "clip_source_resolution": { + "height": "720", + "pixelAspect": 1.0, + "width": "956" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 74, + "clipIn": 655, + "clipOut": 729, + "folderPath": "/shots/sq01/Video_1sq01sh010", + "fps": "from_selection", + "frameEnd": 1075, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 6, + "sourceOut": 80, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/sq01/Video_1sq01sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq01", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq01", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "5109899f-d744-4ed3-8547-8585ef9b703b", + "label": "/shots/sq01/Video_1sq01sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq01", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "sq01", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "ade94deb-f104-47dc-b8e9-04943f900914", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AyonData", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 91083.625 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": {}, + "name": "output.[1000-1100].exr", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 87399.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\Users\\robin\\OneDrive\\Bureau\\dev_ayon\\samples\\exr_embedded_tc", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 1000, + "frame_step": 1, + "rate": 24.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_no_handles.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_no_handles.json new file mode 100644 index 0000000000..9dccb51197 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_no_handles.json @@ -0,0 +1,363 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "Resolve_OTIO": {} + }, + "name": "output.[1000-1100].tif", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Transform", + "Enabled": true, + "Name": "Transform", + "Parameters": [], + "Type": 2 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Cropping", + "Enabled": true, + "Name": "Cropping", + "Parameters": [], + "Type": 3 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Dynamic Zoom", + "Enabled": false, + "Name": "Dynamic Zoom", + "Parameters": [ + { + "Default Parameter Value": [ + 0.0, + 0.0 + ], + "Key Frames": { + "0": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + "1000": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + } + }, + "Parameter ID": "dynamicZoomCenter", + "Parameter Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + { + "Default Parameter Value": 1.0, + "Key Frames": { + "0": { + "Value": 0.8, + "Variant Type": "Double" + }, + "1000": { + "Value": 1.0, + "Variant Type": "Double" + } + }, + "Parameter ID": "dynamicZoomScale", + "Parameter Value": 1.0, + "Variant Type": "Double", + "maxValue": 100.0, + "minValue": 0.01 + } + ], + "Type": 59 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Composite", + "Enabled": true, + "Name": "Composite", + "Parameters": [], + "Type": 1 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Lens Correction", + "Enabled": true, + "Name": "Lens Correction", + "Parameters": [], + "Type": 43 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Retime and Scaling", + "Enabled": true, + "Name": "Retime and Scaling", + "Parameters": [], + "Type": 22 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Video Faders", + "Enabled": true, + "Name": "Video Faders", + "Parameters": [], + "Type": 36 + } + }, + "name": "", + "effect_name": "Resolve Effect" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "Resolve_OTIO": { + "Keywords": [], + "Note": "{\"resolve_sub_products\": {\"io.ayon.creators.resolve.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.shot\", \"variant\": \"Main\", \"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"cfea9be4-3ecd-4253-a0a9-1a2bc9a4a184\", \"clip_source_resolution\": {\"width\": \"1920\", \"height\": \"1080\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/sq01\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"647b2ba6-6fca-4219-b163-cd321df9652f\", \"reviewTrack\": null, \"label\": \"/shots/sq01/Video_1sq01sh010 shot\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"2ab8f149-e32c-40f5-a6cb-ad1ca567ccc1\", \"creator_attributes\": {\"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1102, \"clipIn\": 509, \"clipOut\": 610, \"clipDuration\": 101, \"sourceIn\": 0, \"sourceOut\": 101, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"cfea9be4-3ecd-4253-a0a9-1a2bc9a4a184\", \"clip_source_resolution\": {\"width\": \"1920\", \"height\": \"1080\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/sq01\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"647b2ba6-6fca-4219-b163-cd321df9652f\", \"reviewTrack\": null, \"parent_instance_id\": \"2ab8f149-e32c-40f5-a6cb-ad1ca567ccc1\", \"label\": \"/shots/sq01/Video_1sq01sh010 plate\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"9f866936-966b-4a73-8e61-1a5b6e648a3f\", \"creator_attributes\": {\"parentInstance\": \"/shots/sq01/Video_1sq01sh010 shot\", \"vSyncOn\": false, \"vSyncTrack\": \"Video 1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"cfea9be4-3ecd-4253-a0a9-1a2bc9a4a184\", \"publish\": true}" + }, + "clip_index": "cfea9be4-3ecd-4253-a0a9-1a2bc9a4a184", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "cfea9be4-3ecd-4253-a0a9-1a2bc9a4a184", + "clip_source_resolution": { + "height": "1080", + "pixelAspect": 1.0, + "width": "1920" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/sq01/Video_1sq01sh010 shot", + "vSyncOn": false, + "vSyncTrack": "Video 1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/sq01/Video_1sq01sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq01", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq01", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "9f866936-966b-4a73-8e61-1a5b6e648a3f", + "label": "/shots/sq01/Video_1sq01sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "2ab8f149-e32c-40f5-a6cb-ad1ca567ccc1", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq01", + "folder_type": "sequence" + } + ], + "productName": "plateVideo_1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "sq01", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "647b2ba6-6fca-4219-b163-cd321df9652f", + "variant": "Video_1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "cfea9be4-3ecd-4253-a0a9-1a2bc9a4a184", + "clip_source_resolution": { + "height": "1080", + "pixelAspect": 1.0, + "width": "1920" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 101, + "clipIn": 509, + "clipOut": 610, + "folderPath": "/shots/sq01/Video_1sq01sh010", + "fps": "from_selection", + "frameEnd": 1102, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 0, + "sourceOut": 101, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/sq01/Video_1sq01sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq01", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq01", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "2ab8f149-e32c-40f5-a6cb-ad1ca567ccc1", + "label": "/shots/sq01/Video_1sq01sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq01", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "sq01", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "647b2ba6-6fca-4219-b163-cd321df9652f", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AyonData", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 50.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": {}, + "name": "output.[1000-1100].tif", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\Users\\robin\\OneDrive\\Bureau\\dev_ayon\\samples", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_with_handles.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_with_handles.json new file mode 100644 index 0000000000..eb8876354c --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_with_handles.json @@ -0,0 +1,363 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "Resolve_OTIO": {} + }, + "name": "output.[1000-1100].tif", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 39.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 34.0 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Transform", + "Enabled": true, + "Name": "Transform", + "Parameters": [], + "Type": 2 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Cropping", + "Enabled": true, + "Name": "Cropping", + "Parameters": [], + "Type": 3 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Dynamic Zoom", + "Enabled": false, + "Name": "Dynamic Zoom", + "Parameters": [ + { + "Default Parameter Value": [ + 0.0, + 0.0 + ], + "Key Frames": { + "-34": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + "966": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + } + }, + "Parameter ID": "dynamicZoomCenter", + "Parameter Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + { + "Default Parameter Value": 1.0, + "Key Frames": { + "-34": { + "Value": 0.8, + "Variant Type": "Double" + }, + "966": { + "Value": 1.0, + "Variant Type": "Double" + } + }, + "Parameter ID": "dynamicZoomScale", + "Parameter Value": 1.0, + "Variant Type": "Double", + "maxValue": 100.0, + "minValue": 0.01 + } + ], + "Type": 59 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Composite", + "Enabled": true, + "Name": "Composite", + "Parameters": [], + "Type": 1 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Lens Correction", + "Enabled": true, + "Name": "Lens Correction", + "Parameters": [], + "Type": 43 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Retime and Scaling", + "Enabled": true, + "Name": "Retime and Scaling", + "Parameters": [], + "Type": 22 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Video Faders", + "Enabled": true, + "Name": "Video Faders", + "Parameters": [], + "Type": 36 + } + }, + "name": "", + "effect_name": "Resolve Effect" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "Resolve_OTIO": { + "Keywords": [], + "Note": "{\"resolve_sub_products\": {\"io.ayon.creators.resolve.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.shot\", \"variant\": \"Main\", \"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"cfea9be4-3ecd-4253-a0a9-1a2bc9a4a184\", \"clip_source_resolution\": {\"width\": \"1920\", \"height\": \"1080\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/sq01\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"5ca7b240-ec4c-49d1-841d-a96c33a08b1b\", \"reviewTrack\": null, \"label\": \"/shots/sq01/Video_1sq01sh010 shot\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"5b526964-5805-4412-af09-2da696c4978b\", \"creator_attributes\": {\"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1040, \"clipIn\": 543, \"clipOut\": 582, \"clipDuration\": 39, \"sourceIn\": 34, \"sourceOut\": 73, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"cfea9be4-3ecd-4253-a0a9-1a2bc9a4a184\", \"clip_source_resolution\": {\"width\": \"1920\", \"height\": \"1080\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/sq01\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"5ca7b240-ec4c-49d1-841d-a96c33a08b1b\", \"reviewTrack\": null, \"parent_instance_id\": \"5b526964-5805-4412-af09-2da696c4978b\", \"label\": \"/shots/sq01/Video_1sq01sh010 plate\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"992ab293-943b-4894-8a7f-c42b54b4d582\", \"creator_attributes\": {\"parentInstance\": \"/shots/sq01/Video_1sq01sh010 shot\", \"vSyncOn\": false, \"vSyncTrack\": \"Video 1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"cfea9be4-3ecd-4253-a0a9-1a2bc9a4a184\", \"publish\": true}" + }, + "clip_index": "cfea9be4-3ecd-4253-a0a9-1a2bc9a4a184", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "cfea9be4-3ecd-4253-a0a9-1a2bc9a4a184", + "clip_source_resolution": { + "height": "1080", + "pixelAspect": 1.0, + "width": "1920" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/sq01/Video_1sq01sh010 shot", + "vSyncOn": false, + "vSyncTrack": "Video 1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/sq01/Video_1sq01sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq01", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq01", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "992ab293-943b-4894-8a7f-c42b54b4d582", + "label": "/shots/sq01/Video_1sq01sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "5b526964-5805-4412-af09-2da696c4978b", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq01", + "folder_type": "sequence" + } + ], + "productName": "plateVideo_1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "sq01", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "5ca7b240-ec4c-49d1-841d-a96c33a08b1b", + "variant": "Video_1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "cfea9be4-3ecd-4253-a0a9-1a2bc9a4a184", + "clip_source_resolution": { + "height": "1080", + "pixelAspect": 1.0, + "width": "1920" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 39, + "clipIn": 543, + "clipOut": 582, + "folderPath": "/shots/sq01/Video_1sq01sh010", + "fps": "from_selection", + "frameEnd": 1040, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 34, + "sourceOut": 73, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/sq01/Video_1sq01sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq01", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq01", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "5b526964-5805-4412-af09-2da696c4978b", + "label": "/shots/sq01/Video_1sq01sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq01", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "sq01", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "5ca7b240-ec4c-49d1-841d-a96c33a08b1b", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AyonData", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 53.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": {}, + "name": "output.[1000-1100].tif", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\Users\\robin\\OneDrive\\Bureau\\dev_ayon\\samples", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/movie_with_handles.json b/tests/client/ayon_core/pipeline/editorial/resources/movie_with_handles.json new file mode 100644 index 0000000000..47b5772b49 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/movie_with_handles.json @@ -0,0 +1,358 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "Resolve_OTIO": { + "Link Group ID": 1 + } + }, + "name": "simple_editorial_setup.mp4", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 171.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Transform", + "Enabled": true, + "Name": "Transform", + "Parameters": [], + "Type": 2 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Cropping", + "Enabled": true, + "Name": "Cropping", + "Parameters": [], + "Type": 3 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Dynamic Zoom", + "Enabled": false, + "Name": "Dynamic Zoom", + "Parameters": [ + { + "Default Parameter Value": [ + 0.0, + 0.0 + ], + "Key Frames": { + "0": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + "1000": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + } + }, + "Parameter ID": "dynamicZoomCenter", + "Parameter Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + { + "Default Parameter Value": 1.0, + "Key Frames": { + "0": { + "Value": 0.8, + "Variant Type": "Double" + }, + "1000": { + "Value": 1.0, + "Variant Type": "Double" + } + }, + "Parameter ID": "dynamicZoomScale", + "Parameter Value": 1.0, + "Variant Type": "Double", + "maxValue": 100.0, + "minValue": 0.01 + } + ], + "Type": 59 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Composite", + "Enabled": true, + "Name": "Composite", + "Parameters": [], + "Type": 1 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Lens Correction", + "Enabled": true, + "Name": "Lens Correction", + "Parameters": [], + "Type": 43 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Retime and Scaling", + "Enabled": true, + "Name": "Retime and Scaling", + "Parameters": [], + "Type": 22 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Video Faders", + "Enabled": true, + "Name": "Video Faders", + "Parameters": [], + "Type": 36 + } + }, + "name": "", + "effect_name": "Resolve Effect" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "Resolve_OTIO": { + "Keywords": [], + "Note": "{\"resolve_sub_products\": {\"io.ayon.creators.resolve.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.shot\", \"variant\": \"Main\", \"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"cef0267f-bbf4-4959-9f22-d225e03f2532\", \"clip_source_resolution\": {\"width\": \"640\", \"height\": \"360\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/sq01\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"3af1b00a-5625-468d-af17-8ed29fa8608a\", \"reviewTrack\": null, \"label\": \"/shots/sq01/Video_1sq01sh010 shot\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"b6e33343-2410-4de4-935e-724bc74301e1\", \"creator_attributes\": {\"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1172, \"clipIn\": 1097, \"clipOut\": 1268, \"clipDuration\": 171, \"sourceIn\": 0, \"sourceOut\": 171, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"cef0267f-bbf4-4959-9f22-d225e03f2532\", \"clip_source_resolution\": {\"width\": \"640\", \"height\": \"360\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/sq01\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"3af1b00a-5625-468d-af17-8ed29fa8608a\", \"reviewTrack\": null, \"parent_instance_id\": \"b6e33343-2410-4de4-935e-724bc74301e1\", \"label\": \"/shots/sq01/Video_1sq01sh010 plate\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"ef8fe238-c970-4a16-be67-76f446113c4b\", \"creator_attributes\": {\"parentInstance\": \"/shots/sq01/Video_1sq01sh010 shot\", \"vSyncOn\": false, \"vSyncTrack\": \"Video 1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"cef0267f-bbf4-4959-9f22-d225e03f2532\", \"publish\": true}" + }, + "clip_index": "cef0267f-bbf4-4959-9f22-d225e03f2532", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "cef0267f-bbf4-4959-9f22-d225e03f2532", + "clip_source_resolution": { + "height": "360", + "pixelAspect": 1.0, + "width": "640" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/sq01/Video_1sq01sh010 shot", + "vSyncOn": false, + "vSyncTrack": "Video 1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/sq01/Video_1sq01sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq01", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq01", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "ef8fe238-c970-4a16-be67-76f446113c4b", + "label": "/shots/sq01/Video_1sq01sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "b6e33343-2410-4de4-935e-724bc74301e1", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq01", + "folder_type": "sequence" + } + ], + "productName": "plateVideo_1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "sq01", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "3af1b00a-5625-468d-af17-8ed29fa8608a", + "variant": "Video_1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "cef0267f-bbf4-4959-9f22-d225e03f2532", + "clip_source_resolution": { + "height": "360", + "pixelAspect": 1.0, + "width": "640" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 171, + "clipIn": 1097, + "clipOut": 1268, + "folderPath": "/shots/sq01/Video_1sq01sh010", + "fps": "from_selection", + "frameEnd": 1172, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 0, + "sourceOut": 171, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/sq01/Video_1sq01sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq01", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq01", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "b6e33343-2410-4de4-935e-724bc74301e1", + "label": "/shots/sq01/Video_1sq01sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq01", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "sq01", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "3af1b00a-5625-468d-af17-8ed29fa8608a", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AyonData", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 85.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": {}, + "name": "simple_editorial_setup.mp4", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 16450.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "available_image_bounds": null, + "target_url": "C:\\Users\\robin\\OneDrive\\Bureau\\dev_ayon\\data\\simple_editorial_setup.mp4" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/qt_embedded_tc.json b/tests/client/ayon_core/pipeline/editorial/resources/qt_embedded_tc.json new file mode 100644 index 0000000000..1b74ea4f37 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/qt_embedded_tc.json @@ -0,0 +1,356 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "Resolve_OTIO": {} + }, + "name": "qt_embedded_tc.mov", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 44.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 90032.0 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Transform", + "Enabled": true, + "Name": "Transform", + "Parameters": [], + "Type": 2 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Cropping", + "Enabled": true, + "Name": "Cropping", + "Parameters": [], + "Type": 3 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Dynamic Zoom", + "Enabled": false, + "Name": "Dynamic Zoom", + "Parameters": [ + { + "Default Parameter Value": [ + 0.0, + 0.0 + ], + "Key Frames": { + "-32": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + "1009": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + } + }, + "Parameter ID": "dynamicZoomCenter", + "Parameter Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + { + "Default Parameter Value": 1.0, + "Key Frames": { + "-32": { + "Value": 0.8, + "Variant Type": "Double" + }, + "1009": { + "Value": 1.0, + "Variant Type": "Double" + } + }, + "Parameter ID": "dynamicZoomScale", + "Parameter Value": 1.0, + "Variant Type": "Double", + "maxValue": 100.0, + "minValue": 0.01 + } + ], + "Type": 59 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Composite", + "Enabled": true, + "Name": "Composite", + "Parameters": [], + "Type": 1 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Lens Correction", + "Enabled": true, + "Name": "Lens Correction", + "Parameters": [], + "Type": 43 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Retime and Scaling", + "Enabled": true, + "Name": "Retime and Scaling", + "Parameters": [], + "Type": 22 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Video Faders", + "Enabled": true, + "Name": "Video Faders", + "Parameters": [], + "Type": 36 + } + }, + "name": "", + "effect_name": "Resolve Effect" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "Resolve_OTIO": { + "Keywords": [], + "Note": "{\"resolve_sub_products\": {\"io.ayon.creators.resolve.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.shot\", \"variant\": \"Main\", \"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"297fbf7a-7636-44b5-a308-809098298fae\", \"clip_source_resolution\": {\"width\": \"956\", \"height\": \"720\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/sq01\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"3e459c3f-cc87-42c6-95c0-f11435ec8ace\", \"reviewTrack\": null, \"label\": \"/shots/sq01/Video_1sq01sh010 shot\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"acebdee4-5f4a-4ebd-8c22-6ef2725c2070\", \"creator_attributes\": {\"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1045, \"clipIn\": 509, \"clipOut\": 553, \"clipDuration\": 44, \"sourceIn\": 32, \"sourceOut\": 76, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"297fbf7a-7636-44b5-a308-809098298fae\", \"clip_source_resolution\": {\"width\": \"956\", \"height\": \"720\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/sq01\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"3e459c3f-cc87-42c6-95c0-f11435ec8ace\", \"reviewTrack\": null, \"parent_instance_id\": \"acebdee4-5f4a-4ebd-8c22-6ef2725c2070\", \"label\": \"/shots/sq01/Video_1sq01sh010 plate\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"ffd09d3c-227c-4be0-8788-dec30daf7f78\", \"creator_attributes\": {\"parentInstance\": \"/shots/sq01/Video_1sq01sh010 shot\", \"vSyncOn\": false, \"vSyncTrack\": \"Video 1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"297fbf7a-7636-44b5-a308-809098298fae\", \"publish\": true}" + }, + "clip_index": "297fbf7a-7636-44b5-a308-809098298fae", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "297fbf7a-7636-44b5-a308-809098298fae", + "clip_source_resolution": { + "height": "720", + "pixelAspect": 1.0, + "width": "956" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/sq01/Video_1sq01sh010 shot", + "vSyncOn": false, + "vSyncTrack": "Video 1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/sq01/Video_1sq01sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq01", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq01", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "ffd09d3c-227c-4be0-8788-dec30daf7f78", + "label": "/shots/sq01/Video_1sq01sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "acebdee4-5f4a-4ebd-8c22-6ef2725c2070", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq01", + "folder_type": "sequence" + } + ], + "productName": "plateVideo_1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "sq01", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "3e459c3f-cc87-42c6-95c0-f11435ec8ace", + "variant": "Video_1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "297fbf7a-7636-44b5-a308-809098298fae", + "clip_source_resolution": { + "height": "720", + "pixelAspect": 1.0, + "width": "956" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 44, + "clipIn": 509, + "clipOut": 553, + "folderPath": "/shots/sq01/Video_1sq01sh010", + "fps": "from_selection", + "frameEnd": 1045, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 32, + "sourceOut": 76, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/sq01/Video_1sq01sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq01", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq01", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "acebdee4-5f4a-4ebd-8c22-6ef2725c2070", + "label": "/shots/sq01/Video_1sq01sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq01", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "sq01", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "3e459c3f-cc87-42c6-95c0-f11435ec8ace", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AyonData", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 90054.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": {}, + "name": "qt_embedded_tc.mov", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 100.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 86400.0 + } + }, + "available_image_bounds": null, + "target_url": "C:\\Users\\robin\\OneDrive\\Bureau\\dev_ayon\\data\\qt_embedded_tc.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/qt_retimed_speed.json b/tests/client/ayon_core/pipeline/editorial/resources/qt_retimed_speed.json new file mode 100644 index 0000000000..61838d2755 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/qt_retimed_speed.json @@ -0,0 +1,365 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "Resolve_OTIO": { + "Link Group ID": 1 + } + }, + "name": "simple_editorial_setup.mp4", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 171.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "LinearTimeWarp.1", + "metadata": {}, + "name": "", + "effect_name": "", + "time_scalar": 2.5 + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Transform", + "Enabled": true, + "Name": "Transform", + "Parameters": [], + "Type": 2 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Cropping", + "Enabled": true, + "Name": "Cropping", + "Parameters": [], + "Type": 3 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Dynamic Zoom", + "Enabled": false, + "Name": "Dynamic Zoom", + "Parameters": [ + { + "Default Parameter Value": [ + 0.0, + 0.0 + ], + "Key Frames": { + "0": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + "1000": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + } + }, + "Parameter ID": "dynamicZoomCenter", + "Parameter Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + { + "Default Parameter Value": 1.0, + "Key Frames": { + "0": { + "Value": 0.8, + "Variant Type": "Double" + }, + "1000": { + "Value": 1.0, + "Variant Type": "Double" + } + }, + "Parameter ID": "dynamicZoomScale", + "Parameter Value": 1.0, + "Variant Type": "Double", + "maxValue": 100.0, + "minValue": 0.01 + } + ], + "Type": 59 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Composite", + "Enabled": true, + "Name": "Composite", + "Parameters": [], + "Type": 1 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Lens Correction", + "Enabled": true, + "Name": "Lens Correction", + "Parameters": [], + "Type": 43 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Retime and Scaling", + "Enabled": true, + "Name": "Retime and Scaling", + "Parameters": [], + "Type": 22 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Video Faders", + "Enabled": true, + "Name": "Video Faders", + "Parameters": [], + "Type": 36 + } + }, + "name": "", + "effect_name": "Resolve Effect" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "Resolve_OTIO": { + "Keywords": [], + "Note": "{\"resolve_sub_products\": {\"io.ayon.creators.resolve.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.shot\", \"variant\": \"Main\", \"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"cef0267f-bbf4-4959-9f22-d225e03f2532\", \"clip_source_resolution\": {\"width\": \"640\", \"height\": \"360\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/sq01\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"2a780b95-14cc-45de-acc0-3ecd1f504325\", \"reviewTrack\": null, \"label\": \"/shots/sq01/Video_1sq01sh010 shot\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"e8af785a-484f-452b-8c9c-ac31ef0696c4\", \"creator_attributes\": {\"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1172, \"clipIn\": 805, \"clipOut\": 976, \"clipDuration\": 171, \"sourceIn\": 0, \"sourceOut\": 171, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/sq01/Video_1sq01sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"cef0267f-bbf4-4959-9f22-d225e03f2532\", \"clip_source_resolution\": {\"width\": \"640\", \"height\": \"360\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/sq01\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"2a780b95-14cc-45de-acc0-3ecd1f504325\", \"reviewTrack\": null, \"parent_instance_id\": \"e8af785a-484f-452b-8c9c-ac31ef0696c4\", \"label\": \"/shots/sq01/Video_1sq01sh010 plate\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"a34e7048-3d86-4c29-88c7-f65b1ba3d777\", \"creator_attributes\": {\"parentInstance\": \"/shots/sq01/Video_1sq01sh010 shot\", \"vSyncOn\": false, \"vSyncTrack\": \"Video 1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"cef0267f-bbf4-4959-9f22-d225e03f2532\", \"publish\": true}" + }, + "clip_index": "cef0267f-bbf4-4959-9f22-d225e03f2532", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "cef0267f-bbf4-4959-9f22-d225e03f2532", + "clip_source_resolution": { + "height": "360", + "pixelAspect": 1.0, + "width": "640" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/sq01/Video_1sq01sh010 shot", + "vSyncOn": false, + "vSyncTrack": "Video 1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/sq01/Video_1sq01sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq01", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq01", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "a34e7048-3d86-4c29-88c7-f65b1ba3d777", + "label": "/shots/sq01/Video_1sq01sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "e8af785a-484f-452b-8c9c-ac31ef0696c4", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq01", + "folder_type": "sequence" + } + ], + "productName": "plateVideo_1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "sq01", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "2a780b95-14cc-45de-acc0-3ecd1f504325", + "variant": "Video_1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "cef0267f-bbf4-4959-9f22-d225e03f2532", + "clip_source_resolution": { + "height": "360", + "pixelAspect": 1.0, + "width": "640" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 171, + "clipIn": 805, + "clipOut": 976, + "folderPath": "/shots/sq01/Video_1sq01sh010", + "fps": "from_selection", + "frameEnd": 1172, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 0, + "sourceOut": 171, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/sq01/Video_1sq01sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq01", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq01", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "e8af785a-484f-452b-8c9c-ac31ef0696c4", + "label": "/shots/sq01/Video_1sq01sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq01", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "sq01", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "2a780b95-14cc-45de-acc0-3ecd1f504325", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AyonData", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 85.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": {}, + "name": "simple_editorial_setup.mp4", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 16450.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "available_image_bounds": null, + "target_url": "C:\\Users\\robin\\OneDrive\\Bureau\\dev_ayon\\data\\simple_editorial_setup.mp4" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file From b0b509d431eee02e193211efd52a0126af18e476 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 18 Sep 2024 16:34:05 -0400 Subject: [PATCH 38/85] Add unit tests --- .../test_media_range_with_retimes.py | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py new file mode 100644 index 0000000000..7dca2e087a --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -0,0 +1,150 @@ +import os +import pytest + +import opentimelineio as otio + +from ayon_core.pipeline.editorial import get_media_range_with_retimes + + +_RESOURCE_DIR = os.path.join( + os.path.dirname(__file__), + "resources" +) + + +def _check_expected_retimed_values( + file_name: str, + expected_retimed_data: dict, + handle_start: int = 10, + handle_end: int = 10, +): + file_path = os.path.join(_RESOURCE_DIR, file_name) + otio_clip = otio.schema.Clip.from_json_file(file_path) + + retimed_data = get_media_range_with_retimes( + otio_clip, handle_start, handle_end + ) + assert retimed_data == expected_retimed_data + + +def test_movie_with_end_handle_end_only(): + """ + Movie clip (no embedded timecode) + available_range = 0-171 25fps + source_range = 0-16450 25fps + """ + expected_data = { + 'mediaIn': 0.0, + 'mediaOut': 170.0, + 'handleStart': 0, + 'handleEnd': 10, + 'speed': 1.0 + } + _check_expected_retimed_values( + "movie_with_handles.json", + expected_data, + ) + + +def test_movie_embedded_tc_handle(): + """ + Movie clip (embedded timecode 1h) + available_range = 86400-86500 24fps + source_range = 90032-90076 25fps + """ + expected_data = { + 'mediaIn': 30.720000000001164, + 'mediaOut': 71.9600000000064, + 'handleStart': 10, + 'handleEnd': 10, + 'speed': 1.0 + } + _check_expected_retimed_values( + "qt_embedded_tc.json", + expected_data + ) + + +def test_movie_retime_effect(): + """ + Movie clip (embedded timecode 1h) + available_range = 0-171 25fps + source_range = 0-16450 25fps + retimed speed: 250% + """ + expected_data = { + 'mediaIn': 0.0, + 'mediaOut': 426.5, + 'handleStart': 0, + 'handleEnd': 25, + 'speed': 2.5, + 'versionData': { + 'retime': True, + 'speed': 2.5, + 'timewarps': [], + 'handleStart': 0, + 'handleEnd': 25 + } + } +# import rpdb ; rpdb.Rpdb().set_trace() + _check_expected_retimed_values( + "qt_retimed_speed.json", + expected_data + ) + + +def test_img_sequence_no_handles(): + """ + Img sequence clip (no embedded timecode) + available files = 1000-1100 + source_range = 0-100 25fps + """ + expected_data = { + 'mediaIn': 1000, + 'mediaOut': 1100, + 'handleStart': 0, + 'handleEnd': 0, + 'speed': 1.0 + } + _check_expected_retimed_values( + "img_seq_no_handles.json", + expected_data + ) + + +def test_img_sequence_with_handles(): + """ + Img sequence clip (no embedded timecode) + available files = 1000-1100 + source_range = 34-72 25fps + """ + expected_data = { + 'mediaIn': 1034, + 'mediaOut': 1072, + 'handleStart': 10, + 'handleEnd': 10, + 'speed': 1.0 + } + _check_expected_retimed_values( + "img_seq_with_handles.json", + expected_data + ) + + +def test_img_sequence_with_embedded_tc_and_handles(): + """ + Img sequence clip (embedded timecode 1h) + available files = 1000-1100 + source_range = 91046.625-91,120.625 25fps + """ + expected_data = { + 'mediaIn': 1005, + 'mediaOut': 1075, + 'handleStart': 5, + 'handleEnd': 10, + 'speed': 1.0 + } + _check_expected_retimed_values( + "img_seq_embedded_tc.json", + expected_data + ) From be2b8d5c60087d53e84f84bf9392f0ece2812c91 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 18 Sep 2024 16:46:49 -0400 Subject: [PATCH 39/85] Fix lint. --- client/ayon_core/pipeline/editorial.py | 8 +++----- .../plugins/publish/extract_otio_trimming_video.py | 3 --- .../pipeline/editorial/test_media_range_with_retimes.py | 1 - 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index bc02ae9b00..ca62c13e7d 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -176,8 +176,6 @@ def _sequence_resize(source, length): def get_media_range_with_retimes(otio_clip, handle_start, handle_end): source_range = otio_clip.source_range available_range = otio_clip.available_range() - - source_range_rate = source_range.start_time.rate available_range_rate = available_range.start_time.rate # Conform source range bounds to available range rate @@ -187,9 +185,9 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # 86400 86500 # # - # 90010 90060 - # src |-----|______duration 2s___|----| 25fps - # 90000 + # 90010 90060 + # src |-----|______duration 2s___|----| 25fps + # 90000 # # # 86409.6 86466.8 diff --git a/client/ayon_core/plugins/publish/extract_otio_trimming_video.py b/client/ayon_core/plugins/publish/extract_otio_trimming_video.py index 0c77602681..59b8a714f0 100644 --- a/client/ayon_core/plugins/publish/extract_otio_trimming_video.py +++ b/client/ayon_core/plugins/publish/extract_otio_trimming_video.py @@ -74,9 +74,6 @@ def _ffmpeg_trim_seqment(self, input_file_path, otio_range): otio_range (opentime.TimeRange): range to trim to """ - # Not all hosts can import this module. - from ayon_core.pipeline.editorial import frames_to_seconds - # create path to destination output_path = self._get_ffmpeg_output(input_file_path) diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index 7dca2e087a..ea0b7fbf82 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -1,5 +1,4 @@ import os -import pytest import opentimelineio as otio From 4f2340adea40e8c2e589387a115ae1c083041d9a Mon Sep 17 00:00:00 2001 From: Robin De Lillo Date: Mon, 23 Sep 2024 08:14:55 -0400 Subject: [PATCH 40/85] Update client/ayon_core/pipeline/editorial.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/editorial.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index ca62c13e7d..4b823f130f 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -262,8 +262,12 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # compute retimed range media_in_trimmed = conformed_source_range.start_time.value + offset_in media_out_trimmed = media_in_trimmed + ( - (conformed_source_range.duration.value * abs( - time_scalar) + offset_out) - 1) + ( + conformed_source_range.duration.value + * abs(time_scalar) + + offset_out + ) - 1 + ) media_in = available_range.start_time.value media_out = available_range.end_time_inclusive().value From f9ed6f58774cb14c132ed53856d8a966838131a1 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 23 Sep 2024 08:19:12 -0400 Subject: [PATCH 41/85] Fix typos. --- client/ayon_core/pipeline/editorial.py | 2 +- .../pipeline/editorial/test_media_range_with_retimes.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 4b823f130f..7d6d6f5882 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -282,7 +282,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): ) if is_input_sequence: - # preserve discreet frame numbers + # preserve discrete frame numbers media_in_trimmed = otio.opentime.RationalTime.from_frames( media_in_trimmed - media_in + media_ref.start_frame, rate=available_range_rate, diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index ea0b7fbf82..82512df7b8 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -85,7 +85,6 @@ def test_movie_retime_effect(): 'handleEnd': 25 } } -# import rpdb ; rpdb.Rpdb().set_trace() _check_expected_retimed_values( "qt_retimed_speed.json", expected_data From d2b933718d85d996fe4217f490e5fbd65a916a42 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 23 Sep 2024 08:22:54 -0400 Subject: [PATCH 42/85] Adjust test docstring. --- .../pipeline/editorial/test_media_range_with_retimes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index 82512df7b8..270b01a799 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -133,7 +133,7 @@ def test_img_sequence_with_embedded_tc_and_handles(): """ Img sequence clip (embedded timecode 1h) available files = 1000-1100 - source_range = 91046.625-91,120.625 25fps + source_range = 91046.625-91120.625 25fps """ expected_data = { 'mediaIn': 1005, From 77fac00ecc68be9189dc321381880a432c7b617c Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 24 Sep 2024 12:50:52 -0400 Subject: [PATCH 43/85] Make it work even with image sequence and embedded timecodes. --- client/ayon_core/pipeline/editorial.py | 80 +++++-- .../plugins/publish/extract_otio_review.py | 204 +++++++++++------- 2 files changed, 187 insertions(+), 97 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 7d6d6f5882..23c49154ac 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -88,7 +88,7 @@ def trim_media_range(media_range, source_range): """ rw_media_start = _ot.RationalTime( - media_range.start_time.value + source_range.start_time.value, + source_range.start_time.value, media_range.start_time.rate ) rw_media_duration = _ot.RationalTime( @@ -173,6 +173,66 @@ def _sequence_resize(source, length): yield (1 - ratio) * source[int(low)] + ratio * source[int(high)] +def is_clip_from_media_sequence(otio_clip): + """ + Args: + otio_clip (otio.schema.Clip): The OTIO clip to check. + + Returns: + bool. Is the provided clip from an input media sequence ? + """ + media_ref = otio_clip.media_reference + metadata = media_ref.metadata + + # OpenTimelineIO 0.13 and newer + is_input_sequence = ( + hasattr(otio.schema, "ImageSequenceReference") and + isinstance(media_ref, otio.schema.ImageSequenceReference) + ) + + # OpenTimelineIO 0.12 and older + is_input_sequence_legacy = bool(metadata.get("padding")) + + return is_input_sequence or is_input_sequence_legacy + + +def remap_range_on_file_sequence(otio_clip, in_out_range): + """ + Args: + otio_clip (otio.schema.Clip): The OTIO clip to check. + in_out_range (tuple[float, float]): The in-out range to remap. + + Returns: + tuple(int, int): The remapped range as discrete frame number. + + Raises: + ValueError. When the otio_clip or provided range is invalid. + """ + if not is_clip_from_media_sequence(otio_clip): + raise ValueError(f"Cannot map on non-file sequence clip {otio_clip}.") + + try: + media_in_trimmed, media_out_trimmed = in_out_range + + except ValueError as error: + raise ValueError("Invalid in_out_range provided.") from error + + media_ref = otio_clip.media_reference + media_in = otio_clip.available_range().start_time.value + available_range_rate = otio_clip.available_range().start_time.rate + + frame_in = otio.opentime.RationalTime.from_frames( + media_in_trimmed - media_in + media_ref.start_frame, + rate=available_range_rate, + ).to_frames() + frame_out = otio.opentime.RationalTime.from_frames( + media_out_trimmed - media_in + media_ref.start_frame, + rate=available_range_rate, + ).to_frames() + + return frame_in, frame_out + + def get_media_range_with_retimes(otio_clip, handle_start, handle_end): source_range = otio_clip.source_range available_range = otio_clip.available_range() @@ -276,22 +336,14 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # mediaIn/mediaOut have to correspond # to frame numbers from source sequence. media_ref = otio_clip.media_reference - is_input_sequence = ( - hasattr(otio.schema, "ImageSequenceReference") and - isinstance(media_ref, otio.schema.ImageSequenceReference) - ) + is_input_sequence = is_clip_from_media_sequence(otio_clip) if is_input_sequence: # preserve discrete frame numbers - media_in_trimmed = otio.opentime.RationalTime.from_frames( - media_in_trimmed - media_in + media_ref.start_frame, - rate=available_range_rate, - ).to_frames() - media_out_trimmed = otio.opentime.RationalTime.from_frames( - media_out_trimmed - media_in + media_ref.start_frame, - rate=available_range_rate, - ).to_frames() - + media_in_trimmed, media_out_trimmed = remap_range_on_file_sequence( + otio_clip, + (media_in_trimmed, media_out_trimmed) + ) media_in = media_ref.start_frame media_out = media_in + available_range.duration.to_frames() - 1 diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 64c73adbd5..b96f716ac9 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -58,7 +58,9 @@ def process(self, instance): import opentimelineio as otio from ayon_core.pipeline.editorial import ( otio_range_to_frame_range, - make_sequence_collection + make_sequence_collection, + remap_range_on_file_sequence, + is_clip_from_media_sequence ) # TODO refactore from using instance variable @@ -105,42 +107,48 @@ def process(self, instance): otio_media = r_otio_cl.media_reference media_metadata = otio_media.metadata + variables = (self.to_width, self.to_height) + keys = ("width", "height") + # get from media reference metadata source # TODO 'openpype' prefix should be removed (added 24/09/03) # NOTE it looks like it is set only in hiero integration - for key in {"ayon.source.width", "openpype.source.width"}: - value = media_metadata.get(key) - if value is not None: - width = int(value) - break - - for key in {"ayon.source.height", "openpype.source.height"}: - value = media_metadata.get(key) - if value is not None: - height = int(value) - break - - # compare and reset - if width != self.to_width: - self.to_width = width - if height != self.to_height: - self.to_height = height + for variable, key in zip(variables, keys): + for meta_prefix in ("ayon.source.", "openpype.source."): + meta_key = f"{meta_prefix}.{key}" + if media_metadata.get(meta_key): + variable = media_metadata[meta_key] + break self.log.debug("> self.to_width x self.to_height: {} x {}".format( self.to_width, self.to_height )) - # get frame range values + # Clip: compute process range from available media range. src_range = r_otio_cl.source_range - start = src_range.start_time.value - duration = src_range.duration.value - available_range = None - self.actual_fps = src_range.duration.rate - - # add available range only if not gap if isinstance(r_otio_cl, otio.schema.Clip): available_range = r_otio_cl.available_range() + processing_range = None self.actual_fps = available_range.duration.rate + start = src_range.start_time.rescaled_to(self.actual_fps) + duration = src_range.duration.rescaled_to(self.actual_fps) + + # Gap: no media, generate range based on source range + else: + available_range = processing_range = None + self.actual_fps = src_range.duration.rate + start = src_range.start_time + duration = src_range.duration + + # Create handle offsets. + handle_start = otio.opentime.RationalTime( + handle_start, + rate=self.actual_fps, + ) + handle_end = otio.opentime.RationalTime( + handle_end, + rate=self.actual_fps, + ) # reframing handles conditions if (len(otio_review_clips) > 1) and (index == 0): @@ -157,35 +165,33 @@ def process(self, instance): duration += (handle_start + handle_end) if available_range: - available_range = self._trim_available_range( - available_range, start, duration, self.actual_fps) + processing_range = self._trim_available_range( + available_range, start, duration) # process all track items of the track if isinstance(r_otio_cl, otio.schema.Clip): # process Clip media_ref = r_otio_cl.media_reference metadata = media_ref.metadata - is_sequence = None - - # check in two way if it is sequence - if hasattr(otio.schema, "ImageSequenceReference"): - # for OpenTimelineIO 0.13 and newer - if isinstance(media_ref, - otio.schema.ImageSequenceReference): - is_sequence = True - else: - # for OpenTimelineIO 0.12 and older - if metadata.get("padding"): - is_sequence = True + is_sequence = is_clip_from_media_sequence(r_otio_cl) + # File sequence way if is_sequence: - # file sequence way + # Remap processing range to input file sequence. + processing_range_as_frames = ( + processing_range.start_time.to_frames(), + processing_range.end_time_inclusive().to_frames() + ) + first, last = remap_range_on_file_sequence( + r_otio_cl, + processing_range_as_frames, + ) + input_fps = processing_range.start_time.rate + if hasattr(media_ref, "target_url_base"): dirname = media_ref.target_url_base head = media_ref.name_prefix tail = media_ref.name_suffix - first, last = otio_range_to_frame_range( - available_range) collection = clique.Collection( head=head, tail=tail, @@ -195,7 +201,7 @@ def process(self, instance): [i for i in range(first, (last + 1))]) # render segment self._render_seqment( - sequence=[dirname, collection]) + sequence=[dirname, collection, input_fps]) # generate used frames self._generate_used_frames( len(collection.indexes)) @@ -204,24 +210,38 @@ def process(self, instance): # `ImageSequenceReference` path = media_ref.target_url collection_data = make_sequence_collection( - path, available_range, metadata) + path, processing_range, metadata) dir_path, collection = collection_data # render segment self._render_seqment( - sequence=[dir_path, collection]) + sequence=[dir_path, collection, input_fps]) # generate used frames self._generate_used_frames( len(collection.indexes)) + + # Single video way. + # Extraction via FFmpeg. else: - # single video file way path = media_ref.target_url + # Set extract range from 0 (FFmpeg ignores embedded timecode). + extract_range = otio.opentime.TimeRange( + otio.opentime.RationalTime( + ( + processing_range.start_time.value + - available_range.start_time.value + ), + rate=available_range.start_time.rate, + ), + duration=processing_range.duration, + ) # render video file to sequence self._render_seqment( - video=[path, available_range]) + video=[path, extract_range]) # generate used frames self._generate_used_frames( - available_range.duration.value) + processing_range.duration.value) + # QUESTION: what if nested track composition is in place? else: # at last process a Gap @@ -276,7 +296,7 @@ def _create_representation(self, start, duration): }) return representation_data - def _trim_available_range(self, avl_range, start, duration, fps): + def _trim_available_range(self, avl_range, start, duration): """ Trim available media range to source range. @@ -285,57 +305,62 @@ def _trim_available_range(self, avl_range, start, duration, fps): Args: avl_range (otio.time.TimeRange): media available time range - start (int): start frame - duration (int): duration frames - fps (float): frame rate + start (otio.time.RationalTime): start + duration (otio.time.RationalTime): duration Returns: otio.time.TimeRange: trimmed available range """ # Not all hosts can import these modules. + import opentimelineio as otio from ayon_core.pipeline.editorial import ( trim_media_range, - range_from_frames ) - avl_start = int(avl_range.start_time.value) - src_start = int(avl_start + start) - avl_durtation = int(avl_range.duration.value) + avl_start = avl_range.start_time + avl_duration = avl_range.duration - self.need_offset = bool(avl_start != 0 and src_start != 0) + # TODO investigate + #self.need_offset = bool(avl_start != 0 and src_start != 0) - # if media start is les then clip requires - if src_start < avl_start: - # calculate gap - gap_duration = avl_start - src_start + # A gap is required before available range + # range to conform start point. + if start < avl_start: + gap_duration = avl_start - start + start = avl_start + duration -= gap_duration # create gap data to disk - self._render_seqment(gap=gap_duration) + self._render_seqment(gap=gap_duration.to_frames()) # generate used frames - self._generate_used_frames(gap_duration) - - # fix start and end to correct values - start = 0 - duration -= gap_duration + self._generate_used_frames(gap_duration.to_frames()) + # A gap is required after available range # if media duration is shorter then clip requirement - if duration > avl_durtation: - # calculate gap - gap_start = int(src_start + avl_durtation) - gap_end = int(src_start + duration) - gap_duration = gap_end - gap_start + end_point = start + duration + avl_end_point = avl_range.end_time_exclusive() + if end_point > avl_end_point: + gap_duration = end_point - avl_end_point + duration -= gap_duration # create gap data to disk - self._render_seqment(gap=gap_duration, end_offset=avl_durtation) + self._render_seqment( + gap=gap_duration.to_frames(), + end_offset=avl_duration.to_frames() + ) # generate used frames - self._generate_used_frames(gap_duration, end_offset=avl_durtation) - - # fix duration lenght - duration = avl_durtation + self._generate_used_frames( + gap_duration.to_frames(), + end_offset=avl_duration.to_frames() + ) # return correct trimmed range return trim_media_range( - avl_range, range_from_frames(start, duration, fps) + avl_range, + otio.opentime.TimeRange( + start, + duration + ) ) def _render_seqment(self, sequence=None, @@ -347,7 +372,7 @@ def _render_seqment(self, sequence=None, to defined image sequence format. Args: - sequence (list): input dir path string, collection object in list + sequence (list): input dir path string, collection object, fps in list video (list)[optional]: video_path string, otio_range in list gap (int)[optional]: gap duration end_offset (int)[optional]: offset gap frame start in frames @@ -369,7 +394,7 @@ def _render_seqment(self, sequence=None, input_extension = None if sequence: - input_dir, collection = sequence + input_dir, collection, sequence_fps = sequence in_frame_start = min(collection.indexes) # converting image sequence to image sequence @@ -377,9 +402,22 @@ def _render_seqment(self, sequence=None, input_path = os.path.join(input_dir, input_file) input_extension = os.path.splitext(input_path)[-1] - # form command for rendering gap files + # form command for rendering sequence files + # (need to explicit set the input frame range + # if case input sequence has framerate metadata + # to preserve frame range and avoid silent dropped + # frames caused by input mismatch with FFmpeg default + # rate 25.0 fps) more info refer to FFmpeg image2 demuxer + # + # Implicit + # [Input 100 frames (24fps from metadata)] -> [Demuxer video 25fps] -> [Output 98 frames, dropped 2] + # + # Explicit with "-framerate" + # [Input 100 frames (24fps from metadata)] -> [Demuxer video 24fps] -> [Output 100 frames] + command.extend([ "-start_number", str(in_frame_start), + "-framerate", str(sequence_fps), "-i", input_path ]) @@ -456,8 +494,8 @@ def _generate_used_frames(self, duration, end_offset=None): # create frame offset offset = 0 - if self.need_offset: - offset = 1 +# if self.need_offset: +# offset = 1 if end_offset: new_frames = list() From 6d31cd723c0043e12a5f64219a39cda981332f5e Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 24 Sep 2024 17:14:42 -0400 Subject: [PATCH 44/85] Add unit tests. --- .../plugins/publish/extract_otio_review.py | 20 +- .../resources/img_seq_embedded_tc_review.json | 363 ++++++++++++++++++ .../editorial/resources/img_seq_review.json | 363 ++++++++++++++++++ .../resources/qt_embedded_tc_review.json | 356 +++++++++++++++++ .../editorial/resources/qt_review.json | 356 +++++++++++++++++ .../editorial/test_extract_otio_review.py | 180 +++++++++ 6 files changed, 1624 insertions(+), 14 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_embedded_tc_review.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_review.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/qt_embedded_tc_review.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/qt_review.json create mode 100644 tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index b96f716ac9..d1ac019c3a 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -320,9 +320,6 @@ def _trim_available_range(self, avl_range, start, duration): avl_start = avl_range.start_time avl_duration = avl_range.duration - # TODO investigate - #self.need_offset = bool(avl_start != 0 and src_start != 0) - # A gap is required before available range # range to conform start point. if start < avl_start: @@ -331,9 +328,9 @@ def _trim_available_range(self, avl_range, start, duration): duration -= gap_duration # create gap data to disk - self._render_seqment(gap=gap_duration.to_frames()) + self._render_seqment(gap=gap_duration.round().to_frames()) # generate used frames - self._generate_used_frames(gap_duration.to_frames()) + self._generate_used_frames(gap_duration.round().to_frames()) # A gap is required after available range # if media duration is shorter then clip requirement @@ -345,12 +342,12 @@ def _trim_available_range(self, avl_range, start, duration): # create gap data to disk self._render_seqment( - gap=gap_duration.to_frames(), + gap=gap_duration.round().to_frames(), end_offset=avl_duration.to_frames() ) # generate used frames self._generate_used_frames( - gap_duration.to_frames(), + gap_duration.round().to_frames(), end_offset=avl_duration.to_frames() ) @@ -492,16 +489,11 @@ def _generate_used_frames(self, duration, end_offset=None): padding = "{{:0{}d}}".format(self.padding) - # create frame offset - offset = 0 -# if self.need_offset: -# offset = 1 - if end_offset: new_frames = list() start_frame = self.used_frames[-1] - for index in range((end_offset + offset), - (int(end_offset + duration) + offset)): + for index in range(end_offset, + (int(end_offset + duration))): seq_number = padding.format(start_frame + index) self.log.debug( "index: `{}` | seq_number: `{}`".format(index, seq_number)) diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_embedded_tc_review.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_embedded_tc_review.json new file mode 100644 index 0000000000..3437692155 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_embedded_tc_review.json @@ -0,0 +1,363 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "Resolve_OTIO": {} + }, + "name": "output.[1000-1100].exr", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 87399.0 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Transform", + "Enabled": true, + "Name": "Transform", + "Parameters": [], + "Type": 2 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Cropping", + "Enabled": true, + "Name": "Cropping", + "Parameters": [], + "Type": 3 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Dynamic Zoom", + "Enabled": false, + "Name": "Dynamic Zoom", + "Parameters": [ + { + "Default Parameter Value": [ + 0.0, + 0.0 + ], + "Key Frames": { + "0": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + "1000": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + } + }, + "Parameter ID": "dynamicZoomCenter", + "Parameter Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + { + "Default Parameter Value": 1.0, + "Key Frames": { + "0": { + "Value": 0.8, + "Variant Type": "Double" + }, + "1000": { + "Value": 1.0, + "Variant Type": "Double" + } + }, + "Parameter ID": "dynamicZoomScale", + "Parameter Value": 1.0, + "Variant Type": "Double", + "maxValue": 100.0, + "minValue": 0.01 + } + ], + "Type": 59 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Composite", + "Enabled": true, + "Name": "Composite", + "Parameters": [], + "Type": 1 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Lens Correction", + "Enabled": true, + "Name": "Lens Correction", + "Parameters": [], + "Type": 43 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Retime and Scaling", + "Enabled": true, + "Name": "Retime and Scaling", + "Parameters": [], + "Type": 22 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Video Faders", + "Enabled": true, + "Name": "Video Faders", + "Parameters": [], + "Type": 36 + } + }, + "name": "", + "effect_name": "Resolve Effect" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "Resolve_OTIO": { + "Keywords": [], + "Note": "{\"resolve_sub_products\": {\"io.ayon.creators.resolve.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.shot\", \"variant\": \"Main\", \"folderPath\": \"/shots/seq_img_tc_handles_out/sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"adca7e5b-b53c-48ab-8469-abe4db3c276a\", \"clip_source_resolution\": {\"width\": \"956\", \"height\": \"720\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_img_tc_handles_out\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/seq_img_tc_handles_out\", \"sourceResolution\": true, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"seq_img_tc_handles_out\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_img_tc_handles_out\", \"track\": \"Video1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"fca94ed7-1e74-4ddc-8d56-05696e8c472a\", \"reviewTrack\": \"Video1\", \"label\": \"/shots/seq_img_tc_handles_out/sh010 shot\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"6c2baba3-183c-41f0-b9a9-596d315fd162\", \"creator_attributes\": {\"folderPath\": \"/shots/seq_img_tc_handles_out/sh010\", \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1102, \"clipIn\": 86524, \"clipOut\": 86625, \"clipDuration\": 101, \"sourceIn\": 0, \"sourceOut\": 101, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.plate\", \"variant\": \"Video1\", \"folderPath\": \"/shots/seq_img_tc_handles_out/sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"adca7e5b-b53c-48ab-8469-abe4db3c276a\", \"clip_source_resolution\": {\"width\": \"956\", \"height\": \"720\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_img_tc_handles_out\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/seq_img_tc_handles_out\", \"sourceResolution\": true, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"seq_img_tc_handles_out\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_img_tc_handles_out\", \"track\": \"Video1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"fca94ed7-1e74-4ddc-8d56-05696e8c472a\", \"reviewTrack\": \"Video1\", \"parent_instance_id\": \"6c2baba3-183c-41f0-b9a9-596d315fd162\", \"label\": \"/shots/seq_img_tc_handles_out/sh010 plate\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"8b1f1e6f-699a-4481-b9be-92d819bc4096\", \"creator_attributes\": {\"parentInstance\": \"/shots/seq_img_tc_handles_out/sh010 shot\", \"vSyncOn\": true, \"vSyncTrack\": \"Video1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"adca7e5b-b53c-48ab-8469-abe4db3c276a\", \"publish\": true}" + }, + "clip_index": "adca7e5b-b53c-48ab-8469-abe4db3c276a", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "adca7e5b-b53c-48ab-8469-abe4db3c276a", + "clip_source_resolution": { + "height": "720", + "pixelAspect": 1.0, + "width": "956" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/seq_img_tc_handles_out/sh010 shot", + "vSyncOn": true, + "vSyncTrack": "Video1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/seq_img_tc_handles_out/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/seq_img_tc_handles_out", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "seq_img_tc_handles_out", + "shot": "sh010", + "track": "Video1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "8b1f1e6f-699a-4481-b9be-92d819bc4096", + "label": "/shots/seq_img_tc_handles_out/sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "6c2baba3-183c-41f0-b9a9-596d315fd162", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "seq_img_tc_handles_out", + "folder_type": "sequence" + } + ], + "productName": "plateVideo1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video1", + "sequence": "seq_img_tc_handles_out", + "shot": "sh###", + "sourceResolution": true, + "task": "Generic", + "track": "{_track_}", + "uuid": "fca94ed7-1e74-4ddc-8d56-05696e8c472a", + "variant": "Video1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "adca7e5b-b53c-48ab-8469-abe4db3c276a", + "clip_source_resolution": { + "height": "720", + "pixelAspect": 1.0, + "width": "956" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 101, + "clipIn": 86524, + "clipOut": 86625, + "folderPath": "/shots/seq_img_tc_handles_out/sh010", + "fps": "from_selection", + "frameEnd": 1102, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 0, + "sourceOut": 101, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/seq_img_tc_handles_out/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/seq_img_tc_handles_out", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "seq_img_tc_handles_out", + "shot": "sh010", + "track": "Video1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "6c2baba3-183c-41f0-b9a9-596d315fd162", + "label": "/shots/seq_img_tc_handles_out/sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "seq_img_tc_handles_out", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video1", + "sequence": "seq_img_tc_handles_out", + "shot": "sh###", + "sourceResolution": true, + "task": "Generic", + "track": "{_track_}", + "uuid": "fca94ed7-1e74-4ddc-8d56-05696e8c472a", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AyonData", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 87449.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": {}, + "name": "output.[1000-1100].exr", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 87399.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\exr_embedded_tc", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 1000, + "frame_step": 1, + "rate": 24.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_review.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_review.json new file mode 100644 index 0000000000..ed19d65744 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_review.json @@ -0,0 +1,363 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "Resolve_OTIO": {} + }, + "name": "output.[1000-1100].tif", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 91.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 5.0 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Transform", + "Enabled": true, + "Name": "Transform", + "Parameters": [], + "Type": 2 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Cropping", + "Enabled": true, + "Name": "Cropping", + "Parameters": [], + "Type": 3 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Dynamic Zoom", + "Enabled": false, + "Name": "Dynamic Zoom", + "Parameters": [ + { + "Default Parameter Value": [ + 0.0, + 0.0 + ], + "Key Frames": { + "-5": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + "955": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + } + }, + "Parameter ID": "dynamicZoomCenter", + "Parameter Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + { + "Default Parameter Value": 1.0, + "Key Frames": { + "-5": { + "Value": 0.8, + "Variant Type": "Double" + }, + "955": { + "Value": 1.0, + "Variant Type": "Double" + } + }, + "Parameter ID": "dynamicZoomScale", + "Parameter Value": 1.0, + "Variant Type": "Double", + "maxValue": 100.0, + "minValue": 0.01 + } + ], + "Type": 59 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Composite", + "Enabled": true, + "Name": "Composite", + "Parameters": [], + "Type": 1 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Lens Correction", + "Enabled": true, + "Name": "Lens Correction", + "Parameters": [], + "Type": 43 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Retime and Scaling", + "Enabled": true, + "Name": "Retime and Scaling", + "Parameters": [], + "Type": 22 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Video Faders", + "Enabled": true, + "Name": "Video Faders", + "Parameters": [], + "Type": 36 + } + }, + "name": "", + "effect_name": "Resolve Effect" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "Resolve_OTIO": { + "Keywords": [], + "Note": "{\"resolve_sub_products\": {\"io.ayon.creators.resolve.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.shot\", \"variant\": \"Main\", \"folderPath\": \"/shots/seq_img_notc_blackhandles/sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"a82520bd-f231-4a23-9cb7-8823141232db\", \"clip_source_resolution\": {\"width\": \"1920\", \"height\": \"1080\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_img_notc_blackhandles\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/seq_img_notc_blackhandles\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"seq_img_notc_blackhandles\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_img_notc_blackhandles\", \"track\": \"Video1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"5d6be326-f1d0-4416-b6aa-780d05a8dd6d\", \"reviewTrack\": \"Video1\", \"label\": \"/shots/seq_img_notc_blackhandles/sh010 shot\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"e196263f-c584-40b4-bc27-018051a3bc92\", \"creator_attributes\": {\"folderPath\": \"/shots/seq_img_notc_blackhandles/sh010\", \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1092, \"clipIn\": 86511, \"clipOut\": 86602, \"clipDuration\": 91, \"sourceIn\": 5, \"sourceOut\": 96, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.plate\", \"variant\": \"Video1\", \"folderPath\": \"/shots/seq_img_notc_blackhandles/sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"a82520bd-f231-4a23-9cb7-8823141232db\", \"clip_source_resolution\": {\"width\": \"1920\", \"height\": \"1080\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_img_notc_blackhandles\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/seq_img_notc_blackhandles\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"seq_img_notc_blackhandles\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_img_notc_blackhandles\", \"track\": \"Video1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"5d6be326-f1d0-4416-b6aa-780d05a8dd6d\", \"reviewTrack\": \"Video1\", \"parent_instance_id\": \"e196263f-c584-40b4-bc27-018051a3bc92\", \"label\": \"/shots/seq_img_notc_blackhandles/sh010 plate\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"ced7e9b8-721a-4377-a827-15fbf7f2831a\", \"creator_attributes\": {\"parentInstance\": \"/shots/seq_img_notc_blackhandles/sh010 shot\", \"vSyncOn\": true, \"vSyncTrack\": \"Video1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"a82520bd-f231-4a23-9cb7-8823141232db\", \"publish\": true}" + }, + "clip_index": "a82520bd-f231-4a23-9cb7-8823141232db", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "a82520bd-f231-4a23-9cb7-8823141232db", + "clip_source_resolution": { + "height": "1080", + "pixelAspect": 1.0, + "width": "1920" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/seq_img_notc_blackhandles/sh010 shot", + "vSyncOn": true, + "vSyncTrack": "Video1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/seq_img_notc_blackhandles/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/seq_img_notc_blackhandles", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "seq_img_notc_blackhandles", + "shot": "sh010", + "track": "Video1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "ced7e9b8-721a-4377-a827-15fbf7f2831a", + "label": "/shots/seq_img_notc_blackhandles/sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "e196263f-c584-40b4-bc27-018051a3bc92", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "seq_img_notc_blackhandles", + "folder_type": "sequence" + } + ], + "productName": "plateVideo1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video1", + "sequence": "seq_img_notc_blackhandles", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "5d6be326-f1d0-4416-b6aa-780d05a8dd6d", + "variant": "Video1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "a82520bd-f231-4a23-9cb7-8823141232db", + "clip_source_resolution": { + "height": "1080", + "pixelAspect": 1.0, + "width": "1920" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 91, + "clipIn": 86511, + "clipOut": 86602, + "folderPath": "/shots/seq_img_notc_blackhandles/sh010", + "fps": "from_selection", + "frameEnd": 1092, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 5, + "sourceOut": 96, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/seq_img_notc_blackhandles/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/seq_img_notc_blackhandles", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "seq_img_notc_blackhandles", + "shot": "sh010", + "track": "Video1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "e196263f-c584-40b4-bc27-018051a3bc92", + "label": "/shots/seq_img_notc_blackhandles/sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "seq_img_notc_blackhandles", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video1", + "sequence": "seq_img_notc_blackhandles", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "5d6be326-f1d0-4416-b6aa-780d05a8dd6d", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AyonData", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 50.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": {}, + "name": "output.[1000-1100].tif", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\tif_seq", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/qt_embedded_tc_review.json b/tests/client/ayon_core/pipeline/editorial/resources/qt_embedded_tc_review.json new file mode 100644 index 0000000000..629e9e04af --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/qt_embedded_tc_review.json @@ -0,0 +1,356 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "Resolve_OTIO": {} + }, + "name": "qt_embedded_tc.mov", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 68.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 86414.0 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Transform", + "Enabled": true, + "Name": "Transform", + "Parameters": [], + "Type": 2 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Cropping", + "Enabled": true, + "Name": "Cropping", + "Parameters": [], + "Type": 3 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Dynamic Zoom", + "Enabled": false, + "Name": "Dynamic Zoom", + "Parameters": [ + { + "Default Parameter Value": [ + 0.0, + 0.0 + ], + "Key Frames": { + "-14": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + "986": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + } + }, + "Parameter ID": "dynamicZoomCenter", + "Parameter Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + { + "Default Parameter Value": 1.0, + "Key Frames": { + "-14": { + "Value": 0.8, + "Variant Type": "Double" + }, + "986": { + "Value": 1.0, + "Variant Type": "Double" + } + }, + "Parameter ID": "dynamicZoomScale", + "Parameter Value": 1.0, + "Variant Type": "Double", + "maxValue": 100.0, + "minValue": 0.01 + } + ], + "Type": 59 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Composite", + "Enabled": true, + "Name": "Composite", + "Parameters": [], + "Type": 1 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Lens Correction", + "Enabled": true, + "Name": "Lens Correction", + "Parameters": [], + "Type": 43 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Retime and Scaling", + "Enabled": true, + "Name": "Retime and Scaling", + "Parameters": [], + "Type": 22 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Video Faders", + "Enabled": true, + "Name": "Video Faders", + "Parameters": [], + "Type": 36 + } + }, + "name": "", + "effect_name": "Resolve Effect" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "Resolve_OTIO": { + "Keywords": [], + "Note": "{\"resolve_sub_products\": {\"io.ayon.creators.resolve.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.shot\", \"variant\": \"Main\", \"folderPath\": \"/shots/seq_qt_tc/sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"12cce00c-eadf-4abd-ac80-0816a24506ab\", \"clip_source_resolution\": {\"width\": \"956\", \"height\": \"720\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_qt_tc\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/seq_qt_tc\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"seq_qt_tc\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_qt_tc\", \"track\": \"Video1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"5dc397e0-1142-4a35-969d-d4c35c512f0f\", \"reviewTrack\": \"Video1\", \"label\": \"/shots/seq_qt_tc/sh010 shot\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"6f4bbf76-6638-4645-9059-0f516c0c12c2\", \"creator_attributes\": {\"folderPath\": \"/shots/seq_qt_tc/sh010\", \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1069, \"clipIn\": 86516, \"clipOut\": 86584, \"clipDuration\": 68, \"sourceIn\": 14, \"sourceOut\": 82, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.plate\", \"variant\": \"Video1\", \"folderPath\": \"/shots/seq_qt_tc/sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"12cce00c-eadf-4abd-ac80-0816a24506ab\", \"clip_source_resolution\": {\"width\": \"956\", \"height\": \"720\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_qt_tc\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/seq_qt_tc\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"seq_qt_tc\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_qt_tc\", \"track\": \"Video1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"5dc397e0-1142-4a35-969d-d4c35c512f0f\", \"reviewTrack\": \"Video1\", \"parent_instance_id\": \"6f4bbf76-6638-4645-9059-0f516c0c12c2\", \"label\": \"/shots/seq_qt_tc/sh010 plate\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"1d11a6b5-cc2b-49d8-8bcb-35187c785b22\", \"creator_attributes\": {\"parentInstance\": \"/shots/seq_qt_tc/sh010 shot\", \"vSyncOn\": true, \"vSyncTrack\": \"Video1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"12cce00c-eadf-4abd-ac80-0816a24506ab\", \"publish\": true}" + }, + "clip_index": "12cce00c-eadf-4abd-ac80-0816a24506ab", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "12cce00c-eadf-4abd-ac80-0816a24506ab", + "clip_source_resolution": { + "height": "720", + "pixelAspect": 1.0, + "width": "956" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/seq_qt_tc/sh010 shot", + "vSyncOn": true, + "vSyncTrack": "Video1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/seq_qt_tc/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/seq_qt_tc", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "seq_qt_tc", + "shot": "sh010", + "track": "Video1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "1d11a6b5-cc2b-49d8-8bcb-35187c785b22", + "label": "/shots/seq_qt_tc/sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "6f4bbf76-6638-4645-9059-0f516c0c12c2", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "seq_qt_tc", + "folder_type": "sequence" + } + ], + "productName": "plateVideo1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video1", + "sequence": "seq_qt_tc", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "5dc397e0-1142-4a35-969d-d4c35c512f0f", + "variant": "Video1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "12cce00c-eadf-4abd-ac80-0816a24506ab", + "clip_source_resolution": { + "height": "720", + "pixelAspect": 1.0, + "width": "956" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 68, + "clipIn": 86516, + "clipOut": 86584, + "folderPath": "/shots/seq_qt_tc/sh010", + "fps": "from_selection", + "frameEnd": 1069, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 14, + "sourceOut": 82, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/seq_qt_tc/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/seq_qt_tc", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "seq_qt_tc", + "shot": "sh010", + "track": "Video1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "6f4bbf76-6638-4645-9059-0f516c0c12c2", + "label": "/shots/seq_qt_tc/sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "seq_qt_tc", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video1", + "sequence": "seq_qt_tc", + "shot": "sh###", + "sourceResolution": false, + "task": "Generic", + "track": "{_track_}", + "uuid": "5dc397e0-1142-4a35-969d-d4c35c512f0f", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AyonData", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 86448.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": {}, + "name": "qt_embedded_tc.mov", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 100.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 86400.0 + } + }, + "available_image_bounds": null, + "target_url": "C:\\data\\qt_embedded_tc.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/qt_review.json b/tests/client/ayon_core/pipeline/editorial/resources/qt_review.json new file mode 100644 index 0000000000..4dabb7d58f --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/qt_review.json @@ -0,0 +1,356 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "Resolve_OTIO": {} + }, + "name": "3 jours dans les coulisses du ZEvent 2024.mp4", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 50.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Transform", + "Enabled": true, + "Name": "Transform", + "Parameters": [], + "Type": 2 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Cropping", + "Enabled": true, + "Name": "Cropping", + "Parameters": [], + "Type": 3 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Dynamic Zoom", + "Enabled": false, + "Name": "Dynamic Zoom", + "Parameters": [ + { + "Default Parameter Value": [ + 0.0, + 0.0 + ], + "Key Frames": { + "0": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + "1000": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + } + }, + "Parameter ID": "dynamicZoomCenter", + "Parameter Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + { + "Default Parameter Value": 1.0, + "Key Frames": { + "0": { + "Value": 0.8, + "Variant Type": "Double" + }, + "1000": { + "Value": 1.0, + "Variant Type": "Double" + } + }, + "Parameter ID": "dynamicZoomScale", + "Parameter Value": 1.0, + "Variant Type": "Double", + "maxValue": 100.0, + "minValue": 0.01 + } + ], + "Type": 59 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Composite", + "Enabled": true, + "Name": "Composite", + "Parameters": [], + "Type": 1 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Lens Correction", + "Enabled": true, + "Name": "Lens Correction", + "Parameters": [], + "Type": 43 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Retime and Scaling", + "Enabled": true, + "Name": "Retime and Scaling", + "Parameters": [], + "Type": 22 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Video Faders", + "Enabled": true, + "Name": "Video Faders", + "Parameters": [], + "Type": 36 + } + }, + "name": "", + "effect_name": "Resolve Effect" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "Resolve_OTIO": { + "Keywords": [], + "Note": "{\"resolve_sub_products\": {\"io.ayon.creators.resolve.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.shot\", \"variant\": \"Main\", \"folderPath\": \"/shots/seq_qt_no_tc/sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"c3d9fb4f-afdf-49e3-9733-bf80e40e0de3\", \"clip_source_resolution\": {\"width\": \"640\", \"height\": \"360\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_qt_no_tc\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/seq_qt_no_tc\", \"sourceResolution\": true, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"seq_qt_no_tc\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_qt_no_tc\", \"track\": \"Video1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"5ab44838-a173-422a-8750-d5265e5a4ab5\", \"reviewTrack\": \"Video1\", \"label\": \"/shots/seq_qt_no_tc/sh010 shot\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"ba8e76cd-7319-449d-93b5-93fd65cf3e83\", \"creator_attributes\": {\"folderPath\": \"/shots/seq_qt_no_tc/sh010\", \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1051, \"clipIn\": 86477, \"clipOut\": 86527, \"clipDuration\": 50, \"sourceIn\": 0, \"sourceOut\": 50, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.plate\", \"variant\": \"Video1\", \"folderPath\": \"/shots/seq_qt_no_tc/sh010\", \"task\": \"Generic\", \"clip_variant\": \"\", \"clip_index\": \"c3d9fb4f-afdf-49e3-9733-bf80e40e0de3\", \"clip_source_resolution\": {\"width\": \"640\", \"height\": \"360\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_qt_no_tc\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/seq_qt_no_tc\", \"sourceResolution\": true, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"seq_qt_no_tc\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_qt_no_tc\", \"track\": \"Video1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"5ab44838-a173-422a-8750-d5265e5a4ab5\", \"reviewTrack\": \"Video1\", \"parent_instance_id\": \"ba8e76cd-7319-449d-93b5-93fd65cf3e83\", \"label\": \"/shots/seq_qt_no_tc/sh010 plate\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"4a1cd220-c638-4e77-855c-cebd43b5dbc3\", \"creator_attributes\": {\"parentInstance\": \"/shots/seq_qt_no_tc/sh010 shot\", \"vSyncOn\": true, \"vSyncTrack\": \"Video1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"c3d9fb4f-afdf-49e3-9733-bf80e40e0de3\", \"publish\": true}" + }, + "clip_index": "c3d9fb4f-afdf-49e3-9733-bf80e40e0de3", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "c3d9fb4f-afdf-49e3-9733-bf80e40e0de3", + "clip_source_resolution": { + "height": "360", + "pixelAspect": 1.0, + "width": "640" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/seq_qt_no_tc/sh010 shot", + "vSyncOn": true, + "vSyncTrack": "Video1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/seq_qt_no_tc/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/seq_qt_no_tc", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "seq_qt_no_tc", + "shot": "sh010", + "track": "Video1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "4a1cd220-c638-4e77-855c-cebd43b5dbc3", + "label": "/shots/seq_qt_no_tc/sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "ba8e76cd-7319-449d-93b5-93fd65cf3e83", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "seq_qt_no_tc", + "folder_type": "sequence" + } + ], + "productName": "plateVideo1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video1", + "sequence": "seq_qt_no_tc", + "shot": "sh###", + "sourceResolution": true, + "task": "Generic", + "track": "{_track_}", + "uuid": "5ab44838-a173-422a-8750-d5265e5a4ab5", + "variant": "Video1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "c3d9fb4f-afdf-49e3-9733-bf80e40e0de3", + "clip_source_resolution": { + "height": "360", + "pixelAspect": 1.0, + "width": "640" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 50, + "clipIn": 86477, + "clipOut": 86527, + "folderPath": "/shots/seq_qt_no_tc/sh010", + "fps": "from_selection", + "frameEnd": 1051, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 0, + "sourceOut": 50, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/seq_qt_no_tc/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/seq_qt_no_tc", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "seq_qt_no_tc", + "shot": "sh010", + "track": "Video1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "ba8e76cd-7319-449d-93b5-93fd65cf3e83", + "label": "/shots/seq_qt_no_tc/sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "seq_qt_no_tc", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video1", + "sequence": "seq_qt_no_tc", + "shot": "sh###", + "sourceResolution": true, + "task": "Generic", + "track": "{_track_}", + "uuid": "5ab44838-a173-422a-8750-d5265e5a4ab5", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AyonData", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 25.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": {}, + "name": "3 jours dans les coulisses du ZEvent 2024.mp4", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 30822.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "available_image_bounds": null, + "target_url": "C:\\data\\movie.mp4" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py new file mode 100644 index 0000000000..3623f6129d --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py @@ -0,0 +1,180 @@ +import mock +import os +import pytest +from typing import NamedTuple + +import opentimelineio as otio + +import ayon_core.lib +from ayon_core.plugins.publish import extract_otio_review + + +_RESOURCE_DIR = os.path.join( + os.path.dirname(__file__), + "resources" +) + + +class MockInstance(): + """ Mock pyblish instance for testing purpose. + """ + def __init__(self, data: dict): + self.data = data + self.context = self + + +class CaptureFFmpegCalls(): + """ Mock calls made to ffmpeg subprocess. + """ + def __init__(self): + self.calls = [] + + def append_call(self, *args, **kwargs): + ffmpeg_args_list, = args + self.calls.append(" ".join(ffmpeg_args_list)) + return True + + def get_fmpeg_executable(self, _): + return ["/path/to/ffmpeg"] + + +def run_process(file_name: str): + """ + """ + # Get OTIO review data from serialized file_name + file_path = os.path.join(_RESOURCE_DIR, file_name) + clip = otio.schema.Clip.from_json_file(file_path) + + # Prepare dummy instance and capture call object + capture_call = CaptureFFmpegCalls() + processor = extract_otio_review.ExtractOTIOReview() + instance = MockInstance({ + "otioReviewClips": [clip], + "handleStart": 10, + "handleEnd": 10, + "workfileFrameStart": 1001, + "folderPath": "/dummy/path", + "anatomy": NamedTuple("Anatomy", [('project_name', "test_project")]) + }) + + # Mock calls to extern and run plugins. + with mock.patch.object( + extract_otio_review, + "get_ffmpeg_tool_args", + side_effect=capture_call.get_fmpeg_executable, + ): + with mock.patch.object( + extract_otio_review, + "run_subprocess", + side_effect=capture_call.append_call, + ): + with mock.patch.object( + processor, + "_get_folder_name_based_prefix", + return_value="C:/result/output." + ): + processor.process(instance) + + # return all calls made to ffmpeg subprocess + return capture_call.calls + + +def test_image_sequence_with_embedded_tc_and_handles_out_of_range(): + """ + Img sequence clip (embedded timecode 1h/24fps) + available_files = 1000-1100 + available_range = 87399-87500 24fps + source_range = 87399-87500 24fps + """ + calls = run_process("img_seq_embedded_tc_review.json") + + expected = [ + # 10 head black handles generated from gap (991-1000) + "/path/to/ffmpeg -t 0.4166666666666667 -r 24.0 -f lavfi -i " + "color=c=black:s=1280x720 -tune stillimage -start_number 991 " + "C:/result/output.%03d.jpg", + + # 10 tail black handles generated from gap (1102-1111) + "/path/to/ffmpeg -t 0.4166666666666667 -r 24.0 -f lavfi -i " + "color=c=black:s=1280x720 -tune stillimage -start_number 1102 " + "C:/result/output.%03d.jpg", + + # Report from source exr (1001-1101) with enforce framerate + "/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i " + "C:\\exr_embedded_tc\\output.%04d.exr -start_number 1001 " + "C:/result/output.%03d.jpg" + ] + + assert calls == expected + + +def test_image_sequence_and_handles_out_of_range(): + """ + Img sequence clip (no timecode) + available_files = 1000-1100 + available_range = 0-101 25fps + source_range = 5-91 24fps + """ + calls = run_process("img_seq_review.json") + + expected = [ + # 5 head black frames generated from gap (991-995) + "/path/to/ffmpeg -t 0.2 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune " + "stillimage -start_number 991 C:/result/output.%03d.jpg", + + # 9 tail back frames generated from gap (1097-1105) + "/path/to/ffmpeg -t 0.36 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune " + "stillimage -start_number 1097 C:/result/output.%03d.jpg", + + # Report from source tiff (996-1096) + # 996-1000 = additional 5 head frames + # 1001-1095 = source range conformed to 25fps + # 1096-1096 = additional 1 tail frames + "/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i " + "C:\\tif_seq\\output.%04d.tif -start_number 996 C:/result/output.%03d.jpg" + ] + + assert calls == expected + + +def test_movie_with_embedded_tc_no_gap_handles(): + """ + Qt movie clip (embedded timecode 1h/24fps) + available_range = 86400-86500 24fps + source_range = 86414-86482 24fps + """ + calls = run_process("qt_embedded_tc_review.json") + + expected = [ + # Handles are all included in media available range. + # Extract source range from Qt + # - first_frame = 14 src - 10 (head tail) = frame 4 = 0.1666s + # - duration = 68fr (source) + 20fr (handles) = 88frames = 3.666s + "/path/to/ffmpeg -ss 0.16666666666666666 -t 3.6666666666666665 " + "-i C:\\data\\qt_embedded_tc.mov -start_number 991 " + "C:/result/output.%03d.jpg" + ] + + assert calls == expected + + +def test_movie_tail_gap_handles(): + """ + Qt movie clip (embedded timecode 1h/24fps) + available_range = 0-30822 25fps + source_range = 0-50 24fps + """ + calls = run_process("qt_review.json") + + expected = [ + # 10 head black frames generated from gap (991-1000) + "/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune " + "stillimage -start_number 991 C:/result/output.%03d.jpg", + + # source range + 10 tail frames + # duration = 50fr (source) + 10fr (tail handle) = 60 fr = 2.4s + "/path/to/ffmpeg -ss 0.0 -t 2.4 -i C:\\data\\movie.mp4 -start_number 1001 " + "C:/result/output.%03d.jpg" + ] + + assert calls == expected From 4b27971a8e6d292d4f06c21733dca33be40a017d Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 24 Sep 2024 17:26:37 -0400 Subject: [PATCH 45/85] Adjust comment. --- client/ayon_core/plugins/publish/extract_otio_review.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index d1ac019c3a..01cd974dad 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -320,8 +320,8 @@ def _trim_available_range(self, avl_range, start, duration): avl_start = avl_range.start_time avl_duration = avl_range.duration - # A gap is required before available range - # range to conform start point. + # An additional gap is required before the available + # range to conform source start point and head handles. if start < avl_start: gap_duration = avl_start - start start = avl_start @@ -332,8 +332,9 @@ def _trim_available_range(self, avl_range, start, duration): # generate used frames self._generate_used_frames(gap_duration.round().to_frames()) - # A gap is required after available range - # if media duration is shorter then clip requirement + # An additional gap is required after the available + # range to conform to source end point + tail handles + # (media duration is shorter then clip requirement). end_point = start + duration avl_end_point = avl_range.end_time_exclusive() if end_point > avl_end_point: From 885f8acd2b59748facb35b2b64b244bd4d670493 Mon Sep 17 00:00:00 2001 From: Robin De Lillo Date: Tue, 24 Sep 2024 17:33:17 -0400 Subject: [PATCH 46/85] Update client/ayon_core/pipeline/editorial.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/editorial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 7d6d6f5882..2dc15bd645 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -277,8 +277,8 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # to frame numbers from source sequence. media_ref = otio_clip.media_reference is_input_sequence = ( - hasattr(otio.schema, "ImageSequenceReference") and - isinstance(media_ref, otio.schema.ImageSequenceReference) + hasattr(otio.schema, "ImageSequenceReference") + and isinstance(media_ref, otio.schema.ImageSequenceReference) ) if is_input_sequence: From 453995ac5795489bd5eb7390717021c832a321b1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Sep 2024 14:16:39 +0200 Subject: [PATCH 47/85] Update imageio config conversion to 0.4.5 version Adjust imageio config conversion function to handle changes in settings from 0.4.4 to 0.4.5, ensuring proper profile usage and plugin conversion consistency. --- server/settings/conversion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index d99483d21f..634e5ab438 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -4,8 +4,8 @@ from .publish_plugins import DEFAULT_PUBLISH_VALUES -def _convert_imageio_configs_0_4_4(overrides): - """Imageio config settings did change to profiles since 0.4.4.""" +def _convert_imageio_configs_0_4_5(overrides): + """Imageio config settings did change to profiles since 0.4.5.""" imageio_overrides = overrides.get("imageio") or {} # make sure settings are already converted to profiles @@ -119,6 +119,6 @@ def convert_settings_overrides( overrides: dict[str, Any], ) -> dict[str, Any]: _convert_imageio_configs_0_3_1(overrides) - _convert_imageio_configs_0_4_4(overrides) + _convert_imageio_configs_0_4_5(overrides) _conver_publish_plugins(overrides) return overrides From 6085b6bd8237b7049714ecf5f03c20cfd738d086 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Sep 2024 14:17:26 +0200 Subject: [PATCH 48/85] Refactor imageio settings conversion logic Simplified conditional check for ocio_config_profiles presence. --- server/settings/conversion.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index 634e5ab438..3b51e46dba 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -9,9 +9,7 @@ def _convert_imageio_configs_0_4_5(overrides): imageio_overrides = overrides.get("imageio") or {} # make sure settings are already converted to profiles - if ( - "ocio_config_profiles" not in imageio_overrides - ): + if "ocio_config_profiles" not in imageio_overrides: return ocio_config_profiles = imageio_overrides["ocio_config_profiles"] From 07ea80d2d70fd611b7687eba45e8d9a3fe294405 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Sep 2024 14:23:31 +0200 Subject: [PATCH 49/85] Update fallback type field names in colorspace and settings modules The commit updates the field name from "type" to "fallback_type" for consistency in the colorspace and settings modules. --- client/ayon_core/pipeline/colorspace.py | 2 +- server/settings/main.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 1e6f98f272..8c4f97ab1c 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -806,7 +806,7 @@ def _get_global_config_data( log.info("Using fallback data for ocio config path.") # in case no product was found we need to use fallback - fallback_type = fallback_data["type"] + fallback_type = fallback_data["fallback_type"] return _get_config_path_from_profile_data( fallback_data, fallback_type, template_data ) diff --git a/server/settings/main.py b/server/settings/main.py index 09c9bf0065..249bab85fd 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -85,7 +85,7 @@ def _ocio_built_in_paths(): class FallbackProductModel(BaseSettingsModel): _layout = "expanded" - type: str = SettingsField( + fallback_type: str = SettingsField( title="Fallback config type", enum_resolver=_fallback_ocio_config_profile_types, conditionalEnum=True, @@ -347,7 +347,7 @@ def validate_json(cls, value): "published_product": { "product_name": "", "fallback": { - "type": "builtin_path", + "fallback_type": "builtin_path", "builtin_path": "ACES 1.2", "custom_path": "" } From 6aa31e8788dbeb316b2c3efcc480675aa9386cc0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Sep 2024 14:24:52 +0200 Subject: [PATCH 50/85] fixing typo Changed function name from '_conver_publish_plugins' to '_convert_publish_plugins' for consistency and clarity. Updated the function call accordingly in 'convert_settings_overrides'. --- server/settings/conversion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index 3b51e46dba..b933d5856f 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -106,7 +106,7 @@ def _convert_validate_version_0_3_3(publish_overrides): validate_version["plugin_state_profiles"] = [profile] -def _conver_publish_plugins(overrides): +def _convert_publish_plugins(overrides): if "publish" not in overrides: return _convert_validate_version_0_3_3(overrides["publish"]) @@ -118,5 +118,5 @@ def convert_settings_overrides( ) -> dict[str, Any]: _convert_imageio_configs_0_3_1(overrides) _convert_imageio_configs_0_4_5(overrides) - _conver_publish_plugins(overrides) + _convert_publish_plugins(overrides) return overrides From 2980f100400ca3951cd82c8fd6c70b346733e1fa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Sep 2024 16:06:24 +0200 Subject: [PATCH 51/85] Add client path to sys.path and run repository from code - Added client path to sys.path in conftest.py - Implemented function to run the repository from code in manage.ps1 --- tests/conftest.py | 9 +++++++++ tools/manage.ps1 | 11 +++++++++++ 2 files changed, 20 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000..a3c46a9dd7 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import sys +from pathlib import Path + +client_path = Path(__file__).resolve().parent.parent / "client" + +# add client path to sys.path +sys.path.append(str(client_path)) + +print(f"Added {client_path} to sys.path") diff --git a/tools/manage.ps1 b/tools/manage.ps1 index 23c52d57be..1fb57fe445 100755 --- a/tools/manage.ps1 +++ b/tools/manage.ps1 @@ -233,6 +233,13 @@ function Invoke-Codespell { & $Poetry $CodespellArgs } +function Run-From-Code { + $Poetry = "$RepoRoot\.poetry\bin\poetry.exe" + $RunArgs = @( "run") + + & $Poetry $RunArgs @arguments +} + function Write-Help { <# .SYNOPSIS @@ -248,6 +255,7 @@ function Write-Help { Write-Info -Text " ruff-check ", "Run Ruff check for the repository" -Color White, Cyan Write-Info -Text " ruff-fix ", "Run Ruff fix for the repository" -Color White, Cyan Write-Info -Text " codespell ", "Run codespell check for the repository" -Color White, Cyan + Write-Info -Text " run ", "Run the repository" -Color White, Cyan Write-Host "" } @@ -269,6 +277,9 @@ function Resolve-Function { } elseif ($FunctionName -eq "codespell") { Set-Cwd Invoke-CodeSpell + } elseif ($FunctionName -eq "run") { + Set-Cwd + Run-From-Code } else { Write-Host "Unknown function ""$FunctionName""" Write-Help From 7a83b8ec97d36ed489b3b7b1ac7e8013c287aa79 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 25 Sep 2024 13:06:28 -0400 Subject: [PATCH 52/85] Add test for tail handles only. --- .../plugins/publish/extract_otio_review.py | 4 +- .../resources/qt_handle_tail_review.json | 417 ++++++++++++++++++ .../editorial/test_extract_otio_review.py | 27 +- 3 files changed, 444 insertions(+), 4 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/qt_handle_tail_review.json diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 01cd974dad..dfc028a785 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -344,12 +344,12 @@ def _trim_available_range(self, avl_range, start, duration): # create gap data to disk self._render_seqment( gap=gap_duration.round().to_frames(), - end_offset=avl_duration.to_frames() + end_offset=duration.to_frames() ) # generate used frames self._generate_used_frames( gap_duration.round().to_frames(), - end_offset=avl_duration.to_frames() + end_offset=duration.to_frames() ) # return correct trimmed range diff --git a/tests/client/ayon_core/pipeline/editorial/resources/qt_handle_tail_review.json b/tests/client/ayon_core/pipeline/editorial/resources/qt_handle_tail_review.json new file mode 100644 index 0000000000..5d97628c47 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/qt_handle_tail_review.json @@ -0,0 +1,417 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "Resolve_OTIO": {} + }, + "name": "qt_no_tc_24fps.mov", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 66.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 35.0 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Transform", + "Enabled": true, + "Name": "Transform", + "Parameters": [], + "Type": 2 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Cropping", + "Enabled": true, + "Name": "Cropping", + "Parameters": [], + "Type": 3 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Dynamic Zoom", + "Enabled": false, + "Name": "Dynamic Zoom", + "Parameters": [ + { + "Default Parameter Value": [ + 0.0, + 0.0 + ], + "Key Frames": { + "-35": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + "965": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + } + }, + "Parameter ID": "dynamicZoomCenter", + "Parameter Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + { + "Default Parameter Value": 1.0, + "Key Frames": { + "-35": { + "Value": 0.8, + "Variant Type": "Double" + }, + "965": { + "Value": 1.0, + "Variant Type": "Double" + } + }, + "Parameter ID": "dynamicZoomScale", + "Parameter Value": 1.0, + "Variant Type": "Double", + "maxValue": 100.0, + "minValue": 0.01 + } + ], + "Type": 59 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Composite", + "Enabled": true, + "Name": "Composite", + "Parameters": [], + "Type": 1 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Lens Correction", + "Enabled": true, + "Name": "Lens Correction", + "Parameters": [], + "Type": 43 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Retime and Scaling", + "Enabled": true, + "Name": "Retime and Scaling", + "Parameters": [], + "Type": 22 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Video Faders", + "Enabled": true, + "Name": "Video Faders", + "Parameters": [], + "Type": 36 + } + }, + "name": "", + "effect_name": "Resolve Effect" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "Resolve_OTIO": { + "Keywords": [], + "Note": "{\"resolve_sub_products\": {\"io.ayon.creators.resolve.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.shot\", \"variant\": \"Main\", \"folderPath\": \"/shots/seq_native_otio_resolve/sh040\", \"task\": \"Generic\", \"clip_variant\": \"main\", \"clip_index\": \"1c8b84d2-4cf0-4528-9854-5c13a7ab64f7\", \"clip_source_resolution\": {\"width\": \"1920\", \"height\": \"1080\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_native_otio_resolve\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/seq_native_otio_resolve\", \"sourceResolution\": true, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"seq_native_otio_resolve\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_native_otio_resolve\", \"track\": \"Video1\", \"shot\": \"sh040\"}, \"heroTrack\": true, \"uuid\": \"6259d185-d57e-444f-b667-b5970a67a655\", \"reviewTrack\": \"Video1\", \"label\": \"/shots/seq_native_otio_resolve/sh040 shot\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"24c94533-8ae5-490c-98cf-cd3a27183d3e\", \"creator_attributes\": {\"folderPath\": \"/shots/seq_native_otio_resolve/sh040\", \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1067, \"clipIn\": 87088, \"clipOut\": 87154, \"clipDuration\": 66, \"sourceIn\": 35, \"sourceOut\": 101, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"platemain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.plate\", \"variant\": \"main\", \"folderPath\": \"/shots/seq_native_otio_resolve/sh040\", \"task\": \"Generic\", \"clip_variant\": \"main\", \"clip_index\": \"1c8b84d2-4cf0-4528-9854-5c13a7ab64f7\", \"clip_source_resolution\": {\"width\": \"1920\", \"height\": \"1080\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_native_otio_resolve\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/seq_native_otio_resolve\", \"sourceResolution\": true, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"seq_native_otio_resolve\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_native_otio_resolve\", \"track\": \"Video1\", \"shot\": \"sh040\"}, \"heroTrack\": true, \"uuid\": \"6259d185-d57e-444f-b667-b5970a67a655\", \"reviewTrack\": \"Video1\", \"parent_instance_id\": \"24c94533-8ae5-490c-98cf-cd3a27183d3e\", \"label\": \"/shots/seq_native_otio_resolve/sh040 plate\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"92adedc5-4e65-4a0a-9f09-e6522f2327d2\", \"creator_attributes\": {\"parentInstance\": \"/shots/seq_native_otio_resolve/sh040 shot\", \"vSyncOn\": true, \"vSyncTrack\": \"Video1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.audio\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"audio\", \"productName\": \"audioMain\", \"active\": false, \"creator_identifier\": \"io.ayon.creators.resolve.audio\", \"variant\": \"Main\", \"folderPath\": \"/shots/seq_native_otio_resolve/sh040\", \"task\": \"Generic\", \"clip_variant\": \"main\", \"clip_index\": \"1c8b84d2-4cf0-4528-9854-5c13a7ab64f7\", \"clip_source_resolution\": {\"width\": \"1920\", \"height\": \"1080\", \"pixelAspect\": 1.0}, \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_native_otio_resolve\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/seq_native_otio_resolve\", \"sourceResolution\": true, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"seq_native_otio_resolve\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"seq_native_otio_resolve\", \"track\": \"Video1\", \"shot\": \"sh040\"}, \"heroTrack\": true, \"uuid\": \"6259d185-d57e-444f-b667-b5970a67a655\", \"reviewTrack\": \"Video1\", \"parent_instance_id\": \"24c94533-8ae5-490c-98cf-cd3a27183d3e\", \"label\": \"/shots/seq_native_otio_resolve/sh040 audio\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"f22878b9-e9d2-415f-93f7-784474d2ff2f\", \"creator_attributes\": {\"parentInstance\": \"/shots/seq_native_otio_resolve/sh040 shot\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"1c8b84d2-4cf0-4528-9854-5c13a7ab64f7\", \"publish\": true}" + }, + "clip_index": "1c8b84d2-4cf0-4528-9854-5c13a7ab64f7", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.audio": { + "active": false, + "clip_index": "1c8b84d2-4cf0-4528-9854-5c13a7ab64f7", + "clip_source_resolution": { + "height": "1080", + "pixelAspect": 1.0, + "width": "1920" + }, + "clip_variant": "main", + "creator_attributes": { + "parentInstance": "/shots/seq_native_otio_resolve/sh040 shot" + }, + "creator_identifier": "io.ayon.creators.resolve.audio", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/seq_native_otio_resolve/sh040", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/seq_native_otio_resolve", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "seq_native_otio_resolve", + "shot": "sh040", + "track": "Video1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "f22878b9-e9d2-415f-93f7-784474d2ff2f", + "label": "/shots/seq_native_otio_resolve/sh040 audio", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "24c94533-8ae5-490c-98cf-cd3a27183d3e", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "seq_native_otio_resolve", + "folder_type": "sequence" + } + ], + "productName": "audioMain", + "productType": "audio", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video1", + "sequence": "seq_native_otio_resolve", + "shot": "sh###", + "sourceResolution": true, + "task": "Generic", + "track": "{_track_}", + "uuid": "6259d185-d57e-444f-b667-b5970a67a655", + "variant": "Main", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "1c8b84d2-4cf0-4528-9854-5c13a7ab64f7", + "clip_source_resolution": { + "height": "1080", + "pixelAspect": 1.0, + "width": "1920" + }, + "clip_variant": "main", + "creator_attributes": { + "parentInstance": "/shots/seq_native_otio_resolve/sh040 shot", + "vSyncOn": true, + "vSyncTrack": "Video1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/seq_native_otio_resolve/sh040", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/seq_native_otio_resolve", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "seq_native_otio_resolve", + "shot": "sh040", + "track": "Video1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "92adedc5-4e65-4a0a-9f09-e6522f2327d2", + "label": "/shots/seq_native_otio_resolve/sh040 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "24c94533-8ae5-490c-98cf-cd3a27183d3e", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "seq_native_otio_resolve", + "folder_type": "sequence" + } + ], + "productName": "platemain", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video1", + "sequence": "seq_native_otio_resolve", + "shot": "sh###", + "sourceResolution": true, + "task": "Generic", + "track": "{_track_}", + "uuid": "6259d185-d57e-444f-b667-b5970a67a655", + "variant": "main", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "1c8b84d2-4cf0-4528-9854-5c13a7ab64f7", + "clip_source_resolution": { + "height": "1080", + "pixelAspect": 1.0, + "width": "1920" + }, + "clip_variant": "main", + "creator_attributes": { + "clipDuration": 66, + "clipIn": 87088, + "clipOut": 87154, + "folderPath": "/shots/seq_native_otio_resolve/sh040", + "fps": "from_selection", + "frameEnd": 1067, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 35, + "sourceOut": 101, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "shots", + "folderPath": "/shots/seq_native_otio_resolve/sh040", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/seq_native_otio_resolve", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "seq_native_otio_resolve", + "shot": "sh040", + "track": "Video1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "24c94533-8ae5-490c-98cf-cd3a27183d3e", + "label": "/shots/seq_native_otio_resolve/sh040 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "seq_native_otio_resolve", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video1", + "sequence": "seq_native_otio_resolve", + "shot": "sh###", + "sourceResolution": true, + "task": "Generic", + "track": "{_track_}", + "uuid": "6259d185-d57e-444f-b667-b5970a67a655", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AyonData", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 68.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": {}, + "name": "qt_no_tc_24fps.mov", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 0.0 + } + }, + "available_image_bounds": null, + "target_url": "C:\\data\\qt_no_tc_24fps.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py index 3623f6129d..f266a40f50 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py +++ b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py @@ -158,9 +158,9 @@ def test_movie_with_embedded_tc_no_gap_handles(): assert calls == expected -def test_movie_tail_gap_handles(): +def test_short_movie_head_gap_handles(): """ - Qt movie clip (embedded timecode 1h/24fps) + Qt movie clip. available_range = 0-30822 25fps source_range = 0-50 24fps """ @@ -178,3 +178,26 @@ def test_movie_tail_gap_handles(): ] assert calls == expected + + +def test_short_movie_tail_gap_handles(): + """ + Qt movie clip. + available_range = 0-101 24fps + source_range = 35-101 24fps + """ + calls = run_process("qt_handle_tail_review.json") + + expected = [ + # 10 tail black frames generated from gap (1067-1076) + "/path/to/ffmpeg -t 0.4166666666666667 -r 24.0 -f lavfi -i " + "color=c=black:s=1280x720 -tune stillimage -start_number 1067 " + "C:/result/output.%03d.jpg", + + # 10 head frames + source range + # duration = 10fr (head handle) + 66fr (source) = 76fr = 3.16s + "/path/to/ffmpeg -ss 1.0416666666666667 -t 3.1666666666666665 -i " + "C:\\data\\qt_no_tc_24fps.mov -start_number 991 C:/result/output.%03d.jpg" + ] + + assert calls == expected From a10f0e5968068d4c747f52669c623fd5b8c86c65 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 25 Sep 2024 17:52:46 -0400 Subject: [PATCH 53/85] Implement linux counterpart to manage.sh --- tools/manage.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/manage.sh b/tools/manage.sh index 923953bf96..02648e3775 100755 --- a/tools/manage.sh +++ b/tools/manage.sh @@ -157,6 +157,7 @@ default_help() { echo -e " ${BWhite}ruff-check${RST} ${BCyan}Run Ruff check for the repository${RST}" echo -e " ${BWhite}ruff-fix${RST} ${BCyan}Run Ruff fix for the repository${RST}" echo -e " ${BWhite}codespell${RST} ${BCyan}Run codespell check for the repository${RST}" + echo -e " ${BWhite}run${RST} ${BCyan}Run the repository${RST}" echo "" } @@ -175,6 +176,12 @@ run_codespell () { "$POETRY_HOME/bin/poetry" run codespell } +run_command () { + echo -e "${BIGreen}>>>${RST} Running ..." + shift; # will remove first arg ("run") from the "$@" + "$POETRY_HOME/bin/poetry" run "$@" +} + main () { detect_python || return 1 @@ -207,6 +214,10 @@ main () { run_codespell || return_code=$? exit $return_code ;; + "run") + run_command "$@" || return_code=$? + exit $return_code + ;; esac if [ "$function_name" != "" ]; then From 7d9390e9d5e3167ccca5063a3231e8381d0feff5 Mon Sep 17 00:00:00 2001 From: ReeceMulley <153881471+ReeceMulley@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:34:31 +1000 Subject: [PATCH 54/85] improved OIIO subimages handling --- client/ayon_core/lib/transcoding.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index ead8b621b9..e9750864ac 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1152,9 +1152,7 @@ def convert_colorspace( input_arg, input_path, # Tell oiiotool which channels should be put to top stack # (and output) - "--ch", channels_arg, - # Use first subimage - "--subimage", "0" + "--ch", channels_arg ]) if all([target_colorspace, view, display]): @@ -1168,12 +1166,12 @@ def convert_colorspace( oiio_cmd.extend(additional_command_args) if target_colorspace: - oiio_cmd.extend(["--colorconvert", + oiio_cmd.extend(["--colorconvert:subimages=0", source_colorspace, target_colorspace]) if view and display: oiio_cmd.extend(["--iscolorspace", source_colorspace]) - oiio_cmd.extend(["--ociodisplay", display, view]) + oiio_cmd.extend(["--ociodisplay:subimages=0", display, view]) oiio_cmd.extend(["-o", output_path]) From beee0efab3593416d261c81451ae828d104f9c13 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 30 Sep 2024 09:59:01 -0400 Subject: [PATCH 55/85] Adjust feedback from PR. --- client/ayon_core/plugins/publish/extract_otio_review.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index dfc028a785..30c6dfd424 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -113,13 +113,16 @@ def process(self, instance): # get from media reference metadata source # TODO 'openpype' prefix should be removed (added 24/09/03) # NOTE it looks like it is set only in hiero integration - for variable, key in zip(variables, keys): + res_data = {"width": self.to_width, "height": self.to_height} + for key in res_data: for meta_prefix in ("ayon.source.", "openpype.source."): meta_key = f"{meta_prefix}.{key}" - if media_metadata.get(meta_key): - variable = media_metadata[meta_key] + value = media_metadata.get(meta_key) + if value is not None: + res_data[key] = value break + self.to_width, self.to_height = res_data["width"], res_data["height"] self.log.debug("> self.to_width x self.to_height: {} x {}".format( self.to_width, self.to_height )) From 3a75ebcc8935ae5fd0f0dd69044e9ea272296e59 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 30 Sep 2024 15:29:57 -0400 Subject: [PATCH 56/85] Adjust manage.sh and manage.ps1 run documentation. --- tools/manage.ps1 | 2 +- tools/manage.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/manage.ps1 b/tools/manage.ps1 index 1fb57fe445..9a9a9a2eff 100755 --- a/tools/manage.ps1 +++ b/tools/manage.ps1 @@ -255,7 +255,7 @@ function Write-Help { Write-Info -Text " ruff-check ", "Run Ruff check for the repository" -Color White, Cyan Write-Info -Text " ruff-fix ", "Run Ruff fix for the repository" -Color White, Cyan Write-Info -Text " codespell ", "Run codespell check for the repository" -Color White, Cyan - Write-Info -Text " run ", "Run the repository" -Color White, Cyan + Write-Info -Text " run ", "Run a poetry command in the repository environment" -Color White, Cyan Write-Host "" } diff --git a/tools/manage.sh b/tools/manage.sh index 02648e3775..6b0a4d6978 100755 --- a/tools/manage.sh +++ b/tools/manage.sh @@ -157,7 +157,7 @@ default_help() { echo -e " ${BWhite}ruff-check${RST} ${BCyan}Run Ruff check for the repository${RST}" echo -e " ${BWhite}ruff-fix${RST} ${BCyan}Run Ruff fix for the repository${RST}" echo -e " ${BWhite}codespell${RST} ${BCyan}Run codespell check for the repository${RST}" - echo -e " ${BWhite}run${RST} ${BCyan}Run the repository${RST}" + echo -e " ${BWhite}run${RST} ${BCyan}Run a poetry command in the repository environment${RST}" echo "" } From 93793655ea2e29ade756bbc66c4087280beeecd4 Mon Sep 17 00:00:00 2001 From: Robin De Lillo Date: Tue, 1 Oct 2024 08:00:43 -0400 Subject: [PATCH 57/85] Update client/ayon_core/plugins/publish/extract_otio_review.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/extract_otio_review.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 30c6dfd424..a7ef819c5d 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -107,9 +107,6 @@ def process(self, instance): otio_media = r_otio_cl.media_reference media_metadata = otio_media.metadata - variables = (self.to_width, self.to_height) - keys = ("width", "height") - # get from media reference metadata source # TODO 'openpype' prefix should be removed (added 24/09/03) # NOTE it looks like it is set only in hiero integration From d69edc69d5ce5201f9185402395374f8e58416ea Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 1 Oct 2024 17:10:41 -0400 Subject: [PATCH 58/85] Add backward-compatibility for relative source ranges. --- client/ayon_core/pipeline/editorial.py | 33 ++- .../resources/legacy_img_sequence.json | 216 ++++++++++++++++++ .../test_media_range_with_retimes.py | 20 ++ 3 files changed, 260 insertions(+), 9 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/legacy_img_sequence.json diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 2dc15bd645..8d81737533 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -178,6 +178,30 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): available_range = otio_clip.available_range() available_range_rate = available_range.start_time.rate + # If media source is an image sequence, returned + # mediaIn/mediaOut have to correspond + # to frame numbers from source sequence. + media_ref = otio_clip.media_reference + is_input_sequence = ( + hasattr(otio.schema, "ImageSequenceReference") + and isinstance(media_ref, otio.schema.ImageSequenceReference) + ) + + # Temporary. + # Some AYON custom OTIO exporter were implemented with relative + # source range for image sequence. Following code maintain + # backward-compatibility by adjusting available range + # while we are updating those. + if ( + is_input_sequence + and available_range.start_time.to_frames() == media_ref.start_frame + and source_range.start_time.to_frames() < media_ref.start_frame + ): + available_range = _ot.TimeRange( + _ot.RationalTime(0, rate=available_range_rate), + available_range.duration, + ) + # Conform source range bounds to available range rate # .e.g. embedded TC of (3600 sec/ 1h), duration 100 frames # @@ -272,15 +296,6 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): media_in = available_range.start_time.value media_out = available_range.end_time_inclusive().value - # If media source is an image sequence, returned - # mediaIn/mediaOut have to correspond - # to frame numbers from source sequence. - media_ref = otio_clip.media_reference - is_input_sequence = ( - hasattr(otio.schema, "ImageSequenceReference") - and isinstance(media_ref, otio.schema.ImageSequenceReference) - ) - if is_input_sequence: # preserve discrete frame numbers media_in_trimmed = otio.opentime.RationalTime.from_frames( diff --git a/tests/client/ayon_core/pipeline/editorial/resources/legacy_img_sequence.json b/tests/client/ayon_core/pipeline/editorial/resources/legacy_img_sequence.json new file mode 100644 index 0000000000..a50ca102fe --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/legacy_img_sequence.json @@ -0,0 +1,216 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output.[1000-1100].exr", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 104.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "clip_index": "18b19490-21ea-4533-9e0c-03f434c66f14", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "18b19490-21ea-4533-9e0c-03f434c66f14", + "clip_source_resolution": { + "height": "720", + "pixelAspect": 1.0, + "width": "956" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/sq_old_otio/sh010 shot", + "vSyncOn": true, + "vSyncTrack": "Video 1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "/shots/sq_old_otio/sh010", + "folderPath": "/shots/sq_old_otio/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq_old_otio", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq_old_otio", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "d27c5f77-7218-44ca-8061-5b6d33f96116", + "label": "/shots/sq_old_otio/sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "24792946-9ac4-4c8d-922f-80a83dea4be1", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq_old_otio", + "folder_type": "sequence" + } + ], + "productName": "plateVideo_1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video 1", + "sequence": "sq_old_otio", + "shot": "sh###", + "sourceResolution": false, + "task": null, + "track": "{_track_}", + "uuid": "dec1a40b-7ce8-43cd-94b8-08a53056a171", + "variant": "Video_1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "18b19490-21ea-4533-9e0c-03f434c66f14", + "clip_source_resolution": { + "height": "720", + "pixelAspect": 1.0, + "width": "956" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 104, + "clipIn": 90000, + "clipOut": 90104, + "fps": "from_selection", + "frameEnd": 1105, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 0, + "sourceOut": 104, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "/shots/sq_old_otio/sh010", + "folderPath": "/shots/sq_old_otio/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq_old_otio", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq_old_otio", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "24792946-9ac4-4c8d-922f-80a83dea4be1", + "label": "/shots/sq_old_otio/sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq_old_otio", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video 1", + "sequence": "sq_old_otio", + "shot": "sh###", + "sourceResolution": false, + "task": null, + "track": "{_track_}", + "uuid": "dec1a40b-7ce8-43cd-94b8-08a53056a171", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AYONData", + "color": "MINT", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 52.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "height": 720, + "isSequence": true, + "padding": 4, + "pixelAspect": 1.0, + "width": 956 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\legacy\\", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 1000, + "frame_step": 1, + "rate": 24.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index 270b01a799..e5f0d335b5 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -146,3 +146,23 @@ def test_img_sequence_with_embedded_tc_and_handles(): "img_seq_embedded_tc.json", expected_data ) + + +def test_img_sequence_relative_source_range(): + """ + Img sequence clip (embedded timecode 1h) + available files = 1000-1100 + source_range = fps + """ + expected_data = { + 'mediaIn': 1000, + 'mediaOut': 1098, + 'handleStart': 0, + 'handleEnd': 2, + 'speed': 1.0 + } + + _check_expected_retimed_values( + "legacy_img_sequence.json", + expected_data + ) From 8182914f112a2790437c986a09d54b08102ce6fb Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 1 Oct 2024 17:48:44 -0400 Subject: [PATCH 59/85] Fix linting. --- client/ayon_core/pipeline/editorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 8d81737533..f42c0a2fe5 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -190,7 +190,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # Temporary. # Some AYON custom OTIO exporter were implemented with relative # source range for image sequence. Following code maintain - # backward-compatibility by adjusting available range + # backward-compatibility by adjusting available range # while we are updating those. if ( is_input_sequence From 1a64490256063dc33fd9e9990329e5182b339838 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Oct 2024 00:26:39 +0200 Subject: [PATCH 60/85] Allow representation switch on update if representation does not exist (e.g. `ma` -> `mb` representation) --- client/ayon_core/pipeline/load/plugins.py | 20 +++++++++++ client/ayon_core/pipeline/load/utils.py | 43 ++++++++++++++++------- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 2475800cbb..28a7e775d6 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -242,6 +242,26 @@ def fname(self): if hasattr(self, "_fname"): return self._fname + def update_allowed_representation_switches(self): + """Return a mapping from source representation names to ordered + destination representation names to which switching is allowed if + the source representation name does not exist for the new version. + + For example, to allow an automated switch on update from representation + `ma` to `mb` or `abc` if the new version does not have a `ma` + representation you can return: + {"ma": ["mb", "abc"]} + + The order of the names in the returned values is important, because + if `ma` is missing and both of the replacement representations are + present than the first one will be chosen. + + Returns: + Dict[str, List[str]]: Mapping from representation names to allowed + alias representation names switching to is allowed on update. + """ + return {} + class ProductLoaderPlugin(LoaderPlugin): """Load product into host application diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 9ba407193e..bb74194ea1 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -505,28 +505,47 @@ def update_container(container, version=-1): project_name, product_entity["folderId"] ) + # Run update on the Loader for this container + Loader = _get_container_loader(container) + if not Loader: + raise LoaderNotFoundError( + "Can't update container because loader '{}' was not found." + .format(container.get("loader")) + ) + repre_name = current_representation["name"] new_representation = ayon_api.get_representation_by_name( project_name, repre_name, new_version["id"] ) if new_representation is None: - raise ValueError( - "Representation '{}' wasn't found on requested version".format( - repre_name + # The representation name is not found in the new version. + # Allow updating to a 'matching' representation if the loader + # has defined compatible update conversions + mapping = Loader.update_allowed_representation_switches() + switch_repre_names = mapping.get(repre_name) + if switch_repre_names: + representations = ayon_api.get_representations( + project_name, + representation_names=switch_repre_names, + version_ids=[new_version["id"]]) + representations_by_name = { + repre["name"]: repre for repre in representations + } + for name in switch_repre_names: + if name in representations_by_name: + new_representation = representations_by_name[name] + break + + if new_representation is None: + raise ValueError( + "Representation '{}' wasn't found on requested version".format( + repre_name + ) ) - ) path = get_representation_path(new_representation) if not path or not os.path.exists(path): raise ValueError("Path {} doesn't exist".format(path)) - - # Run update on the Loader for this container - Loader = _get_container_loader(container) - if not Loader: - raise LoaderNotFoundError( - "Can't update container because loader '{}' was not found." - .format(container.get("loader")) - ) project_entity = ayon_api.get_project(project_name) context = { "project": project_entity, From 1dbaa57e0926fabc6afa9b85a5ae3e7856dccb07 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Oct 2024 00:39:04 +0200 Subject: [PATCH 61/85] Fix call to method --- client/ayon_core/pipeline/load/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index bb74194ea1..b258d20a3d 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -521,7 +521,7 @@ def update_container(container, version=-1): # The representation name is not found in the new version. # Allow updating to a 'matching' representation if the loader # has defined compatible update conversions - mapping = Loader.update_allowed_representation_switches() + mapping = Loader().update_allowed_representation_switches() switch_repre_names = mapping.get(repre_name) if switch_repre_names: representations = ayon_api.get_representations( From 09b0eb9ed2e0150d9121b1e268254e8c185404c2 Mon Sep 17 00:00:00 2001 From: ReeceMulley <153881471+ReeceMulley@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:13:57 +1000 Subject: [PATCH 62/85] taskType bugfix --- .../ayon_core/tools/push_to_project/models/integrate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 5937ffa4da..ba603699bc 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -777,7 +777,7 @@ def _fill_or_create_destination_folder(self): task_info = copy.deepcopy(task_info) task_info["name"] = dst_task_name # Fill rest of task information based on task type - task_type_name = task_info["type"] + task_type_name = task_info["taskType"] task_types_by_name = { task_type["name"]: task_type for task_type in self._project_entity["taskTypes"] @@ -821,7 +821,7 @@ def _determine_product_name(self): task_name = task_type = None if task_info: task_name = task_info["name"] - task_type = task_info["type"] + task_type = task_info["taskType"] product_name = get_product_name( self._item.dst_project_name, @@ -905,7 +905,7 @@ def _make_sure_version_exists(self): project_name, self.host_name, task_name=self._task_info["name"], - task_type=self._task_info["type"], + task_type=self._task_info["taskType"], product_type=product_type, product_name=product_entity["name"] ) @@ -959,7 +959,7 @@ def _real_integrate_representations(self): formatting_data = get_template_data( self._project_entity, self._folder_entity, - self._task_info.get("name"), + self._task_info, self.host_name ) formatting_data.update({ From e299b405fc01fc6d5daac5d40a6da1b3955853c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 2 Oct 2024 10:01:33 +0200 Subject: [PATCH 63/85] Update server/settings/conversion.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/conversion.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index b933d5856f..aabf41f8d3 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -14,30 +14,19 @@ def _convert_imageio_configs_0_4_5(overrides): ocio_config_profiles = imageio_overrides["ocio_config_profiles"] - for inx, profile in enumerate(ocio_config_profiles): - if profile["type"] != "product_name": + for profile in ocio_config_profiles: + if profile.get("type") != "product_name": continue - - # create new profile - new_profile = { - "type": "published_product", - "published_product": { - "product_name": profile["product_name"], - "fallback": { - "type": "builtin_path", - "builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio", - }, + + profile["type"] = "published_product" + profile["published_product"] = { + "product_name": profile.pop("product_name"), + "fallback": { + "type": "builtin_path", + "builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio", }, - "host_names": profile["host_names"], - "task_names": profile["task_names"], - "task_types": profile["task_types"], - "custom_path": profile["custom_path"], - "builtin_path": profile["builtin_path"], } - # replace old profile with new profile - ocio_config_profiles[inx] = new_profile - def _convert_imageio_configs_0_3_1(overrides): """Imageio config settings did change to profiles since 0.3.1. .""" From 8b14e79629058d4c44f2ecc89f1262bb0a157825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 2 Oct 2024 10:03:28 +0200 Subject: [PATCH 64/85] Update server/settings/conversion.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/conversion.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index aabf41f8d3..315f5a2027 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -9,11 +9,10 @@ def _convert_imageio_configs_0_4_5(overrides): imageio_overrides = overrides.get("imageio") or {} # make sure settings are already converted to profiles - if "ocio_config_profiles" not in imageio_overrides: + ocio_config_profiles = imageio_overrides.get("ocio_config_profiles") + if not ocio_config_profiles: return - ocio_config_profiles = imageio_overrides["ocio_config_profiles"] - for profile in ocio_config_profiles: if profile.get("type") != "product_name": continue From 589a642d69e85d4a9b9b6364b57cfa3a4d20624b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Oct 2024 10:10:57 +0200 Subject: [PATCH 65/85] Merge branch 'develop' into enhancement/AY-6198_OCIO-fallback-for-profiles-and-templated-values --- .github/workflows/release_trigger.yml | 12 +++ client/ayon_core/hooks/pre_ocio_hook.py | 3 +- client/ayon_core/lib/path_tools.py | 5 +- client/ayon_core/pipeline/__init__.py | 2 - client/ayon_core/pipeline/constants.py | 17 ---- client/ayon_core/plugins/load/delivery.py | 18 +++- .../publish/extract_color_transcode.py | 19 +++- client/ayon_core/plugins/publish/integrate.py | 7 +- .../plugins/publish/validate_file_saved.py | 3 +- client/ayon_core/tools/publisher/window.py | 5 +- server/settings/conversion.py | 35 ++++++- server/settings/publish_plugins.py | 92 ++++++++++++++++--- 12 files changed, 172 insertions(+), 46 deletions(-) create mode 100644 .github/workflows/release_trigger.yml diff --git a/.github/workflows/release_trigger.yml b/.github/workflows/release_trigger.yml new file mode 100644 index 0000000000..01a3b3a682 --- /dev/null +++ b/.github/workflows/release_trigger.yml @@ -0,0 +1,12 @@ +name: 🚀 Release Trigger + +on: + workflow_dispatch: + +jobs: + call-release-trigger: + uses: ynput/ops-repo-automation/.github/workflows/release_trigger.yml@main + secrets: + token: ${{ secrets.YNPUT_BOT_TOKEN }} + email: ${{ secrets.CI_EMAIL }} + user: ${{ secrets.CI_USER }} diff --git a/client/ayon_core/hooks/pre_ocio_hook.py b/client/ayon_core/hooks/pre_ocio_hook.py index 6c30b267bc..7406aa42cf 100644 --- a/client/ayon_core/hooks/pre_ocio_hook.py +++ b/client/ayon_core/hooks/pre_ocio_hook.py @@ -19,7 +19,8 @@ class OCIOEnvHook(PreLaunchHook): "nuke", "hiero", "resolve", - "openrv" + "openrv", + "cinema4d" } launch_types = set() diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index a65f0f8e13..5c81fbfebf 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -81,7 +81,10 @@ def collect_frames(files): dict: {'/folder/product_v001.0001.png': '0001', ....} """ - patterns = [clique.PATTERNS["frames"]] + # clique.PATTERNS["frames"] supports only `.1001.exr` not `_1001.exr` so + # we use a customized pattern. + pattern = "[_.](?P(?P0*)\\d+)\\.\\D+\\d?$" + patterns = [pattern] collections, remainder = clique.assemble( files, minimum_items=1, patterns=patterns) diff --git a/client/ayon_core/pipeline/__init__.py b/client/ayon_core/pipeline/__init__.py index 4fcea60d5e..8e89029e7b 100644 --- a/client/ayon_core/pipeline/__init__.py +++ b/client/ayon_core/pipeline/__init__.py @@ -3,7 +3,6 @@ AVALON_INSTANCE_ID, AYON_CONTAINER_ID, AYON_INSTANCE_ID, - HOST_WORKFILE_EXTENSIONS, ) from .anatomy import Anatomy @@ -114,7 +113,6 @@ "AVALON_INSTANCE_ID", "AYON_CONTAINER_ID", "AYON_INSTANCE_ID", - "HOST_WORKFILE_EXTENSIONS", # --- Anatomy --- "Anatomy", diff --git a/client/ayon_core/pipeline/constants.py b/client/ayon_core/pipeline/constants.py index 7a08cbb3aa..e6156b3138 100644 --- a/client/ayon_core/pipeline/constants.py +++ b/client/ayon_core/pipeline/constants.py @@ -4,20 +4,3 @@ # Backwards compatibility AVALON_CONTAINER_ID = "pyblish.avalon.container" AVALON_INSTANCE_ID = "pyblish.avalon.instance" - -# TODO get extensions from host implementations -HOST_WORKFILE_EXTENSIONS = { - "blender": [".blend"], - "celaction": [".scn"], - "tvpaint": [".tvpp"], - "fusion": [".comp"], - "harmony": [".zip"], - "houdini": [".hip", ".hiplc", ".hipnc"], - "maya": [".ma", ".mb"], - "nuke": [".nk"], - "hiero": [".hrox"], - "photoshop": [".psd", ".psb"], - "premiere": [".prproj"], - "resolve": [".drp"], - "aftereffects": [".aep"] -} diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index 6a0947cc42..5c53d170eb 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -230,6 +230,11 @@ def deliver(self): self.log ] + # TODO: This will currently incorrectly detect 'resources' + # that are published along with the publish, because those should + # not adhere to the template directly but are ingested in a + # customized way. For example, maya look textures or any publish + # that directly adds files into `instance.data["transfers"]` src_paths = [] for repre_file in repre["files"]: src_path = self.anatomy.fill_root(repre_file["path"]) @@ -261,7 +266,18 @@ def deliver(self): frame = dst_frame if frame is not None: - anatomy_data["frame"] = frame + if repre["context"].get("frame"): + anatomy_data["frame"] = frame + elif repre["context"].get("udim"): + anatomy_data["udim"] = frame + else: + # Fallback + self.log.warning( + "Representation context has no frame or udim" + " data. Supplying sequence frame to '{frame}'" + " formatting data." + ) + anatomy_data["frame"] = frame new_report_items, uploaded = deliver_single_file(*args) report_items.update(new_report_items) self._update_progress(uploaded) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index a28a761e7e..3e54d324e3 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -122,13 +122,22 @@ def process(self, instance): transcoding_type = output_def["transcoding_type"] target_colorspace = view = display = None + # NOTE: we use colorspace_data as the fallback values for + # the target colorspace. if transcoding_type == "colorspace": + # TODO: Should we fallback to the colorspace + # (which used as source above) ? + # or should we compute the target colorspace from + # current view and display ? target_colorspace = (output_def["colorspace"] or colorspace_data.get("colorspace")) - else: - view = output_def["view"] or colorspace_data.get("view") - display = (output_def["display"] or - colorspace_data.get("display")) + elif transcoding_type == "display_view": + display_view = output_def["display_view"] + view = display_view["view"] or colorspace_data.get("view") + display = ( + display_view["display"] + or colorspace_data.get("display") + ) # both could be already collected by DCC, # but could be overwritten when transcoding @@ -192,7 +201,7 @@ def process(self, instance): new_repre["files"] = new_repre["files"][0] # If the source representation has "review" tag, but its not - # part of the output defintion tags, then both the + # 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", []): diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index d3f6c04333..e8fe09bab7 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -509,8 +509,11 @@ def _validate_repre_files(self, files, is_sequence_representation): if not is_sequence_representation: files = [files] - if any(os.path.isabs(fname) for fname in files): - raise KnownPublishError("Given file names contain full paths") + for fname in files: + if os.path.isabs(fname): + raise KnownPublishError( + f"Representation file names contains full paths: {fname}" + ) if not is_sequence_representation: return diff --git a/client/ayon_core/plugins/publish/validate_file_saved.py b/client/ayon_core/plugins/publish/validate_file_saved.py index d132ba8d3a..f52998cef3 100644 --- a/client/ayon_core/plugins/publish/validate_file_saved.py +++ b/client/ayon_core/plugins/publish/validate_file_saved.py @@ -36,7 +36,8 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin): label = "Validate File Saved" order = pyblish.api.ValidatorOrder - 0.1 - hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter"] + hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter", + "cinema4d"] actions = [SaveByVersionUpAction, ShowWorkfilesAction] def process(self, context): diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index a8ca605ecb..434c2ca602 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -439,10 +439,13 @@ def set_comment(self, comment): def make_sure_is_visible(self): if self._window_is_visible: self.setWindowState(QtCore.Qt.WindowActive) - else: self.show() + self.raise_() + self.activateWindow() + self.showNormal() + def showEvent(self, event): self._window_is_visible = True super().showEvent(event) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index 315f5a2027..34820b5b32 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -16,7 +16,7 @@ def _convert_imageio_configs_0_4_5(overrides): for profile in ocio_config_profiles: if profile.get("type") != "product_name": continue - + profile["type"] = "published_product" profile["published_product"] = { "product_name": profile.pop("product_name"), @@ -94,10 +94,43 @@ def _convert_validate_version_0_3_3(publish_overrides): validate_version["plugin_state_profiles"] = [profile] +def _convert_oiio_transcode_0_4_5(publish_overrides): + """ExtractOIIOTranscode plugin changed in 0.4.5.""" + if "ExtractOIIOTranscode" not in publish_overrides: + return + + transcode_profiles = publish_overrides["ExtractOIIOTranscode"].get( + "profiles") + if not transcode_profiles: + return + + for profile in transcode_profiles: + outputs = profile.get("outputs") + if outputs is None: + return + + for output in outputs: + # Already new settings + if "display_view" in output: + break + + # Fix 'display' -> 'display_view' in 'transcoding_type' + transcode_type = output.get("transcoding_type") + if transcode_type == "display": + output["transcoding_type"] = "display_view" + + # Convert 'display' and 'view' to new values + output["display_view"] = { + "display": output.pop("display", ""), + "view": output.pop("view", ""), + } + + def _convert_publish_plugins(overrides): if "publish" not in overrides: return _convert_validate_version_0_3_3(overrides["publish"]) + _convert_oiio_transcode_0_4_5(overrides["publish"]) def convert_settings_overrides( diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 61972e64c4..cdcd28a9ce 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -268,13 +268,36 @@ class ExtractThumbnailModel(BaseSettingsModel): def _extract_oiio_transcoding_type(): return [ {"value": "colorspace", "label": "Use Colorspace"}, - {"value": "display", "label": "Use Display&View"} + {"value": "display_view", "label": "Use Display&View"} ] class OIIOToolArgumentsModel(BaseSettingsModel): additional_command_args: list[str] = SettingsField( - default_factory=list, title="Arguments") + default_factory=list, + title="Arguments", + description="Additional command line arguments for *oiiotool*." + ) + + +class UseDisplayViewModel(BaseSettingsModel): + _layout = "expanded" + display: str = SettingsField( + "", + title="Target Display", + description=( + "Display of the target transform. If left empty, the" + " source Display value will be used." + ) + ) + view: str = SettingsField( + "", + title="Target View", + description=( + "View of the target transform. If left empty, the" + " source View value will be used." + ) + ) class ExtractOIIOTranscodeOutputModel(BaseSettingsModel): @@ -285,22 +308,57 @@ class ExtractOIIOTranscodeOutputModel(BaseSettingsModel): description="Output name (no space)", regex=r"[a-zA-Z0-9_]([a-zA-Z0-9_\.\-]*[a-zA-Z0-9_])?$", ) - extension: str = SettingsField("", title="Extension") + extension: str = SettingsField( + "", + title="Extension", + description=( + "Target extension. If left empty, original" + " extension is used." + ), + ) transcoding_type: str = SettingsField( "colorspace", title="Transcoding type", - enum_resolver=_extract_oiio_transcoding_type + enum_resolver=_extract_oiio_transcoding_type, + conditionalEnum=True, + description=( + "Select the transcoding type for your output, choosing either " + "*Colorspace* or *Display&View* transform." + " Only one option can be applied per output definition." + ), + ) + colorspace: str = SettingsField( + "", + title="Target Colorspace", + description=( + "Choose the desired target colorspace, confirming its availability" + " in the active OCIO config. If left empty, the" + " source colorspace value will be used, resulting in no" + " colorspace conversion." + ) + ) + display_view: UseDisplayViewModel = SettingsField( + title="Use Display&View", + default_factory=UseDisplayViewModel ) - colorspace: str = SettingsField("", title="Colorspace") - display: str = SettingsField("", title="Display") - view: str = SettingsField("", title="View") + oiiotool_args: OIIOToolArgumentsModel = SettingsField( default_factory=OIIOToolArgumentsModel, title="OIIOtool arguments") - tags: list[str] = SettingsField(default_factory=list, title="Tags") + 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" + default_factory=list, + title="Custom Tags", + description="Additional custom tags that will be added to the created representation." ) @@ -328,7 +386,13 @@ class ExtractOIIOTranscodeProfileModel(BaseSettingsModel): ) delete_original: bool = SettingsField( True, - title="Delete Original Representation" + 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[ExtractOIIOTranscodeOutputModel] = SettingsField( default_factory=list, @@ -371,7 +435,7 @@ class ExtractReviewFFmpegModel(BaseSettingsModel): def extract_review_filter_enum(): return [ { - "value": "everytime", + "value": "everytime", # codespell:ignore everytime "label": "Always" }, { @@ -393,7 +457,7 @@ class ExtractReviewFilterModel(BaseSettingsModel): default_factory=list, title="Custom Tags" ) single_frame_filter: str = SettingsField( - "everytime", + "everytime", # codespell:ignore everytime description=( "Use output always / only if input is 1 frame" " image / only if has 2+ frames or is video" @@ -791,7 +855,7 @@ class IntegrateHeroVersionModel(BaseSettingsModel): class CleanUpModel(BaseSettingsModel): _isGroup = True - paterns: list[str] = SettingsField( + paterns: list[str] = SettingsField( # codespell:ignore paterns default_factory=list, title="Patterns (regex)" ) @@ -1225,7 +1289,7 @@ class PublishPuginsModel(BaseSettingsModel): "use_hardlinks": False }, "CleanUp": { - "paterns": [], + "paterns": [], # codespell:ignore paterns "remove_temp_renders": False }, "CleanUpFarm": { From 0b9cea926cf8d061128321708d10f395453cf18c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Oct 2024 10:12:51 +0200 Subject: [PATCH 66/85] Refactor code for better readability and consistency. - Refactored code to improve readability by adjusting indentation and line breaks. - Made changes to ensure consistent formatting of the code. --- server/settings/conversion.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index 04ab0f8d81..34820b5b32 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -99,7 +99,8 @@ def _convert_oiio_transcode_0_4_5(publish_overrides): if "ExtractOIIOTranscode" not in publish_overrides: return - transcode_profiles = publish_overrides["ExtractOIIOTranscode"].get("profiles") + transcode_profiles = publish_overrides["ExtractOIIOTranscode"].get( + "profiles") if not transcode_profiles: return @@ -108,7 +109,7 @@ def _convert_oiio_transcode_0_4_5(publish_overrides): if outputs is None: return - for output in outputs : + for output in outputs: # Already new settings if "display_view" in output: break From 7298ddb745e003ca8e317eae3baccab246a2ad37 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 2 Oct 2024 12:27:32 +0300 Subject: [PATCH 67/85] add note about using more accurate variable names --- .../ayon_core/pipeline/workfile/workfile_template_builder.py | 4 +++- 1 file changed, 3 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 17debdb2e8..4412e4489b 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -521,6 +521,9 @@ def build_template( scene state. """ + # More accurate variable name + # - logic related to workfile creation should be moved out in future + explicit_build_requested = not workfile_creation_enabled # Get default values if not provided if ( @@ -538,7 +541,6 @@ def build_template( # Build the template if we are explicitly requesting it or if it's # an unsaved "new file". is_new_file = not self.host.get_current_workfile() - explicit_build_requested = not workfile_creation_enabled if is_new_file or explicit_build_requested: self.log.info(f"Building the workfile template: {template_path}") self.import_template(template_path) From 05291b2fe9b46e513c0d8bdae6640e4a36ddb8a8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Oct 2024 11:51:21 +0200 Subject: [PATCH 68/85] ruff suggestions --- .../plugins/publish/extract_otio_review.py | 61 ++++++++++--------- .../editorial/test_extract_otio_review.py | 28 +++++---- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 2a0b51b123..55eff782d9 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -57,13 +57,12 @@ def process(self, instance): # Not all hosts can import these modules. import opentimelineio as otio from ayon_core.pipeline.editorial import ( - otio_range_to_frame_range, make_sequence_collection, remap_range_on_file_sequence, is_clip_from_media_sequence ) - # TODO refactore from using instance variable + # TODO refactor from using instance variable self.temp_file_head = self._get_folder_name_based_prefix(instance) # TODO: convert resulting image sequence to mp4 @@ -75,8 +74,8 @@ def process(self, instance): otio_review_clips = instance.data["otioReviewClips"] # add plugin wide attributes - self.representation_files = list() - self.used_frames = list() + self.representation_files = [] + self.used_frames = [] self.workfile_start = int(instance.data.get( "workfileFrameStart", 1001)) - handle_start self.padding = len(str(self.workfile_start)) @@ -101,9 +100,7 @@ def process(self, instance): for index, r_otio_cl in enumerate(otio_review_clips): # QUESTION: what if transition on clip? - # check if resolution is the same - width = self.to_width - height = self.to_height + # check if resolution is the same as source otio_media = r_otio_cl.media_reference media_metadata = otio_media.metadata @@ -151,7 +148,7 @@ def process(self, instance): # Gap: no media, generate range based on source range else: - available_range = processing_range = None + available_range = processing_range = None self.actual_fps = src_range.duration.rate start = src_range.start_time duration = src_range.duration @@ -216,7 +213,7 @@ def process(self, instance): collection.indexes.update( [i for i in range(first, (last + 1))]) # render segment - self._render_seqment( + self._render_segment( sequence=[dirname, collection, input_fps]) # generate used frames self._generate_used_frames( @@ -230,7 +227,7 @@ def process(self, instance): dir_path, collection = collection_data # render segment - self._render_seqment( + self._render_segment( sequence=[dir_path, collection, input_fps]) # generate used frames self._generate_used_frames( @@ -252,7 +249,7 @@ def process(self, instance): duration=processing_range.duration, ) # render video file to sequence - self._render_seqment( + self._render_segment( video=[path, extract_range]) # generate used frames self._generate_used_frames( @@ -261,7 +258,7 @@ def process(self, instance): # QUESTION: what if nested track composition is in place? else: # at last process a Gap - self._render_seqment(gap=duration) + self._render_segment(gap=duration) # generate used frames self._generate_used_frames(duration) @@ -334,7 +331,6 @@ def _trim_available_range(self, avl_range, start, duration): ) avl_start = avl_range.start_time - avl_duration = avl_range.duration # An additional gap is required before the available # range to conform source start point and head handles. @@ -344,7 +340,7 @@ def _trim_available_range(self, avl_range, start, duration): duration -= gap_duration # create gap data to disk - self._render_seqment(gap=gap_duration.round().to_frames()) + self._render_segment(gap=gap_duration.round().to_frames()) # generate used frames self._generate_used_frames(gap_duration.round().to_frames()) @@ -358,7 +354,7 @@ def _trim_available_range(self, avl_range, start, duration): duration -= gap_duration # create gap data to disk - self._render_seqment( + self._render_segment( gap=gap_duration.round().to_frames(), end_offset=duration.to_frames() ) @@ -377,10 +373,10 @@ def _trim_available_range(self, avl_range, start, duration): ) ) - def _render_seqment(self, sequence=None, + def _render_segment(self, sequence=None, video=None, gap=None, end_offset=None): """ - Render seqment into image sequence frames. + Render segment into image sequence frames. Using ffmpeg to convert compatible video and image source to defined image sequence format. @@ -416,18 +412,24 @@ def _render_seqment(self, sequence=None, input_path = os.path.join(input_dir, input_file) input_extension = os.path.splitext(input_path)[-1] - # form command for rendering sequence files - # (need to explicit set the input frame range - # if case input sequence has framerate metadata - # to preserve frame range and avoid silent dropped - # frames caused by input mismatch with FFmpeg default - # rate 25.0 fps) more info refer to FFmpeg image2 demuxer - # - # Implicit - # [Input 100 frames (24fps from metadata)] -> [Demuxer video 25fps] -> [Output 98 frames, dropped 2] - # - # Explicit with "-framerate" - # [Input 100 frames (24fps from metadata)] -> [Demuxer video 24fps] -> [Output 100 frames] + """ + Form Command for Rendering Sequence Files + + To explicitly set the input frame range and preserve the frame + range, avoid silent dropped frames caused by input mismatch + with FFmpeg's default rate of 25.0 fps. For more info, + refer to the FFmpeg image2 demuxer. + + Implicit: + - Input: 100 frames (24fps from metadata) + - Demuxer: video 25fps + - Output: 98 frames, dropped 2 + + Explicit with "-framerate": + - Input: 100 frames (24fps from metadata) + - Demuxer: video 24fps + - Output: 100 frames, no dropped frames + """ command.extend([ "-start_number", str(in_frame_start), @@ -566,4 +568,3 @@ def _get_folder_name_based_prefix(self, instance): self.log.debug(f"file_prefix::{file_prefix}") return file_prefix - diff --git a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py index f266a40f50..0a38301755 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py +++ b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py @@ -1,11 +1,10 @@ import mock import os -import pytest +import pytest # noqa from typing import NamedTuple import opentimelineio as otio -import ayon_core.lib from ayon_core.plugins.publish import extract_otio_review @@ -19,7 +18,7 @@ class MockInstance(): """ Mock pyblish instance for testing purpose. """ def __init__(self, data: dict): - self.data = data + self.data = data self.context = self @@ -34,7 +33,7 @@ def append_call(self, *args, **kwargs): self.calls.append(" ".join(ffmpeg_args_list)) return True - def get_fmpeg_executable(self, _): + def get_ffmpeg_executable(self, _): return ["/path/to/ffmpeg"] @@ -48,20 +47,23 @@ def run_process(file_name: str): # Prepare dummy instance and capture call object capture_call = CaptureFFmpegCalls() processor = extract_otio_review.ExtractOTIOReview() - instance = MockInstance({ - "otioReviewClips": [clip], - "handleStart": 10, - "handleEnd": 10, - "workfileFrameStart": 1001, - "folderPath": "/dummy/path", - "anatomy": NamedTuple("Anatomy", [('project_name', "test_project")]) - }) + Anatomy = NamedTuple("Anatomy", [("project_name")]) + instance = MockInstance( + { + "otioReviewClips": [clip], + "handleStart": 10, + "handleEnd": 10, + "workfileFrameStart": 1001, + "folderPath": "/dummy/path", + "anatomy": Anatomy("test_project"), + } + ) # Mock calls to extern and run plugins. with mock.patch.object( extract_otio_review, "get_ffmpeg_tool_args", - side_effect=capture_call.get_fmpeg_executable, + side_effect=capture_call.get_ffmpeg_executable, ): with mock.patch.object( extract_otio_review, From 7bd382a1874eb22b822790a6461e39d1399956d3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Oct 2024 11:57:38 +0200 Subject: [PATCH 69/85] Refactor Anatomy NamedTuple for project_name\nUpdate Anatomy NamedTuple to specify project_name as string type. This change ensures consistency and clarity in the codebase. --- .../ayon_core/pipeline/editorial/test_extract_otio_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py index 0a38301755..7bc1a750d7 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py +++ b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py @@ -47,7 +47,7 @@ def run_process(file_name: str): # Prepare dummy instance and capture call object capture_call = CaptureFFmpegCalls() processor = extract_otio_review.ExtractOTIOReview() - Anatomy = NamedTuple("Anatomy", [("project_name")]) + Anatomy = NamedTuple("Anatomy", project_name=str) instance = MockInstance( { "otioReviewClips": [clip], From db41e53511870112df72631ad5123566ca04e943 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:40:01 +0200 Subject: [PATCH 70/85] updated create package script --- create_package.py | 490 +++++++++++++++++++++++++++++----------------- 1 file changed, 314 insertions(+), 176 deletions(-) diff --git a/create_package.py b/create_package.py index 48952c43c5..843e993de1 100644 --- a/create_package.py +++ b/create_package.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """Prepares server package from addon repo to upload to server. Requires Python 3.9. (Or at least 3.8+). @@ -22,32 +24,39 @@ import os import sys import re +import io import shutil -import argparse import platform +import argparse import logging import collections import zipfile -import hashlib - -from typing import Optional - -CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) -PACKAGE_PATH = os.path.join(CURRENT_DIR, "package.py") -package_content = {} -with open(PACKAGE_PATH, "r") as stream: - exec(stream.read(), package_content) - -ADDON_VERSION = package_content["version"] -ADDON_NAME = package_content["name"] -ADDON_CLIENT_DIR = package_content["client_dir"] -CLIENT_VERSION_CONTENT = '''# -*- coding: utf-8 -*- -"""Package declaring AYON core addon version.""" -__version__ = "{}" +import subprocess +from typing import Optional, Iterable, Pattern, Union, List, Tuple + +import package + +FileMapping = Tuple[Union[str, io.BytesIO], str] +ADDON_NAME: str = package.name +ADDON_VERSION: str = package.version +ADDON_CLIENT_DIR: Union[str, None] = getattr(package, "client_dir", None) + +CURRENT_ROOT: str = os.path.dirname(os.path.abspath(__file__)) +SERVER_ROOT: str = os.path.join(CURRENT_ROOT, "server") +FRONTEND_ROOT: str = os.path.join(CURRENT_ROOT, "frontend") +FRONTEND_DIST_ROOT: str = os.path.join(FRONTEND_ROOT, "dist") +DST_DIST_DIR: str = os.path.join("frontend", "dist") +PRIVATE_ROOT: str = os.path.join(CURRENT_ROOT, "private") +PUBLIC_ROOT: str = os.path.join(CURRENT_ROOT, "public") +CLIENT_ROOT: str = os.path.join(CURRENT_ROOT, "client") + +VERSION_PY_CONTENT = f'''# -*- coding: utf-8 -*- +"""Package declaring AYON addon '{ADDON_NAME}' version.""" +__version__ = "{ADDON_VERSION}" ''' # Patterns of directories to be skipped for server part of addon -IGNORE_DIR_PATTERNS = [ +IGNORE_DIR_PATTERNS: List[Pattern] = [ re.compile(pattern) for pattern in { # Skip directories starting with '.' @@ -58,7 +67,7 @@ ] # Patterns of files to be skipped for server part of addon -IGNORE_FILE_PATTERNS = [ +IGNORE_FILE_PATTERNS: List[Pattern] = [ re.compile(pattern) for pattern in { # Skip files starting with '.' @@ -70,15 +79,6 @@ ] -def calculate_file_checksum(filepath, hash_algorithm, chunk_size=10000): - func = getattr(hashlib, hash_algorithm) - hash_obj = func() - with open(filepath, "rb") as f: - for chunk in iter(lambda: f.read(chunk_size), b""): - hash_obj.update(chunk) - return hash_obj.hexdigest() - - class ZipFileLongPaths(zipfile.ZipFile): """Allows longer paths in zip files. @@ -97,12 +97,28 @@ def _extract_member(self, member, tpath, pwd): else: tpath = "\\\\?\\" + tpath - return super(ZipFileLongPaths, self)._extract_member( - member, tpath, pwd - ) + return super()._extract_member(member, tpath, pwd) -def safe_copy_file(src_path, dst_path): +def _get_yarn_executable() -> Union[str, None]: + cmd = "which" + if platform.system().lower() == "windows": + cmd = "where" + + for line in subprocess.check_output( + [cmd, "yarn"], encoding="utf-8" + ).splitlines(): + if not line or not os.path.exists(line): + continue + try: + subprocess.call([line, "--version"]) + return line + except OSError: + continue + return None + + +def safe_copy_file(src_path: str, dst_path: str): """Copy file and make sure destination directory exists. Ignore if destination already contains directories from source. @@ -115,210 +131,335 @@ def safe_copy_file(src_path, dst_path): if src_path == dst_path: return - dst_dir = os.path.dirname(dst_path) - try: - os.makedirs(dst_dir) - except Exception: - pass + dst_dir: str = os.path.dirname(dst_path) + os.makedirs(dst_dir, exist_ok=True) shutil.copy2(src_path, dst_path) -def _value_match_regexes(value, regexes): - for regex in regexes: - if regex.search(value): - return True - return False +def _value_match_regexes(value: str, regexes: Iterable[Pattern]) -> bool: + return any( + regex.search(value) + for regex in regexes + ) def find_files_in_subdir( - src_path, - ignore_file_patterns=None, - ignore_dir_patterns=None -): + src_path: str, + ignore_file_patterns: Optional[List[Pattern]] = None, + ignore_dir_patterns: Optional[List[Pattern]] = None +) -> List[Tuple[str, str]]: + """Find all files to copy in subdirectories of given path. + + All files that match any of the patterns in 'ignore_file_patterns' will + be skipped and any directories that match any of the patterns in + 'ignore_dir_patterns' will be skipped with all subfiles. + + Args: + src_path (str): Path to directory to search in. + ignore_file_patterns (Optional[list[Pattern]]): List of regexes + to match files to ignore. + ignore_dir_patterns (Optional[list[Pattern]]): List of regexes + to match directories to ignore. + + Returns: + list[tuple[str, str]]: List of tuples with path to file and parent + directories relative to 'src_path'. + """ + if ignore_file_patterns is None: ignore_file_patterns = IGNORE_FILE_PATTERNS if ignore_dir_patterns is None: ignore_dir_patterns = IGNORE_DIR_PATTERNS - output = [] + output: List[Tuple[str, str]] = [] + if not os.path.exists(src_path): + return output - hierarchy_queue = collections.deque() + hierarchy_queue: collections.deque = collections.deque() hierarchy_queue.append((src_path, [])) while hierarchy_queue: - item = hierarchy_queue.popleft() + item: Tuple[str, str] = hierarchy_queue.popleft() dirpath, parents = item for name in os.listdir(dirpath): - path = os.path.join(dirpath, name) + path: str = os.path.join(dirpath, name) if os.path.isfile(path): if not _value_match_regexes(name, ignore_file_patterns): - items = list(parents) + items: List[str] = list(parents) items.append(name) output.append((path, os.path.sep.join(items))) continue if not _value_match_regexes(name, ignore_dir_patterns): - items = list(parents) + items: List[str] = list(parents) items.append(name) hierarchy_queue.append((path, items)) return output -def copy_server_content(addon_output_dir, current_dir, log): - """Copies server side folders to 'addon_package_dir' +def update_client_version(logger): + """Update version in client code if version.py is present.""" + if not ADDON_CLIENT_DIR: + return - Args: - addon_output_dir (str): package dir in addon repo dir - current_dir (str): addon repo dir - log (logging.Logger) - """ + version_path: str = os.path.join( + CLIENT_ROOT, ADDON_CLIENT_DIR, "version.py" + ) + if not os.path.exists(version_path): + logger.debug("Did not find version.py in client directory") + return - log.info("Copying server content") + logger.info("Updating client version") + with open(version_path, "w") as stream: + stream.write(VERSION_PY_CONTENT) - filepaths_to_copy = [] - server_dirpath = os.path.join(current_dir, "server") - for item in find_files_in_subdir(server_dirpath): - src_path, dst_subpath = item - dst_path = os.path.join(addon_output_dir, "server", dst_subpath) - filepaths_to_copy.append((src_path, dst_path)) +def update_pyproject_toml(logger): + filepath = os.path.join(CURRENT_ROOT, "pyproject.toml") + new_lines = [] + with open(filepath, "r") as stream: + version_found = False + for line in stream.readlines(): + if not version_found and line.startswith("version ="): + line = f'version = "{ADDON_VERSION}"\n' + version_found = True - # Copy files - for src_path, dst_path in filepaths_to_copy: - safe_copy_file(src_path, dst_path) + new_lines.append(line) + with open(filepath, "w") as stream: + stream.write("".join(new_lines)) -def _update_client_version(client_addon_dir): - """Write version.py file to 'client' directory. - Make sure the version in client dir is the same as in package.py. +def build_frontend(): + yarn_executable = _get_yarn_executable() + if yarn_executable is None: + raise RuntimeError("Yarn executable was not found.") - Args: - client_addon_dir (str): Directory path of client addon. - """ + subprocess.run([yarn_executable, "install"], cwd=FRONTEND_ROOT) + subprocess.run([yarn_executable, "build"], cwd=FRONTEND_ROOT) + if not os.path.exists(FRONTEND_DIST_ROOT): + raise RuntimeError( + "Frontend build failed. Did not find 'dist' folder." + ) - dst_version_path = os.path.join(client_addon_dir, "version.py") - with open(dst_version_path, "w") as stream: - stream.write(CLIENT_VERSION_CONTENT.format(ADDON_VERSION)) +def get_client_files_mapping() -> List[Tuple[str, str]]: + """Mapping of source client code files to destination paths. -def zip_client_side(addon_package_dir, current_dir, log): - """Copy and zip `client` content into 'addon_package_dir'. + Example output: + [ + ( + "C:/addons/MyAddon/version.py", + "my_addon/version.py" + ), + ( + "C:/addons/MyAddon/client/my_addon/__init__.py", + "my_addon/__init__.py" + ) + ] + + Returns: + list[tuple[str, str]]: List of path mappings to copy. The destination + path is relative to expected output directory. - Args: - addon_package_dir (str): Output package directory path. - current_dir (str): Directory path of addon source. - log (logging.Logger): Logger object. """ + # Add client code content to zip + client_code_dir: str = os.path.join(CLIENT_ROOT, ADDON_CLIENT_DIR) + mapping = [ + (path, os.path.join(ADDON_CLIENT_DIR, sub_path)) + for path, sub_path in find_files_in_subdir(client_code_dir) + ] - client_dir = os.path.join(current_dir, "client") - client_addon_dir = os.path.join(client_dir, ADDON_CLIENT_DIR) - if not os.path.isdir(client_addon_dir): - raise ValueError( - f"Failed to find client directory '{client_addon_dir}'" - ) + license_path = os.path.join(CURRENT_ROOT, "LICENSE") + if os.path.exists(license_path): + mapping.append((license_path, f"{ADDON_CLIENT_DIR}/LICENSE")) + return mapping + +def get_client_zip_content(log) -> io.BytesIO: log.info("Preparing client code zip") - private_dir = os.path.join(addon_package_dir, "private") + files_mapping: List[Tuple[str, str]] = get_client_files_mapping() + stream = io.BytesIO() + with ZipFileLongPaths(stream, "w", zipfile.ZIP_DEFLATED) as zipf: + for src_path, subpath in files_mapping: + zipf.write(src_path, subpath) + stream.seek(0) + return stream + + +def get_base_files_mapping() -> List[FileMapping]: + filepaths_to_copy: List[FileMapping] = [ + ( + os.path.join(CURRENT_ROOT, "package.py"), + "package.py" + ) + ] + # Add license file to package if exists + license_path = os.path.join(CURRENT_ROOT, "LICENSE") + if os.path.exists(license_path): + filepaths_to_copy.append((license_path, "LICENSE")) + + # Go through server, private and public directories and find all files + for dirpath in (SERVER_ROOT, PRIVATE_ROOT, PUBLIC_ROOT): + if not os.path.exists(dirpath): + continue + + dirname = os.path.basename(dirpath) + for src_file, subpath in find_files_in_subdir(dirpath): + dst_subpath = os.path.join(dirname, subpath) + filepaths_to_copy.append((src_file, dst_subpath)) + + if os.path.exists(FRONTEND_DIST_ROOT): + for src_file, subpath in find_files_in_subdir(FRONTEND_DIST_ROOT): + dst_subpath = os.path.join(DST_DIST_DIR, subpath) + filepaths_to_copy.append((src_file, dst_subpath)) + + pyproject_toml = os.path.join(CLIENT_ROOT, "pyproject.toml") + if os.path.exists(pyproject_toml): + filepaths_to_copy.append( + (pyproject_toml, "private/pyproject.toml") + ) + + return filepaths_to_copy + + +def copy_client_code(output_dir: str, log: logging.Logger): + """Copies server side folders to 'addon_package_dir' - if not os.path.exists(private_dir): - os.makedirs(private_dir) + Args: + output_dir (str): Output directory path. + log (logging.Logger) - _update_client_version(client_addon_dir) + """ + log.info(f"Copying client for {ADDON_NAME}-{ADDON_VERSION}") - zip_filepath = os.path.join(os.path.join(private_dir, "client.zip")) - with ZipFileLongPaths(zip_filepath, "w", zipfile.ZIP_DEFLATED) as zipf: - # Add client code content to zip - for path, sub_path in find_files_in_subdir(client_addon_dir): - sub_path = os.path.join(ADDON_CLIENT_DIR, sub_path) - zipf.write(path, sub_path) + full_output_path = os.path.join( + output_dir, f"{ADDON_NAME}_{ADDON_VERSION}" + ) + if os.path.exists(full_output_path): + shutil.rmtree(full_output_path) + os.makedirs(full_output_path, exist_ok=True) - shutil.copy(os.path.join(client_dir, "pyproject.toml"), private_dir) + for src_path, dst_subpath in get_client_files_mapping(): + dst_path = os.path.join(full_output_path, dst_subpath) + safe_copy_file(src_path, dst_path) + log.info("Client copy finished") -def create_server_package( + +def copy_addon_package( output_dir: str, - addon_output_dir: str, + files_mapping: List[FileMapping], log: logging.Logger ): - """Create server package zip file. - - The zip file can be installed to a server using UI or rest api endpoints. + """Copy client code to output directory. Args: - output_dir (str): Directory path to output zip file. - addon_output_dir (str): Directory path to addon output directory. + output_dir (str): Directory path to output client code. + files_mapping (List[FileMapping]): List of tuples with source file + and destination subpath. log (logging.Logger): Logger object. + """ + log.info(f"Copying package for {ADDON_NAME}-{ADDON_VERSION}") + + # Add addon name and version to output directory + addon_output_dir: str = os.path.join( + output_dir, ADDON_NAME, ADDON_VERSION + ) + if os.path.isdir(addon_output_dir): + log.info(f"Purging {addon_output_dir}") + shutil.rmtree(addon_output_dir) - log.info("Creating server package") + os.makedirs(addon_output_dir, exist_ok=True) + + # Copy server content + for src_file, dst_subpath in files_mapping: + dst_path: str = os.path.join(addon_output_dir, dst_subpath) + dst_dir: str = os.path.dirname(dst_path) + os.makedirs(dst_dir, exist_ok=True) + if isinstance(src_file, io.BytesIO): + with open(dst_path, "wb") as stream: + stream.write(src_file.getvalue()) + else: + safe_copy_file(src_file, dst_path) + + log.info("Package copy finished") + + +def create_addon_package( + output_dir: str, + files_mapping: List[FileMapping], + log: logging.Logger +): + log.info(f"Creating package for {ADDON_NAME}-{ADDON_VERSION}") + + os.makedirs(output_dir, exist_ok=True) output_path = os.path.join( output_dir, f"{ADDON_NAME}-{ADDON_VERSION}.zip" ) - with ZipFileLongPaths(output_path, "w", zipfile.ZIP_DEFLATED) as zipf: - # Move addon content to zip into 'addon' directory - addon_output_dir_offset = len(addon_output_dir) + 1 - for root, _, filenames in os.walk(addon_output_dir): - if not filenames: - continue - dst_root = None - if root != addon_output_dir: - dst_root = root[addon_output_dir_offset:] - for filename in filenames: - src_path = os.path.join(root, filename) - dst_path = filename - if dst_root: - dst_path = os.path.join(dst_root, dst_path) - zipf.write(src_path, dst_path) + with ZipFileLongPaths(output_path, "w", zipfile.ZIP_DEFLATED) as zipf: + # Copy server content + for src_file, dst_subpath in files_mapping: + if isinstance(src_file, io.BytesIO): + zipf.writestr(dst_subpath, src_file.getvalue()) + else: + zipf.write(src_file, dst_subpath) - log.info(f"Output package can be found: {output_path}") + log.info("Package created") def main( - output_dir: Optional[str]=None, - skip_zip: bool=False, - keep_sources: bool=False, - clear_output_dir: bool=False + output_dir: Optional[str] = None, + skip_zip: Optional[bool] = False, + only_client: Optional[bool] = False ): - log = logging.getLogger("create_package") - log.info("Start creating package") + log: logging.Logger = logging.getLogger("create_package") + log.info("Package creation started") - current_dir = os.path.dirname(os.path.abspath(__file__)) if not output_dir: - output_dir = os.path.join(current_dir, "package") + output_dir = os.path.join(CURRENT_ROOT, "package") + has_client_code = bool(ADDON_CLIENT_DIR) + if has_client_code: + client_dir: str = os.path.join(CLIENT_ROOT, ADDON_CLIENT_DIR) + if not os.path.exists(client_dir): + raise RuntimeError( + f"Client directory was not found '{client_dir}'." + " Please check 'client_dir' in 'package.py'." + ) + update_client_version(log) - new_created_version_dir = os.path.join( - output_dir, ADDON_NAME, ADDON_VERSION - ) + update_pyproject_toml(log) + + if only_client: + if not has_client_code: + raise RuntimeError("Client code is not available. Skipping") - if os.path.isdir(new_created_version_dir) and clear_output_dir: - log.info(f"Purging {new_created_version_dir}") - shutil.rmtree(output_dir) + copy_client_code(output_dir, log) + return log.info(f"Preparing package for {ADDON_NAME}-{ADDON_VERSION}") - addon_output_root = os.path.join(output_dir, ADDON_NAME) - addon_output_dir = os.path.join(addon_output_root, ADDON_VERSION) - if not os.path.exists(addon_output_dir): - os.makedirs(addon_output_dir) + if os.path.exists(FRONTEND_ROOT): + build_frontend() - copy_server_content(addon_output_dir, current_dir, log) - safe_copy_file( - PACKAGE_PATH, - os.path.join(addon_output_dir, os.path.basename(PACKAGE_PATH)) - ) - zip_client_side(addon_output_dir, current_dir, log) + files_mapping: List[FileMapping] = [] + files_mapping.extend(get_base_files_mapping()) + + if has_client_code: + files_mapping.append( + (get_client_zip_content(log), "private/client.zip") + ) # Skip server zipping - if not skip_zip: - create_server_package(output_dir, addon_output_dir, log) - # Remove sources only if zip file is created - if not keep_sources: - log.info("Removing source files for server package") - shutil.rmtree(addon_output_root) + if skip_zip: + copy_addon_package(output_dir, files_mapping, log) + else: + create_addon_package(output_dir, files_mapping, log) + log.info("Package creation finished") @@ -334,36 +475,33 @@ def main( ) ) parser.add_argument( - "--keep-sources", - dest="keep_sources", - action="store_true", + "-o", "--output", + dest="output_dir", + default=None, help=( - "Keep folder structure when server package is created." + "Directory path where package will be created" + " (Will be purged if already exists!)" ) ) parser.add_argument( - "-c", "--clear-output-dir", - dest="clear_output_dir", + "--only-client", + dest="only_client", action="store_true", help=( - "Clear output directory before package creation." + "Extract only client code. This is useful for development." + " Requires '-o', '--output' argument to be filled." ) ) - parser.add_argument( - "-o", "--output", - dest="output_dir", - default=None, - help=( - "Directory path where package will be created" - " (Will be purged if already exists!)" - ) + "--debug", + dest="debug", + action="store_true", + help="Debug log messages." ) args = parser.parse_args(sys.argv[1:]) - main( - args.output_dir, - args.skip_zip, - args.keep_sources, - args.clear_output_dir - ) + level = logging.INFO + if args.debug: + level = logging.DEBUG + logging.basicConfig(level=level) + main(args.output_dir, args.skip_zip, args.only_client) From ba002d56377a5c221c80bb91fd515bade2c87dbf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:40:13 +0200 Subject: [PATCH 71/85] bump version to '1.0.0' --- 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 3ee3c976b9..0b6322645f 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON core addon version.""" -__version__ = "0.4.5-dev.1" +__version__ = "1.0.0" diff --git a/package.py b/package.py index 26c004ae84..b06959d5cf 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "0.4.5-dev.1" +version = "1.0.0" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index db98ee4eba..091cdc273d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "0.4.3-dev.1" +version = "1.0.0" description = "" authors = ["Ynput Team "] readme = "README.md" From 10ffb37518349101598a23399878c8a86957fd5e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:41:27 +0200 Subject: [PATCH 72/85] bump version to '1.0.0+dev' --- client/ayon_core/version.py | 4 ++-- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 0b6322645f..75116c703e 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -"""Package declaring AYON core addon version.""" -__version__ = "1.0.0" +"""Package declaring AYON addon 'core' version.""" +__version__ = "1.0.0+dev" diff --git a/package.py b/package.py index b06959d5cf..1466031daa 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.0" +version = "1.0.0+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 091cdc273d..4a63529c67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.0" +version = "1.0.0+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From a834e2c6639c22e617caa6cab0cb4dd3ee9f74f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 2 Oct 2024 12:43:29 +0000 Subject: [PATCH 73/85] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e6badf936a..54f5d68b98 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ name: Bug Report description: File a bug report -title: 'Your issue title here' +title: Your issue title here labels: - 'type: bug' body: @@ -36,6 +36,16 @@ body: description: What version are you running? Look to AYON Tray options: - 1.0.0 + - 0.4.4 + - 0.4.3 + - 0.4.2 + - 0.4.1 + - 0.4.0 + - 0.3.2 + - 0.3.1 + - 0.3.0 + - 0.2.1 + - 0.2.0 validations: required: true - type: dropdown From 41302936c26156b36627a43a413bd1b95cecd511 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Oct 2024 11:06:29 -0400 Subject: [PATCH 74/85] Fix multiple review clips in OTIO review plugins with tests. --- .../plugins/publish/extract_otio_review.py | 61 +- .../resources/multiple_review_clips.json | 1511 +++++++++++++++++ .../resources/multiple_review_clips_gap.json | 289 ++++ .../editorial/test_extract_otio_review.py | 153 +- 4 files changed, 1969 insertions(+), 45 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/multiple_review_clips.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/multiple_review_clips_gap.json diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 55eff782d9..00a90df695 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -100,30 +100,30 @@ def process(self, instance): for index, r_otio_cl in enumerate(otio_review_clips): # QUESTION: what if transition on clip? - # check if resolution is the same as source - otio_media = r_otio_cl.media_reference - media_metadata = otio_media.metadata - - # get from media reference metadata source - # TODO 'openpype' prefix should be removed (added 24/09/03) - # NOTE it looks like it is set only in hiero integration - res_data = {"width": self.to_width, "height": self.to_height} - for key in res_data: - for meta_prefix in ("ayon.source.", "openpype.source."): - meta_key = f"{meta_prefix}.{key}" - value = media_metadata.get(meta_key) - if value is not None: - res_data[key] = value - break - - self.to_width, self.to_height = res_data["width"], res_data["height"] - self.log.debug("> self.to_width x self.to_height: {} x {}".format( - self.to_width, self.to_height - )) - # Clip: compute process range from available media range. src_range = r_otio_cl.source_range if isinstance(r_otio_cl, otio.schema.Clip): + # check if resolution is the same as source + media_ref = r_otio_cl.media_reference + media_metadata = media_ref.metadata + + # get from media reference metadata source + # TODO 'openpype' prefix should be removed (added 24/09/03) + # NOTE it looks like it is set only in hiero integration + res_data = {"width": self.to_width, "height": self.to_height} + for key in res_data: + for meta_prefix in ("ayon.source.", "openpype.source."): + meta_key = f"{meta_prefix}.{key}" + value = media_metadata.get(meta_key) + if value is not None: + res_data[key] = value + break + + self.to_width, self.to_height = res_data["width"], res_data["height"] + self.log.debug("> self.to_width x self.to_height: {} x {}".format( + self.to_width, self.to_height + )) + available_range = r_otio_cl.available_range() processing_range = None self.actual_fps = available_range.duration.rate @@ -135,7 +135,6 @@ def process(self, instance): # source range for image sequence. Following code maintain # backward-compatibility by adjusting available range # while we are updating those. - media_ref = r_otio_cl.media_reference if ( is_clip_from_media_sequence(r_otio_cl) and available_range.start_time.to_frames() == media_ref.start_frame @@ -154,11 +153,11 @@ def process(self, instance): duration = src_range.duration # Create handle offsets. - handle_start = otio.opentime.RationalTime( + clip_handle_start = otio.opentime.RationalTime( handle_start, rate=self.actual_fps, ) - handle_end = otio.opentime.RationalTime( + clip_handle_end = otio.opentime.RationalTime( handle_end, rate=self.actual_fps, ) @@ -166,16 +165,16 @@ def process(self, instance): # reframing handles conditions if (len(otio_review_clips) > 1) and (index == 0): # more clips | first clip reframing with handle - start -= handle_start - duration += handle_start + start -= clip_handle_start + duration += clip_handle_start elif len(otio_review_clips) > 1 \ and (index == len(otio_review_clips) - 1): # more clips | last clip reframing with handle - duration += handle_end + duration += clip_handle_end elif len(otio_review_clips) == 1: # one clip | add both handles - start -= handle_start - duration += (handle_start + handle_end) + start -= clip_handle_start + duration += (clip_handle_start + clip_handle_end) if available_range: processing_range = self._trim_available_range( @@ -258,9 +257,9 @@ def process(self, instance): # QUESTION: what if nested track composition is in place? else: # at last process a Gap - self._render_segment(gap=duration) + self._render_segment(gap=duration.to_frames()) # generate used frames - self._generate_used_frames(duration) + self._generate_used_frames(duration.to_frames()) # creating and registering representation representation = self._create_representation(start, duration) diff --git a/tests/client/ayon_core/pipeline/editorial/resources/multiple_review_clips.json b/tests/client/ayon_core/pipeline/editorial/resources/multiple_review_clips.json new file mode 100644 index 0000000000..dcf60abb7d --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/multiple_review_clips.json @@ -0,0 +1,1511 @@ +{ + "OTIO_SCHEMA": "Track.1", + "metadata": {}, + "name": "review", + "source_range": null, + "effects": [], + "markers": [], + "enabled": true, + "children": [ + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "matte_paint", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.tif 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Int8) Open Color IO space: 5", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.tif 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "1000", + "foundry.source.umid": "918126ef-7109-4835-492e-67d31cd7f798", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "matte_paint", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAABAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAA", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-23 08:36:04", + "media.input.filereader": "tiff", + "media.input.filesize": "784446", + "media.input.frame": "1", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:36:04", + "media.input.width": "1920", + "media.tiff.resolution_unit": "1", + "media.tiff.xresolution": "72", + "media.tiff.yresolution": "72", + "openpype.source.colourtransform": "matte_paint", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\no_tc", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "scene_linear", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.exr 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "24", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 7", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.exr 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "87399", + "foundry.source.umid": "bbfe0c90-5b76-424a-6351-4dac36a8dde7", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "scene_linear", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "24", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "2", + "media.exr.compressionName": "Zip (1 scanline)", + "media.exr.dataWindow": "1,1,1918,1078", + "media.exr.displayWindow": "0,0,1919,1079", + "media.exr.lineOrder": "0", + "media.exr.nuke.input.frame_rate": "24", + "media.exr.nuke.input.timecode": "01:00:41:15", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2024-09-23 08:37:23", + "media.input.filereader": "exr", + "media.input.filesize": "1095868", + "media.input.frame": "1", + "media.input.frame_rate": "24", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:37:23", + "media.input.timecode": "01:00:41:15", + "media.input.width": "1920", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "9b", + "media.nuke.version": "15.1v2", + "openpype.source.colourtransform": "scene_linear", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\with_tc", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 1000, + "frame_step": 1, + "rate": 24.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "matte_paint", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.tif 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Int8) Open Color IO space: 5", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.tif 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "1000", + "foundry.source.umid": "918126ef-7109-4835-492e-67d31cd7f798", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "matte_paint", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAABAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAA", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-23 08:36:04", + "media.input.filereader": "tiff", + "media.input.filesize": "784446", + "media.input.frame": "1", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:36:04", + "media.input.width": "1920", + "media.tiff.resolution_unit": "1", + "media.tiff.xresolution": "72", + "media.tiff.yresolution": "72", + "openpype.source.colourtransform": "matte_paint", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\no_tc", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "scene_linear", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.exr 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "24", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 7", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.exr 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "87399", + "foundry.source.umid": "bbfe0c90-5b76-424a-6351-4dac36a8dde7", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "scene_linear", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "24", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "2", + "media.exr.compressionName": "Zip (1 scanline)", + "media.exr.dataWindow": "1,1,1918,1078", + "media.exr.displayWindow": "0,0,1919,1079", + "media.exr.lineOrder": "0", + "media.exr.nuke.input.frame_rate": "24", + "media.exr.nuke.input.timecode": "01:00:41:15", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2024-09-23 08:37:23", + "media.input.filereader": "exr", + "media.input.filesize": "1095868", + "media.input.frame": "1", + "media.input.frame_rate": "24", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:37:23", + "media.input.timecode": "01:00:41:15", + "media.input.width": "1920", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "9b", + "media.nuke.version": "15.1v2", + "openpype.source.colourtransform": "scene_linear", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\with_tc", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 1000, + "frame_step": 1, + "rate": 24.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "matte_paint", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.tif 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Int8) Open Color IO space: 5", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.tif 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "1000", + "foundry.source.umid": "918126ef-7109-4835-492e-67d31cd7f798", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "matte_paint", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAABAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAA", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-23 08:36:04", + "media.input.filereader": "tiff", + "media.input.filesize": "784446", + "media.input.frame": "1", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:36:04", + "media.input.width": "1920", + "media.tiff.resolution_unit": "1", + "media.tiff.xresolution": "72", + "media.tiff.yresolution": "72", + "openpype.source.colourtransform": "matte_paint", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\no_tc", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "matte_paint", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.tif 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Int8) Open Color IO space: 5", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.tif 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "1000", + "foundry.source.umid": "918126ef-7109-4835-492e-67d31cd7f798", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "matte_paint", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAABAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAA", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-23 08:36:04", + "media.input.filereader": "tiff", + "media.input.filesize": "784446", + "media.input.frame": "1", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:36:04", + "media.input.width": "1920", + "media.tiff.resolution_unit": "1", + "media.tiff.xresolution": "72", + "media.tiff.yresolution": "72", + "openpype.source.colourtransform": "matte_paint", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\no_tc", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "matte_paint", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.tif 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Int8) Open Color IO space: 5", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.tif 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "1000", + "foundry.source.umid": "918126ef-7109-4835-492e-67d31cd7f798", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "matte_paint", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAABAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAA", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-23 08:36:04", + "media.input.filereader": "tiff", + "media.input.filesize": "784446", + "media.input.frame": "1", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:36:04", + "media.input.width": "1920", + "media.tiff.resolution_unit": "1", + "media.tiff.xresolution": "72", + "media.tiff.yresolution": "72", + "openpype.source.colourtransform": "matte_paint", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\no_tc", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "matte_paint", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.tif 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Int8) Open Color IO space: 5", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.tif 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "1000", + "foundry.source.umid": "918126ef-7109-4835-492e-67d31cd7f798", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "matte_paint", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAABAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAA", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-23 08:36:04", + "media.input.filereader": "tiff", + "media.input.filesize": "784446", + "media.input.frame": "1", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:36:04", + "media.input.width": "1920", + "media.tiff.resolution_unit": "1", + "media.tiff.xresolution": "72", + "media.tiff.yresolution": "72", + "openpype.source.colourtransform": "matte_paint", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\no_tc", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "matte_paint", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.tif 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Int8) Open Color IO space: 5", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.tif 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "1000", + "foundry.source.umid": "918126ef-7109-4835-492e-67d31cd7f798", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "matte_paint", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAABAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAA", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-23 08:36:04", + "media.input.filereader": "tiff", + "media.input.filesize": "784446", + "media.input.frame": "1", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:36:04", + "media.input.width": "1920", + "media.tiff.resolution_unit": "1", + "media.tiff.xresolution": "72", + "media.tiff.yresolution": "72", + "openpype.source.colourtransform": "matte_paint", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\no_tc", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "matte_paint", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.tif 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Int8) Open Color IO space: 5", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.tif 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "1000", + "foundry.source.umid": "918126ef-7109-4835-492e-67d31cd7f798", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "matte_paint", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAABAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAA", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-23 08:36:04", + "media.input.filereader": "tiff", + "media.input.filesize": "784446", + "media.input.frame": "1", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:36:04", + "media.input.width": "1920", + "media.tiff.resolution_unit": "1", + "media.tiff.xresolution": "72", + "media.tiff.yresolution": "72", + "openpype.source.colourtransform": "matte_paint", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\no_tc", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "matte_paint", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.tif 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Int8) Open Color IO space: 5", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.tif 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "1000", + "foundry.source.umid": "918126ef-7109-4835-492e-67d31cd7f798", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "matte_paint", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAABAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAA", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-23 08:36:04", + "media.input.filereader": "tiff", + "media.input.filesize": "784446", + "media.input.frame": "1", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:36:04", + "media.input.width": "1920", + "media.tiff.resolution_unit": "1", + "media.tiff.xresolution": "72", + "media.tiff.yresolution": "72", + "openpype.source.colourtransform": "matte_paint", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\no_tc", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "matte_paint", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.tif 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Int8) Open Color IO space: 5", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.tif 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "1000", + "foundry.source.umid": "918126ef-7109-4835-492e-67d31cd7f798", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "matte_paint", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAABAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAA", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-23 08:36:04", + "media.input.filereader": "tiff", + "media.input.filesize": "784446", + "media.input.frame": "1", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:36:04", + "media.input.width": "1920", + "media.tiff.resolution_unit": "1", + "media.tiff.xresolution": "72", + "media.tiff.yresolution": "72", + "openpype.source.colourtransform": "matte_paint", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\no_tc", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 31.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "matte_paint", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.tif 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Int8) Open Color IO space: 5", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.tif 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "1000", + "foundry.source.umid": "918126ef-7109-4835-492e-67d31cd7f798", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "matte_paint", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAABAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAA", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-23 08:36:04", + "media.input.filereader": "tiff", + "media.input.filesize": "784446", + "media.input.frame": "1", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:36:04", + "media.input.width": "1920", + "media.tiff.resolution_unit": "1", + "media.tiff.xresolution": "72", + "media.tiff.yresolution": "72", + "openpype.source.colourtransform": "matte_paint", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\no_tc", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + } + ], + "kind": "Video" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/multiple_review_clips_gap.json b/tests/client/ayon_core/pipeline/editorial/resources/multiple_review_clips_gap.json new file mode 100644 index 0000000000..85667a00dc --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/multiple_review_clips_gap.json @@ -0,0 +1,289 @@ +{ + "OTIO_SCHEMA": "Track.1", + "metadata": {}, + "name": "Video 2", + "source_range": null, + "effects": [], + "markers": [], + "enabled": true, + "children": [ + { + "OTIO_SCHEMA": "Gap.1", + "metadata": {}, + "name": "", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 2.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 88.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default ()", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "scene_linear", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.exr 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "24", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 7", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.exr 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "87399", + "foundry.source.umid": "bbfe0c90-5b76-424a-6351-4dac36a8dde7", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "scene_linear", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "24", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "2", + "media.exr.compressionName": "Zip (1 scanline)", + "media.exr.dataWindow": "1,1,1918,1078", + "media.exr.displayWindow": "0,0,1919,1079", + "media.exr.lineOrder": "0", + "media.exr.nuke.input.frame_rate": "24", + "media.exr.nuke.input.timecode": "01:00:41:15", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2024-09-23 08:37:23", + "media.input.filereader": "exr", + "media.input.filesize": "1095868", + "media.input.frame": "1", + "media.input.frame_rate": "24", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:37:23", + "media.input.timecode": "01:00:41:15", + "media.input.width": "1920", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "9b", + "media.nuke.version": "15.1v2", + "openpype.source.colourtransform": "scene_linear", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\with_tc", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 1000, + "frame_step": 1, + "rate": 24.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 11.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default ()", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "scene_linear", + "foundry.source.duration": "101", + "foundry.source.filename": "output.%04d.exr 1000-1100", + "foundry.source.filesize": "", + "foundry.source.fragments": "101", + "foundry.source.framerate": "24", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 7", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.exr 1000-1100", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "87399", + "foundry.source.umid": "bbfe0c90-5b76-424a-6351-4dac36a8dde7", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "scene_linear", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "24", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "2", + "media.exr.compressionName": "Zip (1 scanline)", + "media.exr.dataWindow": "1,1,1918,1078", + "media.exr.displayWindow": "0,0,1919,1079", + "media.exr.lineOrder": "0", + "media.exr.nuke.input.frame_rate": "24", + "media.exr.nuke.input.timecode": "01:00:41:15", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2024-09-23 08:37:23", + "media.input.filereader": "exr", + "media.input.filesize": "1095868", + "media.input.frame": "1", + "media.input.frame_rate": "24", + "media.input.height": "1080", + "media.input.mtime": "2024-09-23 08:37:23", + "media.input.timecode": "01:00:41:15", + "media.input.width": "1920", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "9b", + "media.nuke.version": "15.1v2", + "openpype.source.colourtransform": "scene_linear", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\with_tc", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 1000, + "frame_step": 1, + "rate": 24.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + } + ], + "kind": "Video" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py index 7bc1a750d7..25d689e7c1 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py +++ b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py @@ -37,27 +37,31 @@ def get_ffmpeg_executable(self, _): return ["/path/to/ffmpeg"] -def run_process(file_name: str): +def run_process(file_name: str, instance_data: dict = None): """ """ - # Get OTIO review data from serialized file_name - file_path = os.path.join(_RESOURCE_DIR, file_name) - clip = otio.schema.Clip.from_json_file(file_path) - # Prepare dummy instance and capture call object capture_call = CaptureFFmpegCalls() processor = extract_otio_review.ExtractOTIOReview() Anatomy = NamedTuple("Anatomy", project_name=str) - instance = MockInstance( - { + + if not instance_data: + # Get OTIO review data from serialized file_name + file_path = os.path.join(_RESOURCE_DIR, file_name) + clip = otio.schema.Clip.from_json_file(file_path) + + instance_data = { "otioReviewClips": [clip], "handleStart": 10, "handleEnd": 10, "workfileFrameStart": 1001, - "folderPath": "/dummy/path", - "anatomy": Anatomy("test_project"), } - ) + + instance_data.update({ + "folderPath": "/dummy/path", + "anatomy": Anatomy("test_project"), + }) + instance = MockInstance(instance_data) # Mock calls to extern and run plugins. with mock.patch.object( @@ -73,9 +77,14 @@ def run_process(file_name: str): with mock.patch.object( processor, "_get_folder_name_based_prefix", - return_value="C:/result/output." + return_value="output." ): - processor.process(instance) + with mock.patch.object( + processor, + "staging_dir", + return_value="C:/result/" + ): + processor.process(instance) # return all calls made to ffmpeg subprocess return capture_call.calls @@ -103,7 +112,7 @@ def test_image_sequence_with_embedded_tc_and_handles_out_of_range(): # Report from source exr (1001-1101) with enforce framerate "/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i " - "C:\\exr_embedded_tc\\output.%04d.exr -start_number 1001 " + f"C:\\exr_embedded_tc{os.sep}output.%04d.exr -start_number 1001 " "C:/result/output.%03d.jpg" ] @@ -133,7 +142,7 @@ def test_image_sequence_and_handles_out_of_range(): # 1001-1095 = source range conformed to 25fps # 1096-1096 = additional 1 tail frames "/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i " - "C:\\tif_seq\\output.%04d.tif -start_number 996 C:/result/output.%03d.jpg" + f"C:\\tif_seq{os.sep}output.%04d.tif -start_number 996 C:/result/output.%03d.jpg" ] assert calls == expected @@ -203,3 +212,119 @@ def test_short_movie_tail_gap_handles(): ] assert calls == expected + +def test_multiple_review_clips_no_gap(): + """ + Use multiple review clips (image sequence). + Timeline 25fps + """ + file_path = os.path.join(_RESOURCE_DIR, "multiple_review_clips.json") + clips = otio.schema.Track.from_json_file(file_path) + instance_data = { + "otioReviewClips": clips, + "handleStart": 10, + "handleEnd": 10, + "workfileFrameStart": 1001, + } + + calls = run_process( + None, + instance_data=instance_data + ) + + expected = [ + # 10 head black frames generated from gap (991-1000) + '/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune ' + 'stillimage -start_number 991 C:/result/output.%03d.jpg', + + # Alternance 25fps tiff sequence and 24fps exr sequence for 100 frames each + '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' + f'C:\\no_tc{os.sep}output.%04d.tif ' + '-start_number 1001 C:/result/output.%03d.jpg', + + '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' + f'C:\\with_tc{os.sep}output.%04d.exr ' + '-start_number 1102 C:/result/output.%03d.jpg', + + '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' + f'C:\\no_tc{os.sep}output.%04d.tif ' + '-start_number 1199 C:/result/output.%03d.jpg', + + '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' + f'C:\\with_tc{os.sep}output.%04d.exr ' + '-start_number 1300 C:/result/output.%03d.jpg', + + # Repeated 25fps tiff sequence multiple times till the end + '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' + f'C:\\no_tc{os.sep}output.%04d.tif ' + '-start_number 1397 C:/result/output.%03d.jpg', + + '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' + f'C:\\no_tc{os.sep}output.%04d.tif ' + '-start_number 1498 C:/result/output.%03d.jpg', + + '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' + f'C:\\no_tc{os.sep}output.%04d.tif ' + '-start_number 1599 C:/result/output.%03d.jpg', + + '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' + f'C:\\no_tc{os.sep}output.%04d.tif ' + '-start_number 1700 C:/result/output.%03d.jpg', + + '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' + f'C:\\no_tc{os.sep}output.%04d.tif ' + '-start_number 1801 C:/result/output.%03d.jpg', + + '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' + f'C:\\no_tc{os.sep}output.%04d.tif ' + '-start_number 1902 C:/result/output.%03d.jpg', + + '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' + f'C:\\no_tc{os.sep}output.%04d.tif ' + '-start_number 2003 C:/result/output.%03d.jpg', + + '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' + f'C:\\no_tc{os.sep}output.%04d.tif ' + '-start_number 2104 C:/result/output.%03d.jpg', + + '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' + f'C:\\no_tc{os.sep}output.%04d.tif ' + '-start_number 2205 C:/result/output.%03d.jpg' + ] + + assert calls == expected + +def test_multiple_review_clips_with_gap(): + """ + Use multiple review clips (image sequence) with gap. + Timeline 24fps + """ + file_path = os.path.join(_RESOURCE_DIR, "multiple_review_clips_gap.json") + clips = otio.schema.Track.from_json_file(file_path) + instance_data = { + "otioReviewClips": clips, + "handleStart": 10, + "handleEnd": 10, + "workfileFrameStart": 1001, + } + + calls = run_process( + None, + instance_data=instance_data + ) + + expected = [ + # Gap on review track (12 frames) + '/path/to/ffmpeg -t 0.5 -r 24.0 -f lavfi -i color=c=black:s=1280x720 -tune ' + 'stillimage -start_number 991 C:/result/output.%03d.jpg', + + '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' + f'C:\\with_tc{os.sep}output.%04d.exr ' + '-start_number 1003 C:/result/output.%03d.jpg', + + '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' + f'C:\\with_tc{os.sep}output.%04d.exr ' + '-start_number 1091 C:/result/output.%03d.jpg' + ] + + assert calls == expected From 1fce6108c69b15a8aeb3660182d4b6c17530bf84 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Oct 2024 11:09:01 -0400 Subject: [PATCH 75/85] Fix linting --- .../pipeline/editorial/test_extract_otio_review.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py index 25d689e7c1..ea31e1a260 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py +++ b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py @@ -83,7 +83,7 @@ def run_process(file_name: str, instance_data: dict = None): processor, "staging_dir", return_value="C:/result/" - ): + ): processor.process(instance) # return all calls made to ffmpeg subprocess @@ -233,11 +233,11 @@ def test_multiple_review_clips_no_gap(): ) expected = [ - # 10 head black frames generated from gap (991-1000) + # 10 head black frames generated from gap (991-1000) '/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune ' 'stillimage -start_number 991 C:/result/output.%03d.jpg', - # Alternance 25fps tiff sequence and 24fps exr sequence for 100 frames each + # Alternance 25fps tiff sequence and 24fps exr sequence for 100 frames each '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' '-start_number 1001 C:/result/output.%03d.jpg', @@ -254,7 +254,7 @@ def test_multiple_review_clips_no_gap(): f'C:\\with_tc{os.sep}output.%04d.exr ' '-start_number 1300 C:/result/output.%03d.jpg', - # Repeated 25fps tiff sequence multiple times till the end + # Repeated 25fps tiff sequence multiple times till the end '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' '-start_number 1397 C:/result/output.%03d.jpg', @@ -292,7 +292,7 @@ def test_multiple_review_clips_no_gap(): '-start_number 2205 C:/result/output.%03d.jpg' ] - assert calls == expected + assert calls == expected def test_multiple_review_clips_with_gap(): """ From 266140ad40f7a651244220533257eda57aa305ba Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Oct 2024 19:23:37 +0200 Subject: [PATCH 76/85] Refactor function `update_allowed_representation_switches` -> `get_representation_name_aliases` --- client/ayon_core/pipeline/load/plugins.py | 26 +++++++++++------------ client/ayon_core/pipeline/load/utils.py | 9 ++++---- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 28a7e775d6..1fb906fd65 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -242,25 +242,25 @@ def fname(self): if hasattr(self, "_fname"): return self._fname - def update_allowed_representation_switches(self): - """Return a mapping from source representation names to ordered - destination representation names to which switching is allowed if - the source representation name does not exist for the new version. + @classmethod + def get_representation_name_aliases(cls, representation_name: str): + """Return representation names to which switching is allowed from + the input representation name, like an alias replacement of the input + `representation_name`. For example, to allow an automated switch on update from representation - `ma` to `mb` or `abc` if the new version does not have a `ma` - representation you can return: - {"ma": ["mb", "abc"]} + `ma` to `mb` or `abc`, then when `representation_name` is `ma` return: + ["mb", "abc"] - The order of the names in the returned values is important, because - if `ma` is missing and both of the replacement representations are - present than the first one will be chosen. + The order of the names in the returned representation names is + important, because the first one existing under the new version will + be chosen. Returns: - Dict[str, List[str]]: Mapping from representation names to allowed - alias representation names switching to is allowed on update. + List[str]: Representation names switching to is allowed on update + if the input representation name is not found on the new version. """ - return {} + return [] class ProductLoaderPlugin(LoaderPlugin): diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index b258d20a3d..ee2c1af07f 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -521,17 +521,16 @@ def update_container(container, version=-1): # The representation name is not found in the new version. # Allow updating to a 'matching' representation if the loader # has defined compatible update conversions - mapping = Loader().update_allowed_representation_switches() - switch_repre_names = mapping.get(repre_name) - if switch_repre_names: + repre_name_aliases = Loader.get_representation_name_aliases(repre_name) + if repre_name_aliases: representations = ayon_api.get_representations( project_name, - representation_names=switch_repre_names, + representation_names=repre_name_aliases, version_ids=[new_version["id"]]) representations_by_name = { repre["name"]: repre for repre in representations } - for name in switch_repre_names: + for name in repre_name_aliases: if name in representations_by_name: new_representation = representations_by_name[name] break From 0ab128b3bfac5aae72f10df11887e1ecf8b26968 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Oct 2024 16:10:27 -0400 Subject: [PATCH 77/85] Add rounding support for OTIO <= 0.16.0 --- .../plugins/publish/extract_otio_review.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 00a90df695..faba9fd36d 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -329,6 +329,20 @@ def _trim_available_range(self, avl_range, start, duration): trim_media_range, ) + def _round_to_frame(rational_time): + """ Handle rounding duration to frame. + """ + # OpentimelineIO >= 0.16.0 + try: + return rational_time.round().to_frames() + + # OpentimelineIO < 0.16.0 + except AttributeError: + return otio.opentime.RationalTime( + round(rational_time.value), + rate=rational_time.rate, + ).to_frames() + avl_start = avl_range.start_time # An additional gap is required before the available @@ -337,11 +351,12 @@ def _trim_available_range(self, avl_range, start, duration): gap_duration = avl_start - start start = avl_start duration -= gap_duration + gap_duration = _round_to_frame(gap_duration) # create gap data to disk - self._render_segment(gap=gap_duration.round().to_frames()) + self._render_segment(gap=gap_duration) # generate used frames - self._generate_used_frames(gap_duration.round().to_frames()) + self._generate_used_frames(gap_duration) # An additional gap is required after the available # range to conform to source end point + tail handles @@ -351,15 +366,16 @@ def _trim_available_range(self, avl_range, start, duration): if end_point > avl_end_point: gap_duration = end_point - avl_end_point duration -= gap_duration + gap_duration = _round_to_frame(gap_duration) # create gap data to disk self._render_segment( - gap=gap_duration.round().to_frames(), + gap=gap_duration, end_offset=duration.to_frames() ) # generate used frames self._generate_used_frames( - gap_duration.round().to_frames(), + gap_duration, end_offset=duration.to_frames() ) From 84d6daf60c357735588f669e4c35a4d5f52febdc Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Oct 2024 08:36:34 -0400 Subject: [PATCH 78/85] Fix NTSC framerates floating issue comparison. --- client/ayon_core/pipeline/editorial.py | 24 +- .../resources/img_seq_23.976_metadata.json | 255 ++++++++++++++++++ .../test_media_range_with_retimes.py | 21 ++ 3 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_23.976_metadata.json diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index f382f91fec..af2a6ef88c 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -292,13 +292,23 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # Note that 24fps is slower than 25fps hence extended duration # to preserve media range - # Compute new source range based on available rate - conformed_src_in = source_range.start_time.rescaled_to(available_range_rate) - conformed_src_duration = source_range.duration.rescaled_to(available_range_rate) - conformed_source_range = otio.opentime.TimeRange( - start_time=conformed_src_in, - duration=conformed_src_duration - ) + # Compute new source range based on available rate. + # NSTC compatibility might introduce floating rates, when these are + # not exactly the same (23.976 vs 23.976024627685547) + # this will cause precision issue in computation. + # Round to 2 decimals for comparison. + rounded_av_rate = round(available_range_rate, 2) + rounded_src_rate = round(source_range.start_time.rate, 2) + if rounded_av_rate != rounded_src_rate: + conformed_src_in = source_range.start_time.rescaled_to(available_range_rate) + conformed_src_duration = source_range.duration.rescaled_to(available_range_rate) + conformed_source_range = otio.opentime.TimeRange( + start_time=conformed_src_in, + duration=conformed_src_duration + ) + + else: + conformed_source_range = source_range # modifiers time_scalar = 1. diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_23.976_metadata.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_23.976_metadata.json new file mode 100644 index 0000000000..af74ab4252 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_23.976_metadata.json @@ -0,0 +1,255 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "active": true, + "applieswhole": 1, + "asset": "sh020", + "audio": true, + "families": [ + "clip" + ], + "family": "plate", + "handleEnd": 8, + "handleStart": 0, + "heroTrack": true, + "hierarchy": "shots/sq001", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq001", + "shot": "sh020", + "track": "reference" + }, + "hiero_source_type": "TrackItem", + "id": "pyblish.avalon.instance", + "label": "openpypeData", + "note": "OpenPype data container", + "parents": [ + { + "entity_name": "shots", + "entity_type": "folder" + }, + { + "entity_name": "sq001", + "entity_type": "sequence" + } + ], + "publish": true, + "reviewTrack": null, + "sourceResolution": false, + "subset": "plateP01", + "variant": "Main", + "workfileFrameStart": 1001 + }, + "name": "sh020", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 51.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + }, + "effects": [], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "active": true, + "applieswhole": 1, + "asset": "sh020", + "audio": true, + "families": [ + "clip" + ], + "family": "plate", + "handleEnd": 8, + "handleStart": 0, + "heroTrack": true, + "hierarchy": "shots/sq001", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq001", + "shot": "sh020", + "track": "reference" + }, + "hiero_source_type": "TrackItem", + "id": "pyblish.avalon.instance", + "label": "openpypeData", + "note": "OpenPype data container", + "parents": [ + { + "entity_name": "shots", + "entity_type": "folder" + }, + { + "entity_name": "sq001", + "entity_type": "sequence" + } + ], + "publish": true, + "reviewTrack": null, + "sourceResolution": false, + "subset": "plateP01", + "variant": "Main", + "workfileFrameStart": 1001 + }, + "name": "openpypeData", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + }, + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": 1, + "family": "task", + "hiero_source_type": "TrackItem", + "label": "comp", + "note": "Compositing", + "type": "Compositing" + }, + "name": "comp", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "ACES - ACES2065-1", + "foundry.source.duration": "59", + "foundry.source.filename": "MER_sq001_sh020_P01.%04d.exr 997-1055", + "foundry.source.filesize": "", + "foundry.source.fragments": "59", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01/MER_sq001_sh020_P01.%04d.exr 997-1055", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "MER_sq001_sh020_P01.%04d.exr 997-1055", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "997", + "foundry.source.timecode": "172800", + "foundry.source.umid": "1bf7437a-b446-440c-07c5-7cae7acf4f5e", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "ACES - ACES2065-1", + "foundry.timeline.duration": "59", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAMAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "8", + "media.exr.compressionName": "DWAA", + "media.exr.dataWindow": "0,0,1919,1079", + "media.exr.displayWindow": "0,0,1919,1079", + "media.exr.dwaCompressionLevel": "90", + "media.exr.lineOrder": "0", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2022-04-21 11:56:03", + "media.input.filename": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01/MER_sq001_sh020_P01.0997.exr", + "media.input.filereader": "exr", + "media.input.filesize": "1235182", + "media.input.frame": "1", + "media.input.frame_rate": "23.976", + "media.input.height": "1080", + "media.input.mtime": "2022-03-06 10:14:41", + "media.input.timecode": "02:00:00:00", + "media.input.width": "1920", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "ffffffffffffffff", + "media.nuke.version": "12.2v3", + "openpype.source.colourtransform": "ACES - ACES2065-1", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 59.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 997.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01\\", + "name_prefix": "MER_sq001_sh020_P01.", + "name_suffix": ".exr", + "start_frame": 997, + "frame_step": 1, + "rate": 23.976, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index e5f0d335b5..7f9256c6d8 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -166,3 +166,24 @@ def test_img_sequence_relative_source_range(): "legacy_img_sequence.json", expected_data ) + +def test_img_sequence_conform_to_23_976fps(): + """ + Img sequence clip + available files = 997-1047 23.976fps + source_range = 997-1055 23.976024627685547fps + """ + expected_data = { + 'mediaIn': 997, + 'mediaOut': 1047, + 'handleStart': 0, + 'handleEnd': 8, + 'speed': 1.0 + } + + _check_expected_retimed_values( + "img_seq_23.976_metadata.json", + expected_data, + handle_start=0, + handle_end=8, + ) From a3c9106f35dd3c5876cb216ff4a9ad4f1ba9cac4 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Oct 2024 09:07:26 -0400 Subject: [PATCH 79/85] Fix typo --- client/ayon_core/pipeline/editorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index af2a6ef88c..94b101d3d3 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -293,7 +293,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # to preserve media range # Compute new source range based on available rate. - # NSTC compatibility might introduce floating rates, when these are + # NTSC compatibility might introduce floating rates, when these are # not exactly the same (23.976 vs 23.976024627685547) # this will cause precision issue in computation. # Round to 2 decimals for comparison. From c315c96755c450849dc5af6ce25a36e7664822b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:51:46 +0200 Subject: [PATCH 80/85] print logs based on env variable --- .../tools/publisher/models/publish.py | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index ff20d8ec3e..6dfda38885 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -4,12 +4,14 @@ import logging import traceback import collections +from contextlib import contextmanager from functools import partial from typing import Optional, Dict, List, Union, Any, Iterable import arrow import pyblish.plugin +from ayon_core.lib import env_value_to_bool from ayon_core.pipeline import ( PublishValidationError, KnownPublishError, @@ -867,6 +869,10 @@ class PublishModel: def __init__(self, controller: AbstractPublisherBackend): self._controller = controller + self._log_to_console: bool = env_value_to_bool( + "AYON_PUBLISHER_PRINT_LOGS", default=False + ) + # Publishing should stop at validation stage self._publish_up_validation: bool = False self._publish_comment_is_set: bool = False @@ -917,7 +923,13 @@ def __init__(self, controller: AbstractPublisherBackend): self._log_handler: MessageHandler = MessageHandler() def reset(self): + # Allow to change behavior during process lifetime + self._log_to_console = env_value_to_bool( + "AYON_PUBLISHER_PRINT_LOGS", default=False + ) + create_context = self._controller.get_create_context() + self._publish_up_validation = False self._publish_comment_is_set = False self._publish_has_started = False @@ -1285,25 +1297,38 @@ def _publish_iterator(self) -> Iterable[partial]: self._set_progress(self._publish_max_progress) yield partial(self.stop_publish) + @contextmanager + def _log_manager(self, plugin: pyblish.api.Plugin): + root = logging.getLogger() + if not self._log_to_console: + plugin.log.propagate = False + plugin.log.addHandler(self._log_handler) + root.addHandler(self._log_handler) + + try: + if self._log_to_console: + yield None + else: + yield self._log_handler + + finally: + if not self._log_to_console: + plugin.log.propagate = True + plugin.log.removeHandler(self._log_handler) + root.removeHandler(self._log_handler) + self._log_handler.clear_records() + def _process_and_continue( self, plugin: pyblish.api.Plugin, instance: pyblish.api.Instance ): - root = logging.getLogger() - self._log_handler.clear_records() - plugin.log.propagate = False - plugin.log.addHandler(self._log_handler) - root.addHandler(self._log_handler) - try: + with self._log_manager(plugin) as log_handler: result = pyblish.plugin.process( plugin, self._publish_context, instance ) - result["records"] = self._log_handler.records - finally: - plugin.log.propagate = True - plugin.log.removeHandler(self._log_handler) - root.removeHandler(self._log_handler) + if log_handler is not None: + result["records"] = log_handler.records exception = result.get("error") if exception: From 412b4b8d3a558449d2d29ef3353cef22dcddfc71 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Oct 2024 10:18:12 -0400 Subject: [PATCH 81/85] Address feedback from PR. --- client/ayon_core/pipeline/editorial.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 94b101d3d3..a49a981d2a 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -293,10 +293,13 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # to preserve media range # Compute new source range based on available rate. + + # Backward-compatibility for Hiero OTIO exporter. # NTSC compatibility might introduce floating rates, when these are # not exactly the same (23.976 vs 23.976024627685547) # this will cause precision issue in computation. - # Round to 2 decimals for comparison. + # Currently round to 2 decimals for comparison, + # but this should always rescale after that. rounded_av_rate = round(available_range_rate, 2) rounded_src_rate = round(source_range.start_time.rate, 2) if rounded_av_rate != rounded_src_rate: From 162a47db60d61a80774b86b00daddcae7d34298c Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 3 Oct 2024 14:27:19 +0000 Subject: [PATCH 82/85] [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 75116c703e..e6d6c6f373 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.0.0+dev" +__version__ = "1.0.1" diff --git a/package.py b/package.py index 1466031daa..b7f74e5126 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.0+dev" +version = "1.0.1" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 4a63529c67..afb48efec3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.0+dev" +version = "1.0.1" description = "" authors = ["Ynput Team "] readme = "README.md" From d2cbdc1d2147a3fa134e651655b61ca3dc5131b4 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 3 Oct 2024 14:27:53 +0000 Subject: [PATCH 83/85] [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 e6d6c6f373..458129f367 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.0.1" +__version__ = "1.0.1+dev" diff --git a/package.py b/package.py index b7f74e5126..c059eed423 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.1" +version = "1.0.1+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index afb48efec3..0a7d0d76c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.1" +version = "1.0.1+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 820fd54a567881bc386a05e69986c7b33a6c9b12 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:39:10 +0200 Subject: [PATCH 84/85] remove pyblish exception logfrom records --- .../tools/publisher/models/publish.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index 6dfda38885..97a956b18f 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -32,17 +32,20 @@ class MessageHandler(logging.Handler): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.records = [] + self._records = [] def clear_records(self): - self.records = [] + self._records = [] def emit(self, record): try: record.msg = record.getMessage() except Exception: record.msg = str(record.msg) - self.records.append(record) + self._records.append(record) + + def get_records(self): + return self._records class PublishErrorInfo: @@ -1328,7 +1331,18 @@ def _process_and_continue( plugin, self._publish_context, instance ) if log_handler is not None: - result["records"] = log_handler.records + records = log_handler.get_records() + exception = result.get("error") + if exception is not None and records: + last_record = records[-1] + if ( + last_record.name == "pyblish.plugin" + and last_record.levelno == logging.ERROR + ): + # Remove last record made by pyblish + # - `log.exception(formatted_traceback)` + records.pop(-1) + result["records"] = records exception = result.get("error") if exception: From ceafd5b6d9e647246b71650d815f89f60828b4a4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 4 Oct 2024 02:11:42 +0200 Subject: [PATCH 85/85] Cinema4D: Open last workfile on launch --- client/ayon_core/hooks/pre_add_last_workfile_arg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hooks/pre_add_last_workfile_arg.py b/client/ayon_core/hooks/pre_add_last_workfile_arg.py index 74964e0df9..d5914c2352 100644 --- a/client/ayon_core/hooks/pre_add_last_workfile_arg.py +++ b/client/ayon_core/hooks/pre_add_last_workfile_arg.py @@ -28,7 +28,8 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "substancepainter", "aftereffects", "wrap", - "openrv" + "openrv", + "cinema4d" } launch_types = {LaunchTypes.local}