Merge branch 'develop' into enhancement/AY-1052_Context-Validation-Repair-Action-enhancements

This commit is contained in:
Kayla Man 2024-04-10 15:12:11 +08:00 committed by GitHub
commit b4e93cade3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 413 additions and 141 deletions

View file

@ -137,7 +137,7 @@ class CreateShotClip(phiero.Creator):
"value": ["<track_name>", "main", "bg", "fg", "bg",
"animatic"],
"type": "QComboBox",
"label": "pRODUCT Name",
"label": "Product Name",
"target": "ui",
"toolTip": "chose product name pattern, if <track_name> is selected, name of track layer will be used", # noqa
"order": 0},
@ -159,7 +159,7 @@ class CreateShotClip(phiero.Creator):
"type": "QCheckBox",
"label": "Include audio",
"target": "tag",
"toolTip": "Process productS with corresponding audio", # noqa
"toolTip": "Process products with corresponding audio", # noqa
"order": 3},
"sourceResolution": {
"value": False,

View file

@ -4,12 +4,12 @@ import pyblish.api
from ayon_core.pipeline import publish
class ExtractThumnail(publish.Extractor):
class ExtractThumbnail(publish.Extractor):
"""
Extractor for track item's tumnails
Extractor for track item's tumbnails
"""
label = "Extract Thumnail"
label = "Extract Thumbnail"
order = pyblish.api.ExtractorOrder
families = ["plate", "take"]
hosts = ["hiero"]
@ -48,7 +48,7 @@ class ExtractThumnail(publish.Extractor):
self.log.debug(
"__ thumb_path: `{}`, frame: `{}`".format(thumbnail, thumb_frame))
self.log.info("Thumnail was generated to: {}".format(thumb_path))
self.log.info("Thumbnail was generated to: {}".format(thumb_path))
thumb_representation = {
'files': thumb_file,
'stagingDir': staging_dir,

View file

@ -1519,24 +1519,30 @@ def extract_alembic(file,
# region ID
def get_id_required_nodes(referenced_nodes=False, nodes=None):
"""Filter out any node which are locked (reference) or readOnly
def get_id_required_nodes(referenced_nodes=False,
nodes=None,
existing_ids=True):
"""Return nodes that should receive a `cbId` attribute.
This includes only mesh and curve nodes, parent transforms of the shape
nodes, file texture nodes and object sets (including shading engines).
This filters out any node which is locked, referenced, read-only,
intermediate object.
Args:
referenced_nodes (bool): set True to filter out reference nodes
referenced_nodes (bool): set True to include referenced nodes
nodes (list, Optional): nodes to consider
existing_ids (bool): set True to include nodes with `cbId` attribute
Returns:
nodes (set): list of filtered nodes
"""
lookup = None
if nodes is None:
# Consider all nodes
nodes = cmds.ls()
else:
# Build a lookup for the only allowed nodes in output based
# on `nodes` input of the function (+ ensure long names)
lookup = set(cmds.ls(nodes, long=True))
if nodes is not None and not nodes:
# User supplied an empty `nodes` list to check so all we can
# do is return the empty result
return set()
def _node_type_exists(node_type):
try:
@ -1545,63 +1551,142 @@ def get_id_required_nodes(referenced_nodes=False, nodes=None):
except RuntimeError:
return False
def iterate(maya_iterator):
while not maya_iterator.isDone():
yield maya_iterator.thisNode()
maya_iterator.next()
# `readOnly` flag is obsolete as of Maya 2016 therefore we explicitly
# remove default nodes and reference nodes
camera_shapes = ["frontShape", "sideShape", "topShape", "perspShape"]
default_camera_shapes = {
"frontShape", "sideShape", "topShape", "perspShape"
}
ignore = set()
if not referenced_nodes:
ignore |= set(cmds.ls(long=True, referencedNodes=True))
# list all defaultNodes to filter out from the rest
ignore |= set(cmds.ls(long=True, defaultNodes=True))
ignore |= set(cmds.ls(camera_shapes, long=True))
# Remove Turtle from the result of `cmds.ls` if Turtle is loaded
# TODO: This should be a less specific check for a single plug-in.
if _node_type_exists("ilrBakeLayer"):
ignore |= set(cmds.ls(type="ilrBakeLayer", long=True))
# Establish set of nodes types to include
types = ["objectSet", "file", "mesh", "nurbsCurve", "nurbsSurface"]
# The filtered types do not include transforms because we only want the
# parent transforms that have a child shape that we filtered to, so we
# include the parents here
types = ["mesh", "nurbsCurve", "nurbsSurface", "file", "objectSet"]
# Check if plugin nodes are available for Maya by checking if the plugin
# is loaded
if cmds.pluginInfo("pgYetiMaya", query=True, loaded=True):
types.append("pgYetiMaya")
# We *always* ignore intermediate shapes, so we filter them out directly
nodes = cmds.ls(nodes, type=types, long=True, noIntermediate=True)
iterator_type = OpenMaya.MIteratorType()
# This tries to be closest matching API equivalents of `types` variable
iterator_type.filterList = [
OpenMaya.MFn.kMesh, # mesh
OpenMaya.MFn.kNurbsSurface, # nurbsSurface
OpenMaya.MFn.kNurbsCurve, # nurbsCurve
OpenMaya.MFn.kFileTexture, # file
OpenMaya.MFn.kSet, # objectSet
OpenMaya.MFn.kPluginShape # pgYetiMaya
]
it = OpenMaya.MItDependencyNodes(iterator_type)
# The items which need to pass the id to their parent
# Add the collected transform to the nodes
dag = cmds.ls(nodes, type="dagNode", long=True) # query only dag nodes
transforms = cmds.listRelatives(dag,
parent=True,
fullPath=True) or []
fn_dep = OpenMaya.MFnDependencyNode()
fn_dag = OpenMaya.MFnDagNode()
result = set()
nodes = set(nodes)
nodes |= set(transforms)
def _should_include_parents(obj):
"""Whether to include parents of obj in output"""
if not obj.hasFn(OpenMaya.MFn.kShape):
return False
nodes -= ignore # Remove the ignored nodes
if not nodes:
return nodes
fn_dag.setObject(obj)
if fn_dag.isIntermediateObject:
return False
# Ensure only nodes from the input `nodes` are returned when a
# filter was applied on function call because we also iterated
# to parents and alike
if lookup is not None:
nodes &= lookup
# Skip default cameras
if (
obj.hasFn(OpenMaya.MFn.kCamera) and
fn_dag.name() in default_camera_shapes
):
return False
# Avoid locked nodes
nodes_list = list(nodes)
locked = cmds.lockNode(nodes_list, query=True, lock=True)
for node, lock in zip(nodes_list, locked):
if lock:
log.warning("Skipping locked node: %s" % node)
nodes.remove(node)
return True
return nodes
def _add_to_result_if_valid(obj):
"""Add to `result` if the object should be included"""
fn_dep.setObject(obj)
if not existing_ids and fn_dep.hasAttribute("cbId"):
return
if not referenced_nodes and fn_dep.isFromReferencedFile:
return
if fn_dep.isDefaultNode:
return
if fn_dep.isLocked:
return
# Skip default cameras
if (
obj.hasFn(OpenMaya.MFn.kCamera) and
fn_dep.name() in default_camera_shapes
):
return
if obj.hasFn(OpenMaya.MFn.kDagNode):
# DAG nodes
fn_dag.setObject(obj)
# Skip intermediate objects
if fn_dag.isIntermediateObject:
return
# DAG nodes can be instanced and thus may have multiple paths.
# We need to identify each path
paths = OpenMaya.MDagPath.getAllPathsTo(obj)
for dag in paths:
path = dag.fullPathName()
result.add(path)
else:
# Dependency node
path = fn_dep.name()
result.add(path)
for obj in iterate(it):
# For any non-intermediate shape node always include the parent
# even if we exclude the shape itself (e.g. when locked, default)
if _should_include_parents(obj):
fn_dag.setObject(obj)
parents = [
fn_dag.parent(index) for index in range(fn_dag.parentCount())
]
for parent_obj in parents:
_add_to_result_if_valid(parent_obj)
_add_to_result_if_valid(obj)
if not result:
return result
# Exclude some additional types
exclude_types = []
if _node_type_exists("ilrBakeLayer"):
# Remove Turtle from the result if Turtle is loaded
exclude_types.append("ilrBakeLayer")
if exclude_types:
exclude_nodes = set(cmds.ls(nodes, long=True, type=exclude_types))
if exclude_nodes:
result -= exclude_nodes
# Filter to explicit input nodes if provided
if nodes is not None:
# The amount of input nodes to filter to can be large and querying
# many nodes can be slow in Maya. As such we want to try and reduce
# it as much as possible, so we include the type filter to try and
# reduce the result of `maya.cmds.ls` here.
nodes = set(cmds.ls(nodes, long=True, type=types + ["dagNode"]))
if nodes:
result &= nodes
else:
return set()
return result
def get_id(node):

View file

@ -580,7 +580,8 @@ def on_save():
_remove_workfile_lock()
# Generate ids of the current context on nodes in the scene
nodes = lib.get_id_required_nodes(referenced_nodes=False)
nodes = lib.get_id_required_nodes(referenced_nodes=False,
existing_ids=False)
for node, new_id in lib.generate_ids(nodes):
lib.set_id(node, new_id, overwrite=False)
@ -653,10 +654,6 @@ def on_task_changed():
"Can't set project for new context because path does not exist: {}"
).format(workdir))
with lib.suspended_refresh():
lib.set_context_settings()
lib.update_content_on_context_change()
global _about_to_save
if not lib.IS_HEADLESS and _about_to_save:
# Let's prompt the user to update the context settings or not

View file

@ -286,7 +286,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin):
if not container:
return
roots = cmds.sets(container, q=True)
roots = cmds.sets(container, q=True) or []
ref_node = None
try:
ref_node = get_reference_node(roots)

View file

@ -1,5 +1,3 @@
import json
from maya import cmds
import pyblish.api
@ -11,18 +9,24 @@ class CollectFileDependencies(pyblish.api.ContextPlugin):
label = "Collect File Dependencies"
order = pyblish.api.CollectorOrder - 0.49
hosts = ["maya"]
families = ["renderlayer"]
@classmethod
def apply_settings(cls, project_settings, system_settings):
# Disable plug-in if not used for deadline submission anyway
settings = project_settings["deadline"]["publish"]["MayaSubmitDeadline"] # noqa
cls.enabled = settings.get("asset_dependencies", True)
def process(self, context):
dependencies = []
dependencies = set()
for node in cmds.ls(type="file"):
path = cmds.getAttr("{}.{}".format(node, "fileTextureName"))
if path not in dependencies:
dependencies.append(path)
dependencies.add(path)
for node in cmds.ls(type="AlembicNode"):
path = cmds.getAttr("{}.{}".format(node, "abc_File"))
if path not in dependencies:
dependencies.append(path)
dependencies.add(path)
context.data["fileDependencies"] = dependencies
self.log.debug(json.dumps(dependencies, indent=4))
context.data["fileDependencies"] = list(dependencies)

View file

@ -1,14 +1,18 @@
import inspect
import pyblish.api
from maya import cmds
from ayon_core.pipeline.publish import (
context_plugin_should_run,
PublishValidationError,
OptionalPyblishPluginMixin
)
class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin,
OptionalPyblishPluginMixin):
"""Validate if current render layer has a renderable camera
"""Validate if current render layer has a renderable camera.
There is a bug in Redshift which occurs when the current render layer
at file open has no renderable camera. The error raised is as follows:
@ -32,8 +36,39 @@ class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin,
if not context_plugin_should_run(self, context):
return
layer = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True)
# This validator only makes sense when publishing renderlayer instances
# with Redshift. We skip validation if there isn't any.
if not any(self.is_active_redshift_render_instance(instance)
for instance in context):
return
cameras = cmds.ls(type="camera", long=True)
renderable = any(c for c in cameras if cmds.getAttr(c + ".renderable"))
assert renderable, ("Current render layer '%s' has no renderable "
"camera" % layer)
if not renderable:
layer = cmds.editRenderLayerGlobals(query=True,
currentRenderLayer=True)
raise PublishValidationError(
"Current render layer '{}' has no renderable camera".format(
layer
),
description=inspect.getdoc(self)
)
@staticmethod
def is_active_redshift_render_instance(instance) -> bool:
"""Return whether instance is an active renderlayer instance set to
render with Redshift renderer."""
if not instance.data.get("active", True):
return False
# Check this before families just because it's a faster check
if not instance.data.get("renderer") == "redshift":
return False
families = set()
families.add(instance.data.get("family"))
families.update(instance.data.get("families", []))
if "renderlayer" not in families:
return False
return True

View file

@ -1,3 +1,5 @@
import inspect
from maya import cmds
import pyblish.api
@ -14,8 +16,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Adheres to the content of 'model' product type
- Must have one top group. (configurable)
- Must only contain: transforms, meshes and groups
See `get_description` for more details.
"""
@ -28,13 +29,16 @@ class ValidateModelContent(pyblish.api.InstancePlugin,
validate_top_group = True
optional = False
allowed = ('mesh', 'transform', 'nurbsCurve', 'nurbsSurface', 'locator')
@classmethod
def get_invalid(cls, instance):
content_instance = instance.data.get("setMembers", None)
if not content_instance:
cls.log.error("Instance has no nodes!")
return [instance.data["name"]]
cls.log.error("Model instance has no nodes. "
"It is not allowed to be empty")
return [instance.data["instance_node"]]
# All children will be included in the extracted export so we also
# validate *all* descendents of the set members and we skip any
@ -46,30 +50,42 @@ class ValidateModelContent(pyblish.api.InstancePlugin,
content_instance = list(set(content_instance + descendants))
# Ensure only valid node types
allowed = ('mesh', 'transform', 'nurbsCurve', 'nurbsSurface', 'locator')
nodes = cmds.ls(content_instance, long=True)
valid = cmds.ls(content_instance, long=True, type=allowed)
valid = cmds.ls(content_instance, long=True, type=cls.allowed)
invalid = set(nodes) - set(valid)
if invalid:
cls.log.error("These nodes are not allowed: %s" % invalid)
# List as bullet points
invalid_bullets = "\n".join(f"- {node}" for node in invalid)
cls.log.error(
"These nodes are not allowed:\n{}\n\n"
"The valid node types are: {}".format(
invalid_bullets, ", ".join(cls.allowed))
)
return list(invalid)
if not valid:
cls.log.error("No valid nodes in the instance")
return True
cls.log.error(
"No valid nodes in the model instance.\n"
"The valid node types are: {}".format(", ".join(cls.allowed))
)
return [instance.data["instance_node"]]
# Ensure it has shapes
shapes = cmds.ls(valid, long=True, shapes=True)
if not shapes:
cls.log.error("No shapes in the model instance")
return True
return [instance.data["instance_node"]]
# Top group
top_parents = set([x.split("|")[1] for x in content_instance])
# Ensure single top group
top_parents = {x.split("|", 2)[1] for x in content_instance}
if cls.validate_top_group and len(top_parents) != 1:
cls.log.error("Must have exactly one top group")
return top_parents
cls.log.error(
"A model instance must have exactly one top group. "
"Found top groups: {}".format(", ".join(top_parents))
)
return list(top_parents)
def _is_visible(node):
"""Return whether node is visible"""
@ -101,5 +117,21 @@ class ValidateModelContent(pyblish.api.InstancePlugin,
if invalid:
raise PublishValidationError(
title="Model content is invalid",
message="See log for more details"
message="Model content is invalid. See log for more details.",
description=self.get_description()
)
@classmethod
def get_description(cls):
return inspect.cleandoc(f"""
### Model content is invalid
Your model instance does not adhere to the rules of a
model product type:
- Must have at least one visible shape in it, like a mesh.
- Must have one root node. When exporting multiple meshes they
must be inside a group.
- May only contain the following node types:
{", ".join(cls.allowed)}
""")

View file

@ -60,7 +60,8 @@ class ValidateNodeIDs(pyblish.api.InstancePlugin):
# We do want to check the referenced nodes as it might be
# part of the end product.
id_nodes = lib.get_id_required_nodes(referenced_nodes=True,
nodes=instance[:])
invalid = [n for n in id_nodes if not lib.get_id(n)]
return invalid
nodes=instance[:],
# Exclude those with already
# existing ids
existing_ids=False)
return id_nodes

View file

@ -37,27 +37,27 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin):
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
("Found folder ids which are not related to "
"current project in instance: `{}`").format(instance.name))
"Found folder ids which are not related to "
"current project in instance: `{}`".format(instance.name))
@classmethod
def get_invalid(cls, instance):
invalid = []
nodes = instance[:]
if not nodes:
return
# Get all id required nodes
id_required_nodes = lib.get_id_required_nodes(referenced_nodes=True,
nodes=instance[:])
id_required_nodes = lib.get_id_required_nodes(referenced_nodes=False,
nodes=nodes)
if not id_required_nodes:
return
# check ids against database ids
project_name = instance.context.data["projectName"]
folder_entities = ayon_api.get_folders(project_name, fields={"id"})
folder_ids = {
folder_entity["id"]
for folder_entity in folder_entities
}
folder_ids = cls.get_project_folder_ids(context=instance.context)
# Get all asset IDs
invalid = []
for node in id_required_nodes:
cb_id = lib.get_id(node)
@ -71,3 +71,31 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin):
invalid.append(node)
return invalid
@classmethod
def get_project_folder_ids(cls, context):
"""Return all folder ids in the current project.
Arguments:
context (pyblish.api.Context): The publish context.
Returns:
set[str]: All folder ids in the current project.
"""
# We query the database only for the first instance instead of
# per instance by storing a cache in the context
key = "__cache_project_folder_ids"
if key in context.data:
return context.data[key]
# check ids against database
project_name = context.data["projectName"]
folder_entities = ayon_api.get_folders(project_name, fields={"id"})
folder_ids = {
folder_entity["id"]
for folder_entity in folder_entities
}
context.data[key] = folder_ids
return folder_ids

View file

@ -8,6 +8,8 @@ from ayon_core.pipeline.publish import (
import ayon_core.hosts.maya.api.action
from ayon_core.hosts.maya.api import lib
from maya import cmds
class ValidateNodeIdsUnique(pyblish.api.InstancePlugin):
"""Validate the nodes in the instance have a unique Colorbleed Id
@ -41,7 +43,7 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin):
if invalid:
label = "Nodes found with non-unique folder ids"
raise PublishValidationError(
message="{}: {}".format(label, invalid),
message="{}, see log".format(label),
title="Non-unique folder ids on nodes",
description="{}\n- {}".format(label,
"\n- ".join(sorted(invalid)))
@ -54,7 +56,6 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin):
# Check only non intermediate shapes
# todo: must the instance itself ensure to have no intermediates?
# todo: how come there are intermediates?
from maya import cmds
instance_members = cmds.ls(instance, noIntermediate=True, long=True)
# Collect each id with their members
@ -67,10 +68,14 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin):
# Take only the ids with more than one member
invalid = list()
_iteritems = getattr(ids, "iteritems", ids.items)
for _ids, members in _iteritems():
for members in ids.values():
if len(members) > 1:
cls.log.error("ID found on multiple nodes: '%s'" % members)
members_text = "\n".join(
"- {}".format(member) for member in sorted(members)
)
cls.log.error(
"ID found on multiple nodes:\n{}".format(members_text)
)
invalid.extend(members)
return invalid

View file

@ -130,6 +130,18 @@ class LoadClip(plugin.NukeLoader):
first = 1
last = first + duration
# If a slate is present, the frame range is 1 frame longer for movies,
# but file sequences its the first frame that is 1 frame lower.
slate_frames = repre_entity["data"].get("slateFrames", 0)
extension = "." + repre_entity["context"]["ext"]
if extension in VIDEO_EXTENSIONS:
last += slate_frames
files_count = len(repre_entity["files"])
if extension in IMAGE_EXTENSIONS and files_count != 1:
first -= slate_frames
# Fallback to folder name when namespace is None
if namespace is None:
namespace = context["folder"]["name"]
@ -167,7 +179,9 @@ class LoadClip(plugin.NukeLoader):
repre_entity
)
self._set_range_to_node(read_node, first, last, start_at_workfile)
self._set_range_to_node(
read_node, first, last, start_at_workfile, slate_frames
)
version_name = version_entity["version"]
if version_name < 0:
@ -402,14 +416,21 @@ class LoadClip(plugin.NukeLoader):
for member in members:
nuke.delete(member)
def _set_range_to_node(self, read_node, first, last, start_at_workfile):
def _set_range_to_node(
self, read_node, first, last, start_at_workfile, slate_frames=0
):
read_node['origfirst'].setValue(int(first))
read_node['first'].setValue(int(first))
read_node['origlast'].setValue(int(last))
read_node['last'].setValue(int(last))
# set start frame depending on workfile or version
self._loader_shift(read_node, start_at_workfile)
if start_at_workfile:
read_node['frame_mode'].setValue("start at")
start_frame = self.script_start - slate_frames
read_node['frame'].setValue(str(start_frame))
def _make_retimes(self, parent_node, version_data):
''' Create all retime and timewarping nodes with copied animation '''
@ -466,18 +487,6 @@ class LoadClip(plugin.NukeLoader):
for i, n in enumerate(dependent_nodes):
last_node.setInput(i, n)
def _loader_shift(self, read_node, workfile_start=False):
""" Set start frame of read node to a workfile start
Args:
read_node (nuke.Node): The nuke's read node
workfile_start (bool): set workfile start frame if true
"""
if workfile_start:
read_node['frame_mode'].setValue("start at")
read_node['frame'].setValue(str(self.script_start))
def _get_node_name(self, context):
folder_entity = context["folder"]
product_name = context["product"]["name"]

View file

@ -300,6 +300,10 @@ class ExtractSlateFrame(publish.Extractor):
self.log.debug(
"__ matching_repre: {}".format(pformat(matching_repre)))
data = matching_repre.get("data", {})
data["slateFrames"] = 1
matching_repre["data"] = data
self.log.info("Added slate frame to representation files")
def add_comment_slate_node(self, instance, node):

View file

@ -67,7 +67,7 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin,
"FTRACK_SERVER",
"AYON_APP_NAME",
"AYON_USERNAME",
"OPENPYPE_SG_USER",
"AYON_SG_USERNAME",
"KITSU_LOGIN",
"KITSU_PWD"
]

View file

@ -130,7 +130,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
"FTRACK_SERVER",
"AYON_APP_NAME",
"AYON_USERNAME",
"OPENPYPE_SG_USER",
"AYON_SG_USERNAME",
"KITSU_LOGIN",
"KITSU_PWD"
]

View file

@ -65,7 +65,7 @@ class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin,
"FTRACK_SERVER",
"AYON_APP_NAME",
"AYON_USERNAME",
"OPENPYPE_SG_USER",
"AYON_SG_USERNAME",
]
priority = 50

View file

@ -91,9 +91,15 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
longest_key = max(self.templates.keys(), key=len)
dropdown.setMinimumContentsLength(len(longest_key))
template_label = QtWidgets.QLabel()
template_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
template_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
template_dir_label = QtWidgets.QLabel()
template_dir_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
template_dir_label.setTextInteractionFlags(
QtCore.Qt.TextSelectableByMouse)
template_file_label = QtWidgets.QLabel()
template_file_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
template_file_label.setTextInteractionFlags(
QtCore.Qt.TextSelectableByMouse)
renumber_frame = QtWidgets.QCheckBox()
@ -123,7 +129,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
input_layout.addRow("Selected representations", selected_label)
input_layout.addRow("Delivery template", dropdown)
input_layout.addRow("Template value", template_label)
input_layout.addRow("Directory template", template_dir_label)
input_layout.addRow("File template", template_file_label)
input_layout.addRow("Renumber Frame", renumber_frame)
input_layout.addRow("Renumber start frame", first_frame_start)
input_layout.addRow("Root", root_line_edit)
@ -151,7 +158,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
layout.addWidget(text_area)
self.selected_label = selected_label
self.template_label = template_label
self.template_dir_label = template_dir_label
self.template_file_label = template_file_label
self.dropdown = dropdown
self.first_frame_start = first_frame_start
self.renumber_frame = renumber_frame
@ -282,11 +290,13 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
"""Adds list of delivery templates from Anatomy to dropdown."""
templates = {}
for template_name, value in anatomy.templates["delivery"].items():
path_template = value["path"]
if (
not isinstance(path_template, str)
or not path_template.startswith('{root')
):
directory_template = value["directory"]
if not directory_template.startswith("{root"):
self.log.warning(
"Skipping template '%s' because directory template does "
"not start with `{root` in value: %s",
template_name, directory_template
)
continue
templates[template_name] = value
@ -350,7 +360,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
name = self.dropdown.currentText()
template_value = self.templates.get(name)
if template_value:
self.template_label.setText(template_value)
self.template_dir_label.setText(template_value["directory"])
self.template_file_label.setText(template_value["file"])
self.btn_delivery.setEnabled(bool(self._get_selected_repres()))
def _update_progress(self, uploaded):

View file

@ -32,6 +32,35 @@ from ayon_core.pipeline.publish import (
from ayon_core.pipeline.publish.lib import add_repre_files_for_cleanup
def frame_to_timecode(frame: int, fps: float) -> str:
"""Convert a frame number and FPS to editorial timecode (HH:MM:SS:FF).
Unlike `ayon_core.pipeline.editorial.frames_to_timecode` this does not
rely on the `opentimelineio` package, so it can be used across hosts that
do not have it available.
Args:
frame (int): The frame number to be converted.
fps (float): The frames per second of the video.
Returns:
str: The timecode in HH:MM:SS:FF format.
"""
# Calculate total seconds
total_seconds = frame / fps
# Extract hours, minutes, and seconds
hours = int(total_seconds // 3600)
minutes = int((total_seconds % 3600) // 60)
seconds = int(total_seconds % 60)
# Adjust for non-integer FPS by rounding the remaining frames appropriately
remaining_frames = round((total_seconds - int(total_seconds)) * fps)
# Format and return the timecode
return f"{hours:02d}:{minutes:02d}:{seconds:02d}:{remaining_frames:02d}"
class ExtractReview(pyblish.api.InstancePlugin):
"""Extracting Review mov file for Ftrack
@ -390,7 +419,16 @@ class ExtractReview(pyblish.api.InstancePlugin):
# add outputName to anatomy format fill_data
fill_data.update({
"output": output_name,
"ext": output_ext
"ext": output_ext,
# By adding `timecode` as data we can use it
# in the ffmpeg arguments for `--timecode` so that editorial
# like Resolve or Premiere can detect the start frame for e.g.
# review output files
"timecode": frame_to_timecode(
frame=temp_data["frame_start_handle"],
fps=float(instance.data["fps"])
)
})
try: # temporary until oiiotool is supported cross platform

View file

@ -0,0 +1,22 @@
import pyblish.api
from ayon_core.pipeline import publish
class ExtractSlateData(publish.Extractor):
"""Add slate data for integration."""
label = "Slate Data"
# Offset from ExtractReviewSlate and ExtractGenerateSlate.
order = pyblish.api.ExtractorOrder + 0.49
families = ["slate", "review"]
hosts = ["nuke", "shell"]
def process(self, instance):
for representation in instance.data.get("representations", []):
if "slate-frame" not in representation.get("tags", []):
continue
data = representation.get("data", {})
data["slateFrames"] = 1
representation["data"] = data

View file

@ -173,6 +173,7 @@ def _product_types_enum():
"rig",
"setdress",
"take",
"usd",
"usdShade",
"vdbcache",
"vrayproxy",