diff --git a/igniter/__init__.py b/igniter/__init__.py index 9b2816a767..6f9757cfc8 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -2,6 +2,10 @@ """Open install dialog.""" import sys + +import os +os.chdir(os.path.dirname(__file__)) # for override sys.path in Deadline + from Qt import QtWidgets # noqa from Qt.QtCore import Signal # noqa diff --git a/pype/hosts/harmony/api/__init__.py b/pype/hosts/harmony/api/__init__.py index c76c306052..1a0255d045 100644 --- a/pype/hosts/harmony/api/__init__.py +++ b/pype/hosts/harmony/api/__init__.py @@ -50,6 +50,8 @@ def get_asset_settings(): fps = asset_data.get("fps") frame_start = asset_data.get("frameStart") frame_end = asset_data.get("frameEnd") + handle_start = asset_data.get("handleStart") + handle_end = asset_data.get("handleEnd") resolution_width = asset_data.get("resolutionWidth") resolution_height = asset_data.get("resolutionHeight") entity_type = asset_data.get("entityType") @@ -58,6 +60,8 @@ def get_asset_settings(): "fps": fps, "frameStart": frame_start, "frameEnd": frame_end, + "handleStart": handle_start, + "handleEnd": handle_end, "resolutionWidth": resolution_width, "resolutionHeight": resolution_height } @@ -150,13 +154,14 @@ def application_launch(): # It is now moved so it it manually called. # ensure_scene_settings() # check_inventory() - pype_harmony_path = Path(__file__).parent / "js" / "PypeHarmony.js" + # fills PYPE_HARMONY_JS + pype_harmony_path = Path(__file__).parent.parent / "js" / "PypeHarmony.js" pype_harmony_js = pype_harmony_path.read_text() # go through js/creators, loaders and publish folders and load all scripts script = "" for item in ["creators", "loaders", "publish"]: - dir_to_scan = Path(__file__).parent / "js" / item + dir_to_scan = Path(__file__).parent.parent / "js" / item for child in dir_to_scan.iterdir(): script += child.read_text() @@ -210,12 +215,14 @@ def uninstall(): def on_pyblish_instance_toggled(instance, old_value, new_value): """Toggle node enabling on instance toggles.""" - try: + node = None + if instance.data.get("setMembers"): + node = instance.data["setMembers"][0] + + if node: harmony.send( { "function": "PypeHarmony.toggleInstance", - "args": [instance[0], new_value] + "args": [node, new_value] } ) - except IndexError: - print(f"Instance '{instance}' is missing node") diff --git a/pype/hosts/harmony/js/PypeHarmony.js b/pype/hosts/harmony/js/PypeHarmony.js index 9d05384461..41c8dc56ce 100644 --- a/pype/hosts/harmony/js/PypeHarmony.js +++ b/pype/hosts/harmony/js/PypeHarmony.js @@ -4,7 +4,8 @@ // *************************************************************************** var LD_OPENHARMONY_PATH = System.getenv('LIB_OPENHARMONY_PATH'); -include(LD_OPENHARMONY_PATH + '/openHarmony.js'); +LD_OPENHARMONY_PATH = LD_OPENHARMONY_PATH + '/openHarmony.js'; +LD_OPENHARMONY_PATH = LD_OPENHARMONY_PATH.replace(/\\/g, "/"); diff --git a/pype/hosts/harmony/js/creators/CreateRender.js b/pype/hosts/harmony/js/creators/CreateRender.js index d8283ea30b..cfb0701df4 100644 --- a/pype/hosts/harmony/js/creators/CreateRender.js +++ b/pype/hosts/harmony/js/creators/CreateRender.js @@ -5,9 +5,9 @@ // 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'); +if (typeof PypeHarmony === 'undefined') { + var PYPE_HARMONY_JS = System.getenv('PYPE_HARMONY_JS') + '/PypeHarmony.js'; + include(PYPE_HARMONY_JS.replace(/\\/g, "/")); } diff --git a/pype/hosts/harmony/js/loaders/ImageSequenceLoader.js b/pype/hosts/harmony/js/loaders/ImageSequenceLoader.js index 3e2c853146..cfa71e2834 100644 --- a/pype/hosts/harmony/js/loaders/ImageSequenceLoader.js +++ b/pype/hosts/harmony/js/loaders/ImageSequenceLoader.js @@ -3,13 +3,15 @@ // * ImageSequenceLoader * // *************************************************************************** - // 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'); +if (typeof PypeHarmony === 'undefined') { + var PYPE_HARMONY_JS = System.getenv('PYPE_HARMONY_JS') + '/PypeHarmony.js'; + include(PYPE_HARMONY_JS.replace(/\\/g, "/")); } +if (typeof $ === 'undefined'){ + $ = this.__proto__['$']; +} /** * @namespace @@ -92,6 +94,9 @@ ImageSequenceLoader.getUniqueColumnName = function(columnPrefix) { * ]; */ ImageSequenceLoader.prototype.importFiles = function(args) { + MessageLog.trace("ImageSequence:: " + typeof PypeHarmony); + MessageLog.trace("ImageSequence $:: " + typeof $); + MessageLog.trace("ImageSequence OH:: " + typeof PypeHarmony.OpenHarmony); var PNGTransparencyMode = 0; // Premultiplied wih Black var TGATransparencyMode = 0; // Premultiplied wih Black var SGITransparencyMode = 0; // Premultiplied wih Black diff --git a/pype/hosts/harmony/js/loaders/TemplateLoader.js b/pype/hosts/harmony/js/loaders/TemplateLoader.js index 0a0b5706d7..160979f943 100644 --- a/pype/hosts/harmony/js/loaders/TemplateLoader.js +++ b/pype/hosts/harmony/js/loaders/TemplateLoader.js @@ -5,12 +5,14 @@ // 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'); +if (typeof PypeHarmony === 'undefined') { + var PYPE_HARMONY_JS = System.getenv('PYPE_HARMONY_JS') + '/PypeHarmony.js'; + include(PYPE_HARMONY_JS.replace(/\\/g, "/")); } - +if (typeof $ === 'undefined'){ + $ = this.__proto__['$']; +} /** * @namespace * @classdesc Image Sequence loader JS code. diff --git a/pype/hosts/harmony/js/publish/CollectCurrentFile.js b/pype/hosts/harmony/js/publish/CollectCurrentFile.js index 61cf31ef9d..d39f23712d 100644 --- a/pype/hosts/harmony/js/publish/CollectCurrentFile.js +++ b/pype/hosts/harmony/js/publish/CollectCurrentFile.js @@ -5,9 +5,9 @@ // 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'); +if (typeof PypeHarmony === 'undefined') { + var PYPE_HARMONY_JS = System.getenv('PYPE_HARMONY_JS') + '/PypeHarmony.js'; + include(PYPE_HARMONY_JS.replace(/\\/g, "/")); } diff --git a/pype/hosts/harmony/js/publish/CollectFarmRender.js b/pype/hosts/harmony/js/publish/CollectFarmRender.js index 153c38e868..7c0cda5165 100644 --- a/pype/hosts/harmony/js/publish/CollectFarmRender.js +++ b/pype/hosts/harmony/js/publish/CollectFarmRender.js @@ -5,9 +5,9 @@ // 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'); +if (typeof PypeHarmony === 'undefined') { + var PYPE_HARMONY_JS = System.getenv('PYPE_HARMONY_JS') + '/PypeHarmony.js'; + include(PYPE_HARMONY_JS.replace(/\\/g, "/")); } @@ -42,7 +42,8 @@ CollectFarmRender.prototype.getRenderNodeSettings = function(n) { n, frame.current(), 'DRAWING_TYPE'), node.getTextAttr( n, frame.current(), 'LEADING_ZEROS'), - node.getTextAttr(n, frame.current(), 'START') + node.getTextAttr(n, frame.current(), 'START'), + node.getEnable(n) ]; return output; diff --git a/pype/hosts/harmony/js/publish/CollectPalettes.js b/pype/hosts/harmony/js/publish/CollectPalettes.js index b2ed9aa761..8fda55ff75 100644 --- a/pype/hosts/harmony/js/publish/CollectPalettes.js +++ b/pype/hosts/harmony/js/publish/CollectPalettes.js @@ -5,9 +5,9 @@ // 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'); +if (typeof PypeHarmony === 'undefined') { + var PYPE_HARMONY_JS = System.getenv('PYPE_HARMONY_JS') + '/PypeHarmony.js'; + include(PYPE_HARMONY_JS.replace(/\\/g, "/")); } diff --git a/pype/hosts/harmony/js/publish/ExtractPalette.js b/pype/hosts/harmony/js/publish/ExtractPalette.js index bc63dcdc7a..794c6fdbb1 100644 --- a/pype/hosts/harmony/js/publish/ExtractPalette.js +++ b/pype/hosts/harmony/js/publish/ExtractPalette.js @@ -5,12 +5,11 @@ // 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'); +if (typeof PypeHarmony === 'undefined') { + var PYPE_HARMONY_JS = System.getenv('PYPE_HARMONY_JS') + '/PypeHarmony.js'; + include(PYPE_HARMONY_JS.replace(/\\/g, "/")); } - /** * @namespace * @classdesc Code for extracting palettes. diff --git a/pype/hosts/harmony/js/publish/ExtractTemplate.js b/pype/hosts/harmony/js/publish/ExtractTemplate.js index eb3668f833..d36a8947f8 100644 --- a/pype/hosts/harmony/js/publish/ExtractTemplate.js +++ b/pype/hosts/harmony/js/publish/ExtractTemplate.js @@ -5,9 +5,9 @@ // 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'); +if (typeof PypeHarmony === 'undefined') { + var PYPE_HARMONY_JS = System.getenv('PYPE_HARMONY_JS') + '/PypeHarmony.js'; + include(PYPE_HARMONY_JS.replace(/\\/g, "/")); } diff --git a/pype/hosts/harmony/plugins/create/create_render.py b/pype/hosts/harmony/plugins/create/create_render.py index a047fbff77..b9a0987b37 100644 --- a/pype/hosts/harmony/plugins/create/create_render.py +++ b/pype/hosts/harmony/plugins/create/create_render.py @@ -9,7 +9,7 @@ class CreateRender(plugin.Creator): name = "renderDefault" label = "Render" - family = "renderLocal" + family = "render" node_type = "WRITE" def __init__(self, *args, **kwargs): diff --git a/pype/hosts/harmony/plugins/load/load_imagesequence.py b/pype/hosts/harmony/plugins/load/load_imagesequence.py index b6f4845983..db7af90b14 100644 --- a/pype/hosts/harmony/plugins/load/load_imagesequence.py +++ b/pype/hosts/harmony/plugins/load/load_imagesequence.py @@ -76,7 +76,7 @@ class ImageSequenceLoader(api.Loader): """ self_name = self.__class__.__name__ - node = harmony.find_node_by_name(container["name"], "READ") + node = container.get("nodes").pop() path = api.get_representation_path(representation) collections, remainder = clique.assemble( @@ -129,7 +129,7 @@ class ImageSequenceLoader(api.Loader): container (dict): Container data. """ - node = harmony.find_node_by_name(container["name"], "READ") + node = container.get("nodes").pop() harmony.send( {"function": "PypeHarmony.deleteNode", "args": [node]} ) diff --git a/pype/hosts/harmony/plugins/publish/collect_audio.py b/pype/hosts/harmony/plugins/publish/collect_audio.py index 58521d6612..40b4107a62 100644 --- a/pype/hosts/harmony/plugins/publish/collect_audio.py +++ b/pype/hosts/harmony/plugins/publish/collect_audio.py @@ -1,6 +1,7 @@ import os import pyblish.api +import pyblish.api class CollectAudio(pyblish.api.InstancePlugin): """ @@ -15,9 +16,10 @@ class CollectAudio(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.499 label = "Collect Audio" hosts = ["harmony"] - families = ["renderlayer"] + families = ["render.farm"] def process(self, instance): + full_file_name = None audio_dir = os.path.join( os.path.dirname(instance.context.data.get("currentFile")), 'audio') if os.path.isdir(audio_dir): @@ -27,7 +29,9 @@ class CollectAudio(pyblish.api.InstancePlugin): if file_ext not in ['.wav', '.mp3', '.aiff']: self.log.error("Unsupported file {}.{}".format(file_name, file_ext)) + full_file_name = None - 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 + if full_file_name: + 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 diff --git a/pype/hosts/harmony/plugins/publish/collect_farm_render.py b/pype/hosts/harmony/plugins/publish/collect_farm_render.py index 5925dafa72..98706ad951 100644 --- a/pype/hosts/harmony/plugins/publish/collect_farm_render.py +++ b/pype/hosts/harmony/plugins/publish/collect_farm_render.py @@ -7,6 +7,7 @@ from avalon import harmony, api import pype.lib.abstract_collect_render from pype.lib.abstract_collect_render import RenderInstance +import pype.lib @attr.s @@ -51,8 +52,8 @@ class CollectFarmRender(pype.lib.abstract_collect_render. This returns full path with file name determined by Write node settings. """ - start = render_instance.frameStart - end = render_instance.frameEnd + start = render_instance.frameStart - render_instance.handleStart + end = render_instance.frameEnd + render_instance.handleEnd node = render_instance.setMembers[0] self_name = self.__class__.__name__ # 0 - filename / 1 - type / 2 - zeros / 3 - start @@ -73,23 +74,19 @@ class CollectFarmRender(pype.lib.abstract_collect_render. 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' - + # '-' in name is important for Harmony17 for frame in range(start, end + 1): expected_files.append( - path / "{}{}.{}".format( - file_prefix, + path / "{}-{}.{}".format( + render_instance.subset, str(frame).rjust(int(info[2]) + 1, "0"), ext ) ) - + self.log.debug("expected_files::{}".format(expected_files)) return expected_files def get_instances(self, context): @@ -116,7 +113,7 @@ class CollectFarmRender(pype.lib.abstract_collect_render. if data["family"] != "renderFarm": continue - # 0 - filename / 1 - type / 2 - zeros / 3 - start + # 0 - filename / 1 - type / 2 - zeros / 3 - start / 4 - enabled info = harmony.send( { "function": f"PypeHarmony.Publish.{self_name}." @@ -126,24 +123,28 @@ class CollectFarmRender(pype.lib.abstract_collect_render. # TODO: handle pixel aspect and frame step # TODO: set Deadline stuff (pools, priority, etc. by presets) - subset_name = node.split("/")[1].replace('Farm', '') + # because of using 'renderFarm' as a family, replace 'Farm' with + # capitalized task name + subset_name = node.split("/")[1].replace( + 'Farm', + context.data["anatomyData"]["task"].capitalize()) render_instance = HarmonyRenderInstance( version=version, time=api.time(), source=context.data["currentFile"], - label=subset_name, + label=node.split("/")[1], subset=subset_name, asset=api.Session["AVALON_ASSET"], attachTo=False, setMembers=[node], - publish=True, + publish=info[4], review=False, renderer=None, priority=50, name=node.split("/")[1], - family="renderlayer", - families=["renderlayer"], + family="render.farm", + families=["render.farm"], resolutionWidth=context.data["resolutionWidth"], resolutionHeight=context.data["resolutionHeight"], @@ -157,12 +158,15 @@ class CollectFarmRender(pype.lib.abstract_collect_render. # time settings frameStart=context.data["frameStart"], frameEnd=context.data["frameEnd"], + handleStart=context.data["handleStart"], # from DB + handleEnd=context.data["handleEnd"], # from DB frameStep=1, outputType="Image", outputFormat=info[1], outputStartFrame=info[3], leadingZeros=info[2], - toBeRenderedOn='deadline' + toBeRenderedOn='deadline', + ignoreFrameHandleCheck=True ) self.log.debug(render_instance) diff --git a/pype/hosts/harmony/plugins/publish/collect_instances.py b/pype/hosts/harmony/plugins/publish/collect_instances.py index c3e551271f..dcff02a646 100644 --- a/pype/hosts/harmony/plugins/publish/collect_instances.py +++ b/pype/hosts/harmony/plugins/publish/collect_instances.py @@ -20,7 +20,7 @@ class CollectInstances(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder hosts = ["harmony"] families_mapping = { - "render": ["imagesequence", "review", "ftrack"], + "render": ["review", "ftrack"], "harmony.template": [], "palette": ["palette", "ftrack"] } @@ -54,8 +54,8 @@ class CollectInstances(pyblish.api.ContextPlugin): continue instance = context.create_instance(node.split("/")[-1]) - instance.append(node) instance.data.update(data) + instance.data["setMembers"] = [node] instance.data["publish"] = harmony.send( {"function": "node.getEnable", "args": [node]} )["result"] diff --git a/pype/hosts/harmony/plugins/publish/collect_palettes.py b/pype/hosts/harmony/plugins/publish/collect_palettes.py index e6795f894b..b8671badb3 100644 --- a/pype/hosts/harmony/plugins/publish/collect_palettes.py +++ b/pype/hosts/harmony/plugins/publish/collect_palettes.py @@ -14,6 +14,7 @@ class CollectPalettes(pyblish.api.ContextPlugin): label = "Palettes" order = pyblish.api.CollectorOrder + 0.003 hosts = ["harmony"] + # list of regexes for task names where collecting should happen allowed_tasks = [] diff --git a/pype/hosts/harmony/plugins/publish/collect_scene.py b/pype/hosts/harmony/plugins/publish/collect_scene.py index afb49369dc..9cfd49b0fe 100644 --- a/pype/hosts/harmony/plugins/publish/collect_scene.py +++ b/pype/hosts/harmony/plugins/publish/collect_scene.py @@ -25,13 +25,28 @@ class CollectScene(pyblish.api.ContextPlugin): context.data["scenePath"] = os.path.join( result[1], result[2] + ".xstage") context.data["frameRate"] = result[3] - context.data["frameStart"] = result[4] - context.data["frameEnd"] = result[5] + context.data["frameStartHandle"] = result[4] + context.data["frameEndHandle"] = result[5] context.data["audioPath"] = result[6] context.data["resolutionWidth"] = result[7] context.data["resolutionHeight"] = result[8] context.data["FOV"] = result[9] + # harmony always starts from 1. frame + # 1001 - 10010 >> 1 - 10 + # frameStart, frameEnd already collected by global plugin + offset = context.data["frameStart"] - 1 + frame_start = context.data["frameStart"] - offset + frames_count = context.data["frameEnd"] - \ + context.data["frameStart"] + 1 + + # increase by handleStart - real frame range + # frameStart != frameStartHandle with handle presence + context.data["frameStart"] = int(frame_start) + \ + context.data["handleStart"] + context.data["frameEnd"] = int(frames_count) + \ + context.data["frameStart"] - 1 + all_nodes = harmony.send( {"function": "node.subNodes", "args": ["Top"]} )["result"] diff --git a/pype/hosts/harmony/plugins/publish/extract_palette.py b/pype/hosts/harmony/plugins/publish/extract_palette.py index 029a4f0f11..39a822153c 100644 --- a/pype/hosts/harmony/plugins/publish/extract_palette.py +++ b/pype/hosts/harmony/plugins/publish/extract_palette.py @@ -7,7 +7,6 @@ from PIL import Image, ImageDraw, ImageFont from avalon import harmony import pype.api -import pype.hosts.harmony class ExtractPalette(pype.api.Extractor): diff --git a/pype/hosts/harmony/plugins/publish/extract_render.py b/pype/hosts/harmony/plugins/publish/extract_render.py index 84fa503f54..551d7afee1 100644 --- a/pype/hosts/harmony/plugins/publish/extract_render.py +++ b/pype/hosts/harmony/plugins/publish/extract_render.py @@ -17,7 +17,7 @@ class ExtractRender(pyblish.api.InstancePlugin): label = "Extract Render" order = pyblish.api.ExtractorOrder hosts = ["harmony"] - families = ["renderLocal"] + families = ["render"] def process(self, instance): # Collect scene data. @@ -47,7 +47,8 @@ class ExtractRender(pyblish.api.InstancePlugin): harmony.send( { "function": func, - "args": [instance[0], path + "/" + instance.data["name"]] + "args": [instance.data["setMembers"][0], + path + "/" + instance.data["name"]] } ) harmony.save_scene() @@ -75,7 +76,7 @@ class ExtractRender(pyblish.api.InstancePlugin): collections, remainder = clique.assemble(files, minimum_items=1) assert not remainder, ( "There should not be a remainder for {0}: {1}".format( - instance[0], remainder + instance.data["setMembers"][0], remainder ) ) self.log.debug(collections) diff --git a/pype/hosts/harmony/plugins/publish/extract_template.py b/pype/hosts/harmony/plugins/publish/extract_template.py index b8437c85ea..842bc77202 100644 --- a/pype/hosts/harmony/plugins/publish/extract_template.py +++ b/pype/hosts/harmony/plugins/publish/extract_template.py @@ -23,7 +23,7 @@ class ExtractTemplate(pype.api.Extractor): self.log.info(f"Outputting template to {staging_dir}") dependencies = [] - self.get_dependencies(instance[0], dependencies) + self.get_dependencies(instance.data["setMembers"][0], dependencies) # Get backdrops. backdrops = {} @@ -46,11 +46,11 @@ class ExtractTemplate(pype.api.Extractor): dependencies.append(node) # Make sure we dont export the instance node. - if instance[0] in dependencies: - dependencies.remove(instance[0]) + if instance.data["setMembers"][0] in dependencies: + dependencies.remove(instance.data["setMembers"][0]) # Export template. - pype.hosts.harmony.export_template( + pype.hosts.harmony.api.export_template( unique_backdrops, dependencies, filepath ) diff --git a/pype/hosts/harmony/plugins/publish/extract_workfile.py b/pype/hosts/harmony/plugins/publish/extract_workfile.py index be0444f0e6..842d0aa8d3 100644 --- a/pype/hosts/harmony/plugins/publish/extract_workfile.py +++ b/pype/hosts/harmony/plugins/publish/extract_workfile.py @@ -5,8 +5,6 @@ import shutil from zipfile import ZipFile import pype.api -from avalon import harmony -import pype.hosts.harmony class ExtractWorkfile(pype.api.Extractor): diff --git a/pype/hosts/harmony/plugins/publish/validate_audio.py b/pype/hosts/harmony/plugins/publish/validate_audio.py index b949b0a6e6..c043b31ca6 100644 --- a/pype/hosts/harmony/plugins/publish/validate_audio.py +++ b/pype/hosts/harmony/plugins/publish/validate_audio.py @@ -19,6 +19,12 @@ class ValidateAudio(pyblish.api.InstancePlugin): optional = True def process(self, instance): + node = None + if instance.data.get("setMembers"): + node = instance.data["setMembers"][0] + + if not node: + return # Collect scene data. func = """function func(write_node) { @@ -29,7 +35,7 @@ class ValidateAudio(pyblish.api.InstancePlugin): func """ result = harmony.send( - {"function": func, "args": [instance[0]]} + {"function": func, "args": [node]} )["result"] audio_path = result[0] diff --git a/pype/hosts/harmony/plugins/publish/validate_instances.py b/pype/hosts/harmony/plugins/publish/validate_instances.py index f084baf790..238f8d1038 100644 --- a/pype/hosts/harmony/plugins/publish/validate_instances.py +++ b/pype/hosts/harmony/plugins/publish/validate_instances.py @@ -25,9 +25,9 @@ class ValidateInstanceRepair(pyblish.api.Action): instances = pyblish.api.instances_by_plugin(failed, plugin) for instance in instances: - data = harmony.read(instance[0]) + data = harmony.read(instance.data["setMembers"][0]) data["asset"] = os.environ["AVALON_ASSET"] - harmony.imprint(instance[0], data) + harmony.imprint(instance.data["setMembers"][0], data) class ValidateInstance(pyblish.api.InstancePlugin): diff --git a/pype/hosts/harmony/plugins/publish/validate_scene_settings.py b/pype/hosts/harmony/plugins/publish/validate_scene_settings.py index 5ab1b11ec9..9b8a6183df 100644 --- a/pype/hosts/harmony/plugins/publish/validate_scene_settings.py +++ b/pype/hosts/harmony/plugins/publish/validate_scene_settings.py @@ -18,9 +18,12 @@ class ValidateSceneSettingsRepair(pyblish.api.Action): def process(self, context, plugin): """Repair action entry point.""" - pype.hosts.harmony.set_scene_settings( - pype.hosts.harmony.get_asset_settings() - ) + expected = pype.hosts.harmony.api.get_asset_settings() + asset_settings = _update_frames(dict.copy(expected)) + asset_settings["frameStart"] = 1 + asset_settings["frameEnd"] = asset_settings["frameEnd"] + \ + asset_settings["handleEnd"] + pype.hosts.harmony.api.set_scene_settings(asset_settings) if not os.path.exists(context.data["scenePath"]): self.log.info("correcting scene name") scene_dir = os.path.dirname(context.data["currentFile"]) @@ -45,16 +48,12 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): def process(self, instance): """Plugin entry point.""" - expected_settings = pype.hosts.harmony.get_asset_settings() + expected_settings = pype.hosts.harmony.api.get_asset_settings() self.log.info(expected_settings) - # Harmony is expected to start at 1. - frame_start = expected_settings["frameStart"] - frame_end = expected_settings["frameEnd"] - expected_settings["frameEnd"] = frame_end - frame_start + 1 - expected_settings["frameStart"] = 1 - - self.log.info(instance.context.data['anatomyData']['asset']) + expected_settings = _update_frames(dict.copy(expected_settings)) + expected_settings["frameEndHandle"] = expected_settings["frameEnd"] +\ + expected_settings["handleEnd"] if any(string in instance.context.data['anatomyData']['asset'] for string in self.frame_check_filter): @@ -73,13 +72,19 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): expected_settings.pop("resolutionWidth") expected_settings.pop("resolutionHeight") + self.log.debug(expected_settings) + current_settings = { "fps": fps, - "frameStart": instance.context.data.get("frameStart"), - "frameEnd": instance.context.data.get("frameEnd"), + "frameStart": instance.context.data["frameStart"], + "frameEnd": instance.context.data["frameEnd"], + "handleStart": instance.context.data.get("handleStart"), + "handleEnd": instance.context.data.get("handleEnd"), + "frameEndHandle": instance.context.data.get("frameEndHandle"), "resolutionWidth": instance.context.data.get("resolutionWidth"), "resolutionHeight": instance.context.data.get("resolutionHeight"), } + self.log.debug("curr:: {}".format(current_settings)) invalid_settings = [] for key, value in expected_settings.items(): @@ -90,6 +95,13 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): "current": current_settings[key] }) + if ((expected_settings["handleStart"] + or expected_settings["handleEnd"]) + and invalid_settings): + msg = "Handles included in calculation. Remove handles in DB " +\ + "or extend frame range in timeline." + invalid_settings[-1]["reason"] = msg + msg = "Found invalid settings:\n{}".format( json.dumps(invalid_settings, sort_keys=True, indent=4) ) @@ -97,3 +109,24 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): assert os.path.exists(instance.context.data.get("scenePath")), ( "Scene file not found (saved under wrong name)" ) + + +def _update_frames(expected_settings): + """ + Calculate proper frame range including handles set in DB. + + Harmony requires rendering from 1, so frame range is always moved + to 1. + Args: + expected_settings (dict): pulled from DB + + Returns: + modified expected_setting (dict) + """ + frames_count = expected_settings["frameEnd"] - \ + expected_settings["frameStart"] + 1 + + expected_settings["frameStart"] = 1.0 + expected_settings["handleStart"] + expected_settings["frameEnd"] = \ + expected_settings["frameStart"] + frames_count - 1 + return expected_settings diff --git a/pype/lib/abstract_collect_render.py b/pype/lib/abstract_collect_render.py index 19e7c37dba..2ac0fe434d 100644 --- a/pype/lib/abstract_collect_render.py +++ b/pype/lib/abstract_collect_render.py @@ -46,6 +46,13 @@ class RenderInstance(object): frameEnd = attr.ib() # start end frameStep = attr.ib() # frame step + handleStart = attr.ib(default=None) # start frame + handleEnd = attr.ib(default=None) # start frame + + # for softwares (like Harmony) where frame range cannot be set by DB + # handles need to be propagated if exist + ignoreFrameHandleCheck = attr.ib(default=False) + # -------------------- # With default values # metadata @@ -154,8 +161,8 @@ class AbstractCollectRender(pyblish.api.ContextPlugin): frame_start_render = int(render_instance.frameStart) frame_end_render = int(render_instance.frameEnd) - - if (int(context.data['frameStartHandle']) == frame_start_render + if (render_instance.ignoreFrameHandleCheck or + int(context.data['frameStartHandle']) == frame_start_render and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 handle_start = context.data['handleStart'] diff --git a/pype/lib/avalon_context.py b/pype/lib/avalon_context.py index a5728dba22..3d3a288b10 100644 --- a/pype/lib/avalon_context.py +++ b/pype/lib/avalon_context.py @@ -80,6 +80,7 @@ def any_outdated(): "database".format(**container)) checked.add(representation) + return False diff --git a/pype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py b/pype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py index fcb97e1281..c1a6de4ce3 100644 --- a/pype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py +++ b/pype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py @@ -63,6 +63,7 @@ class AfterEffectsSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline "AVALON_PROJECT", "AVALON_ASSET", "AVALON_TASK", + "AVALON_APP_NAME", "PYPE_USERNAME", "PYPE_DEV", "PYPE_LOG_NO_COLORS" @@ -76,6 +77,8 @@ class AfterEffectsSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline dln_job_info.EnvironmentKeyValue = "{key}={value}".format( key=key, value=val) + # to recognize job from PYPE for turning Event On/Off + dln_job_info.EnvironmentKeyValue = "PYPE_RENDER_JOB=1" return dln_job_info diff --git a/pype/hosts/harmony/plugins/publish/submit_harmony_deadline..py b/pype/modules/deadline/plugins/publish/submit_harmony_deadline..py similarity index 97% rename from pype/hosts/harmony/plugins/publish/submit_harmony_deadline..py rename to pype/modules/deadline/plugins/publish/submit_harmony_deadline..py index e40ff02d08..8e85937353 100644 --- a/pype/hosts/harmony/plugins/publish/submit_harmony_deadline..py +++ b/pype/modules/deadline/plugins/publish/submit_harmony_deadline..py @@ -236,7 +236,7 @@ class HarmonySubmitDeadline( label = "Submit to Deadline" order = pyblish.api.IntegratorOrder + 0.1 hosts = ["harmony"] - families = ["renderlayer"] + families = ["render.farm"] if not os.environ.get("DEADLINE_REST_URL"): optional = False active = False @@ -254,8 +254,8 @@ class HarmonySubmitDeadline( job_info.Name = self._instance.data["name"] job_info.Plugin = "HarmonyPype" job_info.Frames = "{}-{}".format( - self._instance.data["frameStart"], - self._instance.data["frameEnd"] + self._instance.data["frameStartHandle"], + self._instance.data["frameEndHandle"] ) # for now, get those from presets. Later on it should be # configurable in Harmony UI directly. @@ -272,6 +272,7 @@ class HarmonySubmitDeadline( "AVALON_PROJECT", "AVALON_ASSET", "AVALON_TASK", + "AVALON_APP_NAME", "PYPE_USERNAME", "PYPE_DEV", "PYPE_LOG_NO_COLORS" @@ -286,6 +287,9 @@ class HarmonySubmitDeadline( key=key, value=val) + # to recognize job from PYPE for turning Event On/Off + job_info.EnvironmentKeyValue = "PYPE_RENDER_JOB=1" + return job_info def _unzip_scene_file(self, published_scene: Path) -> Path: diff --git a/pype/modules/deadline/plugins/publish/submit_maya_deadline.py b/pype/modules/deadline/plugins/publish/submit_maya_deadline.py index 08b7479350..55705d1bbb 100644 --- a/pype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/pype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -431,6 +431,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "AVALON_PROJECT", "AVALON_ASSET", "AVALON_TASK", + "AVALON_APP_NAME", "PYPE_USERNAME", "PYPE_DEV", "PYPE_LOG_NO_COLORS" @@ -440,6 +441,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): if key in os.environ}, **api.Session) environment["PYPE_LOG_NO_COLORS"] = "1" environment["PYPE_MAYA_VERSION"] = cmds.about(v=True) + # to recognize job from PYPE for turning Event On/Off + environment["PYPE_RENDER_JOB"] = "1" self.payload_skeleton["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( key=key, diff --git a/pype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/pype/modules/deadline/plugins/publish/submit_nuke_deadline.py index af8694dd22..60cc179a9b 100644 --- a/pype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/pype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -218,6 +218,10 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): "PYTHONPATH", "PATH", "AVALON_SCHEMA", + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK", + "AVALON_APP_NAME", "FTRACK_API_KEY", "FTRACK_API_USER", "FTRACK_SERVER", @@ -265,7 +269,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): clean_environment[key] = clean_path environment = clean_environment - + # to recognize job from PYPE for turning Event On/Off + environment["PYPE_RENDER_JOB"] = "1" payload["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( key=key, diff --git a/pype/modules/deadline/plugins/publish/submit_publish_job.py b/pype/modules/deadline/plugins/publish/submit_publish_job.py index 62682ad976..38d328b1cb 100644 --- a/pype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/pype/modules/deadline/plugins/publish/submit_publish_job.py @@ -5,6 +5,7 @@ import os import json import re from copy import copy, deepcopy +import sys import pype.api from avalon import api, io @@ -13,36 +14,6 @@ from avalon.vendor import requests, clique import pyblish.api -def _get_script(path): - - # pass input path if exists - if path: - if os.path.exists(path): - return str(path) - else: - raise - - """Get path to the image sequence script.""" - try: - from pathlib import Path - except ImportError: - from pathlib2 import Path - - try: - from pype.scripts import publish_filesequence - except Exception: - assert False, "Expected module 'publish_deadline'to be available" - - module_path = publish_filesequence.__file__ - if module_path.endswith(".pyc"): - module_path = module_path[: -len(".pyc")] + ".py" - - path = Path(os.path.normpath(module_path)).resolve(strict=True) - assert path is not None, ("Cannot determine path") - - return str(path) - - def get_resources(version, extension=None): """Get the files from the specific version.""" query = {"type": "representation", "parent": version["_id"]} @@ -127,6 +98,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): label = "Submit image sequence jobs to Deadline or Muster" order = pyblish.api.IntegratorOrder + 0.2 icon = "tractor" + deadline_plugin = "Pype" hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony"] @@ -144,8 +116,14 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "FTRACK_SERVER", "PYPE_METADATA_FILE", "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK", + "AVALON_APP_NAME", + "PYPE_PUBLISH_JOB" "PYPE_LOG_NO_COLORS", - "PYPE_USERNAME" + "PYPE_USERNAME", + "PYPE_RENDER_JOB", + "PYPE_PUBLISH_JOB" ] # custom deadline atributes @@ -171,7 +149,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # list of family names to transfer to new family if present families_transfer = ["render3d", "render2d", "ftrack", "slate"] - plugin_python_version = "3.7" + plugin_pype_version = "3.0" # script path for publish_filesequence.py publishing_script = None @@ -207,7 +185,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): ).format(output_dir)) roothless_mtdt_p = metadata_path - return (metadata_path, roothless_mtdt_p) + return metadata_path, roothless_mtdt_p def _submit_deadline_post_job(self, instance, job, instances): """Submit publish job to Deadline. @@ -235,10 +213,30 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): 'render', override_version) + # Transfer the environment from the original job to this dependent + # job so they use the same environment + metadata_path, roothless_metadata_path = \ + self._create_metadata_path(instance) + + environment = job["Props"].get("Env", {}) + environment["AVALON_PROJECT"] = io.Session["AVALON_PROJECT"] + environment["AVALON_ASSET"] = io.Session["AVALON_ASSET"] + environment["AVALON_TASK"] = io.Session["AVALON_TASK"] + environment["AVALON_APP_NAME"] = os.environ.get("AVALON_APP_NAME") + environment["PYPE_LOG_NO_COLORS"] = "1" + environment["PYPE_USERNAME"] = instance.context.data["user"] + environment["PYPE_PUBLISH_JOB"] = "1" + environment["PYPE_RENDER_JOB"] = "0" + + args = [ + 'publish', + roothless_metadata_path + ] + # Generate the payload for Deadline submission payload = { "JobInfo": { - "Plugin": "Python", + "Plugin": self.deadline_plugin, "BatchName": job["Props"]["Batch"], "Name": job_name, "UserName": job["Props"]["User"], @@ -255,9 +253,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "OutputDirectory0": output_dir }, "PluginInfo": { - "Version": self.plugin_python_version, - "ScriptFile": _get_script(self.publishing_script), - "Arguments": "", + "Version": self.plugin_pype_version, + "Arguments": " ".join(args), "SingleFrameOnly": "True", }, # Mandatory for Deadline, may be empty @@ -274,20 +271,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): else: payload["JobInfo"]["JobDependency0"] = job["_id"] - # Transfer the environment from the original job to this dependent - # job so they use the same environment - metadata_path, roothless_metadata_path = self._create_metadata_path( - instance) - environment = job["Props"].get("Env", {}) - environment["PYPE_METADATA_FILE"] = roothless_metadata_path - environment["AVALON_PROJECT"] = io.Session["AVALON_PROJECT"] - environment["PYPE_LOG_NO_COLORS"] = "1" - environment["PYPE_USERNAME"] = instance.context.data["user"] - try: - environment["PYPE_PYTHON_EXE"] = os.environ["PYPE_PYTHON_EXE"] - except KeyError: - # PYPE_PYTHON_EXE not set - pass i = 0 for index, key in enumerate(environment): if key.upper() in self.enviro_filter: @@ -1065,4 +1048,4 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # Directory publish_folder = os.path.dirname(file_path) - return publish_folder + return publish_folder \ No newline at end of file diff --git a/pype/pype_commands.py b/pype/pype_commands.py index e4e68e767a..ea94a35e3a 100644 --- a/pype/pype_commands.py +++ b/pype/pype_commands.py @@ -6,6 +6,7 @@ import json from pathlib import Path from pype.lib import PypeLogger +from pype.api import get_app_environments_for_context class PypeCommands: @@ -63,6 +64,14 @@ class PypeCommands: import pyblish.api import pyblish.util + env = get_app_environments_for_context( + os.environ["AVALON_PROJECT"], + os.environ["AVALON_ASSET"], + os.environ["AVALON_TASK"], + os.environ["AVALON_APP_NAME"] + ) + os.environ.update(env) + log = Logger.get_logger() install() diff --git a/pype/settings/defaults/project_settings/deadline.json b/pype/settings/defaults/project_settings/deadline.json index 6844979ddb..9e5665bee9 100644 --- a/pype/settings/defaults/project_settings/deadline.json +++ b/pype/settings/defaults/project_settings/deadline.json @@ -12,7 +12,7 @@ "optional": false, "use_published": true, "priority": 50, - "Chunk Size": 10, + "chunk_size": 10, "primary_pool": "", "secondary_pool": "", "group": "", @@ -23,7 +23,7 @@ "optional": false, "use_published": true, "priority": 50, - "Chunk Size": 10000, + "chunk_size": 10000, "primary_pool": "", "secondary_pool": "", "group": "", @@ -34,7 +34,7 @@ "optional": false, "use_published": true, "priority": 50, - "Chunk Size": 10000, + "chunk_size": 10000, "primary_pool": "", "secondary_pool": "", "group": "", diff --git a/pype/settings/defaults/system_settings/applications.json b/pype/settings/defaults/system_settings/applications.json index 18e46a790c..b78d23f6ff 100644 --- a/pype/settings/defaults/system_settings/applications.json +++ b/pype/settings/defaults/system_settings/applications.json @@ -1015,12 +1015,10 @@ "host_name": "harmony", "environment": { "AVALON_HARMONY_WORKFILES_ON_LAUNCH": "1", - "PYBLISH_GUI_ALWAYS_EXEC": "1", "LIB_OPENHARMONY_PATH": "{PYPE_ROOT}/pype/vendor/OpenHarmony", "__environment_keys__": { "harmony": [ "AVALON_HARMONY_WORKFILES_ON_LAUNCH", - "PYBLISH_GUI_ALWAYS_EXEC", "LIB_OPENHARMONY_PATH" ] } diff --git a/pype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/pype/settings/entities/schemas/projects_schema/schema_project_deadline.json index ea76f4e62e..c103f9467c 100644 --- a/pype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -84,7 +84,7 @@ }, { "type": "number", - "key": "Chunk Size", + "key": "chunk_size", "label": "Chunk Size" }, { @@ -138,7 +138,7 @@ }, { "type": "number", - "key": "Chunk Size", + "key": "chunk_size", "label": "Chunk Size" }, { @@ -192,7 +192,7 @@ }, { "type": "number", - "key": "Chunk Size", + "key": "chunk_size", "label": "Chunk Size" }, { diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 88dce679f7..ab05e7d665 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -106,7 +106,7 @@ class IntentModel(QtGui.QStandardItemModel): intents_preset = ( get_system_settings() .get("modules", {}) - .get("Ftrack", {}) + .get("ftrack", {}) .get("intent", {}) ) diff --git a/repos/avalon-core b/repos/avalon-core index 9e6b0d02e5..8d3364dc8a 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 9e6b0d02e5a147cbafdcaeee7d786d4767e14c94 +Subproject commit 8d3364dc8ae73a33726ba3279ff75adff73c6239 diff --git a/start.py b/start.py index b5e0fee847..875a69f604 100644 --- a/start.py +++ b/start.py @@ -116,7 +116,8 @@ from igniter.tools import get_pype_path_from_db # noqa from igniter.bootstrap_repos import PypeVersion # noqa: E402 bootstrap = BootstrapRepos() -silent_commands = ["run", "igniter", "standalonepublisher"] +silent_commands = ["run", "igniter", "standalonepublisher", + "extractenvironments"] def set_pype_global_environments() -> None: @@ -129,7 +130,10 @@ def set_pype_global_environments() -> None: # TODO Global environments will be stored in "general" settings so loading # will be modified and can be done in igniter. - env = acre.merge(all_env["global"], dict(os.environ)) + env = acre.merge( + acre.parse(all_env["global"]), + dict(os.environ) + ) os.environ.clear() os.environ.update(env) @@ -528,8 +532,8 @@ def boot(): from igniter.terminal_splash import play_animation # don't play for silenced commands - if all(item not in sys.argv for item in silent_commands): - play_animation() + # if all(item not in sys.argv for item in silent_commands): + # play_animation() # ------------------------------------------------------------------------ # Process arguments @@ -607,9 +611,6 @@ def boot(): except KeyError: pass - from pype import cli - from pype.lib import terminal as t - from pype.version import __version__ print(">>> loading environments ...") # Avalon environments must be set before avalon module is imported print(" - for Avalon ...") @@ -619,6 +620,10 @@ def boot(): print(" - for modules ...") set_modules_environments() + from pype import cli + from pype.lib import terminal as t + from pype.version import __version__ + assert version_path, "Version path not defined." info = get_info() info.insert(0, f">>> Using Pype from [ {version_path} ]") diff --git a/vendor/deadline/custom/events/Pype/Pype.param b/vendor/deadline/custom/events/Pype/Pype.param new file mode 100644 index 0000000000..1452163bdd --- /dev/null +++ b/vendor/deadline/custom/events/Pype/Pype.param @@ -0,0 +1,37 @@ +[State] +Type=Enum +Items=Global Enabled;Opt-In;Disabled +Category=Options +CategoryOrder=0 +CategoryIndex=0 +Label=State +Default=Global Enabled +Description=How this event plug-in should respond to events. If Global, all jobs and slaves will trigger the events for this plugin. If Opt-In, jobs and slaves can choose to trigger the events for this plugin. If Disabled, no events are triggered for this plugin. + +[PythonSearchPaths] +Type=MultiLineMultiFolder +Label=Additional Python Search Paths +Category=Options +CategoryOrder=0 +CategoryIndex=1 +Default= +Description=The list of paths to append to the PYTHONPATH environment variable. This allows the Python job to find custom modules in non-standard locations. + +[LoggingLevel] +Type=Enum +Label=Logging Level +Category=Options +CategoryOrder=0 +CategoryIndex=2 +Items=DEBUG;INFO;WARNING;ERROR +Default=DEBUG +Description=Logging level where printing will start. + +[PypeExecutable] +Type=MultiLineMultiFolder +Label=Path to Pype executable dir +Category=Job Plugins +CategoryOrder=1 +CategoryIndex=1 +Default= +Description= \ No newline at end of file diff --git a/vendor/deadline/custom/events/Pype/Pype.py b/vendor/deadline/custom/events/Pype/Pype.py new file mode 100644 index 0000000000..6b91ba4303 --- /dev/null +++ b/vendor/deadline/custom/events/Pype/Pype.py @@ -0,0 +1,190 @@ +import Deadline.Events +import Deadline.Scripting + + +def GetDeadlineEventListener(): + return PypeEventListener() + + +def CleanupDeadlineEventListener(eventListener): + eventListener.Cleanup() + + +class PypeEventListener(Deadline.Events.DeadlineEventListener): + """ + Called on every Deadline plugin event, used for injecting Pype + environment variables into rendering process. + + Expects that job already contains env vars: + AVALON_PROJECT + AVALON_ASSET + AVALON_TASK + AVALON_APP_NAME + Without these only global environment would be pulled from Pype + + Configure 'Path to Pype executable dir' in Deadlines + 'Tools > Configure Events > pype ' + Only directory path is needed. + + """ + def __init__(self): + self.OnJobSubmittedCallback += self.OnJobSubmitted + self.OnJobStartedCallback += self.OnJobStarted + self.OnJobFinishedCallback += self.OnJobFinished + self.OnJobRequeuedCallback += self.OnJobRequeued + self.OnJobFailedCallback += self.OnJobFailed + self.OnJobSuspendedCallback += self.OnJobSuspended + self.OnJobResumedCallback += self.OnJobResumed + self.OnJobPendedCallback += self.OnJobPended + self.OnJobReleasedCallback += self.OnJobReleased + self.OnJobDeletedCallback += self.OnJobDeleted + self.OnJobErrorCallback += self.OnJobError + self.OnJobPurgedCallback += self.OnJobPurged + + self.OnHouseCleaningCallback += self.OnHouseCleaning + self.OnRepositoryRepairCallback += self.OnRepositoryRepair + + self.OnSlaveStartedCallback += self.OnSlaveStarted + self.OnSlaveStoppedCallback += self.OnSlaveStopped + self.OnSlaveIdleCallback += self.OnSlaveIdle + self.OnSlaveRenderingCallback += self.OnSlaveRendering + self.OnSlaveStartingJobCallback += self.OnSlaveStartingJob + self.OnSlaveStalledCallback += self.OnSlaveStalled + + self.OnIdleShutdownCallback += self.OnIdleShutdown + self.OnMachineStartupCallback += self.OnMachineStartup + self.OnThermalShutdownCallback += self.OnThermalShutdown + self.OnMachineRestartCallback += self.OnMachineRestart + + def Cleanup(self): + del self.OnJobSubmittedCallback + del self.OnJobStartedCallback + del self.OnJobFinishedCallback + del self.OnJobRequeuedCallback + del self.OnJobFailedCallback + del self.OnJobSuspendedCallback + del self.OnJobResumedCallback + del self.OnJobPendedCallback + del self.OnJobReleasedCallback + del self.OnJobDeletedCallback + del self.OnJobErrorCallback + del self.OnJobPurgedCallback + + del self.OnHouseCleaningCallback + del self.OnRepositoryRepairCallback + + del self.OnSlaveStartedCallback + del self.OnSlaveStoppedCallback + del self.OnSlaveIdleCallback + del self.OnSlaveRenderingCallback + del self.OnSlaveStartingJobCallback + del self.OnSlaveStalledCallback + + del self.OnIdleShutdownCallback + del self.OnMachineStartupCallback + del self.OnThermalShutdownCallback + del self.OnMachineRestartCallback + + def set_pype_executable_path(self, job): + """ + Sets configurable PypeExecutable value to job extra infos. + + GlobalJobPreLoad takes this value, pulls env vars for each task + from specific worker itself. GlobalJobPreLoad is not easily + configured, so we are configuring Event itself. + """ + pype_execs = self.GetConfigEntryWithDefault("PypeExecutable", "") + job.SetJobExtraInfoKeyValue("pype_executables", pype_execs) + + Deadline.Scripting.RepositoryUtils.SaveJob(job) + + def updateFtrackStatus(self, job, statusName, createIfMissing=False): + """Updates version status on ftrack""" + pass + + def OnJobSubmitted(self, job): + # self.LogInfo("OnJobSubmitted LOGGING") + # for 1st time submit + self.set_pype_executable_path(job) + self.updateFtrackStatus(job, "Render Queued") + + def OnJobStarted(self, job): + # self.LogInfo("OnJobStarted") + self.set_pype_executable_path(job) + self.updateFtrackStatus(job, "Rendering") + + def OnJobFinished(self, job): + # self.LogInfo("OnJobFinished") + self.updateFtrackStatus(job, "Artist Review") + + def OnJobRequeued(self, job): + # self.LogInfo("OnJobRequeued LOGGING") + self.set_pype_executable_path(job) + + def OnJobFailed(self, job): + pass + + def OnJobSuspended(self, job): + # self.LogInfo("OnJobSuspended LOGGING") + self.updateFtrackStatus(job, "Render Queued") + + def OnJobResumed(self, job): + # self.LogInfo("OnJobResumed LOGGING") + self.set_pype_executable_path(job) + self.updateFtrackStatus(job, "Rendering") + + def OnJobPended(self, job): + # self.LogInfo("OnJobPended LOGGING") + pass + + def OnJobReleased(self, job): + pass + + def OnJobDeleted(self, job): + pass + + def OnJobError(self, job, task, report): + # self.LogInfo("OnJobError LOGGING") + pass + + def OnJobPurged(self, job): + pass + + def OnHouseCleaning(self): + pass + + def OnRepositoryRepair(self, job, *args): + pass + + def OnSlaveStarted(self, job): + # self.LogInfo("OnSlaveStarted LOGGING") + pass + + def OnSlaveStopped(self, job): + pass + + def OnSlaveIdle(self, job): + pass + + def OnSlaveRendering(self, host_name, job): + # self.LogInfo("OnSlaveRendering LOGGING") + pass + + def OnSlaveStartingJob(self, host_name, job): + # self.LogInfo("OnSlaveStartingJob LOGGING") + self.set_pype_executable_path(job) + + def OnSlaveStalled(self, job): + pass + + def OnIdleShutdown(self, job): + pass + + def OnMachineStartup(self, job): + pass + + def OnThermalShutdown(self, job): + pass + + def OnMachineRestart(self, job): + pass diff --git a/vendor/deadline/custom/plugins/GlobalJobPreLoad.py b/vendor/deadline/custom/plugins/GlobalJobPreLoad.py new file mode 100644 index 0000000000..01f6d3fdf7 --- /dev/null +++ b/vendor/deadline/custom/plugins/GlobalJobPreLoad.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +import os +import tempfile +import time +import subprocess +import json +from Deadline.Scripting import RepositoryUtils, FileUtils + + +def inject_pype_environment(deadlinePlugin): + job = deadlinePlugin.GetJob() + job = RepositoryUtils.GetJob(job.JobId, True) # invalidates cache + + pype_render_job = job.GetJobEnvironmentKeyValue('PYPE_RENDER_JOB') \ + or '0' + pype_publish_job = job.GetJobEnvironmentKeyValue('PYPE_PUBLISH_JOB') \ + or '0' + + if pype_publish_job == '1' and pype_render_job == '1': + raise RuntimeError("Misconfiguration. Job couldn't be both " + + "render and publish.") + + if pype_publish_job == '1': + print("Publish job, skipping inject.") + return + elif pype_render_job == '0': + # not pype triggered job + return + + print("inject_pype_environment start") + try: + exe_list = job.GetJobExtraInfoKeyValue("pype_executables") + pype_app = FileUtils.SearchFileList(exe_list) + if pype_app == "": + raise RuntimeError( + "Pype executable was not found " + + "in the semicolon separated list \"" + exe_list + "\". " + + "The path to the render executable can be configured " + + "from the Plugin Configuration in the Deadline Monitor.") + + # tempfile.TemporaryFile cannot be used because of locking + export_url = os.path.join(tempfile.gettempdir(), + time.strftime('%Y%m%d%H%M%S'), + 'env.json') # add HHMMSS + delete later + print("export_url {}".format(export_url)) + + args = [ + pype_app, + 'extractenvironments', + export_url + ] + + add_args = {} + add_args['project'] = \ + job.GetJobEnvironmentKeyValue('AVALON_PROJECT') + add_args['asset'] = job.GetJobEnvironmentKeyValue('AVALON_ASSET') + add_args['task'] = job.GetJobEnvironmentKeyValue('AVALON_TASK') + add_args['app'] = job.GetJobEnvironmentKeyValue('AVALON_APP_NAME') + + if all(add_args.values()): + for key, value in add_args.items(): + args.append("--{}".format(key)) + args.append(value) + else: + msg = "Required env vars: AVALON_PROJECT, AVALON_ASSET, " + \ + "AVALON_TASK, AVALON_APP_NAME" + raise RuntimeError(msg) + + print("args::{}".format(args)) + + exit_code = subprocess.call(args, shell=True) + if exit_code != 0: + raise RuntimeError("Publishing failed, check worker's log") + + with open(export_url) as fp: + contents = json.load(fp) + for key, value in contents.items(): + deadlinePlugin.SetEnvironmentVariable(key, value) + + os.remove(export_url) + + print("inject_pype_environment end") + except Exception: + import traceback + print(traceback.format_exc()) + print("inject_pype_environment failed") + RepositoryUtils.FailJob(job) + raise + + +def __main__(deadlinePlugin): + inject_pype_environment(deadlinePlugin) diff --git a/vendor/deadline/custom/plugins/Pype/Pype.ico b/vendor/deadline/custom/plugins/Pype/Pype.ico new file mode 100644 index 0000000000..746fc36ba2 Binary files /dev/null and b/vendor/deadline/custom/plugins/Pype/Pype.ico differ diff --git a/vendor/deadline/custom/plugins/Pype/Pype.options b/vendor/deadline/custom/plugins/Pype/Pype.options new file mode 100644 index 0000000000..df75bbe0fb --- /dev/null +++ b/vendor/deadline/custom/plugins/Pype/Pype.options @@ -0,0 +1,41 @@ +[ScriptFile] +Type=filename +Label=Script File +Category=Python Options +CategoryOrder=0 +Index=0 +Description=The script file to be executed. +Required=false +DisableIfBlank=true + +[Arguments] +Type=string +Label=Arguments +Category=Python Options +CategoryOrder=0 +Index=1 +Description=The arguments to pass to the script. If no arguments are required, leave this blank. +Required=false +DisableIfBlank=true + +[Version] +Type=enum +Values=3.0 +Label=Version +Category=Python Options +CategoryOrder=0 +Index=2 +Description=The version of Python to use. +Required=false +DisableIfBlank=true + +[SingleFramesOnly] +Type=boolean +Label=Single Frames Only +Category=Job Options +CategoryOrder=1 +Index=0 +Description=If enabled, the plugin will only render one frame at a time even if a single task contains a chunk of frames. +Required=true +DisableIfBlank=true +Default=false diff --git a/vendor/deadline/custom/plugins/Pype/Pype.param b/vendor/deadline/custom/plugins/Pype/Pype.param new file mode 100644 index 0000000000..b55fd9433e --- /dev/null +++ b/vendor/deadline/custom/plugins/Pype/Pype.param @@ -0,0 +1,27 @@ +[About] +Type=label +Label=About +Category=About Plugin +CategoryOrder=-1 +Index=0 +Default=Pype Plugin for Deadline +Description=Not configurable + +[ConcurrentTasks] +Type=label +Label=ConcurrentTasks +Category=About Plugin +CategoryOrder=-1 +Index=0 +Default=True +Description=Not configurable + +[Pype_Executable_3_0] +Type=multilinemultifilename +Label=Pype 3.0 Executable +Category=Pype Executables +CategoryOrder=0 +Index=0 +Default= +Description=The path to the Pype executable. Enter alternative paths on separate lines. + diff --git a/vendor/deadline/custom/plugins/Pype/Pype.py b/vendor/deadline/custom/plugins/Pype/Pype.py new file mode 100644 index 0000000000..23207e644b --- /dev/null +++ b/vendor/deadline/custom/plugins/Pype/Pype.py @@ -0,0 +1,116 @@ +from System.IO import Path +from System.Text.RegularExpressions import Regex + +from Deadline.Plugins import PluginType, DeadlinePlugin +from Deadline.Scripting import StringUtils, FileUtils, RepositoryUtils + +import re + + +###################################################################### +# This is the function that Deadline calls to get an instance of the +# main DeadlinePlugin class. +###################################################################### +def GetDeadlinePlugin(): + return PypeDeadlinePlugin() + + +def CleanupDeadlinePlugin(deadlinePlugin): + deadlinePlugin.Cleanup() + + +class PypeDeadlinePlugin(DeadlinePlugin): + """ + Standalone plugin for publishing from Pype. + + Calls Pype executable 'pype_console' from first correctly found + file based on plugin configuration. Uses 'publish' command and passes + path to metadata json file, which contains all needed information + for publish process. + """ + def __init__(self): + self.InitializeProcessCallback += self.InitializeProcess + self.RenderExecutableCallback += self.RenderExecutable + self.RenderArgumentCallback += self.RenderArgument + + def Cleanup(self): + for stdoutHandler in self.StdoutHandlers: + del stdoutHandler.HandleCallback + + del self.InitializeProcessCallback + del self.RenderExecutableCallback + del self.RenderArgumentCallback + + def InitializeProcess(self): + self.PluginType = PluginType.Simple + self.StdoutHandling = True + + self.SingleFramesOnly = self.GetBooleanPluginInfoEntryWithDefault( + "SingleFramesOnly", False) + self.LogInfo("Single Frames Only: %s" % self.SingleFramesOnly) + + self.AddStdoutHandlerCallback( + ".*Progress: (\d+)%.*").HandleCallback += self.HandleProgress + + def RenderExecutable(self): + version = self.GetPluginInfoEntry("Version") + + exeList = self.GetConfigEntry( + "Pype_Executable_" + version.replace(".", "_")) + exe = FileUtils.SearchFileList(exeList) + if exe == "": + self.FailRender( + "Pype " + version + " executable was not found " + + "in the semicolon separated list \"" + exeList + "\". " + + "The path to the render executable can be configured " + + "from the Plugin Configuration in the Deadline Monitor.") + return exe + + def RenderArgument(self): + arguments = str(self.GetPluginInfoEntryWithDefault("Arguments", "")) + arguments = RepositoryUtils.CheckPathMapping(arguments) + + arguments = re.sub(r"<(?i)STARTFRAME>", str(self.GetStartFrame()), + arguments) + arguments = re.sub(r"<(?i)ENDFRAME>", str(self.GetEndFrame()), + arguments) + arguments = re.sub(r"<(?i)QUOTE>", "\"", arguments) + + arguments = self.ReplacePaddedFrame(arguments, + "<(?i)STARTFRAME%([0-9]+)>", + self.GetStartFrame()) + arguments = self.ReplacePaddedFrame(arguments, + "<(?i)ENDFRAME%([0-9]+)>", + self.GetEndFrame()) + + count = 0 + for filename in self.GetAuxiliaryFilenames(): + localAuxFile = Path.Combine(self.GetJobsDataDirectory(), filename) + arguments = re.sub(r"<(?i)AUXFILE" + str(count) + r">", + localAuxFile.replace("\\", "/"), arguments) + count += 1 + + return arguments + + def ReplacePaddedFrame(self, arguments, pattern, frame): + frameRegex = Regex(pattern) + while True: + frameMatch = frameRegex.Match(arguments) + if frameMatch.Success: + paddingSize = int(frameMatch.Groups[1].Value) + if paddingSize > 0: + padding = StringUtils.ToZeroPaddedString(frame, + paddingSize, + False) + else: + padding = str(frame) + arguments = arguments.replace(frameMatch.Groups[0].Value, + padding) + else: + break + + return arguments + + def HandleProgress(self): + progress = float(self.GetRegexMatch(1)) + self.SetProgress(progress) diff --git a/vendor/deadline/custom/plugins/PypeTileAssembler/PypeTileAssembler.ico b/vendor/deadline/custom/plugins/PypeTileAssembler/PypeTileAssembler.ico new file mode 100644 index 0000000000..70b2398730 Binary files /dev/null and b/vendor/deadline/custom/plugins/PypeTileAssembler/PypeTileAssembler.ico differ diff --git a/vendor/deadline/custom/plugins/PypeTileAssembler/PypeTileAssembler.options b/vendor/deadline/custom/plugins/PypeTileAssembler/PypeTileAssembler.options new file mode 100644 index 0000000000..dcfc0c6496 --- /dev/null +++ b/vendor/deadline/custom/plugins/PypeTileAssembler/PypeTileAssembler.options @@ -0,0 +1,35 @@ +[OIIOToolPath] +Type=filename +Label=OIIO Tool location +Category=OIIO +Index=0 +Description=OIIO Tool executable to use. +Required=false +DisableIfBlank=true + +[OutputFile] +Type=filenamesave +Label=Output File +Category=Output +Index=0 +Description=The scene filename as it exists on the network +Required=false +DisableIfBlank=true + +[CleanupTiles] +Type=boolean +Category=Options +Index=0 +Label=Cleanup Tiles +Required=false +DisableIfBlank=true +Description=If enabled, the Pype Tile Assembler will cleanup all tiles after assembly. + +[Renderer] +Type=string +Label=Renderer +Category=Quicktime Info +Index=0 +Description=Renderer name +Required=false +DisableIfBlank=true diff --git a/vendor/deadline/custom/plugins/PypeTileAssembler/PypeTileAssembler.param b/vendor/deadline/custom/plugins/PypeTileAssembler/PypeTileAssembler.param new file mode 100644 index 0000000000..5312b8c14c --- /dev/null +++ b/vendor/deadline/custom/plugins/PypeTileAssembler/PypeTileAssembler.param @@ -0,0 +1,17 @@ +[About] +Type=label +Label=About +Category=About Plugin +CategoryOrder=-1 +Index=0 +Default=Pype Tile Assembler Plugin for Deadline +Description=Not configurable + +[OIIOTool_RenderExecutable] +Type=multilinemultifilename +Label=OIIO Tool Executable +Category=Render Executables +CategoryOrder=0 +Default=C:\Program Files\OIIO\bin\oiiotool.exe;/usr/bin/oiiotool +Description=The path to the Open Image IO Tool executable file used for rendering. Enter alternative paths on separate lines. +W diff --git a/vendor/deadline/custom/plugins/PypeTileAssembler/PypeTileAssembler.py b/vendor/deadline/custom/plugins/PypeTileAssembler/PypeTileAssembler.py new file mode 100644 index 0000000000..dddc379273 --- /dev/null +++ b/vendor/deadline/custom/plugins/PypeTileAssembler/PypeTileAssembler.py @@ -0,0 +1,372 @@ +# -*- coding: utf-8 -*- +"""Tile Assembler Plugin using Open Image IO tool. + +Todo: + Currently we support only EXRs with their data window set. +""" +import os +import subprocess +from xml.dom import minidom + +from System.IO import Path + +from Deadline.Plugins import DeadlinePlugin +from Deadline.Scripting import ( + FileUtils, RepositoryUtils, SystemUtils) + + +INT_KEYS = { + "x", "y", "height", "width", "full_x", "full_y", + "full_width", "full_height", "full_depth", "full_z", + "tile_width", "tile_height", "tile_depth", "deep", "depth", + "nchannels", "z_channel", "alpha_channel", "subimages" +} +LIST_KEYS = { + "channelnames" +} + + +def GetDeadlinePlugin(): # noqa: N802 + """Helper.""" + return PypeTileAssembler() + + +def CleanupDeadlinePlugin(deadlinePlugin): # noqa: N802, N803 + """Helper.""" + deadlinePlugin.cleanup() + + +class PypeTileAssembler(DeadlinePlugin): + """Deadline plugin for assembling tiles using OIIO.""" + + def __init__(self): + """Init.""" + self.InitializeProcessCallback += self.initialize_process + self.RenderExecutableCallback += self.render_executable + self.RenderArgumentCallback += self.render_argument + self.PreRenderTasksCallback += self.pre_render_tasks + self.PostRenderTasksCallback += self.post_render_tasks + + def cleanup(self): + """Cleanup function.""" + for stdoutHandler in self.StdoutHandlers: + del stdoutHandler.HandleCallback + + del self.InitializeProcessCallback + del self.RenderExecutableCallback + del self.RenderArgumentCallback + del self.PreRenderTasksCallback + del self.PostRenderTasksCallback + + def initialize_process(self): + """Initialization.""" + self.SingleFramesOnly = True + self.StdoutHandling = True + self.renderer = self.GetPluginInfoEntryWithDefault( + "Renderer", "undefined") + self.AddStdoutHandlerCallback( + ".*Error.*").HandleCallback += self.handle_stdout_error + + def render_executable(self): + """Get render executable name. + + Get paths from plugin configuration, find executable and return it. + + Returns: + (str): Render executable. + + """ + oiiotool_exe_list = self.GetConfigEntry("OIIOTool_RenderExecutable") + oiiotool_exe = FileUtils.SearchFileList(oiiotool_exe_list) + + if oiiotool_exe == "": + self.FailRender(("No file found in the semicolon separated " + "list \"{}\". The path to the render executable " + "can be configured from the Plugin Configuration " + "in the Deadline Monitor.").format( + oiiotool_exe_list)) + + return oiiotool_exe + + def render_argument(self): + """Generate command line arguments for render executable. + + Returns: + (str): arguments to add to render executable. + + """ + # Read tile config file. This file is in compatible format with + # Draft Tile Assembler + data = {} + with open(self.config_file, "rU") as f: + for text in f: + # Parsing key-value pair and removing white-space + # around the entries + info = [x.strip() for x in text.split("=", 1)] + + if len(info) > 1: + try: + data[str(info[0])] = info[1] + except Exception as e: + # should never be called + self.FailRender( + "Cannot parse config file: {}".format(e)) + + # Get output file. We support only EXRs now. + output_file = data["ImageFileName"] + output_file = RepositoryUtils.CheckPathMapping(output_file) + output_file = self.process_path(output_file) + """ + _, ext = os.path.splitext(output_file) + if "exr" not in ext: + self.FailRender( + "[{}] Only EXR format is supported for now.".format(ext)) + """ + tile_info = [] + for tile in range(int(data["TileCount"])): + tile_info.append({ + "filepath": data["Tile{}".format(tile)], + "pos_x": int(data["Tile{}X".format(tile)]), + "pos_y": int(data["Tile{}Y".format(tile)]), + "height": int(data["Tile{}Height".format(tile)]), + "width": int(data["Tile{}Width".format(tile)]) + }) + + # FFMpeg doesn't support tile coordinates at the moment. + # arguments = self.tile_completer_ffmpeg_args( + # int(data["ImageWidth"]), int(data["ImageHeight"]), + # tile_info, output_file) + + arguments = self.tile_oiio_args( + int(data["ImageWidth"]), int(data["ImageHeight"]), + tile_info, output_file) + self.LogInfo( + "Using arguments: {}".format(" ".join(arguments))) + self.tiles = tile_info + return " ".join(arguments) + + def process_path(self, filepath): + """Handle slashes in file paths.""" + if SystemUtils.IsRunningOnWindows(): + filepath = filepath.replace("/", "\\") + if filepath.startswith("\\") and not filepath.startswith("\\\\"): + filepath = "\\" + filepath + else: + filepath = filepath.replace("\\", "/") + return filepath + + def pre_render_tasks(self): + """Load config file and do remapping.""" + self.LogInfo("Pype Tile Assembler starting...") + scene_filename = self.GetDataFilename() + + temp_scene_directory = self.CreateTempDirectory( + "thread" + str(self.GetThreadNumber())) + temp_scene_filename = Path.GetFileName(scene_filename) + self.config_file = Path.Combine( + temp_scene_directory, temp_scene_filename) + + if SystemUtils.IsRunningOnWindows(): + RepositoryUtils.CheckPathMappingInFileAndReplaceSeparator( + scene_filename, self.config_file, "/", "\\") + else: + RepositoryUtils.CheckPathMappingInFileAndReplaceSeparator( + scene_filename, self.config_file, "\\", "/") + os.chmod(self.config_file, os.stat(self.config_file).st_mode) + + def post_render_tasks(self): + """Cleanup tiles if required.""" + if self.GetBooleanPluginInfoEntryWithDefault("CleanupTiles", False): + self.LogInfo("Cleaning up Tiles...") + for tile in self.tiles: + try: + self.LogInfo("Deleting: {}".format(tile["filepath"])) + os.remove(tile["filepath"]) + # By this time we would have errored out + # if error on missing was enabled + except KeyError: + pass + except OSError: + self.LogInfo("Failed to delete: {}".format( + tile["filepath"])) + pass + + self.LogInfo("Pype Tile Assembler Job finished.") + + def handle_stdout_error(self): + """Handle errors in stdout.""" + self.FailRender(self.GetRegexMatch(0)) + + def tile_oiio_args( + self, output_width, output_height, tile_info, output_path): + """Generate oiio tool arguments for tile assembly. + + Args: + output_width (int): Width of output image. + output_height (int): Height of output image. + tiles_info (list): List of tile items, each item must be + dictionary with `filepath`, `pos_x` and `pos_y` keys + representing path to file and x, y coordinates on output + image where top-left point of tile item should start. + output_path (str): Path to file where should be output stored. + + Returns: + (list): oiio tools arguments. + + """ + args = [] + + # Create new image with output resolution, and with same type and + # channels as input + first_tile_path = tile_info[0]["filepath"] + first_tile_info = self.info_about_input(first_tile_path) + create_arg_template = "--create{} {}x{} {}" + + image_type = "" + image_format = first_tile_info.get("format") + if image_format: + image_type = ":type={}".format(image_format) + + create_arg = create_arg_template.format( + image_type, output_width, + output_height, first_tile_info["nchannels"] + ) + args.append(create_arg) + + for tile in tile_info: + path = tile["filepath"] + pos_x = tile["pos_x"] + tile_height = self.info_about_input(path)["height"] + if self.renderer == "vray": + pos_y = tile["pos_y"] + else: + pos_y = output_height - tile["pos_y"] - tile_height + + # Add input path and make sure inputs origin is 0, 0 + args.append(path) + args.append("--origin +0+0") + # Swap to have input as foreground + args.append("--swap") + # Paste foreground to background + args.append("--paste +{}+{}".format(pos_x, pos_y)) + + args.append("-o") + args.append(output_path) + + return args + + def tile_completer_ffmpeg_args( + self, output_width, output_height, tiles_info, output_path): + """Generate ffmpeg arguments for tile assembly. + + Expected inputs are tiled images. + + Args: + output_width (int): Width of output image. + output_height (int): Height of output image. + tiles_info (list): List of tile items, each item must be + dictionary with `filepath`, `pos_x` and `pos_y` keys + representing path to file and x, y coordinates on output + image where top-left point of tile item should start. + output_path (str): Path to file where should be output stored. + + Returns: + (list): ffmpeg arguments. + + """ + previous_name = "base" + ffmpeg_args = [] + filter_complex_strs = [] + + filter_complex_strs.append("nullsrc=size={}x{}[{}]".format( + output_width, output_height, previous_name + )) + + new_tiles_info = {} + for idx, tile_info in enumerate(tiles_info): + # Add input and store input index + filepath = tile_info["filepath"] + ffmpeg_args.append("-i \"{}\"".format(filepath.replace("\\", "/"))) + + # Prepare initial filter complex arguments + index_name = "input{}".format(idx) + filter_complex_strs.append( + "[{}]setpts=PTS-STARTPTS[{}]".format(idx, index_name) + ) + tile_info["index"] = idx + new_tiles_info[index_name] = tile_info + + # Set frames to 1 + ffmpeg_args.append("-frames 1") + + # Concatenation filter complex arguments + global_index = 1 + total_index = len(new_tiles_info) + for index_name, tile_info in new_tiles_info.items(): + item_str = ( + "[{previous_name}][{index_name}]overlay={pos_x}:{pos_y}" + ).format( + previous_name=previous_name, + index_name=index_name, + pos_x=tile_info["pos_x"], + pos_y=tile_info["pos_y"] + ) + new_previous = "tmp{}".format(global_index) + if global_index != total_index: + item_str += "[{}]".format(new_previous) + filter_complex_strs.append(item_str) + previous_name = new_previous + global_index += 1 + + joined_parts = ";".join(filter_complex_strs) + filter_complex_str = "-filter_complex \"{}\"".format(joined_parts) + + ffmpeg_args.append(filter_complex_str) + ffmpeg_args.append("-y") + ffmpeg_args.append("\"{}\"".format(output_path)) + + return ffmpeg_args + + def info_about_input(self, input_path): + args = [self.render_executable(), "--info:format=xml", input_path] + popen = subprocess.Popen( + " ".join(args), + shell=True, + stdout=subprocess.PIPE + ) + popen_output = popen.communicate()[0].replace(b"\r\n", b"") + + xmldoc = minidom.parseString(popen_output) + image_spec = None + for main_child in xmldoc.childNodes: + if main_child.nodeName.lower() == "imagespec": + image_spec = main_child + break + + info = {} + if not image_spec: + return info + + def child_check(node): + if len(node.childNodes) != 1: + self.FailRender(( + "Implementation BUG. Node {} has more children than 1" + ).format(node.nodeName)) + + for child in image_spec.childNodes: + if child.nodeName in LIST_KEYS: + values = [] + for node in child.childNodes: + child_check(node) + values.append(node.childNodes[0].nodeValue) + + info[child.nodeName] = values + + elif child.nodeName in INT_KEYS: + child_check(child) + info[child.nodeName] = int(child.childNodes[0].nodeValue) + + else: + child_check(child) + info[child.nodeName] = child.childNodes[0].nodeValue + return info diff --git a/vendor/deadline/readme.md b/vendor/deadline/readme.md new file mode 100644 index 0000000000..1c79c89b28 --- /dev/null +++ b/vendor/deadline/readme.md @@ -0,0 +1,3 @@ +## Pype Deadline repository overlay + + This directory is overlay for Deadline repository. It means that you can copy whole hierarchy to Deadline repository and it should work.