diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 7fc253b1b8..98e4b46e07 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.6.12 - 1.6.11 - 1.6.10 - 1.6.9 diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 076ee79665..b4a3e77f5a 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1232,6 +1232,14 @@ def oiio_color_convert( # Handle the different conversion cases # Source view and display are known if source_view and source_display: + color_convert_args = None + ocio_display_args = None + oiio_cmd.extend([ + "--ociodisplay:inverse=1:subimages=0", + source_display, + source_view, + ]) + if target_colorspace: # This is a two-step conversion process since there's no direct # display/view to colorspace command @@ -1241,22 +1249,25 @@ def oiio_color_convert( elif source_display != target_display or source_view != target_view: # Complete display/view pair conversion # - go through a reference space - color_convert_args = (target_display, target_view) + ocio_display_args = (target_display, target_view) else: - color_convert_args = None logger.debug( "Source and target display/view pairs are identical." " No color conversion needed." ) if color_convert_args: + # Use colorconvert for colorspace target oiio_cmd.extend([ - "--ociodisplay:inverse=1:subimages=0", - source_display, - source_view, "--colorconvert:subimages=0", *color_convert_args ]) + elif ocio_display_args: + # Use ociodisplay for display/view target + oiio_cmd.extend([ + "--ociodisplay:subimages=0", + *ocio_display_args + ]) elif target_colorspace: # Standard color space to color space conversion @@ -1281,24 +1292,6 @@ def oiio_color_convert( run_subprocess(oiio_cmd, logger=logger) -def split_cmd_args(in_args): - """Makes sure all entered arguments are separated in individual items. - - Split each argument string with " -" to identify if string contains - one or more arguments. - Args: - in_args (list): of arguments ['-n', '-d uint10'] - Returns - (list): ['-n', '-d', 'unint10'] - """ - splitted_args = [] - for arg in in_args: - if not arg.strip(): - continue - splitted_args.extend(arg.split(" ")) - return splitted_args - - def get_rescaled_command_arguments( application, input_path, diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 41241e17ca..7a4d9dda50 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -7,6 +7,7 @@ import platform import tempfile import warnings from copy import deepcopy +from dataclasses import dataclass import ayon_api @@ -26,6 +27,18 @@ from ayon_core.pipeline.load import get_representation_path_with_anatomy log = Logger.get_logger(__name__) +@dataclass +class ConfigData: + """OCIO Config to use in a certain context. + + When enabled and no path/template are set, it will be considered invalid + and will error on OCIO path not found. Enabled must be False to explicitly + allow OCIO to be disabled.""" + path: str = "" + template: str = "" + enabled: bool = True + + class CachedData: remapping = {} has_compatible_ocio_package = None @@ -710,7 +723,7 @@ def _get_config_path_from_profile_data( template_data (dict[str, Any]): Template data. Returns: - dict[str, str]: Config data with path and template. + ConfigData: Config data with path and template. """ template = profile[profile_type] result = StringTemplate.format_strict_template( @@ -719,12 +732,12 @@ def _get_config_path_from_profile_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 ConfigData() # Return invalid config data - return { - "path": normalized_path, - "template": template - } + return ConfigData( + path=normalized_path, + template=template + ) def _get_global_config_data( @@ -735,7 +748,7 @@ def _get_global_config_data( imageio_global, folder_id, log, -): +) -> ConfigData: """Get global config data. Global config from core settings is using profiles that are based on @@ -759,8 +772,7 @@ def _get_global_config_data( log (logging.Logger): Logger object. Returns: - Union[dict[str, str], None]: Config data with path and template - or None. + ConfigData: Config data with path and template. """ task_name = task_type = None @@ -779,12 +791,14 @@ def _get_global_config_data( ) if profile is None: log.info(f"No config profile matched filters {str(filter_values)}") - return None + return ConfigData(enabled=False) profile_type = profile["type"] - if profile_type in ("builtin_path", "custom_path"): + if profile_type in {"builtin_path", "custom_path"}: return _get_config_path_from_profile_data( profile, profile_type, template_data) + elif profile_type == "disabled": + return ConfigData(enabled=False) # TODO decide if this is the right name for representation repre_name = "ocioconfig" @@ -798,7 +812,7 @@ def _get_global_config_data( "Colorspace OCIO config path cannot be set. " "Profile is set to published product but `Product name` is empty." ) - return None + return ConfigData() folder_info = template_data.get("folder") if not folder_info: @@ -819,7 +833,7 @@ def _get_global_config_data( ) if not folder_entity: log.warning(f"Folder entity '{folder_path}' was not found..") - return None + return ConfigData() folder_id = folder_entity["id"] product_entities_by_name = { @@ -855,7 +869,7 @@ def _get_global_config_data( log.info( f"Product '{product_name}' does not have available any versions." ) - return None + return ConfigData() # Find 'ocioconfig' representation entity repre_entity = ayon_api.get_representation_by_name( @@ -868,15 +882,15 @@ def _get_global_config_data( f"Representation '{repre_name}'" f" not found on product '{product_name}'." ) - return None + return ConfigData() path = get_representation_path_with_anatomy(repre_entity, anatomy) template = repre_entity["attrib"]["template"] - return { - "path": path, - "template": template, - } + return ConfigData( + path=path, + template=template + ) def get_imageio_config_preset( @@ -1015,13 +1029,19 @@ def get_imageio_config_preset( host_ocio_config["filepath"], template_data ) - if not config_data: + if not config_data.enabled: + return {} # OCIO management disabled + + if not config_data.path: raise FileExistsError( "No OCIO config found in settings. It is" " either missing or there is typo in path inputs" ) - return config_data + return { + "path": config_data.path, + "template": config_data.template, + } def _get_host_config_data(templates, template_data): diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index ecffa4a340..5596cec0ce 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -41,8 +41,8 @@ def get_product_name_template( profiles = tools_settings["creator"]["product_name_profiles"] filtering_criteria = { "product_types": product_type, - "hosts": host_name, - "tasks": task_name, + "host_names": host_name, + "task_names": task_name, "task_types": task_type } diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 265d79b53e..a58cecf68c 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -253,6 +253,19 @@ def create_skeleton_instance( "reuseLastVersion": data.get("reuseLastVersion", False), } + # Pass on the OCIO metadata of what the source display and view are + # so that the farm can correctly set up color management. + if "sceneDisplay" in data and "sceneView" in data: + instance_skeleton_data["sceneDisplay"] = data["sceneDisplay"] + instance_skeleton_data["sceneView"] = data["sceneView"] + elif "colorspaceDisplay" in data and "colorspaceView" in data: + # Backwards compatibility for sceneDisplay and sceneView + instance_skeleton_data["colorspaceDisplay"] = data["colorspaceDisplay"] + instance_skeleton_data["colorspaceView"] = data["colorspaceView"] + if "sourceDisplay" in data and "sourceView" in data: + instance_skeleton_data["sourceDisplay"] = data["sourceDisplay"] + instance_skeleton_data["sourceView"] = data["sourceView"] + if data.get("renderlayer"): instance_skeleton_data["renderlayer"] = data["renderlayer"] @@ -589,7 +602,6 @@ def create_instances_for_aov( """ # we cannot attach AOVs to other products as we consider every # AOV product of its own. - log = Logger.get_logger("farm_publishing") # if there are product to attach to and more than one AOV, @@ -612,8 +624,8 @@ def create_instances_for_aov( additional_data.update({ "colorspaceConfig": colorspace_config, # Display/View are optional - "display": instance.data.get("colorspaceDisplay"), - "view": instance.data.get("colorspaceView") + "display": instance.data.get("sourceDisplay"), + "view": instance.data.get("sourceView") }) # Get templated path from absolute config path. diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index c6efbebcaf..c2dcc89cd5 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -816,7 +816,22 @@ def replace_with_published_scene_path(instance, replace_in_path=True): template_data["comment"] = None anatomy = instance.context.data["anatomy"] - template = anatomy.get_template_item("publish", "default", "path") + project_name = anatomy.project_name + task_name = task_type = None + task_entity = instance.data.get("taskEntity") + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] + project_settings = instance.context.data["project_settings"] + template_name = get_publish_template_name( + project_name=project_name, + host_name=instance.context.data["hostName"], + product_type=workfile_instance.data["productType"], + task_name=task_name, + task_type=task_type, + project_settings=project_settings, + ) + template = anatomy.get_template_item("publish", template_name, "path") template_filled = template.format_strict(template_data) file_path = os.path.normpath(template_filled) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 1a2c85e597..b293bd29c3 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -87,15 +87,19 @@ class ExtractOIIOTranscode(publish.Extractor): profile_output_defs = profile["outputs"] new_representations = [] repres = instance.data["representations"] - for idx, repre in enumerate(list(repres)): - # target space, display and view might be defined upstream - # TODO: address https://github.com/ynput/ayon-core/pull/1268#discussion_r2156555474 - # Implement upstream logic to handle target_colorspace, - # target_display, target_view in other DCCs - target_colorspace = False - target_display = instance.data.get("colorspaceDisplay") - target_view = instance.data.get("colorspaceView") + scene_display = instance.data.get( + "sceneDisplay", + # Backward compatibility + instance.data.get("colorspaceDisplay") + ) + scene_view = instance.data.get( + "sceneView", + # Backward compatibility + instance.data.get("colorspaceView") + ) + + for idx, repre in enumerate(list(repres)): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) if not self._repre_is_valid(repre): continue @@ -142,24 +146,18 @@ class ExtractOIIOTranscode(publish.Extractor): transcoding_type = output_def["transcoding_type"] - # NOTE: we use colorspace_data as the fallback values for - # the target colorspace. + # Set target colorspace/display/view based on transcoding type + target_colorspace = None + target_view = None + target_display = None 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")) + target_colorspace = output_def["colorspace"] elif transcoding_type == "display_view": display_view = output_def["display_view"] - target_view = ( - display_view["view"] - or colorspace_data.get("view")) - target_display = ( - display_view["display"] - or colorspace_data.get("display") - ) + # If empty values are provided in output definition, + # fallback to scene display/view that is collected from DCC + target_view = display_view["view"] or scene_view + target_display = display_view["display"] or scene_display # both could be already collected by DCC, # but could be overwritten when transcoding diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 56863921c0..16fb22524c 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -163,7 +163,8 @@ class ExtractReview(pyblish.api.InstancePlugin): "flame", "unreal", "batchdelivery", - "photoshop" + "photoshop", + "substancepainter", ] settings_category = "core" diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index d5c5aca0f2..2c4cc5aac2 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -500,7 +500,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "asset" if profile.get("contribution_target_product") == "usdAsset" else "shot") - init_as_visible = False + init_as_visible = True # Attributes logic publish_attributes = instance["publish_attributes"].get( @@ -844,6 +844,7 @@ class ExtractUSDAssetContribution(publish.Extractor): # If no existing publish of this product exists then we initialize # the layer as either a default asset or shot structure. init_type = instance.data["contribution_target_product_init"] + self.log.debug("Initializing layer as type: %s", init_type) asset_layer, payload_layer = self.init_layer( asset_name=asset_name, init_type=init_type ) @@ -925,7 +926,7 @@ class ExtractUSDAssetContribution(publish.Extractor): payload_layer.Export(payload_path, args={"format": "usda"}) self.add_relative_file(instance, payload_path) - def init_layer(self, asset_name, init_type): + def init_layer(self, asset_name: str, init_type: str): """Initialize layer if no previous version exists""" if init_type == "asset": diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index a3e1a6c939..3e6b3794b1 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.11+dev" +__version__ = "1.6.12+dev" diff --git a/client/pyproject.toml b/client/pyproject.toml index c98591b707..5ae71de18b 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -3,7 +3,6 @@ name="core" description="AYON core addon." [tool.poetry.dependencies] -python = ">=3.9.1,<3.10" markdown = "^3.4.1" clique = "1.6.*" jsonschema = "^2.6.0" diff --git a/package.py b/package.py index 62231060f0..fbf7021b8e 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.11+dev" +version = "1.6.12+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index d568edefc0..208bbec85f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.11+dev" +version = "1.6.12+dev" description = "" authors = ["Ynput Team "] readme = "README.md" diff --git a/server/settings/conversion.py b/server/settings/conversion.py index 846b91edab..757818a9ff 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -164,5 +164,6 @@ def convert_settings_overrides( ) -> dict[str, Any]: _convert_imageio_configs_0_3_1(overrides) _convert_imageio_configs_0_4_5(overrides) + _convert_imageio_configs_1_6_5(overrides) _convert_publish_plugins(overrides) return overrides diff --git a/server/settings/main.py b/server/settings/main.py index cca885303f..3bd9549116 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -59,6 +59,7 @@ def _ocio_config_profile_types(): {"value": "builtin_path", "label": "AYON built-in OCIO config"}, {"value": "custom_path", "label": "Path to OCIO config"}, {"value": "published_product", "label": "Published product"}, + {"value": "disabled", "label": "Disable OCIO management"}, ] diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index a5b84354bd..d7b794cb5b 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -456,7 +456,7 @@ class UseDisplayViewModel(BaseSettingsModel): title="Target Display", description=( "Display of the target transform. If left empty, the" - " source Display value will be used." + " scene Display value will be used." ) ) view: str = SettingsField( @@ -464,7 +464,7 @@ class UseDisplayViewModel(BaseSettingsModel): title="Target View", description=( "View of the target transform. If left empty, the" - " source View value will be used." + " scene View value will be used." ) ) @@ -1584,6 +1584,105 @@ DEFAULT_PUBLISH_VALUES = { "fill_missing_frames": "closest_existing" } ] + }, + { + "product_types": [], + "hosts": ["substancepainter"], + "task_types": [], + "outputs": [ + { + "name": "png", + "ext": "png", + "tags": [ + "ftrackreview", + "kitsureview", + "webreview" + ], + "burnins": [], + "ffmpeg_args": { + "video_filters": [], + "audio_filters": [], + "input": [], + "output": [] + }, + "filter": { + "families": [ + "render", + "review", + "ftrack" + ], + "product_names": [], + "custom_tags": [], + "single_frame_filter": "single_frame" + }, + "overscan_crop": "", + # "overscan_color": [0, 0, 0], + "overscan_color": [0, 0, 0, 0.0], + "width": 1920, + "height": 1080, + "scale_pixel_aspect": True, + "bg_color": [0, 0, 0, 0.0], + "letter_box": { + "enabled": False, + "ratio": 0.0, + "fill_color": [0, 0, 0, 1.0], + "line_thickness": 0, + "line_color": [255, 0, 0, 1.0] + }, + "fill_missing_frames": "only_rendered" + }, + { + "name": "h264", + "ext": "mp4", + "tags": [ + "burnin", + "ftrackreview", + "kitsureview", + "webreview" + ], + "burnins": [], + "ffmpeg_args": { + "video_filters": [], + "audio_filters": [], + "input": [ + "-apply_trc gamma22" + ], + "output": [ + "-pix_fmt yuv420p", + "-crf 18", + "-c:a aac", + "-b:a 192k", + "-g 1", + "-movflags faststart" + ] + }, + "filter": { + "families": [ + "render", + "review", + "ftrack" + ], + "product_names": [], + "custom_tags": [], + "single_frame_filter": "multi_frame" + }, + "overscan_crop": "", + # "overscan_color": [0, 0, 0], + "overscan_color": [0, 0, 0, 0.0], + "width": 0, + "height": 0, + "scale_pixel_aspect": True, + "bg_color": [0, 0, 0, 0.0], + "letter_box": { + "enabled": False, + "ratio": 0.0, + "fill_color": [0, 0, 0, 1.0], + "line_thickness": 0, + "line_color": [255, 0, 0, 1.0] + }, + "fill_missing_frames": "only_rendered" + } + ] } ] },