Merge branch 'develop' into enhancement/AY-1110_Deadline-enhance-url-definition

This commit is contained in:
Petr Kalis 2024-05-20 14:42:44 +02:00 committed by GitHub
commit 65b6f0e3c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 1965 additions and 1130 deletions

View file

@ -1,7 +1,7 @@
from ayon_applications import PreLaunchHook
from ayon_core.pipeline.colorspace import get_imageio_config
from ayon_core.pipeline.template_data import get_template_data_with_names
from ayon_core.pipeline.colorspace import get_imageio_config_preset
from ayon_core.pipeline.template_data import get_template_data
class OCIOEnvHook(PreLaunchHook):
@ -26,32 +26,38 @@ class OCIOEnvHook(PreLaunchHook):
def execute(self):
"""Hook entry method."""
template_data = get_template_data_with_names(
project_name=self.data["project_name"],
folder_path=self.data["folder_path"],
task_name=self.data["task_name"],
folder_entity = self.data["folder_entity"]
template_data = get_template_data(
self.data["project_entity"],
folder_entity=folder_entity,
task_entity=self.data["task_entity"],
host_name=self.host_name,
settings=self.data["project_settings"]
settings=self.data["project_settings"],
)
config_data = get_imageio_config(
project_name=self.data["project_name"],
host_name=self.host_name,
project_settings=self.data["project_settings"],
anatomy_data=template_data,
config_data = get_imageio_config_preset(
self.data["project_name"],
self.data["folder_path"],
self.data["task_name"],
self.host_name,
anatomy=self.data["anatomy"],
project_settings=self.data["project_settings"],
template_data=template_data,
env=self.launch_context.env,
folder_id=folder_entity["id"],
)
if config_data:
ocio_path = config_data["path"]
if self.host_name in ["nuke", "hiero"]:
ocio_path = ocio_path.replace("\\", "/")
self.log.info(
f"Setting OCIO environment to config path: {ocio_path}")
self.launch_context.env["OCIO"] = ocio_path
else:
if not config_data:
self.log.debug("OCIO not set or enabled")
return
ocio_path = config_data["path"]
if self.host_name in ["nuke", "hiero"]:
ocio_path = ocio_path.replace("\\", "/")
self.log.info(
f"Setting OCIO environment to config path: {ocio_path}")
self.launch_context.env["OCIO"] = ocio_path

View file

@ -58,3 +58,55 @@ class SelectInvalidAction(pyblish.api.Action):
self.log.info(
"Selecting invalid tools: %s" % ", ".join(sorted(names))
)
class SelectToolAction(pyblish.api.Action):
"""Select invalid output tool in Fusion when plug-in failed.
"""
label = "Select saver"
on = "failed" # This action is only available on a failed plug-in
icon = "search" # Icon from Awesome Icon
def process(self, context, plugin):
errored_instances = get_errored_instances_from_context(
context,
plugin=plugin,
)
# Get the invalid nodes for the plug-ins
self.log.info("Finding invalid nodes..")
tools = []
for instance in errored_instances:
tool = instance.data.get("tool")
if tool is not None:
tools.append(tool)
else:
self.log.warning(
"Plug-in returned to be invalid, "
f"but has no saver for instance {instance.name}."
)
if not tools:
# Assume relevant comp is current comp and clear selection
self.log.info("No invalid tools found.")
comp = get_current_comp()
flow = comp.CurrentFrame.FlowView
flow.Select() # No args equals clearing selection
return
# Assume a single comp
first_tool = tools[0]
comp = first_tool.Comp()
flow = comp.CurrentFrame.FlowView
flow.Select() # No args equals clearing selection
names = set()
for tool in tools:
flow.Select(tool, True)
comp.SetActiveTool(tool)
names.add(tool.Name)
self.log.info(
"Selecting invalid tools: %s" % ", ".join(sorted(names))
)

View file

@ -52,7 +52,7 @@ class CollectFusionRender(
if product_type not in ["render", "image"]:
continue
task_name = context.data["task"]
task_name = inst.data["task"]
tool = inst.data["transientData"]["tool"]
instance_families = inst.data.get("families", [])

View file

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
"""Validate if instance context is the same as publish context."""
import pyblish.api
from ayon_core.hosts.fusion.api.action import SelectToolAction
from ayon_core.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
PublishValidationError,
OptionalPyblishPluginMixin
)
class ValidateInstanceInContextFusion(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Validator to check if instance context matches context of publish.
When working in per-shot style you always publish data in context of
current asset (shot). This validator checks if this is so. It is optional
so it can be disabled when needed.
"""
# Similar to maya and houdini-equivalent `ValidateInstanceInContext`
order = ValidateContentsOrder
label = "Instance in same Context"
optional = True
hosts = ["fusion"]
actions = [SelectToolAction, RepairAction]
def process(self, instance):
if not self.is_active(instance.data):
return
instance_context = self.get_context(instance.data)
context = self.get_context(instance.context.data)
if instance_context != context:
context_label = "{} > {}".format(*context)
instance_label = "{} > {}".format(*instance_context)
raise PublishValidationError(
message=(
"Instance '{}' publishes to different asset than current "
"context: {}. Current context: {}".format(
instance.name, instance_label, context_label
)
),
description=(
"## Publishing to a different asset\n"
"There are publish instances present which are publishing "
"into a different asset than your current context.\n\n"
"Usually this is not what you want but there can be cases "
"where you might want to publish into another asset or "
"shot. If that's the case you can disable the validation "
"on the instance to ignore it."
)
)
@classmethod
def repair(cls, instance):
create_context = instance.context.data["create_context"]
instance_id = instance.data.get("instance_id")
created_instance = create_context.get_instance_by_id(
instance_id
)
if created_instance is None:
raise RuntimeError(
f"No CreatedInstances found with id '{instance_id} "
f"in {create_context.instances_by_id}"
)
context_asset, context_task = cls.get_context(instance.context.data)
created_instance["folderPath"] = context_asset
created_instance["task"] = context_task
create_context.save_changes()
@staticmethod
def get_context(data):
"""Return asset, task from publishing context data"""
return data["folderPath"], data["task"]

View file

@ -1110,10 +1110,7 @@ def apply_colorspace_project():
'''
# backward compatibility layer
# TODO: remove this after some time
config_data = get_imageio_config(
project_name=get_current_project_name(),
host_name="hiero"
)
config_data = get_current_context_imageio_config_preset()
if config_data:
presets.update({

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- OPMenu Stencil.
It's used to extend the OPMenu.
-->
<menuDocument>
<menu>
<!-- Operator type and asset options. -->
<subMenu id="opmenu.vhda_options_create">
<insertBefore>opmenu.unsynchronize</insertBefore>
<scriptItem id="opmenu.vhda_create_ayon">
<insertAfter>opmenu.vhda_create</insertAfter>
<label>Create New (AYON)...</label>
<context>
</context>
<scriptCode>
<![CDATA[
from ayon_core.hosts.houdini.api.creator_node_shelves import create_interactive
node = kwargs["node"]
if node not in hou.selectedNodes():
node.setSelected(True)
create_interactive("io.openpype.creators.houdini.hda", **kwargs)
]]>
</scriptCode>
</scriptItem>
</subMenu>
</menu>
</menuDocument>

View file

@ -369,12 +369,8 @@ def reset_colorspace():
"""
if int(get_max_version()) < 2024:
return
project_name = get_current_project_name()
colorspace_mgr = rt.ColorPipelineMgr
project_settings = get_project_settings(project_name)
max_config_data = colorspace.get_imageio_config(
project_name, "max", project_settings)
max_config_data = colorspace.get_current_context_imageio_config_preset()
if max_config_data:
ocio_config_path = max_config_data["path"]
colorspace_mgr = rt.ColorPipelineMgr
@ -389,10 +385,7 @@ def check_colorspace():
"because Max main window can't be found.")
if int(get_max_version()) >= 2024:
color_mgr = rt.ColorPipelineMgr
project_name = get_current_project_name()
project_settings = get_project_settings(project_name)
max_config_data = colorspace.get_imageio_config(
project_name, "max", project_settings)
max_config_data = colorspace.get_current_context_imageio_config_preset()
if max_config_data and color_mgr.Mode != rt.Name("OCIO_Custom"):
if not is_headless():
from ayon_core.tools.utils import SimplePopup

View file

@ -52,11 +52,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
self._has_been_setup = True
def context_setting():
return lib.set_context_setting()
rt.callbacks.addScript(rt.Name('systemPostNew'),
context_setting)
rt.callbacks.addScript(rt.Name('systemPostNew'), on_new)
rt.callbacks.addScript(rt.Name('filePostOpen'),
lib.check_colorspace)
@ -163,6 +159,14 @@ def ls() -> list:
yield lib.read(container)
def on_new():
lib.set_context_setting()
if rt.checkForSave():
rt.resetMaxFile(rt.Name("noPrompt"))
rt.clearUndoBuffer()
rt.redrawViews()
def containerise(name: str, nodes: list, context,
namespace=None, loader=None, suffix="_CON"):
data = {

View file

@ -47,7 +47,7 @@ class FBXExtractor:
"smoothMesh": bool,
"instances": bool,
# "referencedContainersContent": bool, # deprecated in Maya 2016+
"bakeComplexAnimation": int,
"bakeComplexAnimation": bool,
"bakeComplexStart": int,
"bakeComplexEnd": int,
"bakeComplexStep": int,
@ -59,6 +59,7 @@ class FBXExtractor:
"constraints": bool,
"lights": bool,
"embeddedTextures": bool,
"includeChildren": bool,
"inputConnections": bool,
"upAxis": str, # x, y or z,
"triangulate": bool,
@ -102,6 +103,7 @@ class FBXExtractor:
"constraints": False,
"lights": True,
"embeddedTextures": False,
"includeChildren": True,
"inputConnections": True,
"upAxis": "y",
"triangulate": False,

View file

@ -8,7 +8,7 @@ from ayon_core.pipeline import (
from ayon_core.pipeline.load.utils import get_representation_path_from_context
from ayon_core.pipeline.colorspace import (
get_imageio_file_rules_colorspace_from_filepath,
get_imageio_config,
get_current_context_imageio_config_preset,
get_imageio_file_rules
)
from ayon_core.settings import get_project_settings
@ -270,8 +270,7 @@ class FileNodeLoader(load.LoaderPlugin):
host_name = get_current_host_name()
project_settings = get_project_settings(project_name)
config_data = get_imageio_config(
project_name, host_name,
config_data = get_current_context_imageio_config_preset(
project_settings=project_settings
)

View file

@ -35,7 +35,8 @@ class ExtractFBXAnimation(publish.Extractor):
fbx_exporter = fbx.FBXExtractor(log=self.log)
out_members = instance.data.get("animated_skeleton", [])
# Export
instance.data["constraints"] = True
# TODO: need to set up the options for users to set up
# the flags they intended to export
instance.data["skeletonDefinitions"] = True
instance.data["referencedAssetsContent"] = True
fbx_exporter.set_options_from_instance(instance)

View file

@ -1,71 +0,0 @@
import pyblish.api
import ayon_core.hosts.maya.api.action
from ayon_core.pipeline.publish import (
PublishValidationError,
ValidateContentsOrder,
OptionalPyblishPluginMixin
)
from maya import cmds
class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Validate all nodes in skeletonAnim_SET are referenced"""
order = ValidateContentsOrder
hosts = ["maya"]
families = ["animation.fbx"]
label = "Animated Reference Rig"
accepted_controllers = ["transform", "locator"]
actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction]
optional = False
def process(self, instance):
if not self.is_active(instance.data):
return
animated_sets = instance.data.get("animated_skeleton", [])
if not animated_sets:
self.log.debug(
"No nodes found in skeletonAnim_SET. "
"Skipping validation of animated reference rig..."
)
return
for animated_reference in animated_sets:
is_referenced = cmds.referenceQuery(
animated_reference, isNodeReferenced=True)
if not bool(is_referenced):
raise PublishValidationError(
"All the content in skeletonAnim_SET"
" should be referenced nodes"
)
invalid_controls = self.validate_controls(animated_sets)
if invalid_controls:
raise PublishValidationError(
"All the content in skeletonAnim_SET"
" should be transforms"
)
@classmethod
def validate_controls(self, set_members):
"""Check if the controller set contains only accepted node types.
Checks if all its set members are within the hierarchy of the root
Checks if the node types of the set members valid
Args:
set_members: list of nodes of the skeleton_anim_set
hierarchy: list of nodes which reside under the root node
Returns:
errors (list)
"""
# Validate control types
invalid = []
set_members = cmds.ls(set_members, long=True)
for node in set_members:
if cmds.nodeType(node) not in self.accepted_controllers:
invalid.append(node)
return invalid

View file

@ -43,7 +43,9 @@ from ayon_core.pipeline import (
from ayon_core.pipeline.context_tools import (
get_current_context_custom_workfile_template
)
from ayon_core.pipeline.colorspace import get_imageio_config
from ayon_core.pipeline.colorspace import (
get_current_context_imageio_config_preset
)
from ayon_core.pipeline.workfile import BuildWorkfile
from . import gizmo_menu
from .constants import ASSIST
@ -1552,10 +1554,7 @@ class WorkfileSettings(object):
imageio_host (dict): host colorspace configurations
'''
config_data = get_imageio_config(
project_name=get_current_project_name(),
host_name="nuke"
)
config_data = get_current_context_imageio_config_preset()
workfile_settings = imageio_host["workfile"]
color_management = workfile_settings["color_management"]

View file

@ -778,6 +778,7 @@ class ExporterReviewMov(ExporterReview):
# deal with now lut defined in viewer lut
self.viewer_lut_raw = klass.viewer_lut_raw
self.write_colorspace = instance.data["colorspace"]
self.color_channels = instance.data["color_channels"]
self.name = name or "baked"
self.ext = ext or "mov"
@ -834,7 +835,7 @@ class ExporterReviewMov(ExporterReview):
self.log.info("Nodes exported...")
return path
def generate_mov(self, farm=False, **kwargs):
def generate_mov(self, farm=False, delete=True, **kwargs):
# colorspace data
colorspace = None
# get colorspace settings
@ -947,6 +948,8 @@ class ExporterReviewMov(ExporterReview):
self.log.debug("Path: {}".format(self.path))
write_node["file"].setValue(str(self.path))
write_node["file_type"].setValue(str(self.ext))
write_node["channels"].setValue(str(self.color_channels))
# Knobs `meta_codec` and `mov64_codec` are not available on centos.
# TODO shouldn't this come from settings on outputs?
try:
@ -987,8 +990,13 @@ class ExporterReviewMov(ExporterReview):
self.render(write_node.name())
# ---------- generate representation data
tags = ["review", "need_thumbnail"]
if delete:
tags.append("delete")
self.get_representation_data(
tags=["review", "need_thumbnail", "delete"] + add_tags,
tags=tags + add_tags,
custom_tags=add_custom_tags,
range=True,
colorspace=colorspace

View file

@ -62,7 +62,7 @@ class LoadBackdropNodes(load.LoaderPlugin):
}
# add attributes from the version to imprint to metadata knob
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes[k]
# getting file path
@ -206,7 +206,7 @@ class LoadBackdropNodes(load.LoaderPlugin):
"colorspaceInput": colorspace,
}
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes[k]
# adding nodes to node graph

View file

@ -48,7 +48,7 @@ class AlembicCameraLoader(load.LoaderPlugin):
"frameEnd": last,
"version": version_entity["version"],
}
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes[k]
# getting file path
@ -123,7 +123,7 @@ class AlembicCameraLoader(load.LoaderPlugin):
}
# add attributes from the version to imprint to metadata knob
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes[k]
# getting file path

View file

@ -9,7 +9,8 @@ from ayon_core.pipeline import (
get_representation_path,
)
from ayon_core.pipeline.colorspace import (
get_imageio_file_rules_colorspace_from_filepath
get_imageio_file_rules_colorspace_from_filepath,
get_current_context_imageio_config_preset,
)
from ayon_core.hosts.nuke.api.lib import (
get_imageio_input_colorspace,
@ -197,7 +198,6 @@ class LoadClip(plugin.NukeLoader):
"frameStart",
"frameEnd",
"source",
"author",
"fps",
"handleStart",
"handleEnd",
@ -347,8 +347,7 @@ class LoadClip(plugin.NukeLoader):
"source": version_attributes.get("source"),
"handleStart": str(self.handle_start),
"handleEnd": str(self.handle_end),
"fps": str(version_attributes.get("fps")),
"author": version_attributes.get("author")
"fps": str(version_attributes.get("fps"))
}
last_version_entity = ayon_api.get_last_version_by_product_id(
@ -547,9 +546,10 @@ class LoadClip(plugin.NukeLoader):
f"Colorspace from representation colorspaceData: {colorspace}"
)
config_data = get_current_context_imageio_config_preset()
# check if any filerules are not applicable
new_parsed_colorspace = get_imageio_file_rules_colorspace_from_filepath( # noqa
filepath, "nuke", project_name
filepath, "nuke", project_name, config_data=config_data
)
self.log.debug(f"Colorspace new filerules: {new_parsed_colorspace}")

View file

@ -69,7 +69,6 @@ class LoadEffects(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@ -189,7 +188,6 @@ class LoadEffects(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps",
]:
data_imprint[k] = version_attributes[k]

View file

@ -69,7 +69,6 @@ class LoadEffectsInputProcess(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@ -192,7 +191,6 @@ class LoadEffectsInputProcess(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]

View file

@ -71,7 +71,6 @@ class LoadGizmo(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@ -139,7 +138,6 @@ class LoadGizmo(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]

View file

@ -73,7 +73,6 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@ -145,7 +144,6 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]

View file

@ -133,7 +133,7 @@ class LoadImage(load.LoaderPlugin):
"version": version_entity["version"],
"colorspace": colorspace,
}
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes.get(k, str(None))
r["tile_color"].setValue(int("0x4ecd25ff", 16))
@ -207,7 +207,6 @@ class LoadImage(load.LoaderPlugin):
"colorspace": version_attributes.get("colorSpace"),
"source": version_attributes.get("source"),
"fps": str(version_attributes.get("fps")),
"author": version_attributes.get("author")
}
# change color of node

View file

@ -47,7 +47,7 @@ class AlembicModelLoader(load.LoaderPlugin):
"version": version_entity["version"]
}
# add attributes from the version to imprint to metadata knob
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes[k]
# getting file path
@ -130,7 +130,7 @@ class AlembicModelLoader(load.LoaderPlugin):
}
# add additional metadata from the version to imprint to Avalon knob
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes[k]
# getting file path

View file

@ -55,7 +55,6 @@ class LinkAsGroup(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@ -131,7 +130,6 @@ class LinkAsGroup(load.LoaderPlugin):
"colorspace": version_attributes.get("colorSpace"),
"source": version_attributes.get("source"),
"fps": version_attributes.get("fps"),
"author": version_attributes.get("author")
}
# Update the imprinted representation

View file

@ -153,6 +153,9 @@ class CollectNukeWrites(pyblish.api.InstancePlugin,
# Determine defined file type
ext = write_node["file_type"].value()
# determine defined channel type
color_channels = write_node["channels"].value()
# get frame range data
handle_start = instance.context.data["handleStart"]
handle_end = instance.context.data["handleEnd"]
@ -172,7 +175,8 @@ class CollectNukeWrites(pyblish.api.InstancePlugin,
"path": write_file_path,
"outputDir": output_dir,
"ext": ext,
"colorspace": colorspace
"colorspace": colorspace,
"color_channels": color_channels
})
if product_type == "render":

View file

@ -136,11 +136,16 @@ class ExtractReviewIntermediates(publish.Extractor):
self, instance, o_name, o_data["extension"],
multiple_presets)
o_data["add_custom_tags"].append("intermediate")
delete = not o_data.get("publish", False)
if instance.data.get("farm"):
if "review" in instance.data["families"]:
instance.data["families"].remove("review")
data = exporter.generate_mov(farm=True, **o_data)
data = exporter.generate_mov(
farm=True, delete=delete, **o_data
)
self.log.debug(
"_ data: {}".format(data))
@ -154,7 +159,7 @@ class ExtractReviewIntermediates(publish.Extractor):
"bakeWriteNodeName": data.get("bakeWriteNodeName")
})
else:
data = exporter.generate_mov(**o_data)
data = exporter.generate_mov(delete=delete, **o_data)
# add representation generated by exporter
generated_repres.extend(data["representations"])

View file

@ -156,14 +156,9 @@ This creator publishes color space look file (LUT).
]
def apply_settings(self, project_settings):
host = self.create_context.host
host_name = host.name
project_name = host.get_current_project_name()
config_data = colorspace.get_imageio_config(
project_name, host_name,
config_data = colorspace.get_current_context_imageio_config_preset(
project_settings=project_settings
)
if not config_data:
self.enabled = False
return

View file

@ -0,0 +1,96 @@
from pathlib import Path
from ayon_core.pipeline import (
CreatedInstance,
)
from ayon_core.lib.attribute_definitions import (
FileDef,
BoolDef,
TextDef,
)
from ayon_core.hosts.traypublisher.api.plugin import TrayPublishCreator
class EditorialPackageCreator(TrayPublishCreator):
"""Creates instance for OTIO file from published folder.
Folder contains OTIO file and exported .mov files. Process should publish
whole folder as single `editorial_pckg` product type and (possibly) convert
.mov files into different format and copy them into `publish` `resources`
subfolder.
"""
identifier = "editorial_pckg"
label = "Editorial package"
product_type = "editorial_pckg"
description = "Publish folder with OTIO file and resources"
# Position batch creator after simple creators
order = 120
conversion_enabled = False
def apply_settings(self, project_settings):
self.conversion_enabled = (
project_settings["traypublisher"]
["publish"]
["ExtractEditorialPckgConversion"]
["conversion_enabled"]
)
def get_icon(self):
return "fa.folder"
def create(self, product_name, instance_data, pre_create_data):
folder_path = pre_create_data.get("folder_path")
if not folder_path:
return
instance_data["creator_attributes"] = {
"folder_path": (Path(folder_path["directory"]) /
Path(folder_path["filenames"][0])).as_posix(),
"conversion_enabled": pre_create_data["conversion_enabled"]
}
# Create new instance
new_instance = CreatedInstance(self.product_type, product_name,
instance_data, self)
self._store_new_instance(new_instance)
def get_pre_create_attr_defs(self):
# Use same attributes as for instance attributes
return [
FileDef(
"folder_path",
folders=True,
single_item=True,
extensions=[],
allow_sequences=False,
label="Folder path"
),
BoolDef("conversion_enabled",
tooltip="Convert to output defined in Settings.",
default=self.conversion_enabled,
label="Convert resources"),
]
def get_instance_attr_defs(self):
return [
TextDef(
"folder_path",
label="Folder path",
disabled=True
),
BoolDef("conversion_enabled",
tooltip="Convert to output defined in Settings.",
label="Convert resources"),
]
def get_detail_description(self):
return """# Publish folder with OTIO file and video clips
Folder contains OTIO file and exported .mov files. Process should
publish whole folder as single `editorial_pckg` product type and
(possibly) convert .mov files into different format and copy them into
`publish` `resources` subfolder.
"""

View file

@ -0,0 +1,58 @@
"""Produces instance.data["editorial_pckg"] data used during integration.
Requires:
instance.data["creator_attributes"]["path"] - from creator
Provides:
instance -> editorial_pckg (dict):
folder_path (str)
otio_path (str) - from dragged folder
resource_paths (list)
"""
import os
import pyblish.api
from ayon_core.lib.transcoding import VIDEO_EXTENSIONS
class CollectEditorialPackage(pyblish.api.InstancePlugin):
"""Collects path to OTIO file and resources"""
label = "Collect Editorial Package"
order = pyblish.api.CollectorOrder - 0.1
hosts = ["traypublisher"]
families = ["editorial_pckg"]
def process(self, instance):
folder_path = instance.data["creator_attributes"]["folder_path"]
if not folder_path or not os.path.exists(folder_path):
self.log.info((
"Instance doesn't contain collected existing folder path."
))
return
instance.data["editorial_pckg"] = {}
instance.data["editorial_pckg"]["folder_path"] = folder_path
otio_path, resource_paths = (
self._get_otio_and_resource_paths(folder_path))
instance.data["editorial_pckg"]["otio_path"] = otio_path
instance.data["editorial_pckg"]["resource_paths"] = resource_paths
def _get_otio_and_resource_paths(self, folder_path):
otio_path = None
resource_paths = []
file_names = os.listdir(folder_path)
for filename in file_names:
_, ext = os.path.splitext(filename)
file_path = os.path.join(folder_path, filename)
if ext == ".otio":
otio_path = file_path
elif ext in VIDEO_EXTENSIONS:
resource_paths.append(file_path)
return otio_path, resource_paths

View file

@ -1,10 +1,7 @@
import pyblish.api
from ayon_core.pipeline import (
publish,
registered_host
)
from ayon_core.lib import EnumDef
from ayon_core.pipeline import colorspace
from ayon_core.pipeline import publish
from ayon_core.pipeline.publish import KnownPublishError
@ -19,9 +16,10 @@ class CollectColorspace(pyblish.api.InstancePlugin,
families = ["render", "plate", "reference", "image", "online"]
enabled = False
colorspace_items = [
default_colorspace_items = [
(None, "Don't override")
]
colorspace_items = list(default_colorspace_items)
colorspace_attr_show = False
config_items = None
@ -69,14 +67,13 @@ class CollectColorspace(pyblish.api.InstancePlugin,
@classmethod
def apply_settings(cls, project_settings):
host = registered_host()
host_name = host.name
project_name = host.get_current_project_name()
config_data = colorspace.get_imageio_config(
project_name, host_name,
config_data = colorspace.get_current_context_imageio_config_preset(
project_settings=project_settings
)
enabled = False
colorspace_items = list(cls.default_colorspace_items)
config_items = None
if config_data:
filepath = config_data["path"]
config_items = colorspace.get_ocio_config_colorspaces(filepath)
@ -85,9 +82,11 @@ class CollectColorspace(pyblish.api.InstancePlugin,
include_aliases=True,
include_roles=True
)
cls.config_items = config_items
cls.colorspace_items.extend(labeled_colorspaces)
cls.enabled = True
colorspace_items.extend(labeled_colorspaces)
cls.config_items = config_items
cls.colorspace_items = colorspace_items
cls.enabled = enabled
@classmethod
def get_attribute_defs(cls):

View file

@ -0,0 +1,232 @@
import copy
import os.path
import subprocess
import opentimelineio
import pyblish.api
from ayon_core.lib import get_ffmpeg_tool_args, run_subprocess
from ayon_core.pipeline import publish
class ExtractEditorialPckgConversion(publish.Extractor):
"""Replaces movie paths in otio file with publish rootless
Prepares movie resources for integration (adds them to `transfers`).
Converts .mov files according to output definition.
"""
label = "Extract Editorial Package"
order = pyblish.api.ExtractorOrder - 0.45
hosts = ["traypublisher"]
families = ["editorial_pckg"]
def process(self, instance):
editorial_pckg_data = instance.data.get("editorial_pckg")
otio_path = editorial_pckg_data["otio_path"]
otio_basename = os.path.basename(otio_path)
staging_dir = self.staging_dir(instance)
editorial_pckg_repre = {
'name': "editorial_pckg",
'ext': "otio",
'files': otio_basename,
"stagingDir": staging_dir,
}
otio_staging_path = os.path.join(staging_dir, otio_basename)
instance.data["representations"].append(editorial_pckg_repre)
publish_resource_folder = self._get_publish_resource_folder(instance)
resource_paths = editorial_pckg_data["resource_paths"]
transfers = self._get_transfers(resource_paths,
publish_resource_folder)
project_settings = instance.context.data["project_settings"]
output_def = (project_settings["traypublisher"]
["publish"]
["ExtractEditorialPckgConversion"]
["output"])
conversion_enabled = (instance.data["creator_attributes"]
["conversion_enabled"])
if conversion_enabled and output_def["ext"]:
transfers = self._convert_resources(output_def, transfers)
instance.data["transfers"] = transfers
source_to_rootless = self._get_resource_path_mapping(instance,
transfers)
otio_data = editorial_pckg_data["otio_data"]
otio_data = self._replace_target_urls(otio_data, source_to_rootless)
opentimelineio.adapters.write_to_file(otio_data, otio_staging_path)
self.log.info("Added Editorial Package representation: {}".format(
editorial_pckg_repre))
def _get_publish_resource_folder(self, instance):
"""Calculates publish folder and create it."""
publish_path = self._get_published_path(instance)
publish_folder = os.path.dirname(publish_path)
publish_resource_folder = os.path.join(publish_folder, "resources")
if not os.path.exists(publish_resource_folder):
os.makedirs(publish_resource_folder, exist_ok=True)
return publish_resource_folder
def _get_resource_path_mapping(self, instance, transfers):
"""Returns dict of {source_mov_path: rootless_published_path}."""
replace_paths = {}
anatomy = instance.context.data["anatomy"]
for source, destination in transfers:
rootless_path = self._get_rootless(anatomy, destination)
source_file_name = os.path.basename(source)
replace_paths[source_file_name] = rootless_path
return replace_paths
def _get_transfers(self, resource_paths, publish_resource_folder):
"""Returns list of tuples (source, destination) with movie paths."""
transfers = []
for res_path in resource_paths:
res_basename = os.path.basename(res_path)
pub_res_path = os.path.join(publish_resource_folder, res_basename)
transfers.append((res_path, pub_res_path))
return transfers
def _replace_target_urls(self, otio_data, replace_paths):
"""Replace original movie paths with published rootless ones."""
for track in otio_data.tracks:
for clip in track:
# Check if the clip has a media reference
if clip.media_reference is not None:
# Access the target_url from the media reference
target_url = clip.media_reference.target_url
if not target_url:
continue
file_name = os.path.basename(target_url)
replace_path = replace_paths.get(file_name)
if replace_path:
clip.media_reference.target_url = replace_path
if clip.name == file_name:
clip.name = os.path.basename(replace_path)
return otio_data
def _get_rootless(self, anatomy, path):
"""Try to find rootless {root[work]} path from `path`"""
success, rootless_path = anatomy.find_root_template_from_path(
path)
if not success:
# `rootless_path` is not set to `output_dir` if none of roots match
self.log.warning(
f"Could not find root path for remapping '{path}'."
)
rootless_path = path
return rootless_path
def _get_published_path(self, instance):
"""Calculates expected `publish` folder"""
# determine published path from Anatomy.
template_data = instance.data.get("anatomyData")
rep = instance.data["representations"][0]
template_data["representation"] = rep.get("name")
template_data["ext"] = rep.get("ext")
template_data["comment"] = None
anatomy = instance.context.data["anatomy"]
template_data["root"] = anatomy.roots
template = anatomy.get_template_item("publish", "default", "path")
template_filled = template.format_strict(template_data)
return os.path.normpath(template_filled)
def _convert_resources(self, output_def, transfers):
"""Converts all resource files to configured format."""
out_extension = output_def["ext"]
if not out_extension:
self.log.warning("No output extension configured in "
"ayon+settings://traypublisher/publish/ExtractEditorialPckgConversion") # noqa
return transfers
final_transfers = []
out_def_ffmpeg_args = output_def["ffmpeg_args"]
ffmpeg_input_args = [
value.strip()
for value in out_def_ffmpeg_args["input"]
if value.strip()
]
ffmpeg_video_filters = [
value.strip()
for value in out_def_ffmpeg_args["video_filters"]
if value.strip()
]
ffmpeg_audio_filters = [
value.strip()
for value in out_def_ffmpeg_args["audio_filters"]
if value.strip()
]
ffmpeg_output_args = [
value.strip()
for value in out_def_ffmpeg_args["output"]
if value.strip()
]
ffmpeg_input_args = self._split_ffmpeg_args(ffmpeg_input_args)
generic_args = [
subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg"))
]
generic_args.extend(ffmpeg_input_args)
if ffmpeg_video_filters:
generic_args.append("-filter:v")
generic_args.append(
"\"{}\"".format(",".join(ffmpeg_video_filters)))
if ffmpeg_audio_filters:
generic_args.append("-filter:a")
generic_args.append(
"\"{}\"".format(",".join(ffmpeg_audio_filters)))
for source, destination in transfers:
base_name = os.path.basename(destination)
file_name, ext = os.path.splitext(base_name)
dest_path = os.path.join(os.path.dirname(destination),
f"{file_name}.{out_extension}")
final_transfers.append((source, dest_path))
all_args = copy.deepcopy(generic_args)
all_args.append(f"-i \"{source}\"")
all_args.extend(ffmpeg_output_args) # order matters
all_args.append(f"\"{dest_path}\"")
subprcs_cmd = " ".join(all_args)
# run subprocess
self.log.debug("Executing: {}".format(subprcs_cmd))
run_subprocess(subprcs_cmd, shell=True, logger=self.log)
return final_transfers
def _split_ffmpeg_args(self, 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.
"""
splitted_args = []
for arg in in_args:
sub_args = arg.split(" -")
if len(sub_args) == 1:
if arg and arg not in splitted_args:
splitted_args.append(arg)
continue
for idx, arg in enumerate(sub_args):
if idx != 0:
arg = "-" + arg
if arg and arg not in splitted_args:
splitted_args.append(arg)
return splitted_args

View file

@ -0,0 +1,68 @@
import os
import opentimelineio
import pyblish.api
from ayon_core.pipeline import PublishValidationError
class ValidateEditorialPackage(pyblish.api.InstancePlugin):
"""Checks that published folder contains all resources from otio
Currently checks only by file names and expects flat structure.
It ignores path to resources in otio file as folder might be dragged in and
published from different location than it was created.
"""
label = "Validate Editorial Package"
order = pyblish.api.ValidatorOrder - 0.49
hosts = ["traypublisher"]
families = ["editorial_pckg"]
def process(self, instance):
editorial_pckg_data = instance.data.get("editorial_pckg")
if not editorial_pckg_data:
raise PublishValidationError("Editorial package not collected")
folder_path = editorial_pckg_data["folder_path"]
otio_path = editorial_pckg_data["otio_path"]
if not otio_path:
raise PublishValidationError(
f"Folder {folder_path} missing otio file")
resource_paths = editorial_pckg_data["resource_paths"]
resource_file_names = {os.path.basename(path)
for path in resource_paths}
otio_data = opentimelineio.adapters.read_from_file(otio_path)
target_urls = self._get_all_target_urls(otio_data)
missing_files = set()
for target_url in target_urls:
target_basename = os.path.basename(target_url)
if target_basename not in resource_file_names:
missing_files.add(target_basename)
if missing_files:
raise PublishValidationError(
f"Otio file contains missing files `{missing_files}`.\n\n"
f"Please add them to `{folder_path}` and republish.")
instance.data["editorial_pckg"]["otio_data"] = otio_data
def _get_all_target_urls(self, otio_data):
target_urls = []
# Iterate through tracks, clips, or other elements
for track in otio_data.tracks:
for clip in track:
# Check if the clip has a media reference
if clip.media_reference is not None:
# Access the target_url from the media reference
target_url = clip.media_reference.target_url
if target_url:
target_urls.append(target_url)
return target_urls

View file

@ -80,17 +80,21 @@ def get_engine_versions(env=None):
def get_editor_exe_path(engine_path: Path, engine_version: str) -> Path:
"""Get UE Editor executable path."""
ue_path = engine_path / "Engine/Binaries"
ue_name = "UnrealEditor"
# handle older versions of Unreal Engine
if engine_version.split(".")[0] == "4":
ue_name = "UE4Editor"
if platform.system().lower() == "windows":
if engine_version.split(".")[0] == "4":
ue_path /= "Win64/UE4Editor.exe"
elif engine_version.split(".")[0] == "5":
ue_path /= "Win64/UnrealEditor.exe"
ue_path /= f"Win64/{ue_name}.exe"
elif platform.system().lower() == "linux":
ue_path /= "Linux/UE4Editor"
ue_path /= f"Linux/{ue_name}"
elif platform.system().lower() == "darwin":
ue_path /= "Mac/UE4Editor"
ue_path /= f"Mac/{ue_name}"
return ue_path

View file

@ -7,7 +7,7 @@ from ayon_core.lib import Logger, run_subprocess, AYONSettingsRegistry
from ayon_core.lib.vendor_bin_utils import find_tool_in_custom_paths
from .rr_job import SubmitFile
from .rr_job import RRjob, SubmitterParameter # noqa F401
from .rr_job import RRJob, SubmitterParameter # noqa F401
class Api:

File diff suppressed because it is too large Load diff

View file

@ -459,36 +459,6 @@ def is_representation_from_latest(representation):
)
def get_template_data_from_session(session=None, settings=None):
"""Template data for template fill from session keys.
Args:
session (Union[Dict[str, str], None]): The Session to use. If not
provided use the currently active global Session.
settings (Optional[Dict[str, Any]]): Prepared studio or project
settings.
Returns:
Dict[str, Any]: All available data from session.
"""
if session is not None:
project_name = session["AYON_PROJECT_NAME"]
folder_path = session["AYON_FOLDER_PATH"]
task_name = session["AYON_TASK_NAME"]
host_name = session["AYON_HOST_NAME"]
else:
context = get_current_context()
project_name = context["project_name"]
folder_path = context["folder_path"]
task_name = context["task_name"]
host_name = get_current_host_name()
return get_template_data_with_names(
project_name, folder_path, task_name, host_name, settings
)
def get_current_context_template_data(settings=None):
"""Prepare template data for current context.

View file

@ -108,70 +108,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
label = "Integrate Asset"
order = pyblish.api.IntegratorOrder
families = ["workfile",
"pointcache",
"pointcloud",
"proxyAbc",
"camera",
"animation",
"model",
"maxScene",
"mayaAscii",
"mayaScene",
"setdress",
"layout",
"ass",
"assProxy",
"vdbcache",
"scene",
"vrayproxy",
"vrayscene_layer",
"render",
"prerender",
"imagesequence",
"review",
"rendersetup",
"rig",
"plate",
"look",
"ociolook",
"audio",
"yetiRig",
"yeticache",
"nukenodes",
"gizmo",
"source",
"matchmove",
"image",
"assembly",
"fbx",
"gltf",
"textures",
"action",
"harmony.template",
"harmony.palette",
"editorial",
"background",
"camerarig",
"redshiftproxy",
"effect",
"xgen",
"hda",
"usd",
"staticMesh",
"skeletalMesh",
"mvLook",
"mvUsd",
"mvUsdComposition",
"mvUsdOverride",
"online",
"uasset",
"blendScene",
"yeticacheUE",
"tycache",
"csv_ingest_file",
"render.local.hou"
]
default_template_name = "publish"

View file

@ -1,28 +1,31 @@
"""OpenColorIO Wrapper.
Only to be interpreted by Python 3. It is run in subprocess in case
Python 2 hosts needs to use it. Or it is used as module for Python 3
processing.
Providing functionality:
- get_colorspace - console command - python 2
- returning all available color spaces
found in input config path.
- _get_colorspace_data - python 3 - module function
- returning all available colorspaces
found in input config path.
- get_views - console command - python 2
- returning all available viewers
found in input config path.
- _get_views_data - python 3 - module function
- returning all available viewers
found in input config path.
Receive OpenColorIO information and store it in JSON format for processed
that don't have access to OpenColorIO or their version of OpenColorIO is
not compatible.
"""
import click
import json
from pathlib import Path
import PyOpenColorIO as ocio
import click
from ayon_core.pipeline.colorspace import (
has_compatible_ocio_package,
get_display_view_colorspace_name,
get_config_file_rules_colorspace_from_filepath,
get_config_version_data,
get_ocio_config_views,
get_ocio_config_colorspaces,
)
def _save_output_to_json_file(output, output_path):
json_path = Path(output_path)
with open(json_path, "w") as stream:
json.dump(output, stream)
print(f"Data are saved to '{json_path}'")
@click.group()
@ -30,404 +33,185 @@ def main():
pass # noqa: WPS100
@main.group()
def config():
"""Config related commands group
Example of use:
> pyton.exe ./ocio_wrapper.py config <command> *args
"""
pass # noqa: WPS100
@main.group()
def colorspace():
"""Colorspace related commands group
Example of use:
> pyton.exe ./ocio_wrapper.py config <command> *args
"""
pass # noqa: WPS100
@config.command(
name="get_colorspace",
help=(
"return all colorspaces from config file "
"--path input arg is required"
)
)
@click.option("--in_path", required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option("--out_path", required=True,
help="path where to write output json file",
type=click.Path())
def get_colorspace(in_path, out_path):
@main.command(
name="get_ocio_config_colorspaces",
help="return all colorspaces from config file")
@click.option(
"--config_path",
required=True,
help="OCIO config path to read ocio config file.",
type=click.Path(exists=True))
@click.option(
"--output_path",
required=True,
help="path where to write output json file",
type=click.Path())
def _get_ocio_config_colorspaces(config_path, output_path):
"""Aggregate all colorspace to file.
Python 2 wrapped console command
Args:
in_path (str): config file path string
out_path (str): temp json file path string
config_path (str): config file path string
output_path (str): temp json file path string
Example of use:
> pyton.exe ./ocio_wrapper.py config get_colorspace
--in_path=<path> --out_path=<path>
--config_path <path> --output_path <path>
"""
json_path = Path(out_path)
out_data = _get_colorspace_data(in_path)
with open(json_path, "w") as f_:
json.dump(out_data, f_)
print(f"Colorspace data are saved to '{json_path}'")
def _get_colorspace_data(config_path):
"""Return all found colorspace data.
Args:
config_path (str): path string leading to config.ocio
Raises:
IOError: Input config does not exist.
Returns:
dict: aggregated available colorspaces
"""
config_path = Path(config_path)
if not config_path.is_file():
raise IOError(
f"Input path `{config_path}` should be `config.ocio` file")
config = ocio.Config().CreateFromFile(str(config_path))
colorspace_data = {
"roles": {},
"colorspaces": {
color.getName(): {
"family": color.getFamily(),
"categories": list(color.getCategories()),
"aliases": list(color.getAliases()),
"equalitygroup": color.getEqualityGroup(),
}
for color in config.getColorSpaces()
},
"displays_views": {
f"{view} ({display})": {
"display": display,
"view": view
}
for display in config.getDisplays()
for view in config.getViews(display)
},
"looks": {}
}
# add looks
looks = config.getLooks()
if looks:
colorspace_data["looks"] = {
look.getName(): {"process_space": look.getProcessSpace()}
for look in looks
}
# add roles
roles = config.getRoles()
if roles:
colorspace_data["roles"] = {
role: {"colorspace": colorspace}
for (role, colorspace) in roles
}
return colorspace_data
@config.command(
name="get_views",
help=(
"return all viewers from config file "
"--path input arg is required"
_save_output_to_json_file(
get_ocio_config_colorspaces(config_path),
output_path
)
)
@click.option("--in_path", required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option("--out_path", required=True,
help="path where to write output json file",
type=click.Path())
def get_views(in_path, out_path):
@main.command(
name="get_ocio_config_views",
help="All viewers from config file")
@click.option(
"--config_path",
required=True,
help="OCIO config path to read ocio config file.",
type=click.Path(exists=True))
@click.option(
"--output_path",
required=True,
help="path where to write output json file",
type=click.Path())
def _get_ocio_config_views(config_path, output_path):
"""Aggregate all viewers to file.
Python 2 wrapped console command
Args:
in_path (str): config file path string
out_path (str): temp json file path string
config_path (str): config file path string
output_path (str): temp json file path string
Example of use:
> pyton.exe ./ocio_wrapper.py config get_views \
--in_path=<path> --out_path=<path>
--config_path <path> --output <path>
"""
json_path = Path(out_path)
out_data = _get_views_data(in_path)
with open(json_path, "w") as f_:
json.dump(out_data, f_)
print(f"Viewer data are saved to '{json_path}'")
def _get_views_data(config_path):
"""Return all found viewer data.
Args:
config_path (str): path string leading to config.ocio
Raises:
IOError: Input config does not exist.
Returns:
dict: aggregated available viewers
"""
config_path = Path(config_path)
if not config_path.is_file():
raise IOError("Input path should be `config.ocio` file")
config = ocio.Config().CreateFromFile(str(config_path))
data_ = {}
for display in config.getDisplays():
for view in config.getViews(display):
colorspace = config.getDisplayViewColorSpaceName(display, view)
# Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa
if colorspace == "<USE_DISPLAY_NAME>":
colorspace = display
data_[f"{display}/{view}"] = {
"display": display,
"view": view,
"colorspace": colorspace
}
return data_
@config.command(
name="get_version",
help=(
"return major and minor version from config file "
"--config_path input arg is required"
"--out_path input arg is required"
_save_output_to_json_file(
get_ocio_config_views(config_path),
output_path
)
)
@click.option("--config_path", required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option("--out_path", required=True,
help="path where to write output json file",
type=click.Path())
def get_version(config_path, out_path):
"""Get version of config.
Python 2 wrapped console command
@main.command(
name="get_config_version_data",
help="Get major and minor version from config file")
@click.option(
"--config_path",
required=True,
help="OCIO config path to read ocio config file.",
type=click.Path(exists=True))
@click.option(
"--output_path",
required=True,
help="path where to write output json file",
type=click.Path())
def _get_config_version_data(config_path, output_path):
"""Get version of config.
Args:
config_path (str): ocio config file path string
out_path (str): temp json file path string
output_path (str): temp json file path string
Example of use:
> pyton.exe ./ocio_wrapper.py config get_version \
--config_path=<path> --out_path=<path>
--config_path <path> --output_path <path>
"""
json_path = Path(out_path)
out_data = _get_version_data(config_path)
with open(json_path, "w") as f_:
json.dump(out_data, f_)
print(f"Config version data are saved to '{json_path}'")
def _get_version_data(config_path):
"""Return major and minor version info.
Args:
config_path (str): path string leading to config.ocio
Raises:
IOError: Input config does not exist.
Returns:
dict: minor and major keys with values
"""
config_path = Path(config_path)
if not config_path.is_file():
raise IOError("Input path should be `config.ocio` file")
config = ocio.Config().CreateFromFile(str(config_path))
return {
"major": config.getMajorVersion(),
"minor": config.getMinorVersion()
}
@colorspace.command(
name="get_config_file_rules_colorspace_from_filepath",
help=(
"return colorspace from filepath "
"--config_path - ocio config file path (input arg is required) "
"--filepath - any file path (input arg is required) "
"--out_path - temp json file path (input arg is required)"
_save_output_to_json_file(
get_config_version_data(config_path),
output_path
)
)
@click.option("--config_path", required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option("--filepath", required=True,
help="path to file to get colorspace from",
type=click.Path())
@click.option("--out_path", required=True,
help="path where to write output json file",
type=click.Path())
def get_config_file_rules_colorspace_from_filepath(
config_path, filepath, out_path
@main.command(
name="get_config_file_rules_colorspace_from_filepath",
help="Colorspace file rules from filepath")
@click.option(
"--config_path",
required=True,
help="OCIO config path to read ocio config file.",
type=click.Path(exists=True))
@click.option(
"--filepath",
required=True,
help="Path to file to get colorspace from.",
type=click.Path())
@click.option(
"--output_path",
required=True,
help="Path where to write output json file.",
type=click.Path())
def _get_config_file_rules_colorspace_from_filepath(
config_path, filepath, output_path
):
"""Get colorspace from file path wrapper.
Python 2 wrapped console command
Args:
config_path (str): config file path string
filepath (str): path string leading to file
out_path (str): temp json file path string
output_path (str): temp json file path string
Example of use:
> pyton.exe ./ocio_wrapper.py \
> python.exe ./ocio_wrapper.py \
colorspace get_config_file_rules_colorspace_from_filepath \
--config_path=<path> --filepath=<path> --out_path=<path>
--config_path <path> --filepath <path> --output_path <path>
"""
json_path = Path(out_path)
colorspace = _get_config_file_rules_colorspace_from_filepath(
config_path, filepath)
with open(json_path, "w") as f_:
json.dump(colorspace, f_)
print(f"Colorspace name is saved to '{json_path}'")
_save_output_to_json_file(
get_config_file_rules_colorspace_from_filepath(config_path, filepath),
output_path
)
def _get_config_file_rules_colorspace_from_filepath(config_path, filepath):
"""Return found colorspace data found in v2 file rules.
Args:
config_path (str): path string leading to config.ocio
filepath (str): path string leading to v2 file rules
Raises:
IOError: Input config does not exist.
Returns:
dict: aggregated available colorspaces
"""
config_path = Path(config_path)
if not config_path.is_file():
raise IOError(
f"Input path `{config_path}` should be `config.ocio` file")
config = ocio.Config().CreateFromFile(str(config_path))
# TODO: use `parseColorSpaceFromString` instead if ocio v1
colorspace = config.getColorSpaceFromFilepath(str(filepath))
return colorspace
def _get_display_view_colorspace_name(config_path, display, view):
"""Returns the colorspace attribute of the (display, view) pair.
Args:
config_path (str): path string leading to config.ocio
display (str): display name e.g. "ACES"
view (str): view name e.g. "sRGB"
Raises:
IOError: Input config does not exist.
Returns:
view color space name (str) e.g. "Output - sRGB"
"""
config_path = Path(config_path)
if not config_path.is_file():
raise IOError("Input path should be `config.ocio` file")
config = ocio.Config.CreateFromFile(str(config_path))
colorspace = config.getDisplayViewColorSpaceName(display, view)
return colorspace
@config.command(
@main.command(
name="get_display_view_colorspace_name",
help=(
"return default view colorspace name "
"for the given display and view "
"--path input arg is required"
)
)
@click.option("--in_path", required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option("--out_path", required=True,
help="path where to write output json file",
type=click.Path())
@click.option("--display", required=True,
help="display name",
type=click.STRING)
@click.option("--view", required=True,
help="view name",
type=click.STRING)
def get_display_view_colorspace_name(in_path, out_path,
display, view):
"Default view colorspace name for the given display and view"
))
@click.option(
"--config_path",
required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option(
"--display",
required=True,
help="Display name",
type=click.STRING)
@click.option(
"--view",
required=True,
help="view name",
type=click.STRING)
@click.option(
"--output_path",
required=True,
help="path where to write output json file",
type=click.Path())
def _get_display_view_colorspace_name(
config_path, display, view, output_path
):
"""Aggregate view colorspace name to file.
Wrapper command for processes without access to OpenColorIO
Args:
in_path (str): config file path string
out_path (str): temp json file path string
config_path (str): config file path string
output_path (str): temp json file path string
display (str): display name e.g. "ACES"
view (str): view name e.g. "sRGB"
Example of use:
> pyton.exe ./ocio_wrapper.py config \
get_display_view_colorspace_name --in_path=<path> \
--out_path=<path> --display=<display> --view=<view>
get_display_view_colorspace_name --config_path <path> \
--output_path <path> --display <display> --view <view>
"""
_save_output_to_json_file(
get_display_view_colorspace_name(config_path, display, view),
output_path
)
out_data = _get_display_view_colorspace_name(in_path,
display,
view)
with open(out_path, "w") as f:
json.dump(out_data, f)
print(f"Display view colorspace saved to '{out_path}'")
if __name__ == '__main__':
if __name__ == "__main__":
if not has_compatible_ocio_package():
raise RuntimeError("OpenColorIO is not available.")
main()

View file

@ -335,9 +335,7 @@ class LoaderWindow(QtWidgets.QWidget):
def closeEvent(self, event):
super(LoaderWindow, self).closeEvent(event)
# Deselect project so current context will be selected
# on next 'showEvent'
self._controller.set_selected_project(None)
self._reset_on_show = True
def keyPressEvent(self, event):

View file

@ -723,7 +723,6 @@ class ProjectPushItemProcess:
dst_project_name = self._item.dst_project_name
dst_folder_id = self._item.dst_folder_id
dst_task_name = self._item.dst_task_name
dst_task_name_low = dst_task_name.lower()
new_folder_name = self._item.new_folder_name
if not dst_folder_id and not new_folder_name:
self._status.set_failed(
@ -765,7 +764,7 @@ class ProjectPushItemProcess:
dst_project_name, folder_ids=[folder_entity["id"]]
)
}
task_info = folder_tasks.get(dst_task_name_low)
task_info = folder_tasks.get(dst_task_name.lower())
if not task_info:
self._status.set_failed(
f"Could find task with name \"{dst_task_name}\""

View file

@ -16,7 +16,7 @@ aiohttp_json_rpc = "*" # TVPaint server
aiohttp-middlewares = "^2.0.0"
wsrpc_aiohttp = "^3.1.1" # websocket server
Click = "^8"
OpenTimelineIO = "0.14.1"
OpenTimelineIO = "0.16.0"
opencolorio = "2.2.1"
Pillow = "9.5.0"
pynput = "^1.7.2" # Timers manager - TODO remove

View file

@ -1,3 +1,5 @@
from typing import Any
from ayon_server.addons import BaseServerAddon
from .settings import CoreSettings, DEFAULT_VALUES
@ -9,3 +11,53 @@ class CoreAddon(BaseServerAddon):
async def get_default_settings(self):
settings_model_cls = self.get_settings_model()
return settings_model_cls(**DEFAULT_VALUES)
async def convert_settings_overrides(
self,
source_version: str,
overrides: dict[str, Any],
) -> dict[str, Any]:
self._convert_imagio_configs_0_3_1(overrides)
# Use super conversion
return await super().convert_settings_overrides(
source_version, overrides
)
def _convert_imagio_configs_0_3_1(self, overrides):
"""Imageio config settings did change to profiles since 0.3.1. ."""
imageio_overrides = overrides.get("imageio") or {}
if (
"ocio_config" not in imageio_overrides
or "filepath" not in imageio_overrides["ocio_config"]
):
return
ocio_config = imageio_overrides.pop("ocio_config")
filepath = ocio_config["filepath"]
if not filepath:
return
first_filepath = filepath[0]
ocio_config_profiles = imageio_overrides.setdefault(
"ocio_config_profiles", []
)
base_value = {
"type": "builtin_path",
"product_name": "",
"host_names": [],
"task_names": [],
"task_types": [],
"custom_path": "",
"builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio"
}
if first_filepath in (
"{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
"{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio",
):
base_value["type"] = "builtin_path"
base_value["builtin_path"] = first_filepath
else:
base_value["type"] = "custom_path"
base_value["custom_path"] = first_filepath
ocio_config_profiles.append(base_value)

View file

@ -54,9 +54,67 @@ class CoreImageIOFileRulesModel(BaseSettingsModel):
return value
class CoreImageIOConfigModel(BaseSettingsModel):
filepath: list[str] = SettingsField(
default_factory=list, title="Config path"
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"},
]
def _ocio_built_in_paths():
return [
{
"value": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
"label": "ACES 1.2",
"description": "Aces 1.2 OCIO config file."
},
{
"value": "{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio",
"label": "Nuke default",
},
]
class CoreImageIOConfigProfilesModel(BaseSettingsModel):
_layout = "expanded"
host_names: list[str] = SettingsField(
default_factory=list,
title="Host names"
)
task_types: list[str] = SettingsField(
default_factory=list,
title="Task types",
enum_resolver=task_types_enum
)
task_names: list[str] = SettingsField(
default_factory=list,
title="Task names"
)
type: str = SettingsField(
title="Profile type",
enum_resolver=_ocio_config_profile_types,
conditionalEnum=True,
default="builtin_path",
section="---",
)
builtin_path: str = SettingsField(
"ACES 1.2",
title="Built-in OCIO config",
enum_resolver=_ocio_built_in_paths,
)
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."
),
)
@ -65,9 +123,8 @@ class CoreImageIOBaseModel(BaseSettingsModel):
False,
title="Enable Color Management"
)
ocio_config: CoreImageIOConfigModel = SettingsField(
default_factory=CoreImageIOConfigModel,
title="OCIO config"
ocio_config_profiles: list[CoreImageIOConfigProfilesModel] = SettingsField(
default_factory=list, title="OCIO config profiles"
)
file_rules: CoreImageIOFileRulesModel = SettingsField(
default_factory=CoreImageIOFileRulesModel,
@ -186,12 +243,17 @@ class CoreSettings(BaseSettingsModel):
DEFAULT_VALUES = {
"imageio": {
"activate_global_color_management": False,
"ocio_config": {
"filepath": [
"{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
"{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio"
]
},
"ocio_config_profiles": [
{
"host_names": [],
"task_types": [],
"task_names": [],
"type": "builtin_path",
"builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
"custom_path": "",
"product_name": "",
}
],
"file_rules": {
"activate_global_file_rules": False,
"rules": [
@ -199,42 +261,57 @@ DEFAULT_VALUES = {
"name": "example",
"pattern": ".*(beauty).*",
"colorspace": "ACES - ACEScg",
"ext": "exr"
"ext": "exr",
}
]
}
],
},
},
"studio_name": "",
"studio_code": "",
"environments": "{\n\"STUDIO_SW\": {\n \"darwin\": \"/mnt/REPO_SW\",\n \"linux\": \"/mnt/REPO_SW\",\n \"windows\": \"P:/REPO_SW\"\n }\n}",
"environments": json.dumps(
{
"STUDIO_SW": {
"darwin": "/mnt/REPO_SW",
"linux": "/mnt/REPO_SW",
"windows": "P:/REPO_SW"
}
},
indent=4
),
"tools": DEFAULT_TOOLS_VALUES,
"version_start_category": {
"profiles": []
},
"publish": DEFAULT_PUBLISH_VALUES,
"project_folder_structure": json.dumps({
"__project_root__": {
"prod": {},
"resources": {
"footage": {
"plates": {},
"offline": {}
"project_folder_structure": json.dumps(
{
"__project_root__": {
"prod": {},
"resources": {
"footage": {
"plates": {},
"offline": {}
},
"audio": {},
"art_dept": {}
},
"audio": {},
"art_dept": {}
},
"editorial": {},
"assets": {
"characters": {},
"locations": {}
},
"shots": {}
}
}, indent=4),
"editorial": {},
"assets": {
"characters": {},
"locations": {}
},
"shots": {}
}
},
indent=4
),
"project_plugins": {
"windows": [],
"darwin": [],
"linux": []
},
"project_environments": "{}"
"project_environments": json.dumps(
{},
indent=4
)
}

View file

@ -1,3 +1,3 @@
name = "maya"
title = "Maya"
version = "0.1.18"
version = "0.1.19"

View file

@ -917,10 +917,6 @@ class PublishersModel(BaseSettingsModel):
default_factory=BasicValidateModel,
title="Validate Rig Controllers",
)
ValidateAnimatedReferenceRig: BasicValidateModel = SettingsField(
default_factory=BasicValidateModel,
title="Validate Animated Reference Rig",
)
ValidateAnimationContent: BasicValidateModel = SettingsField(
default_factory=BasicValidateModel,
title="Validate Animation Content",
@ -1447,11 +1443,6 @@ DEFAULT_PUBLISH_SETTINGS = {
"optional": True,
"active": True
},
"ValidateAnimatedReferenceRig": {
"enabled": True,
"optional": False,
"active": True
},
"ValidateAnimationContent": {
"enabled": True,
"optional": False,

View file

@ -1,3 +1,3 @@
name = "nuke"
title = "Nuke"
version = "0.1.11"
version = "0.1.12"

View file

@ -125,6 +125,7 @@ class ReformatNodesConfigModel(BaseSettingsModel):
class IntermediateOutputModel(BaseSettingsModel):
name: str = SettingsField(title="Output name")
publish: bool = SettingsField(title="Publish")
filter: BakingStreamFilterModel = SettingsField(
title="Filter", default_factory=BakingStreamFilterModel)
read_raw: bool = SettingsField(
@ -346,6 +347,7 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = {
"outputs": [
{
"name": "baking",
"publish": False,
"filter": {
"task_types": [],
"product_types": [],
@ -401,6 +403,7 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = {
"outputs": [
{
"name": "baking",
"publish": False,
"filter": {
"task_types": [],
"product_types": [],

View file

@ -1,3 +1,3 @@
name = "traypublisher"
title = "TrayPublisher"
version = "0.1.4"
version = "0.1.5"

View file

@ -1,4 +1,7 @@
from ayon_server.settings import BaseSettingsModel, SettingsField
from ayon_server.settings import (
BaseSettingsModel,
SettingsField,
)
class ValidatePluginModel(BaseSettingsModel):
@ -14,6 +17,45 @@ class ValidateFrameRangeModel(ValidatePluginModel):
'my_asset_to_publish.mov')"""
class ExtractEditorialPckgFFmpegModel(BaseSettingsModel):
video_filters: list[str] = SettingsField(
default_factory=list,
title="Video filters"
)
audio_filters: list[str] = SettingsField(
default_factory=list,
title="Audio filters"
)
input: list[str] = SettingsField(
default_factory=list,
title="Input arguments"
)
output: list[str] = SettingsField(
default_factory=list,
title="Output arguments"
)
class ExtractEditorialPckgOutputDefModel(BaseSettingsModel):
_layout = "expanded"
ext: str = SettingsField("", title="Output extension")
ffmpeg_args: ExtractEditorialPckgFFmpegModel = SettingsField(
default_factory=ExtractEditorialPckgFFmpegModel,
title="FFmpeg arguments"
)
class ExtractEditorialPckgConversionModel(BaseSettingsModel):
"""Set output definition if resource files should be converted."""
conversion_enabled: bool = SettingsField(True,
title="Conversion enabled")
output: ExtractEditorialPckgOutputDefModel = SettingsField(
default_factory=ExtractEditorialPckgOutputDefModel,
title="Output Definitions",
)
class TrayPublisherPublishPlugins(BaseSettingsModel):
CollectFrameDataFromAssetEntity: ValidatePluginModel = SettingsField(
default_factory=ValidatePluginModel,
@ -28,6 +70,13 @@ class TrayPublisherPublishPlugins(BaseSettingsModel):
default_factory=ValidatePluginModel,
)
ExtractEditorialPckgConversion: ExtractEditorialPckgConversionModel = (
SettingsField(
default_factory=ExtractEditorialPckgConversionModel,
title="Extract Editorial Package Conversion"
)
)
DEFAULT_PUBLISH_PLUGINS = {
"CollectFrameDataFromAssetEntity": {
@ -44,5 +93,24 @@ DEFAULT_PUBLISH_PLUGINS = {
"enabled": True,
"optional": True,
"active": True
},
"ExtractEditorialPckgConversion": {
"optional": False,
"conversion_enabled": True,
"output": {
"ext": "",
"ffmpeg_args": {
"video_filters": [],
"audio_filters": [],
"input": [
"-apply_trc gamma22"
],
"output": [
"-pix_fmt yuv420p",
"-crf 18",
"-intra"
]
}
}
}
}