mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #911 from pypeclub/3/feature/harmony-deadline-submission
Harmony deadline submission
This commit is contained in:
commit
a3fdff91dc
19 changed files with 774 additions and 37 deletions
|
|
@ -26,7 +26,7 @@ class HarmonyPrelaunchHook(PreLaunchHook):
|
|||
(
|
||||
"import avalon.harmony;"
|
||||
"avalon.harmony.launch(\"{}\")"
|
||||
).format(harmony_executable)
|
||||
).format(harmony_executable.replace("\\", "/"))
|
||||
]
|
||||
|
||||
# Append as whole list as these areguments should not be separated
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import avalon.tools.sceneinventory
|
|||
import pyblish.api
|
||||
|
||||
from pype import lib
|
||||
from pype.api import get_current_project_settings
|
||||
from pype.api import (get_current_project_settings)
|
||||
|
||||
|
||||
def set_scene_settings(settings):
|
||||
|
|
@ -48,20 +48,13 @@ def get_asset_settings():
|
|||
"resolutionWidth": resolution_width,
|
||||
"resolutionHeight": resolution_height
|
||||
}
|
||||
settings = get_current_project_settings()
|
||||
|
||||
try:
|
||||
skip_resolution_check = (
|
||||
get_current_project_settings()
|
||||
["harmony"]
|
||||
["general"]
|
||||
["skip_resolution_check"]
|
||||
)
|
||||
skip_timelines_check = (
|
||||
get_current_project_settings()
|
||||
["harmony"]
|
||||
["general"]
|
||||
["skip_timelines_check"]
|
||||
)
|
||||
skip_resolution_check = \
|
||||
settings["harmony"]["general"]["skip_resolution_check"]
|
||||
skip_timelines_check = \
|
||||
settings["harmony"]["general"]["skip_timelines_check"]
|
||||
except KeyError:
|
||||
skip_resolution_check = []
|
||||
skip_timelines_check = []
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
var LD_OPENHARMONY_PATH = System.getenv('LIB_OPENHARMONY_PATH');
|
||||
include(LD_OPENHARMONY_PATH + '/openHarmony.js');
|
||||
this.__proto__['$'] = $;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -79,7 +79,8 @@ PypeHarmony.getSceneSettings = function() {
|
|||
scene.getStopFrame(),
|
||||
sound.getSoundtrackAll().path(),
|
||||
scene.defaultResolutionX(),
|
||||
scene.defaultResolutionY()
|
||||
scene.defaultResolutionY(),
|
||||
scene.defaultResolutionFOV()
|
||||
];
|
||||
};
|
||||
|
||||
|
|
@ -200,3 +201,16 @@ PypeHarmony.getDependencies = function(_node) {
|
|||
}
|
||||
return dependencies;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* return version of running Harmony instance.
|
||||
* @function
|
||||
* @return {array} [major_version, minor_version]
|
||||
*/
|
||||
PypeHarmony.getVersion = function() {
|
||||
return [
|
||||
about.getMajorVersion(),
|
||||
about.getMinorVersion()
|
||||
];
|
||||
};
|
||||
|
|
|
|||
52
pype/hosts/harmony/js/publish/CollectFarmRender.js
Normal file
52
pype/hosts/harmony/js/publish/CollectFarmRender.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/* global PypeHarmony:writable, include */
|
||||
// ***************************************************************************
|
||||
// * CollectFarmRender *
|
||||
// ***************************************************************************
|
||||
|
||||
|
||||
// check if PypeHarmony is defined and if not, load it.
|
||||
if (typeof PypeHarmony !== 'undefined') {
|
||||
var PYPE_HARMONY_JS = System.getenv('PYPE_HARMONY_JS');
|
||||
include(PYPE_HARMONY_JS + '/pype_harmony.js');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @namespace
|
||||
* @classdesc Image Sequence loader JS code.
|
||||
*/
|
||||
var CollectFarmRender = function() {};
|
||||
|
||||
|
||||
/**
|
||||
* Get information important for render output.
|
||||
* @function
|
||||
* @param node {String} node name.
|
||||
* @return {array} array of render info.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* var ret = [
|
||||
* file_prefix, // like foo/bar-
|
||||
* type, // PNG4, ...
|
||||
* leading_zeros, // 3 - for 0001
|
||||
* start // start frame
|
||||
* ]
|
||||
*/
|
||||
CollectFarmRender.prototype.getRenderNodeSettings = function(n) {
|
||||
// this will return
|
||||
var output = [
|
||||
node.getTextAttr(
|
||||
n, frame.current(), 'DRAWING_NAME'),
|
||||
node.getTextAttr(
|
||||
n, frame.current(), 'DRAWING_TYPE'),
|
||||
node.getTextAttr(
|
||||
n, frame.current(), 'LEADING_ZEROS'),
|
||||
node.getTextAttr(n, frame.current(), 'START')
|
||||
];
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
// add self to Pype Loaders
|
||||
PypeHarmony.Publish.CollectFarmRender = new CollectFarmRender();
|
||||
|
|
@ -128,13 +128,14 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
order = pyblish.api.IntegratorOrder + 0.2
|
||||
icon = "tractor"
|
||||
|
||||
hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects"]
|
||||
hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony"]
|
||||
|
||||
families = ["render.farm", "prerener",
|
||||
families = ["render.farm", "prerender",
|
||||
"renderlayer", "imagesequence", "vrayscene"]
|
||||
|
||||
aov_filter = {"maya": [r".+(?:\.|_)([Bb]eauty)(?:\.|_).*"],
|
||||
"aftereffects": [r".*"], # for everything from AE
|
||||
"harmony": [r".*"], # for everything from AE
|
||||
"celaction": [r".*"]}
|
||||
|
||||
enviro_filter = [
|
||||
|
|
|
|||
31
pype/plugins/harmony/create/create_farm_render.py
Normal file
31
pype/plugins/harmony/create/create_farm_render.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Create Composite node for render on farm."""
|
||||
from avalon import harmony
|
||||
|
||||
|
||||
class CreateFarmRender(harmony.Creator):
|
||||
"""Composite node for publishing renders."""
|
||||
|
||||
name = "renderDefault"
|
||||
label = "Render on Farm"
|
||||
family = "renderFarm"
|
||||
node_type = "WRITE"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Constructor."""
|
||||
super(CreateFarmRender, self).__init__(*args, **kwargs)
|
||||
|
||||
def setup_node(self, node):
|
||||
"""Set render node."""
|
||||
path = "render/{0}/{0}.".format(node.split("/")[-1])
|
||||
harmony.send(
|
||||
{
|
||||
"function": f"PypeHarmony.Creators.CreateRender.create",
|
||||
"args": [node, path]
|
||||
})
|
||||
harmony.send(
|
||||
{
|
||||
"function": f"PypeHarmony.color",
|
||||
"args": [[0.9, 0.75, 0.3, 1.0]]
|
||||
}
|
||||
)
|
||||
|
|
@ -8,7 +8,7 @@ class CreateRender(harmony.Creator):
|
|||
|
||||
name = "renderDefault"
|
||||
label = "Render"
|
||||
family = "render"
|
||||
family = "renderLocal"
|
||||
node_type = "WRITE"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
|
@ -18,7 +18,7 @@ class CreateRender(harmony.Creator):
|
|||
def setup_node(self, node):
|
||||
"""Set render node."""
|
||||
self_name = self.__class__.__name__
|
||||
path = "{0}/{0}".format(node.split("/")[-1])
|
||||
path = "render/{0}/{0}.".format(node.split("/")[-1])
|
||||
harmony.send(
|
||||
{
|
||||
"function": f"PypeHarmony.Creators.{self_name}.create",
|
||||
|
|
|
|||
33
pype/plugins/harmony/publish/collect_audio.py
Normal file
33
pype/plugins/harmony/publish/collect_audio.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectAudio(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Collect relative path for audio file to instance.
|
||||
|
||||
Harmony api `getSoundtrackAll` returns useless path to temp folder,
|
||||
for render on farm we look into 'audio' folder and select first file.
|
||||
|
||||
Correct path needs to be calculated in `submit_harmony_deadline.py`
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.499
|
||||
label = "Collect Audio"
|
||||
hosts = ["harmony"]
|
||||
families = ["renderlayer"]
|
||||
|
||||
def process(self, instance):
|
||||
audio_dir = os.path.join(
|
||||
os.path.dirname(instance.context.data.get("currentFile")), 'audio')
|
||||
if os.path.isdir(audio_dir):
|
||||
for full_file_name in os.listdir(audio_dir):
|
||||
file_name, file_ext = os.path.splitext(full_file_name)
|
||||
|
||||
if file_ext not in ['.wav', '.mp3', '.aiff']:
|
||||
self.log.error("Unsupported file {}.{}".format(file_name,
|
||||
file_ext))
|
||||
|
||||
audio_file_path = os.path.join('audio', full_file_name)
|
||||
self.log.debug("audio_file_path {}".format(audio_file_path))
|
||||
instance.data["audioFile"] = audio_file_path
|
||||
176
pype/plugins/harmony/publish/collect_farm_render.py
Normal file
176
pype/plugins/harmony/publish/collect_farm_render.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect data to render from scene."""
|
||||
from pathlib import Path
|
||||
|
||||
import attr
|
||||
from avalon import harmony, api
|
||||
|
||||
import pype.lib.abstract_collect_render
|
||||
from pype.lib.abstract_collect_render import RenderInstance
|
||||
|
||||
|
||||
@attr.s
|
||||
class HarmonyRenderInstance(RenderInstance):
|
||||
outputType = attr.ib(default="Image")
|
||||
outputFormat = attr.ib(default="PNG4")
|
||||
outputStartFrame = attr.ib(default=1)
|
||||
leadingZeros = attr.ib(default=3)
|
||||
|
||||
|
||||
class CollectFarmRender(pype.lib.abstract_collect_render.
|
||||
AbstractCollectRender):
|
||||
"""Gather all publishable renders."""
|
||||
|
||||
# https://docs.toonboom.com/help/harmony-17/premium/reference/node/output/write-node-image-formats.html
|
||||
ext_mapping = {
|
||||
"tvg": ["TVG"],
|
||||
"tga": ["TGA", "TGA4", "TGA3", "TGA1"],
|
||||
"sgi": ["SGI", "SGI4", "SGA3", "SGA1", "SGIDP", "SGIDP4", "SGIDP3"],
|
||||
"psd": ["PSD", "PSD1", "PSD3", "PSD4", "PSDDP", "PSDDP1", "PSDDP3",
|
||||
"PSDDP4"],
|
||||
"yuv": ["YUV"],
|
||||
"pal": ["PAL"],
|
||||
"scan": ["SCAN"],
|
||||
"png": ["PNG", "PNG4", "PNGDP", "PNGDP3", "PNGDP4"],
|
||||
"jpg": ["JPG"],
|
||||
"bmp": ["BMP", "BMP4"],
|
||||
"opt": ["OPT", "OPT1", "OPT3", "OPT4"],
|
||||
"var": ["VAR"],
|
||||
"tif": ["TIF"],
|
||||
"dpx": ["DPX", "DPX3_8", "DPX3_10", "DPX3_12", "DPX3_16",
|
||||
"DPX3_10_INVERTED_CHANNELS", "DPX3_12_INVERTED_CHANNELS",
|
||||
"DPX3_16_INVERTED_CHANNELS"],
|
||||
"exr": ["EXR"],
|
||||
"pdf": ["PDF"],
|
||||
"dtext": ["DTEX"]
|
||||
}
|
||||
|
||||
def get_expected_files(self, render_instance):
|
||||
"""Get list of expected files to be rendered from Harmony.
|
||||
|
||||
This returns full path with file name determined by Write node
|
||||
settings.
|
||||
"""
|
||||
start = render_instance.frameStart
|
||||
end = render_instance.frameEnd
|
||||
node = render_instance.setMembers[0]
|
||||
self_name = self.__class__.__name__
|
||||
# 0 - filename / 1 - type / 2 - zeros / 3 - start
|
||||
info = harmony.send(
|
||||
{
|
||||
"function": f"PypeHarmony.Publish.{self_name}."
|
||||
"getRenderNodeSettings",
|
||||
"args": node
|
||||
})["result"]
|
||||
|
||||
ext = None
|
||||
for k, v in self.ext_mapping.items():
|
||||
if info[1] in v:
|
||||
ext = k
|
||||
|
||||
if not ext:
|
||||
raise AssertionError(
|
||||
f"Cannot determine file extension for {info[1]}")
|
||||
|
||||
path = Path(render_instance.source).parent
|
||||
|
||||
# is sequence start node on write node offsetting whole sequence?
|
||||
expected_files = []
|
||||
|
||||
# Harmony 17 needs at least one '.' in file_prefix, but not at end
|
||||
file_prefix = info[0]
|
||||
file_prefix += '.temp'
|
||||
|
||||
for frame in range(start, end + 1):
|
||||
expected_files.append(
|
||||
path / "{}{}.{}".format(
|
||||
file_prefix,
|
||||
str(frame).rjust(int(info[2]) + 1, "0"),
|
||||
ext
|
||||
)
|
||||
)
|
||||
|
||||
return expected_files
|
||||
|
||||
def get_instances(self, context):
|
||||
"""Get instances per Write node in `renderFarm` family."""
|
||||
version = None
|
||||
if self.sync_workfile_version:
|
||||
version = context.data["version"]
|
||||
|
||||
instances = []
|
||||
|
||||
self_name = self.__class__.__name__
|
||||
|
||||
for node in context.data["allNodes"]:
|
||||
data = harmony.read(node)
|
||||
|
||||
# Skip non-tagged nodes.
|
||||
if not data:
|
||||
continue
|
||||
|
||||
# Skip containers.
|
||||
if "container" in data["id"]:
|
||||
continue
|
||||
|
||||
if data["family"] != "renderFarm":
|
||||
continue
|
||||
|
||||
# 0 - filename / 1 - type / 2 - zeros / 3 - start
|
||||
info = harmony.send(
|
||||
{
|
||||
"function": f"PypeHarmony.Publish.{self_name}."
|
||||
"getRenderNodeSettings",
|
||||
"args": node
|
||||
})["result"]
|
||||
|
||||
# TODO: handle pixel aspect and frame step
|
||||
# TODO: set Deadline stuff (pools, priority, etc. by presets)
|
||||
subset_name = node.split("/")[1].replace('Farm', '')
|
||||
render_instance = HarmonyRenderInstance(
|
||||
version=version,
|
||||
time=api.time(),
|
||||
source=context.data["currentFile"],
|
||||
label=subset_name,
|
||||
subset=subset_name,
|
||||
asset=api.Session["AVALON_ASSET"],
|
||||
attachTo=False,
|
||||
setMembers=[node],
|
||||
publish=True,
|
||||
review=False,
|
||||
renderer=None,
|
||||
priority=50,
|
||||
name=node.split("/")[1],
|
||||
|
||||
family="renderlayer",
|
||||
families=["renderlayer"],
|
||||
|
||||
resolutionWidth=context.data["resolutionWidth"],
|
||||
resolutionHeight=context.data["resolutionHeight"],
|
||||
pixelAspect=1.0,
|
||||
multipartExr=False,
|
||||
tileRendering=False,
|
||||
tilesX=0,
|
||||
tilesY=0,
|
||||
convertToScanline=False,
|
||||
|
||||
# time settings
|
||||
frameStart=context.data["frameStart"],
|
||||
frameEnd=context.data["frameEnd"],
|
||||
frameStep=1,
|
||||
outputType="Image",
|
||||
outputFormat=info[1],
|
||||
outputStartFrame=info[3],
|
||||
leadingZeros=info[2],
|
||||
toBeRenderedOn='deadline'
|
||||
|
||||
)
|
||||
self.log.debug(render_instance)
|
||||
instances.append(render_instance)
|
||||
|
||||
return instances
|
||||
|
||||
def add_additional_data(self, instance):
|
||||
instance["FOV"] = self._context.data["FOV"]
|
||||
|
||||
return instance
|
||||
|
|
@ -49,6 +49,10 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
if "container" in data["id"]:
|
||||
continue
|
||||
|
||||
# skip render farm family as it is collected separately
|
||||
if data["family"] == "renderFarm":
|
||||
continue
|
||||
|
||||
instance = context.create_instance(node.split("/")[-1])
|
||||
instance.append(node)
|
||||
instance.data.update(data)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class CollectPalettes(pyblish.api.ContextPlugin):
|
|||
instance.data.update({
|
||||
"id": id,
|
||||
"family": "harmony.palette",
|
||||
'families': [],
|
||||
"asset": os.environ["AVALON_ASSET"],
|
||||
"subset": "{}{}".format("palette", name)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -30,9 +30,24 @@ class CollectScene(pyblish.api.ContextPlugin):
|
|||
context.data["audioPath"] = result[6]
|
||||
context.data["resolutionWidth"] = result[7]
|
||||
context.data["resolutionHeight"] = result[8]
|
||||
context.data["FOV"] = result[9]
|
||||
|
||||
all_nodes = harmony.send(
|
||||
{"function": "node.subNodes", "args": ["Top"]}
|
||||
)["result"]
|
||||
|
||||
context.data["allNodes"] = all_nodes
|
||||
|
||||
# collect all write nodes to be able disable them in Deadline
|
||||
all_write_nodes = harmony.send(
|
||||
{"function": "node.getNodes", "args": ["WRITE"]}
|
||||
)["result"]
|
||||
|
||||
context.data["all_write_nodes"] = all_write_nodes
|
||||
|
||||
result = harmony.send(
|
||||
{
|
||||
f"function": "PypeHarmony.getVersion",
|
||||
"args": []}
|
||||
)["result"]
|
||||
context.data["harmonyVersion"] = "{}.{}".format(result[0], result[1])
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
|
|||
"label": basename,
|
||||
"name": basename,
|
||||
"family": family,
|
||||
"families": [],
|
||||
"families": [family],
|
||||
"representations": [],
|
||||
"asset": os.environ["AVALON_ASSET"]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class ExtractRender(pyblish.api.InstancePlugin):
|
|||
label = "Extract Render"
|
||||
order = pyblish.api.ExtractorOrder
|
||||
hosts = ["harmony"]
|
||||
families = ["render"]
|
||||
families = ["renderLocal"]
|
||||
|
||||
def process(self, instance):
|
||||
# Collect scene data.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import pype.hosts.harmony
|
|||
|
||||
|
||||
class ExtractWorkfile(pype.api.Extractor):
|
||||
"""Extract the connected nodes to the composite instance."""
|
||||
"""Extract and zip complete workfile folder into zip."""
|
||||
|
||||
label = "Extract Workfile"
|
||||
hosts = ["harmony"]
|
||||
|
|
@ -18,15 +18,11 @@ class ExtractWorkfile(pype.api.Extractor):
|
|||
|
||||
def process(self, instance):
|
||||
"""Plugin entry point."""
|
||||
# Export template.
|
||||
backdrops = harmony.send(
|
||||
{"function": "Backdrop.backdrops", "args": ["Top"]}
|
||||
)["result"]
|
||||
nodes = instance.context.data.get("allNodes")
|
||||
staging_dir = self.staging_dir(instance)
|
||||
filepath = os.path.join(staging_dir, "{}.tpl".format(instance.name))
|
||||
|
||||
pype.hosts.harmony.export_template(backdrops, nodes, filepath)
|
||||
src = os.path.dirname(instance.context.data["currentFile"])
|
||||
self.log.info("Copying to {}".format(filepath))
|
||||
shutil.copytree(src, filepath)
|
||||
|
||||
# Prep representation.
|
||||
os.chdir(staging_dir)
|
||||
|
|
|
|||
411
pype/plugins/harmony/publish/submit_harmony_deadline..py
Normal file
411
pype/plugins/harmony/publish/submit_harmony_deadline..py
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Submitting render job to Deadline."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from collections import OrderedDict
|
||||
from zipfile import ZipFile, is_zipfile
|
||||
import re
|
||||
|
||||
import attr
|
||||
import pyblish.api
|
||||
|
||||
import pype.lib.abstract_submit_deadline
|
||||
from pype.lib.abstract_submit_deadline import DeadlineJobInfo
|
||||
from avalon import api
|
||||
|
||||
|
||||
class _ZipFile(ZipFile):
|
||||
"""Extended check for windows invalid characters."""
|
||||
|
||||
# this is extending default zipfile table for few invalid characters
|
||||
# that can come from Mac
|
||||
_windows_illegal_characters = ":<>|\"?*\r\n\x00"
|
||||
_windows_illegal_name_trans_table = str.maketrans(
|
||||
_windows_illegal_characters,
|
||||
"_" * len(_windows_illegal_characters)
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class PluginInfo(object):
|
||||
"""Plugin info structure for Harmony Deadline plugin."""
|
||||
|
||||
SceneFile = attr.ib()
|
||||
# Harmony version
|
||||
Version = attr.ib()
|
||||
|
||||
Camera = attr.ib(default="")
|
||||
FieldOfView = attr.ib(default=41.11)
|
||||
IsDatabase = attr.ib(default=False)
|
||||
ResolutionX = attr.ib(default=1920)
|
||||
ResolutionY = attr.ib(default=1080)
|
||||
|
||||
# Resolution name preset, default
|
||||
UsingResPreset = attr.ib(default=False)
|
||||
ResolutionName = attr.ib(default="HDTV_1080p24")
|
||||
|
||||
PreRenderInlineScript = attr.ib(default=None)
|
||||
|
||||
# --------------------------------------------------
|
||||
_outputNode = attr.ib(factory=list)
|
||||
|
||||
@property
|
||||
def OutputNode(self): # noqa: N802
|
||||
"""Return all output nodes formatted for Deadline.
|
||||
|
||||
Returns:
|
||||
dict: as `{'Output0Node', 'Top/renderFarmDefault'}`
|
||||
|
||||
"""
|
||||
out = {}
|
||||
for index, v in enumerate(self._outputNode):
|
||||
out["Output{}Node".format(index)] = v
|
||||
return out
|
||||
|
||||
@OutputNode.setter
|
||||
def OutputNode(self, val): # noqa: N802
|
||||
self._outputNode.append(val)
|
||||
|
||||
# --------------------------------------------------
|
||||
_outputType = attr.ib(factory=list)
|
||||
|
||||
@property
|
||||
def OutputType(self): # noqa: N802
|
||||
"""Return output nodes type formatted for Deadline.
|
||||
|
||||
Returns:
|
||||
dict: as `{'Output0Type', 'Image'}`
|
||||
|
||||
"""
|
||||
out = {}
|
||||
for index, v in enumerate(self._outputType):
|
||||
out["Output{}Type".format(index)] = v
|
||||
return out
|
||||
|
||||
@OutputType.setter
|
||||
def OutputType(self, val): # noqa: N802
|
||||
self._outputType.append(val)
|
||||
|
||||
# --------------------------------------------------
|
||||
_outputLeadingZero = attr.ib(factory=list)
|
||||
|
||||
@property
|
||||
def OutputLeadingZero(self): # noqa: N802
|
||||
"""Return output nodes type formatted for Deadline.
|
||||
|
||||
Returns:
|
||||
dict: as `{'Output0LeadingZero', '3'}`
|
||||
|
||||
"""
|
||||
out = {}
|
||||
for index, v in enumerate(self._outputLeadingZero):
|
||||
out["Output{}LeadingZero".format(index)] = v
|
||||
return out
|
||||
|
||||
@OutputLeadingZero.setter
|
||||
def OutputLeadingZero(self, val): # noqa: N802
|
||||
self._outputLeadingZero.append(val)
|
||||
|
||||
# --------------------------------------------------
|
||||
_outputFormat = attr.ib(factory=list)
|
||||
|
||||
@property
|
||||
def OutputFormat(self): # noqa: N802
|
||||
"""Return output nodes format formatted for Deadline.
|
||||
|
||||
Returns:
|
||||
dict: as `{'Output0Type', 'PNG4'}`
|
||||
|
||||
"""
|
||||
out = {}
|
||||
for index, v in enumerate(self._outputFormat):
|
||||
out["Output{}Format".format(index)] = v
|
||||
return out
|
||||
|
||||
@OutputFormat.setter
|
||||
def OutputFormat(self, val): # noqa: N802
|
||||
self._outputFormat.append(val)
|
||||
|
||||
# --------------------------------------------------
|
||||
_outputStartFrame = attr.ib(factory=list)
|
||||
|
||||
@property
|
||||
def OutputStartFrame(self): # noqa: N802
|
||||
"""Return start frame for output nodes formatted for Deadline.
|
||||
|
||||
Returns:
|
||||
dict: as `{'Output0StartFrame', '1'}`
|
||||
|
||||
"""
|
||||
out = {}
|
||||
for index, v in enumerate(self._outputStartFrame):
|
||||
out["Output{}StartFrame".format(index)] = v
|
||||
return out
|
||||
|
||||
@OutputStartFrame.setter
|
||||
def OutputStartFrame(self, val): # noqa: N802
|
||||
self._outputStartFrame.append(val)
|
||||
|
||||
# --------------------------------------------------
|
||||
_outputPath = attr.ib(factory=list)
|
||||
|
||||
@property
|
||||
def OutputPath(self): # noqa: N802
|
||||
"""Return output paths for nodes formatted for Deadline.
|
||||
|
||||
Returns:
|
||||
dict: as `{'Output0Path', '/output/path'}`
|
||||
|
||||
"""
|
||||
out = {}
|
||||
for index, v in enumerate(self._outputPath):
|
||||
out["Output{}Path".format(index)] = v
|
||||
return out
|
||||
|
||||
@OutputPath.setter
|
||||
def OutputPath(self, val): # noqa: N802
|
||||
self._outputPath.append(val)
|
||||
|
||||
def set_output(self, node, image_format, output,
|
||||
output_type="Image", zeros=3, start_frame=1):
|
||||
"""Helper to set output.
|
||||
|
||||
This should be used instead of setting properties individually
|
||||
as so index remain consistent.
|
||||
|
||||
Args:
|
||||
node (str): harmony write node name
|
||||
image_format (str): format of output (PNG4, TIF, ...)
|
||||
output (str): output path
|
||||
output_type (str, optional): "Image" or "Movie" (not supported).
|
||||
zeros (int, optional): Leading zeros (for 0001 = 3)
|
||||
start_frame (int, optional): Sequence offset.
|
||||
|
||||
"""
|
||||
|
||||
self.OutputNode = node
|
||||
self.OutputFormat = image_format
|
||||
self.OutputPath = output
|
||||
self.OutputType = output_type
|
||||
self.OutputLeadingZero = zeros
|
||||
self.OutputStartFrame = start_frame
|
||||
|
||||
def serialize(self):
|
||||
"""Return all data serialized as dictionary.
|
||||
|
||||
Returns:
|
||||
OrderedDict: all serialized data.
|
||||
|
||||
"""
|
||||
def filter_data(a, v):
|
||||
if a.name.startswith("_"):
|
||||
return False
|
||||
if v is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
serialized = attr.asdict(
|
||||
self, dict_factory=OrderedDict, filter=filter_data)
|
||||
serialized.update(self.OutputNode)
|
||||
serialized.update(self.OutputFormat)
|
||||
serialized.update(self.OutputPath)
|
||||
serialized.update(self.OutputType)
|
||||
serialized.update(self.OutputLeadingZero)
|
||||
serialized.update(self.OutputStartFrame)
|
||||
|
||||
return serialized
|
||||
|
||||
|
||||
class HarmonySubmitDeadline(
|
||||
pype.lib.abstract_submit_deadline.AbstractSubmitDeadline):
|
||||
"""Submit render write of Harmony scene to Deadline.
|
||||
|
||||
Renders are submitted to a Deadline Web Service as
|
||||
supplied via the environment variable ``DEADLINE_REST_URL``.
|
||||
|
||||
Note:
|
||||
If Deadline configuration is not detected, this plugin will
|
||||
be disabled.
|
||||
|
||||
Attributes:
|
||||
use_published (bool): Use published scene to render instead of the
|
||||
one in work area.
|
||||
|
||||
"""
|
||||
|
||||
label = "Submit to Deadline"
|
||||
order = pyblish.api.IntegratorOrder + 0.1
|
||||
hosts = ["harmony"]
|
||||
families = ["renderlayer"]
|
||||
if not os.environ.get("DEADLINE_REST_URL"):
|
||||
optional = False
|
||||
active = False
|
||||
else:
|
||||
optional = True
|
||||
|
||||
use_published = False
|
||||
primary_pool = ""
|
||||
secondary_pool = ""
|
||||
priority = 50
|
||||
chunk_size = 1000000
|
||||
|
||||
def get_job_info(self):
|
||||
job_info = DeadlineJobInfo("Harmony")
|
||||
job_info.Name = self._instance.data["name"]
|
||||
job_info.Plugin = "HarmonyPype"
|
||||
job_info.Frames = "{}-{}".format(
|
||||
self._instance.data["frameStart"],
|
||||
self._instance.data["frameEnd"]
|
||||
)
|
||||
# for now, get those from presets. Later on it should be
|
||||
# configurable in Harmony UI directly.
|
||||
job_info.Priority = self.priority
|
||||
job_info.Pool = self.primary_pool
|
||||
job_info.SecondaryPool = self.secondary_pool
|
||||
job_info.ChunkSize = self.chunk_size
|
||||
job_info.BatchName = os.path.basename(self._instance.data["source"])
|
||||
|
||||
keys = [
|
||||
"FTRACK_API_KEY",
|
||||
"FTRACK_API_USER",
|
||||
"FTRACK_SERVER",
|
||||
"AVALON_PROJECT",
|
||||
"AVALON_ASSET",
|
||||
"AVALON_TASK",
|
||||
"PYPE_USERNAME",
|
||||
"PYPE_DEV",
|
||||
"PYPE_LOG_NO_COLORS"
|
||||
]
|
||||
|
||||
environment = dict({key: os.environ[key] for key in keys
|
||||
if key in os.environ}, **api.Session)
|
||||
for key in keys:
|
||||
val = environment.get(key)
|
||||
if val:
|
||||
job_info.EnvironmentKeyValue = "{key}={value}".format(
|
||||
key=key,
|
||||
value=val)
|
||||
|
||||
return job_info
|
||||
|
||||
def _unzip_scene_file(self, published_scene: Path) -> Path:
|
||||
"""Unzip scene zip file to its directory.
|
||||
|
||||
Unzip scene file (if it is zip file) to its current directory and
|
||||
return path to xstage file there. Xstage file is determined by its
|
||||
name.
|
||||
|
||||
Args:
|
||||
published_scene (Path): path to zip file.
|
||||
|
||||
Returns:
|
||||
Path: The path to unzipped xstage.
|
||||
"""
|
||||
# if not zip, bail out.
|
||||
if "zip" not in published_scene.suffix or not is_zipfile(
|
||||
published_scene.as_posix()
|
||||
):
|
||||
self.log.error("Published scene is not in zip.")
|
||||
self.log.error(published_scene)
|
||||
raise AssertionError("invalid scene format")
|
||||
|
||||
xstage_path = (
|
||||
published_scene.parent
|
||||
/ published_scene.stem
|
||||
/ f"{published_scene.stem}.xstage"
|
||||
)
|
||||
|
||||
unzip_dir = (published_scene.parent / published_scene.stem)
|
||||
with _ZipFile(published_scene, "r") as zip_ref:
|
||||
zip_ref.extractall(unzip_dir.as_posix())
|
||||
|
||||
# find any xstage files in directory, prefer the one with the same name
|
||||
# as directory (plus extension)
|
||||
xstage_files = []
|
||||
for scene in unzip_dir.iterdir():
|
||||
if scene.suffix == ".xstage":
|
||||
xstage_files.append(scene)
|
||||
|
||||
# there must be at least one (but maybe not more?) xstage file
|
||||
if not xstage_files:
|
||||
self.log.error("No xstage files found in zip")
|
||||
raise AssertionError("Invalid scene archive")
|
||||
|
||||
ideal_scene = False
|
||||
# find the one with the same name as zip. In case there can be more
|
||||
# then one xtage file.
|
||||
for scene in xstage_files:
|
||||
# if /foo/bar/baz.zip == /foo/bar/baz/baz.xstage
|
||||
# ^^^ ^^^
|
||||
if scene.stem == published_scene.stem:
|
||||
xstage_path = scene
|
||||
ideal_scene = True
|
||||
|
||||
# but sometimes xstage file has different name then zip - in that case
|
||||
# use that one.
|
||||
if not ideal_scene:
|
||||
xstage_path = xstage_files[0]
|
||||
|
||||
return xstage_path
|
||||
|
||||
def get_plugin_info(self):
|
||||
work_scene = Path(self._instance.data["source"])
|
||||
|
||||
# this is path to published scene workfile _ZIP_. Before
|
||||
# rendering, we need to unzip it.
|
||||
published_scene = Path(
|
||||
self.from_published_scene(False))
|
||||
self.log.info(f"Processing {published_scene.as_posix()}")
|
||||
xstage_path = self._unzip_scene_file(published_scene)
|
||||
render_path = xstage_path.parent / "renders"
|
||||
|
||||
# for submit_publish job to create .json file in
|
||||
self._instance.data["outputDir"] = render_path
|
||||
new_expected_files = []
|
||||
work_path_str = str(work_scene.parent.as_posix())
|
||||
render_path_str = str(render_path.as_posix())
|
||||
for file in self._instance.data["expectedFiles"]:
|
||||
_file = str(Path(file).as_posix())
|
||||
new_expected_files.append(
|
||||
_file.replace(work_path_str, render_path_str)
|
||||
)
|
||||
|
||||
audio_file = self._instance.data.get("audioFile")
|
||||
if audio_file:
|
||||
abs_path = xstage_path.parent / audio_file
|
||||
self._instance.context.data["audioFile"] = str(abs_path)
|
||||
|
||||
self._instance.data["source"] = str(published_scene.as_posix())
|
||||
self._instance.data["expectedFiles"] = new_expected_files
|
||||
harmony_plugin_info = PluginInfo(
|
||||
SceneFile=xstage_path.as_posix(),
|
||||
Version=(
|
||||
self._instance.context.data["harmonyVersion"].split(".")[0]),
|
||||
FieldOfView=self._instance.context.data["FOV"],
|
||||
ResolutionX=self._instance.data["resolutionWidth"],
|
||||
ResolutionY=self._instance.data["resolutionHeight"]
|
||||
)
|
||||
|
||||
pattern = '[0]{' + str(self._instance.data["leadingZeros"]) + \
|
||||
'}1\.[a-zA-Z]{3}'
|
||||
render_prefix = re.sub(pattern, '',
|
||||
self._instance.data["expectedFiles"][0])
|
||||
harmony_plugin_info.set_output(
|
||||
self._instance.data["setMembers"][0],
|
||||
self._instance.data["outputFormat"],
|
||||
render_prefix,
|
||||
self._instance.data["outputType"],
|
||||
self._instance.data["leadingZeros"],
|
||||
self._instance.data["outputStartFrame"]
|
||||
)
|
||||
|
||||
all_write_nodes = self._instance.context.data["all_write_nodes"]
|
||||
disable_nodes = []
|
||||
for node in all_write_nodes:
|
||||
# disable all other write nodes
|
||||
if node != self._instance.data["setMembers"][0]:
|
||||
disable_nodes.append("node.setEnable('{}', false)"
|
||||
.format(node))
|
||||
harmony_plugin_info.PreRenderInlineScript = ';'.join(disable_nodes)
|
||||
|
||||
return harmony_plugin_info.serialize()
|
||||
|
|
@ -21,7 +21,7 @@ class ValidateSceneSettingsRepair(pyblish.api.Action):
|
|||
pype.hosts.harmony.set_scene_settings(
|
||||
pype.hosts.harmony.get_asset_settings()
|
||||
)
|
||||
if not os.patch.exists(context.data["scenePath"]):
|
||||
if not os.path.exists(context.data["scenePath"]):
|
||||
self.log.info("correcting scene name")
|
||||
scene_dir = os.path.dirname(context.data["currentFile"])
|
||||
scene_path = os.path.join(
|
||||
|
|
@ -40,6 +40,8 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
|||
actions = [ValidateSceneSettingsRepair]
|
||||
|
||||
frame_check_filter = ["_ch_", "_pr_", "_intd_", "_extd_"]
|
||||
# used for skipping resolution validation for render tasks
|
||||
render_check_filter = ["render", "Render"]
|
||||
|
||||
def process(self, instance):
|
||||
"""Plugin entry point."""
|
||||
|
|
@ -65,6 +67,12 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
|||
fps = float(
|
||||
"{:.2f}".format(instance.context.data.get("frameRate")))
|
||||
|
||||
if any(string in instance.context.data['anatomyData']['task']
|
||||
for string in self.render_check_filter):
|
||||
self.log.debug("Render task detected, resolution check skipped")
|
||||
expected_settings.pop("resolutionWidth")
|
||||
expected_settings.pop("resolutionHeight")
|
||||
|
||||
current_settings = {
|
||||
"fps": fps,
|
||||
"frameStart": instance.context.data.get("frameStart"),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"publish": {},
|
||||
"general": {
|
||||
"skip_resolution_check": false,
|
||||
"skip_timelines_check": false
|
||||
"skip_resolution_check": [],
|
||||
"skip_timelines_check": []
|
||||
}
|
||||
}
|
||||
|
|
@ -19,14 +19,16 @@
|
|||
"label": "General",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"type": "list",
|
||||
"key": "skip_resolution_check",
|
||||
"label": "Skip Resolution Check"
|
||||
"object_type": "text",
|
||||
"label": "Skip Resolution Check for Tasks"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"type": "list",
|
||||
"key": "skip_timelines_check",
|
||||
"label": "Skip Timeliene Check"
|
||||
"object_type": "text",
|
||||
"label": "Skip Timeliene Check for Tasks"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue