Merge branch 'develop' into 3.0/poetry

This commit is contained in:
Milan Kolar 2021-02-02 14:53:11 +01:00
commit da094ac510
207 changed files with 4641 additions and 4255 deletions

View file

@ -1,60 +0,0 @@
import os
import sys
import traceback
from avalon import api as avalon
from pyblish import api as pyblish
import bpy
from pype import PLUGINS_DIR
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "blender", "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "blender", "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "blender", "create")
ORIGINAL_EXCEPTHOOK = sys.excepthook
def pype_excepthook_handler(*args):
traceback.print_exception(*args)
def install():
"""Install Blender configuration for Avalon."""
sys.excepthook = pype_excepthook_handler
pyblish.register_plugin_path(str(PUBLISH_PATH))
avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH))
avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH))
avalon.on("new", on_new)
avalon.on("open", on_open)
def uninstall():
"""Uninstall Blender configuration for Avalon."""
sys.excepthook = ORIGINAL_EXCEPTHOOK
pyblish.deregister_plugin_path(str(PUBLISH_PATH))
avalon.deregister_plugin_path(avalon.Loader, str(LOAD_PATH))
avalon.deregister_plugin_path(avalon.Creator, str(CREATE_PATH))
def set_start_end_frames():
from avalon import io
asset_name = io.Session["AVALON_ASSET"]
asset_doc = io.find_one({
"type": "asset",
"name": asset_name
})
bpy.context.scene.frame_start = asset_doc["data"]["frameStart"]
bpy.context.scene.frame_end = asset_doc["data"]["frameEnd"]
def on_new(arg1, arg2):
set_start_end_frames()
def on_open(arg1, arg2):
set_start_end_frames()

View file

@ -0,0 +1,63 @@
import os
import sys
import traceback
import bpy
from avalon import api as avalon
from pyblish import api as pyblish
import pype.hosts.blender
HOST_DIR = os.path.dirname(os.path.abspath(pype.hosts.blender.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
ORIGINAL_EXCEPTHOOK = sys.excepthook
def pype_excepthook_handler(*args):
traceback.print_exception(*args)
def install():
"""Install Blender configuration for Avalon."""
sys.excepthook = pype_excepthook_handler
pyblish.register_plugin_path(str(PUBLISH_PATH))
avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH))
avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH))
avalon.on("new", on_new)
avalon.on("open", on_open)
def uninstall():
"""Uninstall Blender configuration for Avalon."""
sys.excepthook = ORIGINAL_EXCEPTHOOK
pyblish.deregister_plugin_path(str(PUBLISH_PATH))
avalon.deregister_plugin_path(avalon.Loader, str(LOAD_PATH))
avalon.deregister_plugin_path(avalon.Creator, str(CREATE_PATH))
def set_start_end_frames():
from avalon import io
asset_name = io.Session["AVALON_ASSET"]
asset_doc = io.find_one({
"type": "asset",
"name": asset_name
})
bpy.context.scene.frame_start = asset_doc["data"]["frameStart"]
bpy.context.scene.frame_end = asset_doc["data"]["frameEnd"]
def on_new(arg1, arg2):
set_start_end_frames()
def on_open(arg1, arg2):
set_start_end_frames()

View file

@ -2,7 +2,7 @@ import bpy
import pyblish.api
from ...action import get_errored_instances_from_context
from pype.api import get_errored_instances_from_context
class SelectInvalidAction(pyblish.api.Action):

View file

@ -4,7 +4,7 @@ import bpy
from avalon import api
from avalon.blender import Creator, lib
import pype.hosts.blender.plugin
import pype.hosts.blender.api.plugin
class CreateAction(Creator):
@ -19,7 +19,7 @@ class CreateAction(Creator):
asset = self.data["asset"]
subset = self.data["subset"]
name = pype.hosts.blender.plugin.asset_name(asset, subset)
name = pype.hosts.blender.api.plugin.asset_name(asset, subset)
collection = bpy.data.collections.new(name=name)
bpy.context.scene.collection.children.link(collection)
self.data['task'] = api.Session.get('AVALON_TASK')

View file

@ -3,7 +3,7 @@
import bpy
from avalon import api, blender
import pype.hosts.blender.plugin
import pype.hosts.blender.api.plugin
class CreateAnimation(blender.Creator):
@ -17,7 +17,7 @@ class CreateAnimation(blender.Creator):
def process(self):
asset = self.data["asset"]
subset = self.data["subset"]
name = pype.hosts.blender.plugin.asset_name(asset, subset)
name = pype.hosts.blender.api.plugin.asset_name(asset, subset)
collection = bpy.data.collections.new(name=name)
bpy.context.scene.collection.children.link(collection)
self.data['task'] = api.Session.get('AVALON_TASK')

View file

@ -4,7 +4,7 @@ import bpy
from avalon import api
from avalon.blender import Creator, lib
import pype.hosts.blender.plugin
import pype.hosts.blender.api.plugin
class CreateCamera(Creator):
@ -19,7 +19,7 @@ class CreateCamera(Creator):
asset = self.data["asset"]
subset = self.data["subset"]
name = pype.hosts.blender.plugin.asset_name(asset, subset)
name = pype.hosts.blender.api.plugin.asset_name(asset, subset)
collection = bpy.data.collections.new(name=name)
bpy.context.scene.collection.children.link(collection)
self.data['task'] = api.Session.get('AVALON_TASK')

View file

@ -4,7 +4,7 @@ import bpy
from avalon import api
from avalon.blender import Creator, lib
import pype.hosts.blender.plugin
import pype.hosts.blender.api.plugin
class CreateLayout(Creator):
@ -19,7 +19,7 @@ class CreateLayout(Creator):
asset = self.data["asset"]
subset = self.data["subset"]
name = pype.hosts.blender.plugin.asset_name(asset, subset)
name = pype.hosts.blender.api.plugin.asset_name(asset, subset)
collection = bpy.data.collections.new(name=name)
bpy.context.scene.collection.children.link(collection)
self.data['task'] = api.Session.get('AVALON_TASK')

View file

@ -4,7 +4,7 @@ import bpy
from avalon import api
from avalon.blender import Creator, lib
import pype.hosts.blender.plugin
import pype.hosts.blender.api.plugin
class CreateModel(Creator):
@ -19,7 +19,7 @@ class CreateModel(Creator):
asset = self.data["asset"]
subset = self.data["subset"]
name = pype.hosts.blender.plugin.asset_name(asset, subset)
name = pype.hosts.blender.api.plugin.asset_name(asset, subset)
collection = bpy.data.collections.new(name=name)
bpy.context.scene.collection.children.link(collection)
self.data['task'] = api.Session.get('AVALON_TASK')

View file

@ -4,7 +4,7 @@ import bpy
from avalon import api
from avalon.blender import Creator, lib
import pype.hosts.blender.plugin
import pype.hosts.blender.api.plugin
class CreateRig(Creator):
@ -19,7 +19,7 @@ class CreateRig(Creator):
asset = self.data["asset"]
subset = self.data["subset"]
name = pype.hosts.blender.plugin.asset_name(asset, subset)
name = pype.hosts.blender.api.plugin.asset_name(asset, subset)
collection = bpy.data.collections.new(name=name)
bpy.context.scene.collection.children.link(collection)
self.data['task'] = api.Session.get('AVALON_TASK')

View file

@ -1,7 +1,7 @@
import bpy
from avalon import api, blender
import pype.hosts.blender.plugin
import pype.hosts.blender.api.plugin
class CreateSetDress(blender.Creator):
"""A grouped package of loaded content"""
@ -15,7 +15,7 @@ class CreateSetDress(blender.Creator):
def process(self):
asset = self.data["asset"]
subset = self.data["subset"]
name = pype.hosts.blender.plugin.asset_name(asset, subset)
name = pype.hosts.blender.api.plugin.asset_name(asset, subset)
collection = bpy.data.collections.new(name=name)
bpy.context.scene.collection.children.link(collection)
self.data['task'] = api.Session.get('AVALON_TASK')

View file

@ -7,12 +7,12 @@ from typing import Dict, List, Optional
from avalon import api, blender
import bpy
import pype.hosts.blender.plugin
import pype.hosts.blender.api.plugin
logger = logging.getLogger("pype").getChild("blender").getChild("load_action")
class BlendActionLoader(pype.hosts.blender.plugin.AssetLoader):
class BlendActionLoader(pype.hosts.blender.api.plugin.AssetLoader):
"""Load action from a .blend file.
Warning:
@ -42,8 +42,8 @@ class BlendActionLoader(pype.hosts.blender.plugin.AssetLoader):
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
lib_container = pype.hosts.blender.plugin.asset_name(asset, subset)
container_name = pype.hosts.blender.plugin.asset_name(
lib_container = pype.hosts.blender.api.plugin.asset_name(asset, subset)
container_name = pype.hosts.blender.api.plugin.asset_name(
asset, subset, namespace
)
@ -149,7 +149,7 @@ class BlendActionLoader(pype.hosts.blender.plugin.AssetLoader):
assert libpath.is_file(), (
f"The file doesn't exist: {libpath}"
)
assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, (
assert extension in pype.hosts.blender.api.plugin.VALID_EXTENSIONS, (
f"Unsupported file: {libpath}"
)

View file

@ -7,14 +7,14 @@ from typing import Dict, List, Optional
from avalon import api, blender
import bpy
import pype.hosts.blender.plugin
import pype.hosts.blender.api.plugin
logger = logging.getLogger("pype").getChild(
"blender").getChild("load_animation")
class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader):
class BlendAnimationLoader(pype.hosts.blender.api.plugin.AssetLoader):
"""Load animations from a .blend file.
Warning:
@ -105,8 +105,8 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader):
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
lib_container = pype.hosts.blender.plugin.asset_name(asset, subset)
container_name = pype.hosts.blender.plugin.asset_name(
lib_container = pype.hosts.blender.api.plugin.asset_name(asset, subset)
container_name = pype.hosts.blender.api.plugin.asset_name(
asset, subset, namespace
)
@ -175,7 +175,7 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader):
assert libpath.is_file(), (
f"The file doesn't exist: {libpath}"
)
assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, (
assert extension in pype.hosts.blender.api.plugin.VALID_EXTENSIONS, (
f"Unsupported file: {libpath}"
)

View file

@ -7,12 +7,12 @@ from typing import Dict, List, Optional
from avalon import api, blender
import bpy
import pype.hosts.blender.plugin
import pype.hosts.blender.api.plugin
logger = logging.getLogger("pype").getChild("blender").getChild("load_camera")
class BlendCameraLoader(pype.hosts.blender.plugin.AssetLoader):
class BlendCameraLoader(pype.hosts.blender.api.plugin.AssetLoader):
"""Load a camera from a .blend file.
Warning:
@ -92,8 +92,8 @@ class BlendCameraLoader(pype.hosts.blender.plugin.AssetLoader):
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
lib_container = pype.hosts.blender.plugin.asset_name(asset, subset)
container_name = pype.hosts.blender.plugin.asset_name(
lib_container = pype.hosts.blender.api.plugin.asset_name(asset, subset)
container_name = pype.hosts.blender.api.plugin.asset_name(
asset, subset, namespace
)
@ -162,7 +162,7 @@ class BlendCameraLoader(pype.hosts.blender.plugin.AssetLoader):
assert libpath.is_file(), (
f"The file doesn't exist: {libpath}"
)
assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, (
assert extension in pype.hosts.blender.api.plugin.VALID_EXTENSIONS, (
f"Unsupported file: {libpath}"
)

View file

@ -11,7 +11,7 @@ from typing import Dict, List, Optional
from avalon import api, blender, pipeline
import bpy
import pype.hosts.blender.plugin as plugin
import pype.hosts.blender.api.plugin as plugin
class BlendLayoutLoader(plugin.AssetLoader):

View file

@ -7,7 +7,7 @@ from typing import Dict, List, Optional
from avalon import api, blender
import bpy
import pype.hosts.blender.plugin as plugin
import pype.hosts.blender.api.plugin as plugin
class BlendModelLoader(plugin.AssetLoader):

View file

@ -7,7 +7,7 @@ from typing import Dict, List, Optional
from avalon import api, blender
import bpy
import pype.hosts.blender.plugin as plugin
import pype.hosts.blender.api.plugin as plugin
class BlendRigLoader(plugin.AssetLoader):

View file

@ -1,7 +1,7 @@
import os
import pype.api
import pype.hosts.blender.plugin
import pype.hosts.blender.api.plugin
import bpy
@ -61,7 +61,8 @@ class ExtractABC(pype.api.Extractor):
except:
continue
new_context = pype.hosts.blender.plugin.create_blender_context(active=selected[0], selected=selected)
new_context = pype.hosts.blender.api.plugin.create_blender_context(
active=selected[0], selected=selected)
# We set the scale of the scene for the export
scene.unit_settings.scale_length = 0.01

View file

@ -6,6 +6,7 @@ import pyblish.api
import bpy
class ExtractSetDress(pype.api.Extractor):
"""Extract setdress."""
@ -21,17 +22,20 @@ class ExtractSetDress(pype.api.Extractor):
json_data = []
for i in instance.context:
collection = i.data.get('name')
collection = i.data.get("name")
container = None
for obj in bpy.data.collections[collection].objects:
if obj.type == 'ARMATURE':
container_name = obj.get('avalon').get('container_name')
if obj.type == "ARMATURE":
container_name = obj.get("avalon").get("container_name")
container = bpy.data.collections[container_name]
if container:
json_dict = {}
json_dict['subset'] = i.data.get('subset')
json_dict['container'] = container.name
json_dict['instance_name'] = container.get('avalon').get('instance_name')
json_dict = {
"subset": i.data.get("subset"),
"container": container.name,
}
json_dict["instance_name"] = container.get("avalon").get(
"instance_name"
)
json_data.append(json_dict)
if "representations" not in instance.data:
@ -44,13 +48,14 @@ class ExtractSetDress(pype.api.Extractor):
json.dump(json_data, fp=file, indent=2)
json_representation = {
'name': 'json',
'ext': 'json',
'files': json_filename,
"name": "json",
"ext": "json",
"files": json_filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(json_representation)
self.log.info("Extracted instance '{}' to: {}".format(
instance.name, json_representation))
self.log.info(
"Extracted instance '{}' to: {}".format(instance.name,
json_representation)
)

View file

@ -1,47 +1,47 @@
import os
import avalon.blender.workio
import pype.api
class ExtractBlend(pype.api.Extractor):
"""Extract a blend file."""
label = "Extract Blend"
hosts = ["blender"]
families = ["model", "camera", "rig", "action", "layout", "animation"]
optional = True
def process(self, instance):
# Define extract output file path
stagingdir = self.staging_dir(instance)
filename = f"{instance.name}.blend"
filepath = os.path.join(stagingdir, filename)
# Perform extraction
self.log.info("Performing extraction..")
# Just save the file to a temporary location. At least for now it's no
# problem to have (possibly) extra stuff in the file.
avalon.blender.workio.save_file(filepath, copy=True)
#
# # Store reference for integration
# if "files" not in instance.data:
# instance.data["files"] = list()
#
# # instance.data["files"].append(filename)
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
'name': 'blend',
'ext': 'blend',
'files': filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s",
instance.name, representation)
import os
import avalon.blender.workio
import pype.api
class ExtractBlend(pype.api.Extractor):
"""Extract a blend file."""
label = "Extract Blend"
hosts = ["blender"]
families = ["model", "camera", "rig", "action", "layout", "animation"]
optional = True
def process(self, instance):
# Define extract output file path
stagingdir = self.staging_dir(instance)
filename = f"{instance.name}.blend"
filepath = os.path.join(stagingdir, filename)
# Perform extraction
self.log.info("Performing extraction..")
# Just save the file to a temporary location. At least for now it's no
# problem to have (possibly) extra stuff in the file.
avalon.blender.workio.save_file(filepath, copy=True)
#
# # Store reference for integration
# if "files" not in instance.data:
# instance.data["files"] = list()
#
# # instance.data["files"].append(filename)
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
'name': 'blend',
'ext': 'blend',
'files': filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s",
instance.name, representation)

View file

@ -3,7 +3,7 @@ from typing import List
import bpy
import pyblish.api
import pype.hosts.blender.action
import pype.hosts.blender.api.action
class ValidateMeshHasUvs(pyblish.api.InstancePlugin):
@ -14,7 +14,7 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin):
families = ["model"]
category = "geometry"
label = "Mesh Has UV's"
actions = [pype.hosts.blender.action.SelectInvalidAction]
actions = [pype.hosts.blender.api.action.SelectInvalidAction]
optional = True
@staticmethod

View file

@ -3,7 +3,7 @@ from typing import List
import bpy
import pyblish.api
import pype.hosts.blender.action
import pype.hosts.blender.api.action
class ValidateMeshNoNegativeScale(pyblish.api.Validator):
@ -13,7 +13,7 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator):
hosts = ["blender"]
families = ["model"]
label = "Mesh No Negative Scale"
actions = [pype.hosts.blender.action.SelectInvalidAction]
actions = [pype.hosts.blender.api.action.SelectInvalidAction]
@staticmethod
def get_invalid(instance) -> List:

View file

@ -0,0 +1,3 @@
from pype.hosts.blender import api
api.install()

View file

@ -7,10 +7,7 @@ from pype.hosts.celaction import api as celaction
class CelactionPrelaunchHook(PreLaunchHook):
"""
This hook will check if current workfile path has Unreal
project inside. IF not, it initialize it and finally it pass
path to the project by environment variable to Unreal launcher
shell script.
Bootstrap celacion with pype
"""
workfile_ext = "scn"
app_groups = ["celaction"]

View file

@ -1,121 +0,0 @@
import os
from pype.api import Logger
from avalon import api as avalon
from pyblish import api as pyblish
from pype import PLUGINS_DIR
from .workio import (
open_file,
save_file,
current_file,
has_unsaved_changes,
file_extensions,
work_root
)
from .menu import (
install as menu_install,
_update_menu_task_label
)
from .events import register_hiero_events
__all__ = [
# Workfiles API
"open_file",
"save_file",
"current_file",
"has_unsaved_changes",
"file_extensions",
"work_root",
]
# get logger
log = Logger().get_logger(__name__)
''' Creating all important host related variables '''
AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype")
# plugin root path
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "hiero", "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "hiero", "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "hiero", "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "hiero", "inventory")
# registering particular pyblish gui but `lite` is recomended!!
if os.getenv("PYBLISH_GUI", None):
pyblish.register_gui(os.getenv("PYBLISH_GUI", None))
def install():
"""
Installing Hiero integration for avalon
Args:
config (obj): avalon config module `pype` in our case, it is not
used but required by avalon.api.install()
"""
# adding all events
_register_events()
log.info("Registering Hiero plug-ins..")
pyblish.register_host("hiero")
pyblish.register_plugin_path(PUBLISH_PATH)
avalon.register_plugin_path(avalon.Loader, LOAD_PATH)
avalon.register_plugin_path(avalon.Creator, CREATE_PATH)
avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH)
# Disable all families except for the ones we explicitly want to see
family_states = [
"write",
"review",
"plate"
]
avalon.data["familiesStateDefault"] = False
avalon.data["familiesStateToggled"] = family_states
# install menu
menu_install()
# register hiero events
register_hiero_events()
def uninstall():
"""
Uninstalling Hiero integration for avalon
"""
log.info("Deregistering Hiero plug-ins..")
pyblish.deregister_host("hiero")
pyblish.deregister_plugin_path(PUBLISH_PATH)
avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH)
avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH)
def _register_events():
"""
Adding all callbacks.
"""
# if task changed then change notext of hiero
avalon.on("taskChanged", _update_menu_task_label)
log.info("Installed event callback for 'taskChanged'..")
def ls():
"""List available containers.
This function is used by the Container Manager in Nuke. You'll
need to implement a for-loop that then *yields* one Container at
a time.
See the `container.json` schema for details on how it should look,
and the Maya equivalent, which is in `avalon.maya.pipeline`
"""
# TODO: listing all availabe containers form sequence
return

View file

@ -0,0 +1,101 @@
from .workio import (
open_file,
save_file,
current_file,
has_unsaved_changes,
file_extensions,
work_root
)
from .pipeline import (
launch_workfiles_app,
ls,
install,
uninstall,
reload_config,
containerise,
publish,
maintained_selection,
parse_container,
update_container,
reset_selection
)
from .lib import (
get_track_items,
get_current_project,
get_current_sequence,
get_current_track,
get_track_item_pype_tag,
set_track_item_pype_tag,
get_track_item_pype_data,
set_publish_attribute,
get_publish_attribute,
imprint,
get_selected_track_items,
set_selected_track_items,
create_nuke_workfile_clips,
create_bin,
apply_colorspace_project,
apply_colorspace_clips,
is_overlapping,
get_sequence_pattern_and_padding
)
from .plugin import (
CreatorWidget,
Creator,
PublishClip,
SequenceLoader,
ClipLoader
)
__all__ = [
# avalon pipeline module
"launch_workfiles_app",
"ls",
"install",
"uninstall",
"reload_config",
"containerise",
"publish",
"maintained_selection",
"parse_container",
"update_container",
"reset_selection",
# Workfiles API
"open_file",
"save_file",
"current_file",
"has_unsaved_changes",
"file_extensions",
"work_root",
# Lib functions
"get_track_items",
"get_current_project",
"get_current_sequence",
"get_current_track",
"get_track_item_pype_tag",
"set_track_item_pype_tag",
"get_track_item_pype_data",
"set_publish_attribute",
"get_publish_attribute",
"imprint",
"get_selected_track_items",
"set_selected_track_items",
"create_nuke_workfile_clips",
"create_bin",
"is_overlapping",
"apply_colorspace_project",
"apply_colorspace_clips",
"get_sequence_pattern_and_padding",
# plugins
"CreatorWidget",
"Creator",
"PublishClip",
"SequenceLoader",
"ClipLoader"
]

View file

@ -1,8 +1,10 @@
import os
import hiero.core.events
import avalon.api as avalon
from pype.api import Logger
from .lib import sync_avalon_data_to_workfile, launch_workfiles_app
from .tags import add_tags_from_presets
from .tags import add_tags_to_workfile
from .menu import update_menu_task_label
log = Logger().get_logger(__name__)
@ -28,7 +30,7 @@ def afterNewProjectCreated(event):
sync_avalon_data_to_workfile()
# add tags from preset
add_tags_from_presets()
add_tags_to_workfile()
# Workfiles.
if int(os.environ.get("WORKFILES_STARTUP", "0")):
@ -48,7 +50,7 @@ def afterProjectLoad(event):
sync_avalon_data_to_workfile()
# add tags from preset
add_tags_from_presets()
add_tags_to_workfile()
def beforeProjectClosed(event):
@ -77,7 +79,7 @@ def register_hiero_events():
"kAfterNewProjectCreated, kBeforeProjectLoad, kAfterProjectLoad, "
"kBeforeProjectSave, kAfterProjectSave, kBeforeProjectClose, "
"kAfterProjectClose, kShutdown, kStartup"
)
)
# hiero.core.events.registerInterest(
# "kBeforeNewProjectCreated", beforeNewProjectCreated)
@ -105,3 +107,13 @@ def register_hiero_events():
# workfiles
hiero.core.events.registerEventType("kStartWorkfiles")
hiero.core.events.registerInterest("kStartWorkfiles", launch_workfiles_app)
def register_events():
"""
Adding all callbacks.
"""
# if task changed then change notext of hiero
avalon.on("taskChanged", update_menu_task_label)
log.info("Installed event callback for 'taskChanged'..")

895
pype/hosts/hiero/api/lib.py Normal file
View file

@ -0,0 +1,895 @@
"""
Host specific functions where host api is connected
"""
import os
import re
import sys
import ast
import hiero
import avalon.api as avalon
import avalon.io
from avalon.vendor.Qt import QtWidgets
from pype.api import (Logger, Anatomy, config)
from . import tags
import shutil
from compiler.ast import flatten
try:
from PySide.QtCore import QFile, QTextStream
from PySide.QtXml import QDomDocument
except ImportError:
from PySide2.QtCore import QFile, QTextStream
from PySide2.QtXml import QDomDocument
# from opentimelineio import opentime
# from pprint import pformat
log = Logger().get_logger(__name__)
self = sys.modules[__name__]
self._has_been_setup = False
self._has_menu = False
self._registered_gui = None
self.pype_tag_name = "Pype Data"
self.default_sequence_name = "PypeSequence"
self.default_bin_name = "PypeBin"
AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype")
def get_current_project(remove_untitled=False):
projects = flatten(hiero.core.projects())
if not remove_untitled:
return next(iter(projects))
# if remove_untitled
for proj in projects:
if "Untitled" in proj.name():
proj.close()
else:
return proj
def get_current_sequence(name=None, new=False):
"""
Get current sequence in context of active project.
Args:
name (str)[optional]: name of sequence we want to return
new (bool)[optional]: if we want to create new one
Returns:
hiero.core.Sequence: the sequence object
"""
sequence = None
project = get_current_project()
root_bin = project.clipsBin()
if new:
# create new
name = name or self.default_sequence_name
sequence = hiero.core.Sequence(name)
root_bin.addItem(hiero.core.BinItem(sequence))
elif name:
# look for sequence by name
sequences = project.sequences()
for _sequence in sequences:
if _sequence.name() == name:
sequence = _sequence
if not sequence:
# if nothing found create new with input name
sequence = get_current_sequence(name, True)
elif not name and not new:
# if name is none and new is False then return current open sequence
sequence = hiero.ui.activeSequence()
return sequence
def get_current_track(sequence, name, audio=False):
"""
Get current track in context of active project.
Creates new if none is found.
Args:
sequence (hiero.core.Sequence): hiero sequene object
name (str): name of track we want to return
audio (bool)[optional]: switch to AudioTrack
Returns:
hiero.core.Track: the track object
"""
tracks = sequence.videoTracks()
if audio:
tracks = sequence.audioTracks()
# get track by name
track = None
for _track in tracks:
if _track.name() in name:
track = _track
if not track:
if not audio:
track = hiero.core.VideoTrack(name)
else:
track = hiero.core.AudioTrack(name)
sequence.addTrack(track)
return track
def get_track_items(
selected=False,
sequence_name=None,
track_item_name=None,
track_name=None,
track_type=None,
check_enabled=True,
check_locked=True,
check_tagged=False):
"""Get all available current timeline track items.
Attribute:
selected (bool)[optional]: return only selected items on timeline
sequence_name (str)[optional]: return only clips from input sequence
track_item_name (str)[optional]: return only item with input name
track_name (str)[optional]: return only items from track name
track_type (str)[optional]: return only items of given type
(`audio` or `video`) default is `video`
check_enabled (bool)[optional]: ignore disabled if True
check_locked (bool)[optional]: ignore locked if True
Return:
list or hiero.core.TrackItem: list of track items or single track item
"""
return_list = list()
track_items = list()
# get selected track items or all in active sequence
if selected:
selected_items = list(hiero.selection)
for item in selected_items:
if track_name and track_name in item.parent().name():
# filter only items fitting input track name
track_items.append(item)
elif not track_name:
# or add all if no track_name was defined
track_items.append(item)
else:
sequence = get_current_sequence(name=sequence_name)
# get all available tracks from sequence
tracks = list(sequence.audioTracks()) + list(sequence.videoTracks())
# loop all tracks
for track in tracks:
if check_locked and track.isLocked():
continue
if check_enabled and not track.isEnabled():
continue
# and all items in track
for item in track.items():
if check_tagged and not item.tags():
continue
# check if track item is enabled
if check_enabled:
if not item.isEnabled():
continue
if track_item_name:
if item.name() in track_item_name:
return item
# make sure only track items with correct track names are added
if track_name and track_name in track.name():
# filter out only defined track_name items
track_items.append(item)
elif not track_name:
# or add all if no track_name is defined
track_items.append(item)
# filter out only track items with defined track_type
for track_item in track_items:
if track_type and track_type == "video" and isinstance(
track_item.parent(), hiero.core.VideoTrack):
# only video track items are allowed
return_list.append(track_item)
elif track_type and track_type == "audio" and isinstance(
track_item.parent(), hiero.core.AudioTrack):
# only audio track items are allowed
return_list.append(track_item)
elif not track_type:
# add all if no track_type is defined
return_list.append(track_item)
return return_list
def get_track_item_pype_tag(track_item):
"""
Get pype track item tag created by creator or loader plugin.
Attributes:
trackItem (hiero.core.TrackItem): hiero object
Returns:
hiero.core.Tag: hierarchy, orig clip attributes
"""
# get all tags from track item
_tags = track_item.tags()
if not _tags:
return None
for tag in _tags:
# return only correct tag defined by global name
if tag.name() in self.pype_tag_name:
return tag
def set_track_item_pype_tag(track_item, data=None):
"""
Set pype track item tag to input track_item.
Attributes:
trackItem (hiero.core.TrackItem): hiero object
Returns:
hiero.core.Tag
"""
data = data or dict()
# basic Tag's attribute
tag_data = {
"editable": "0",
"note": "Pype data holder",
"icon": "pype_icon.png",
"metadata": {k: v for k, v in data.items()}
}
# get available pype tag if any
_tag = get_track_item_pype_tag(track_item)
if _tag:
# it not tag then create one
tag = tags.update_tag(_tag, tag_data)
else:
# if pype tag available then update with input data
tag = tags.create_tag(self.pype_tag_name, tag_data)
# add it to the input track item
track_item.addTag(tag)
return tag
def get_track_item_pype_data(track_item):
"""
Get track item's pype tag data.
Attributes:
trackItem (hiero.core.TrackItem): hiero object
Returns:
dict: data found on pype tag
"""
data = dict()
# get pype data tag from track item
tag = get_track_item_pype_tag(track_item)
if not tag:
return None
# get tag metadata attribut
tag_data = tag.metadata()
# convert tag metadata to normal keys names and values to correct types
for k, v in dict(tag_data).items():
key = k.replace("tag.", "")
try:
# capture exceptions which are related to strings only
value = ast.literal_eval(v)
except (ValueError, SyntaxError):
value = v
data.update({key: value})
return data
def imprint(track_item, data=None):
"""
Adding `Avalon data` into a hiero track item tag.
Also including publish attribute into tag.
Arguments:
track_item (hiero.core.TrackItem): hiero track item object
data (dict): Any data which needst to be imprinted
Examples:
data = {
'asset': 'sq020sh0280',
'family': 'render',
'subset': 'subsetMain'
}
"""
data = data or {}
tag = set_track_item_pype_tag(track_item, data)
# add publish attribute
set_publish_attribute(tag, True)
def set_publish_attribute(tag, value):
""" Set Publish attribute in input Tag object
Attribute:
tag (hiero.core.Tag): a tag object
value (bool): True or False
"""
tag_data = tag.metadata()
# set data to the publish attribute
tag_data.setValue("tag.publish", str(value))
def get_publish_attribute(tag):
""" Get Publish attribute from input Tag object
Attribute:
tag (hiero.core.Tag): a tag object
value (bool): True or False
"""
tag_data = tag.metadata()
# get data to the publish attribute
value = tag_data.value("tag.publish")
# return value converted to bool value. Atring is stored in tag.
return ast.literal_eval(value)
def sync_avalon_data_to_workfile():
# import session to get project dir
project_name = avalon.Session["AVALON_PROJECT"]
anatomy = Anatomy(project_name)
work_template = anatomy.templates["work"]["path"]
work_root = anatomy.root_value_for_template(work_template)
active_project_root = (
os.path.join(work_root, project_name)
).replace("\\", "/")
# getting project
project = get_current_project()
if "Tag Presets" in project.name():
return
log.debug("Synchronizing Pype metadata to project: {}".format(
project.name()))
# set project root with backward compatibility
try:
project.setProjectDirectory(active_project_root)
except Exception:
# old way of seting it
project.setProjectRoot(active_project_root)
# get project data from avalon db
project_doc = avalon.io.find_one({"type": "project"})
project_data = project_doc["data"]
log.debug("project_data: {}".format(project_data))
# get format and fps property from avalon db on project
width = project_data["resolutionWidth"]
height = project_data["resolutionHeight"]
pixel_aspect = project_data["pixelAspect"]
fps = project_data['fps']
format_name = project_data['code']
# create new format in hiero project
format = hiero.core.Format(width, height, pixel_aspect, format_name)
project.setOutputFormat(format)
# set fps to hiero project
project.setFramerate(fps)
# TODO: add auto colorspace set from project drop
log.info("Project property has been synchronised with Avalon db")
def launch_workfiles_app(event):
"""
Event for launching workfiles after hiero start
Args:
event (obj): required but unused
"""
from . import launch_workfiles_app
launch_workfiles_app()
def setup(console=False, port=None, menu=True):
"""Setup integration
Registers Pyblish for Hiero plug-ins and appends an item to the File-menu
Arguments:
console (bool): Display console with GUI
port (int, optional): Port from which to start looking for an
available port to connect with Pyblish QML, default
provided by Pyblish Integration.
menu (bool, optional): Display file menu in Hiero.
"""
if self._has_been_setup:
teardown()
add_submission()
if menu:
add_to_filemenu()
self._has_menu = True
self._has_been_setup = True
log.debug("pyblish: Loaded successfully.")
def teardown():
"""Remove integration"""
if not self._has_been_setup:
return
if self._has_menu:
remove_from_filemenu()
self._has_menu = False
self._has_been_setup = False
log.debug("pyblish: Integration torn down successfully")
def remove_from_filemenu():
raise NotImplementedError("Implement me please.")
def add_to_filemenu():
PublishAction()
class PyblishSubmission(hiero.exporters.FnSubmission.Submission):
def __init__(self):
hiero.exporters.FnSubmission.Submission.__init__(self)
def addToQueue(self):
from . import publish
# Add submission to Hiero module for retrieval in plugins.
hiero.submission = self
publish()
def add_submission():
registry = hiero.core.taskRegistry
registry.addSubmission("Pyblish", PyblishSubmission)
class PublishAction(QtWidgets.QAction):
"""
Action with is showing as menu item
"""
def __init__(self):
QtWidgets.QAction.__init__(self, "Publish", None)
self.triggered.connect(self.publish)
for interest in ["kShowContextMenu/kTimeline",
"kShowContextMenukBin",
"kShowContextMenu/kSpreadsheet"]:
hiero.core.events.registerInterest(interest, self.eventHandler)
self.setShortcut("Ctrl+Alt+P")
def publish(self):
from . import publish
# Removing "submission" attribute from hiero module, to prevent tasks
# from getting picked up when not using the "Export" dialog.
if hasattr(hiero, "submission"):
del hiero.submission
publish()
def eventHandler(self, event):
# Add the Menu to the right-click menu
event.menu.addAction(self)
# def CreateNukeWorkfile(nodes=None,
# nodes_effects=None,
# to_timeline=False,
# **kwargs):
# ''' Creating nuke workfile with particular version with given nodes
# Also it is creating timeline track items as precomps.
#
# Arguments:
# nodes(list of dict): each key in dict is knob order is important
# to_timeline(type): will build trackItem with metadata
#
# Returns:
# bool: True if done
#
# Raises:
# Exception: with traceback
#
# '''
# import hiero.core
# from avalon.nuke import imprint
# from pype.hosts.nuke import (
# lib as nklib
# )
#
# # check if the file exists if does then Raise "File exists!"
# if os.path.exists(filepath):
# raise FileExistsError("File already exists: `{}`".format(filepath))
#
# # if no representations matching then
# # Raise "no representations to be build"
# if len(representations) == 0:
# raise AttributeError("Missing list of `representations`")
#
# # check nodes input
# if len(nodes) == 0:
# log.warning("Missing list of `nodes`")
#
# # create temp nk file
# nuke_script = hiero.core.nuke.ScriptWriter()
#
# # create root node and save all metadata
# root_node = hiero.core.nuke.RootNode()
#
# anatomy = Anatomy(os.environ["AVALON_PROJECT"])
# work_template = anatomy.templates["work"]["path"]
# root_path = anatomy.root_value_for_template(work_template)
#
# nuke_script.addNode(root_node)
#
# # here to call pype.hosts.nuke.lib.BuildWorkfile
# script_builder = nklib.BuildWorkfile(
# root_node=root_node,
# root_path=root_path,
# nodes=nuke_script.getNodes(),
# **kwargs
# )
def create_nuke_workfile_clips(nuke_workfiles, seq=None):
'''
nuke_workfiles is list of dictionaries like:
[{
'path': 'P:/Jakub_testy_pipeline/test_v01.nk',
'name': 'test',
'handleStart': 15, # added asymetrically to handles
'handleEnd': 10, # added asymetrically to handles
"clipIn": 16,
"frameStart": 991,
"frameEnd": 1023,
'task': 'Comp-tracking',
'work_dir': 'VFX_PR',
'shot': '00010'
}]
'''
proj = hiero.core.projects()[-1]
root = proj.clipsBin()
if not seq:
seq = hiero.core.Sequence('NewSequences')
root.addItem(hiero.core.BinItem(seq))
# todo will ned to define this better
# track = seq[1] # lazy example to get a destination# track
clips_lst = []
for nk in nuke_workfiles:
task_path = '/'.join([nk['work_dir'], nk['shot'], nk['task']])
bin = create_bin(task_path, proj)
if nk['task'] not in seq.videoTracks():
track = hiero.core.VideoTrack(nk['task'])
seq.addTrack(track)
else:
track = seq.tracks(nk['task'])
# create clip media
media = hiero.core.MediaSource(nk['path'])
media_in = int(media.startTime() or 0)
media_duration = int(media.duration() or 0)
handle_start = nk.get("handleStart")
handle_end = nk.get("handleEnd")
if media_in:
source_in = media_in + handle_start
else:
source_in = nk["frameStart"] + handle_start
if media_duration:
source_out = (media_in + media_duration - 1) - handle_end
else:
source_out = nk["frameEnd"] - handle_end
source = hiero.core.Clip(media)
name = os.path.basename(os.path.splitext(nk['path'])[0])
split_name = split_by_client_version(name)[0] or name
# add to bin as clip item
items_in_bin = [b.name() for b in bin.items()]
if split_name not in items_in_bin:
binItem = hiero.core.BinItem(source)
bin.addItem(binItem)
new_source = [
item for item in bin.items() if split_name in item.name()
][0].items()[0].item()
# add to track as clip item
trackItem = hiero.core.TrackItem(
split_name, hiero.core.TrackItem.kVideo)
trackItem.setSource(new_source)
trackItem.setSourceIn(source_in)
trackItem.setSourceOut(source_out)
trackItem.setTimelineIn(nk["clipIn"])
trackItem.setTimelineOut(nk["clipIn"] + (source_out - source_in))
track.addTrackItem(trackItem)
clips_lst.append(trackItem)
return clips_lst
def create_bin(path=None, project=None):
'''
Create bin in project.
If the path is "bin1/bin2/bin3" it will create whole depth
and return `bin3`
'''
# get the first loaded project
project = project or get_current_project()
path = path or self.default_bin_name
path = path.replace("\\", "/").split("/")
root_bin = project.clipsBin()
done_bin_lst = []
for i, b in enumerate(path):
if i == 0 and len(path) > 1:
if b in [bin.name() for bin in root_bin.bins()]:
bin = [bin for bin in root_bin.bins() if b in bin.name()][0]
done_bin_lst.append(bin)
else:
create_bin = hiero.core.Bin(b)
root_bin.addItem(create_bin)
done_bin_lst.append(create_bin)
elif i >= 1 and i < len(path) - 1:
if b in [bin.name() for bin in done_bin_lst[i - 1].bins()]:
bin = [
bin for bin in done_bin_lst[i - 1].bins()
if b in bin.name()
][0]
done_bin_lst.append(bin)
else:
create_bin = hiero.core.Bin(b)
done_bin_lst[i - 1].addItem(create_bin)
done_bin_lst.append(create_bin)
elif i == len(path) - 1:
if b in [bin.name() for bin in done_bin_lst[i - 1].bins()]:
bin = [
bin for bin in done_bin_lst[i - 1].bins()
if b in bin.name()
][0]
done_bin_lst.append(bin)
else:
create_bin = hiero.core.Bin(b)
done_bin_lst[i - 1].addItem(create_bin)
done_bin_lst.append(create_bin)
return done_bin_lst[-1]
def split_by_client_version(string):
regex = r"[/_.]v\d+"
try:
matches = re.findall(regex, string, re.IGNORECASE)
return string.split(matches[0])
except Exception as error:
log.error(error)
return None
def get_selected_track_items(sequence=None):
_sequence = sequence or get_current_sequence()
# Getting selection
timeline_editor = hiero.ui.getTimelineEditor(_sequence)
return timeline_editor.selection()
def set_selected_track_items(track_items_list, sequence=None):
_sequence = sequence or get_current_sequence()
# Getting selection
timeline_editor = hiero.ui.getTimelineEditor(_sequence)
return timeline_editor.setSelection(track_items_list)
def _read_doc_from_path(path):
# reading QDomDocument from HROX path
hrox_file = QFile(path)
if not hrox_file.open(QFile.ReadOnly):
raise RuntimeError("Failed to open file for reading")
doc = QDomDocument()
doc.setContent(hrox_file)
hrox_file.close()
return doc
def _write_doc_to_path(doc, path):
# write QDomDocument to path as HROX
hrox_file = QFile(path)
if not hrox_file.open(QFile.WriteOnly):
raise RuntimeError("Failed to open file for writing")
stream = QTextStream(hrox_file)
doc.save(stream, 1)
hrox_file.close()
def _set_hrox_project_knobs(doc, **knobs):
# set attributes to Project Tag
proj_elem = doc.documentElement().firstChildElement("Project")
for k, v in knobs.items():
proj_elem.setAttribute(k, v)
def apply_colorspace_project():
# get path the the active projects
project = get_current_project(remove_untitled=True)
current_file = project.path()
# close the active project
project.close()
# get presets for hiero
presets = config.get_init_presets()
colorspace = presets["colorspace"]
hiero_project_clrs = colorspace.get("hiero", {}).get("project", {})
# save the workfile as subversion "comment:_colorspaceChange"
split_current_file = os.path.splitext(current_file)
copy_current_file = current_file
if "_colorspaceChange" not in current_file:
copy_current_file = (
split_current_file[0]
+ "_colorspaceChange"
+ split_current_file[1]
)
try:
# duplicate the file so the changes are applied only to the copy
shutil.copyfile(current_file, copy_current_file)
except shutil.Error:
# in case the file already exists and it want to copy to the
# same filewe need to do this trick
# TEMP file name change
copy_current_file_tmp = copy_current_file + "_tmp"
# create TEMP file
shutil.copyfile(current_file, copy_current_file_tmp)
# remove original file
os.remove(current_file)
# copy TEMP back to original name
shutil.copyfile(copy_current_file_tmp, copy_current_file)
# remove the TEMP file as we dont need it
os.remove(copy_current_file_tmp)
# use the code from bellow for changing xml hrox Attributes
hiero_project_clrs.update({"name": os.path.basename(copy_current_file)})
# read HROX in as QDomSocument
doc = _read_doc_from_path(copy_current_file)
# apply project colorspace properties
_set_hrox_project_knobs(doc, **hiero_project_clrs)
# write QDomSocument back as HROX
_write_doc_to_path(doc, copy_current_file)
# open the file as current project
hiero.core.openProject(copy_current_file)
def apply_colorspace_clips():
project = get_current_project(remove_untitled=True)
clips = project.clips()
# get presets for hiero
presets = config.get_init_presets()
colorspace = presets["colorspace"]
hiero_clips_clrs = colorspace.get("hiero", {}).get("clips", {})
for clip in clips:
clip_media_source_path = clip.mediaSource().firstpath()
clip_name = clip.name()
clip_colorspace = clip.sourceMediaColourTransform()
if "default" in clip_colorspace:
continue
# check if any colorspace presets for read is mathing
preset_clrsp = next((hiero_clips_clrs[k]
for k in hiero_clips_clrs
if bool(re.search(k, clip_media_source_path))),
None)
if preset_clrsp:
log.debug("Changing clip.path: {}".format(clip_media_source_path))
log.info("Changing clip `{}` colorspace {} to {}".format(
clip_name, clip_colorspace, preset_clrsp))
# set the found preset to the clip
clip.setSourceMediaColourTransform(preset_clrsp)
# save project after all is changed
project.save()
def is_overlapping(ti_test, ti_original, strict=False):
covering_exp = bool(
(ti_test.timelineIn() <= ti_original.timelineIn())
and (ti_test.timelineOut() >= ti_original.timelineOut())
)
inside_exp = bool(
(ti_test.timelineIn() >= ti_original.timelineIn())
and (ti_test.timelineOut() <= ti_original.timelineOut())
)
overlaying_right_exp = bool(
(ti_test.timelineIn() < ti_original.timelineOut())
and (ti_test.timelineOut() >= ti_original.timelineOut())
)
overlaying_left_exp = bool(
(ti_test.timelineOut() > ti_original.timelineIn())
and (ti_test.timelineIn() <= ti_original.timelineIn())
)
if not strict:
return any((
covering_exp,
inside_exp,
overlaying_right_exp,
overlaying_left_exp
))
else:
return covering_exp
def get_sequence_pattern_and_padding(file):
""" Return sequence pattern and padding from file
Attributes:
file (string): basename form path
Example:
Can find file.0001.ext, file.%02d.ext, file.####.ext
Return:
string: any matching sequence patern
int: padding of sequnce numbering
"""
foundall = re.findall(
r"(#+)|(%\d+d)|(?<=[^a-zA-Z0-9])(\d+)(?=\.\w+$)", file)
if foundall:
found = sorted(list(set(foundall[0])))[-1]
if "%" in found:
padding = int(re.findall(r"\d+", found)[-1])
else:
padding = len(found)
return found, padding
else:
return None, None

View file

@ -5,12 +5,7 @@ from pype.api import Logger
from avalon.api import Session
from hiero.ui import findMenuAction
from .tags import add_tags_from_presets
from .lib import (
reload_config,
set_workfiles
)
from . import tags
log = Logger().get_logger(__name__)
@ -18,7 +13,7 @@ self = sys.modules[__name__]
self._change_context_menu = None
def _update_menu_task_label(*args):
def update_menu_task_label(*args):
"""Update the task label in Avalon menu to current session"""
object_name = self._change_context_menu
@ -36,14 +31,17 @@ def _update_menu_task_label(*args):
menu.setTitle(label)
def install():
def menu_install():
"""
Installing menu into Hiero
"""
from . import (
publish, launch_workfiles_app, reload_config,
apply_colorspace_project, apply_colorspace_clips
)
# here is the best place to add menu
from avalon.tools import publish, cbloader
from avalon.tools import cbloader, creator, sceneinventory
from avalon.vendor.Qt import QtGui
menu_name = os.environ['AVALON_LABEL']
@ -72,36 +70,45 @@ def install():
workfiles_action = menu.addAction("Work Files...")
workfiles_action.setIcon(QtGui.QIcon("icons:Position.png"))
workfiles_action.triggered.connect(set_workfiles)
workfiles_action.triggered.connect(launch_workfiles_app)
default_tags_action = menu.addAction("Create Default Tags...")
default_tags_action.setIcon(QtGui.QIcon("icons:Position.png"))
default_tags_action.triggered.connect(add_tags_from_presets)
default_tags_action.triggered.connect(tags.add_tags_to_workfile)
menu.addSeparator()
publish_action = menu.addAction("Publish...")
publish_action.setIcon(QtGui.QIcon("icons:Output.png"))
publish_action.triggered.connect(
lambda *args: publish.show(hiero.ui.mainWindow())
lambda *args: publish(hiero.ui.mainWindow())
)
creator_action = menu.addAction("Create...")
creator_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
creator_action.triggered.connect(creator.show)
loader_action = menu.addAction("Load...")
loader_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
loader_action.triggered.connect(cbloader.show)
sceneinventory_action = menu.addAction("Manage...")
sceneinventory_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
sceneinventory_action.triggered.connect(sceneinventory.show)
menu.addSeparator()
reload_action = menu.addAction("Reload pipeline...")
reload_action.setIcon(QtGui.QIcon("icons:ColorAdd.png"))
reload_action.triggered.connect(reload_config)
# Is this required?
# hiero.ui.registerAction(context_label_action)
# hiero.ui.registerAction(workfiles_action)
# hiero.ui.registerAction(default_tags_action)
# hiero.ui.registerAction(publish_action)
# hiero.ui.registerAction(loader_action)
# hiero.ui.registerAction(reload_action)
menu.addSeparator()
apply_colorspace_p_action = menu.addAction("Apply Colorspace Project...")
apply_colorspace_p_action.setIcon(QtGui.QIcon("icons:ColorAdd.png"))
apply_colorspace_p_action.triggered.connect(apply_colorspace_project)
apply_colorspace_c_action = menu.addAction("Apply Colorspace Clips...")
apply_colorspace_c_action.setIcon(QtGui.QIcon("icons:ColorAdd.png"))
apply_colorspace_c_action.triggered.connect(apply_colorspace_clips)
self.context_label_action = context_label_action
self.workfile_actions = workfiles_action

View file

@ -0,0 +1,300 @@
"""
Basic avalon integration
"""
import os
import contextlib
from collections import OrderedDict
from avalon.tools import (
workfiles,
publish as _publish
)
from avalon.pipeline import AVALON_CONTAINER_ID
from avalon import api as avalon
from avalon import schema
from pyblish import api as pyblish
from pype.api import Logger
from . import lib, menu, events
log = Logger().get_logger(__name__)
AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype")
# plugin paths
API_DIR = os.path.dirname(os.path.abspath(__file__))
HOST_DIR = os.path.dirname(API_DIR)
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish").replace("\\", "/")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load").replace("\\", "/")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create").replace("\\", "/")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory").replace("\\", "/")
AVALON_CONTAINERS = ":AVALON_CONTAINERS"
def install():
"""
Installing Hiero integration for avalon
Args:
config (obj): avalon config module `pype` in our case, it is not
used but required by avalon.api.install()
"""
# adding all events
events.register_events()
log.info("Registering Hiero plug-ins..")
pyblish.register_host("hiero")
pyblish.register_plugin_path(PUBLISH_PATH)
avalon.register_plugin_path(avalon.Loader, LOAD_PATH)
avalon.register_plugin_path(avalon.Creator, CREATE_PATH)
avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH)
# register callback for switching publishable
pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled)
# Disable all families except for the ones we explicitly want to see
family_states = [
"write",
"review",
"plate"
]
avalon.data["familiesStateDefault"] = False
avalon.data["familiesStateToggled"] = family_states
# install menu
menu.menu_install()
# register hiero events
events.register_hiero_events()
def uninstall():
"""
Uninstalling Hiero integration for avalon
"""
log.info("Deregistering Hiero plug-ins..")
pyblish.deregister_host("hiero")
pyblish.deregister_plugin_path(PUBLISH_PATH)
avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH)
avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH)
# register callback for switching publishable
pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled)
def containerise(track_item,
name,
namespace,
context,
loader=None,
data=None):
"""Bundle Hiero's object into an assembly and imprint it with metadata
Containerisation enables a tracking of version, author and origin
for loaded assets.
Arguments:
track_item (hiero.core.TrackItem): object to imprint as container
name (str): Name of resulting assembly
namespace (str): Namespace under which to host container
context (dict): Asset information
loader (str, optional): Name of node used to produce this container.
Returns:
track_item (hiero.core.TrackItem): containerised object
"""
data_imprint = OrderedDict({
"schema": "avalon-core:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": str(name),
"namespace": str(namespace),
"loader": str(loader),
"representation": str(context["representation"]["_id"]),
})
if data:
for k, v in data.items():
data_imprint.update({k: v})
log.debug("_ data_imprint: {}".format(data_imprint))
lib.set_track_item_pype_tag(track_item, data_imprint)
return track_item
def ls():
"""List available containers.
This function is used by the Container Manager in Nuke. You'll
need to implement a for-loop that then *yields* one Container at
a time.
See the `container.json` schema for details on how it should look,
and the Maya equivalent, which is in `avalon.maya.pipeline`
"""
# get all track items from current timeline
all_track_items = lib.get_track_items()
for track_item in all_track_items:
container = parse_container(track_item)
if container:
yield container
def parse_container(track_item, validate=True):
"""Return container data from track_item's pype tag.
Args:
track_item (hiero.core.TrackItem): A containerised track item.
validate (bool)[optional]: validating with avalon scheme
Returns:
dict: The container schema data for input containerized track item.
"""
# convert tag metadata to normal keys names
data = lib.get_track_item_pype_data(track_item)
if validate and data and data.get("schema"):
schema.validate(data)
if not isinstance(data, dict):
return
# If not all required data return the empty container
required = ['schema', 'id', 'name',
'namespace', 'loader', 'representation']
if not all(key in data for key in required):
return
container = {key: data[key] for key in required}
container["objectName"] = track_item.name()
# Store reference to the node object
container["_track_item"] = track_item
return container
def update_container(track_item, data=None):
"""Update container data to input track_item's pype tag.
Args:
track_item (hiero.core.TrackItem): A containerised track item.
data (dict)[optional]: dictionery with data to be updated
Returns:
bool: True if container was updated correctly
"""
data = data or dict()
container = lib.get_track_item_pype_data(track_item)
for _key, _value in container.items():
try:
container[_key] = data[_key]
except KeyError:
pass
log.info("Updating container: `{}`".format(track_item.name()))
return bool(lib.set_track_item_pype_tag(track_item, container))
def launch_workfiles_app(*args):
''' Wrapping function for workfiles launcher '''
workdir = os.environ["AVALON_WORKDIR"]
# show workfile gui
workfiles.show(workdir)
def publish(parent):
"""Shorthand to publish from within host"""
return _publish.show(parent)
@contextlib.contextmanager
def maintained_selection():
"""Maintain selection during context
Example:
>>> with maintained_selection():
... for track_item in track_items:
... < do some stuff >
"""
from .lib import (
set_selected_track_items,
get_selected_track_items
)
previous_selection = get_selected_track_items()
reset_selection()
try:
# do the operation
yield
finally:
reset_selection()
set_selected_track_items(previous_selection)
def reset_selection():
"""Deselect all selected nodes
"""
from .lib import set_selected_track_items
set_selected_track_items([])
def reload_config():
"""Attempt to reload pipeline at run-time.
CAUTION: This is primarily for development and debugging purposes.
"""
import importlib
for module in (
"avalon",
"avalon.lib",
"avalon.pipeline",
"pyblish",
"pypeapp",
"{}.api".format(AVALON_CONFIG),
"{}.hosts.hiero.lib".format(AVALON_CONFIG),
"{}.hosts.hiero.menu".format(AVALON_CONFIG),
"{}.hosts.hiero.tags".format(AVALON_CONFIG)
):
log.info("Reloading module: {}...".format(module))
try:
module = importlib.import_module(module)
import imp
imp.reload(module)
except Exception as e:
log.warning("Cannot reload module: {}".format(e))
importlib.reload(module)
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle node passthrough states on instance toggles."""
log.info("instance toggle: {}, old_value: {}, new_value:{} ".format(
instance, old_value, new_value))
from pype.hosts.hiero.api import (
get_track_item_pype_tag,
set_publish_attribute
)
# Whether instances should be passthrough based on new value
track_item = instance.data["item"]
tag = get_track_item_pype_tag(track_item)
set_publish_attribute(tag, new_value)

View file

@ -0,0 +1,909 @@
import re
import os
import hiero
from Qt import QtWidgets, QtCore
from avalon.vendor import qargparse
import avalon.api as avalon
import pype.api as pype
from . import lib
log = pype.Logger().get_logger(__name__)
def load_stylesheet():
path = os.path.join(os.path.dirname(__file__), "style.css")
if not os.path.exists(path):
log.warning("Unable to load stylesheet, file not found in resources")
return ""
with open(path, "r") as file_stream:
stylesheet = file_stream.read()
return stylesheet
class CreatorWidget(QtWidgets.QDialog):
# output items
items = dict()
def __init__(self, name, info, ui_inputs, parent=None):
super(CreatorWidget, self).__init__(parent)
self.setObjectName(name)
self.setWindowFlags(
QtCore.Qt.Window
| QtCore.Qt.CustomizeWindowHint
| QtCore.Qt.WindowTitleHint
| QtCore.Qt.WindowCloseButtonHint
| QtCore.Qt.WindowStaysOnTopHint
)
self.setWindowTitle(name or "Pype Creator Input")
self.resize(500, 700)
# Where inputs and labels are set
self.content_widget = [QtWidgets.QWidget(self)]
top_layout = QtWidgets.QFormLayout(self.content_widget[0])
top_layout.setObjectName("ContentLayout")
top_layout.addWidget(Spacer(5, self))
# first add widget tag line
top_layout.addWidget(QtWidgets.QLabel(info))
# main dynamic layout
self.scroll_area = QtWidgets.QScrollArea(self, widgetResizable=True)
self.scroll_area.setVerticalScrollBarPolicy(
QtCore.Qt.ScrollBarAsNeeded)
self.scroll_area.setVerticalScrollBarPolicy(
QtCore.Qt.ScrollBarAlwaysOn)
self.scroll_area.setHorizontalScrollBarPolicy(
QtCore.Qt.ScrollBarAlwaysOff)
self.scroll_area.setWidgetResizable(True)
self.content_widget.append(self.scroll_area)
scroll_widget = QtWidgets.QWidget(self)
in_scroll_area = QtWidgets.QVBoxLayout(scroll_widget)
self.content_layout = [in_scroll_area]
# add preset data into input widget layout
self.items = self.populate_widgets(ui_inputs)
self.scroll_area.setWidget(scroll_widget)
# Confirmation buttons
btns_widget = QtWidgets.QWidget(self)
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
cancel_btn = QtWidgets.QPushButton("Cancel")
btns_layout.addWidget(cancel_btn)
ok_btn = QtWidgets.QPushButton("Ok")
btns_layout.addWidget(ok_btn)
# Main layout of the dialog
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(0)
# adding content widget
for w in self.content_widget:
main_layout.addWidget(w)
main_layout.addWidget(btns_widget)
ok_btn.clicked.connect(self._on_ok_clicked)
cancel_btn.clicked.connect(self._on_cancel_clicked)
stylesheet = load_stylesheet()
self.setStyleSheet(stylesheet)
def _on_ok_clicked(self):
self.result = self.value(self.items)
self.close()
def _on_cancel_clicked(self):
self.result = None
self.close()
def value(self, data, new_data=None):
new_data = new_data or dict()
for k, v in data.items():
new_data[k] = {
"target": None,
"value": None
}
if v["type"] == "dict":
new_data[k]["target"] = v["target"]
new_data[k]["value"] = self.value(v["value"])
if v["type"] == "section":
new_data.pop(k)
new_data = self.value(v["value"], new_data)
elif getattr(v["value"], "currentText", None):
new_data[k]["target"] = v["target"]
new_data[k]["value"] = v["value"].currentText()
elif getattr(v["value"], "isChecked", None):
new_data[k]["target"] = v["target"]
new_data[k]["value"] = v["value"].isChecked()
elif getattr(v["value"], "value", None):
new_data[k]["target"] = v["target"]
new_data[k]["value"] = v["value"].value()
elif getattr(v["value"], "text", None):
new_data[k]["target"] = v["target"]
new_data[k]["value"] = v["value"].text()
return new_data
def camel_case_split(self, text):
matches = re.finditer(
'.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)', text)
return " ".join([str(m.group(0)).capitalize() for m in matches])
def create_row(self, layout, type, text, **kwargs):
# get type attribute from qwidgets
attr = getattr(QtWidgets, type)
# convert label text to normal capitalized text with spaces
label_text = self.camel_case_split(text)
# assign the new text to lable widget
label = QtWidgets.QLabel(label_text)
label.setObjectName("LineLabel")
# create attribute name text strip of spaces
attr_name = text.replace(" ", "")
# create attribute and assign default values
setattr(
self,
attr_name,
attr(parent=self))
# assign the created attribute to variable
item = getattr(self, attr_name)
for func, val in kwargs.items():
if getattr(item, func):
func_attr = getattr(item, func)
func_attr(val)
# add to layout
layout.addRow(label, item)
return item
def populate_widgets(self, data, content_layout=None):
"""
Populate widget from input dict.
Each plugin has its own set of widget rows defined in dictionary
each row values should have following keys: `type`, `target`,
`label`, `order`, `value` and optionally also `toolTip`.
Args:
data (dict): widget rows or organized groups defined
by types `dict` or `section`
content_layout (QtWidgets.QFormLayout)[optional]: used when nesting
Returns:
dict: redefined data dict updated with created widgets
"""
content_layout = content_layout or self.content_layout[-1]
# fix order of process by defined order value
ordered_keys = data.keys()
for k, v in data.items():
try:
# try removing a key from index which should
# be filled with new
ordered_keys.pop(v["order"])
except IndexError:
pass
# add key into correct order
ordered_keys.insert(v["order"], k)
# process ordered
for k in ordered_keys:
v = data[k]
tool_tip = v.get("toolTip", "")
if v["type"] == "dict":
# adding spacer between sections
self.content_layout.append(QtWidgets.QWidget(self))
content_layout.addWidget(self.content_layout[-1])
self.content_layout[-1].setObjectName("sectionHeadline")
headline = QtWidgets.QVBoxLayout(self.content_layout[-1])
headline.addWidget(Spacer(20, self))
headline.addWidget(QtWidgets.QLabel(v["label"]))
# adding nested layout with label
self.content_layout.append(QtWidgets.QWidget(self))
self.content_layout[-1].setObjectName("sectionContent")
nested_content_layout = QtWidgets.QFormLayout(
self.content_layout[-1])
nested_content_layout.setObjectName("NestedContentLayout")
content_layout.addWidget(self.content_layout[-1])
# add nested key as label
data[k]["value"] = self.populate_widgets(
v["value"], nested_content_layout)
if v["type"] == "section":
# adding spacer between sections
self.content_layout.append(QtWidgets.QWidget(self))
content_layout.addWidget(self.content_layout[-1])
self.content_layout[-1].setObjectName("sectionHeadline")
headline = QtWidgets.QVBoxLayout(self.content_layout[-1])
headline.addWidget(Spacer(20, self))
headline.addWidget(QtWidgets.QLabel(v["label"]))
# adding nested layout with label
self.content_layout.append(QtWidgets.QWidget(self))
self.content_layout[-1].setObjectName("sectionContent")
nested_content_layout = QtWidgets.QFormLayout(
self.content_layout[-1])
nested_content_layout.setObjectName("NestedContentLayout")
content_layout.addWidget(self.content_layout[-1])
# add nested key as label
data[k]["value"] = self.populate_widgets(
v["value"], nested_content_layout)
elif v["type"] == "QLineEdit":
data[k]["value"] = self.create_row(
content_layout, "QLineEdit", v["label"],
setText=v["value"], setToolTip=tool_tip)
elif v["type"] == "QComboBox":
data[k]["value"] = self.create_row(
content_layout, "QComboBox", v["label"],
addItems=v["value"], setToolTip=tool_tip)
elif v["type"] == "QCheckBox":
data[k]["value"] = self.create_row(
content_layout, "QCheckBox", v["label"],
setChecked=v["value"], setToolTip=tool_tip)
elif v["type"] == "QSpinBox":
data[k]["value"] = self.create_row(
content_layout, "QSpinBox", v["label"],
setValue=v["value"], setMaximum=10000, setToolTip=tool_tip)
return data
class Spacer(QtWidgets.QWidget):
def __init__(self, height, *args, **kwargs):
super(self.__class__, self).__init__(*args, **kwargs)
self.setFixedHeight(height)
real_spacer = QtWidgets.QWidget(self)
real_spacer.setObjectName("Spacer")
real_spacer.setFixedHeight(height)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(real_spacer)
self.setLayout(layout)
def get_reference_node_parents(ref):
"""Return all parent reference nodes of reference node
Args:
ref (str): reference node.
Returns:
list: The upstream parent reference nodes.
"""
parents = []
return parents
class SequenceLoader(avalon.Loader):
"""A basic SequenceLoader for Resolve
This will implement the basic behavior for a loader to inherit from that
will containerize the reference and will implement the `remove` and
`update` logic.
"""
options = [
qargparse.Boolean(
"handles",
label="Include handles",
default=0,
help="Load with handles or without?"
),
qargparse.Choice(
"load_to",
label="Where to load clips",
items=[
"Current timeline",
"New timeline"
],
default="Current timeline",
help="Where do you want clips to be loaded?"
),
qargparse.Choice(
"load_how",
label="How to load clips",
items=[
"Original timing",
"Sequentially in order"
],
default="Original timing",
help="Would you like to place it at orignal timing?"
)
]
def load(
self,
context,
name=None,
namespace=None,
options=None
):
pass
def update(self, container, representation):
"""Update an existing `container`
"""
pass
def remove(self, container):
"""Remove an existing `container`
"""
pass
class ClipLoader:
active_bin = None
data = dict()
def __init__(self, cls, context, **options):
""" Initialize object
Arguments:
cls (avalon.api.Loader): plugin object
context (dict): loader plugin context
options (dict)[optional]: possible keys:
projectBinPath: "path/to/binItem"
"""
self.__dict__.update(cls.__dict__)
self.context = context
self.active_project = lib.get_current_project()
# try to get value from options or evaluate key value for `handles`
self.with_handles = options.get("handles") or bool(
options.get("handles") is True)
# try to get value from options or evaluate key value for `load_how`
self.sequencial_load = options.get("sequencially") or bool(
"Sequentially in order" in options.get("load_how", ""))
# try to get value from options or evaluate key value for `load_to`
self.new_sequence = options.get("newSequence") or bool(
"New timeline" in options.get("load_to", ""))
assert self._populate_data(), str(
"Cannot Load selected data, look into database "
"or call your supervisor")
# inject asset data to representation dict
self._get_asset_data()
log.debug("__init__ self.data: `{}`".format(self.data))
# add active components to class
if self.new_sequence:
if options.get("sequence"):
# if multiselection is set then use options sequence
self.active_sequence = options["sequence"]
else:
# create new sequence
self.active_sequence = lib.get_current_sequence(new=True)
self.active_sequence.setFramerate(
hiero.core.TimeBase.fromString(
str(self.data["assetData"]["fps"])))
else:
self.active_sequence = lib.get_current_sequence()
if options.get("track"):
# if multiselection is set then use options track
self.active_track = options["track"]
else:
self.active_track = lib.get_current_track(
self.active_sequence, self.data["track_name"])
def _populate_data(self):
""" Gets context and convert it to self.data
data structure:
{
"name": "assetName_subsetName_representationName"
"path": "path/to/file/created/by/get_repr..",
"binPath": "projectBinPath",
}
"""
# create name
repr = self.context["representation"]
repr_cntx = repr["context"]
asset = str(repr_cntx["asset"])
subset = str(repr_cntx["subset"])
representation = str(repr_cntx["representation"])
self.data["clip_name"] = "_".join([asset, subset, representation])
self.data["track_name"] = "_".join([subset, representation])
self.data["versionData"] = self.context["version"]["data"]
# gets file path
file = self.fname
if not file:
repr_id = repr["_id"]
log.warning(
"Representation id `{}` is failing to load".format(repr_id))
return None
self.data["path"] = file.replace("\\", "/")
# convert to hashed path
if repr_cntx.get("frame"):
self._fix_path_hashes()
# solve project bin structure path
hierarchy = str("/".join((
"Loader",
repr_cntx["hierarchy"].replace("\\", "/"),
asset
)))
self.data["binPath"] = hierarchy
return True
def _fix_path_hashes(self):
""" Convert file path where it is needed padding with hashes
"""
file = self.data["path"]
if "#" not in file:
frame = self.context["representation"]["context"].get("frame")
padding = len(frame)
file = file.replace(frame, "#" * padding)
self.data["path"] = file
def _get_asset_data(self):
""" Get all available asset data
joint `data` key with asset.data dict into the representaion
"""
asset_name = self.context["representation"]["context"]["asset"]
self.data["assetData"] = pype.get_asset(asset_name)["data"]
def _make_track_item(self, source_bin_item, audio=False):
""" Create track item with """
clip = source_bin_item.activeItem()
# add to track as clip item
if not audio:
track_item = hiero.core.TrackItem(
self.data["clip_name"], hiero.core.TrackItem.kVideo)
else:
track_item = hiero.core.TrackItem(
self.data["clip_name"], hiero.core.TrackItem.kAudio)
track_item.setSource(clip)
track_item.setSourceIn(self.handle_start)
track_item.setTimelineIn(self.timeline_in)
track_item.setSourceOut(self.media_duration - self.handle_end)
track_item.setTimelineOut(self.timeline_out)
track_item.setPlaybackSpeed(1)
self.active_track.addTrackItem(track_item)
return track_item
def load(self):
# create project bin for the media to be imported into
self.active_bin = lib.create_bin(self.data["binPath"])
# create mediaItem in active project bin
# create clip media
self.media = hiero.core.MediaSource(self.data["path"])
self.media_duration = int(self.media.duration())
# get handles
self.handle_start = self.data["versionData"].get("handleStart")
self.handle_end = self.data["versionData"].get("handleEnd")
if self.handle_start is None:
self.handle_start = int(self.data["assetData"]["handleStart"])
if self.handle_end is None:
self.handle_end = int(self.data["assetData"]["handleEnd"])
if self.sequencial_load:
last_track_item = lib.get_track_items(
sequence_name=self.active_sequence.name(),
track_name=self.active_track.name())
if len(last_track_item) == 0:
last_timeline_out = 0
else:
last_track_item = last_track_item[-1]
last_timeline_out = int(last_track_item.timelineOut()) + 1
self.timeline_in = last_timeline_out
self.timeline_out = last_timeline_out + int(
self.data["assetData"]["clipOut"]
- self.data["assetData"]["clipIn"])
else:
self.timeline_in = int(self.data["assetData"]["clipIn"])
self.timeline_out = int(self.data["assetData"]["clipOut"])
# check if slate is included
# either in version data families or by calculating frame diff
slate_on = next(
# check iterate if slate is in families
(f for f in self.context["version"]["data"]["families"]
if "slate" in f),
# if nothing was found then use default None
# so other bool could be used
None) or bool(((
# put together duration of clip attributes
self.timeline_out - self.timeline_in + 1) \
+ self.handle_start \
+ self.handle_end
# and compare it with meda duration
) > self.media_duration)
print("__ slate_on: `{}`".format(slate_on))
# if slate is on then remove the slate frame from begining
if slate_on:
self.media_duration -= 1
self.handle_start += 1
# create Clip from Media
clip = hiero.core.Clip(self.media)
clip.setName(self.data["clip_name"])
# add Clip to bin if not there yet
if self.data["clip_name"] not in [
b.name() for b in self.active_bin.items()]:
bin_item = hiero.core.BinItem(clip)
self.active_bin.addItem(bin_item)
# just make sure the clip is created
# there were some cases were hiero was not creating it
source_bin_item = None
for item in self.active_bin.items():
if self.data["clip_name"] in item.name():
source_bin_item = item
if not source_bin_item:
log.warning("Problem with created Source clip: `{}`".format(
self.data["clip_name"]))
# include handles
if self.with_handles:
self.timeline_in -= self.handle_start
self.timeline_out += self.handle_end
self.handle_start = 0
self.handle_end = 0
# make track item from source in bin as item
track_item = self._make_track_item(source_bin_item)
log.info("Loading clips: `{}`".format(self.data["clip_name"]))
return track_item
class Creator(avalon.Creator):
"""Creator class wrapper
"""
clip_color = "Purple"
rename_index = None
def __init__(self, *args, **kwargs):
import pype.hosts.hiero.api as phiero
super(Creator, self).__init__(*args, **kwargs)
self.presets = pype.get_current_project_settings()[
"hiero"]["create"].get(self.__class__.__name__, {})
# adding basic current context resolve objects
self.project = phiero.get_current_project()
self.sequence = phiero.get_current_sequence()
if (self.options or {}).get("useSelection"):
self.selected = phiero.get_track_items(selected=True)
else:
self.selected = phiero.get_track_items()
self.widget = CreatorWidget
class PublishClip:
"""
Convert a track item to publishable instance
Args:
track_item (hiero.core.TrackItem): hiero track item object
kwargs (optional): additional data needed for rename=True (presets)
Returns:
hiero.core.TrackItem: hiero track item object with pype tag
"""
vertical_clip_match = dict()
tag_data = dict()
types = {
"shot": "shot",
"folder": "folder",
"episode": "episode",
"sequence": "sequence",
"track": "sequence",
}
# parents search patern
parents_search_patern = r"\{([a-z]*?)\}"
# default templates for non-ui use
rename_default = False
hierarchy_default = "{_folder_}/{_sequence_}/{_track_}"
clip_name_default = "shot_{_trackIndex_:0>3}_{_clipIndex_:0>4}"
subset_name_default = "<track_name>"
review_track_default = "< none >"
subset_family_default = "plate"
count_from_default = 10
count_steps_default = 10
vertical_sync_default = False
driving_layer_default = ""
def __init__(self, cls, track_item, **kwargs):
# populate input cls attribute onto self.[attr]
self.__dict__.update(cls.__dict__)
# get main parent objects
self.track_item = track_item
sequence_name = lib.get_current_sequence().name()
self.sequence_name = str(sequence_name).replace(" ", "_")
# track item (clip) main attributes
self.ti_name = track_item.name()
self.ti_index = int(track_item.eventNumber())
# get track name and index
track_name = track_item.parent().name()
self.track_name = str(track_name).replace(" ", "_")
self.track_index = int(track_item.parent().trackIndex())
# adding tag.family into tag
if kwargs.get("avalon"):
self.tag_data.update(kwargs["avalon"])
# adding ui inputs if any
self.ui_inputs = kwargs.get("ui_inputs", {})
# populate default data before we get other attributes
self._populate_track_item_default_data()
# use all populated default data to create all important attributes
self._populate_attributes()
# create parents with correct types
self._create_parents()
def convert(self):
# solve track item data and add them to tag data
self._convert_to_tag_data()
# if track name is in review track name and also if driving track name
# is not in review track name: skip tag creation
if (self.track_name in self.review_layer) and (
self.driving_layer not in self.review_layer):
return
# deal with clip name
new_name = self.tag_data.pop("newClipName")
if self.rename:
# rename track item
self.track_item.setName(new_name)
self.tag_data["asset"] = new_name
else:
self.tag_data["asset"] = self.ti_name
# create pype tag on track_item and add data
lib.imprint(self.track_item, self.tag_data)
return self.track_item
def _populate_track_item_default_data(self):
""" Populate default formating data from track item. """
self.track_item_default_data = {
"_folder_": "shots",
"_sequence_": self.sequence_name,
"_track_": self.track_name,
"_clip_": self.ti_name,
"_trackIndex_": self.track_index,
"_clipIndex_": self.ti_index
}
def _populate_attributes(self):
""" Populate main object attributes. """
# track item frame range and parent track name for vertical sync check
self.clip_in = int(self.track_item.timelineIn())
self.clip_out = int(self.track_item.timelineOut())
# define ui inputs if non gui mode was used
self.shot_num = self.ti_index
log.debug(
"____ self.shot_num: {}".format(self.shot_num))
# ui_inputs data or default values if gui was not used
self.rename = self.ui_inputs.get(
"clipRename", {}).get("value") or self.rename_default
self.clip_name = self.ui_inputs.get(
"clipName", {}).get("value") or self.clip_name_default
self.hierarchy = self.ui_inputs.get(
"hierarchy", {}).get("value") or self.hierarchy_default
self.hierarchy_data = self.ui_inputs.get(
"hierarchyData", {}).get("value") or \
self.track_item_default_data.copy()
self.count_from = self.ui_inputs.get(
"countFrom", {}).get("value") or self.count_from_default
self.count_steps = self.ui_inputs.get(
"countSteps", {}).get("value") or self.count_steps_default
self.subset_name = self.ui_inputs.get(
"subsetName", {}).get("value") or self.subset_name_default
self.subset_family = self.ui_inputs.get(
"subsetFamily", {}).get("value") or self.subset_family_default
self.vertical_sync = self.ui_inputs.get(
"vSyncOn", {}).get("value") or self.vertical_sync_default
self.driving_layer = self.ui_inputs.get(
"vSyncTrack", {}).get("value") or self.driving_layer_default
self.review_track = self.ui_inputs.get(
"reviewTrack", {}).get("value") or self.review_track_default
self.audio = self.ui_inputs.get(
"audio", {}).get("value") or False
# build subset name from layer name
if self.subset_name == "<track_name>":
self.subset_name = self.track_name
# create subset for publishing
self.subset = self.subset_family + self.subset_name.capitalize()
def _replace_hash_to_expression(self, name, text):
""" Replace hash with number in correct padding. """
_spl = text.split("#")
_len = (len(_spl) - 1)
_repl = "{{{0}:0>{1}}}".format(name, _len)
new_text = text.replace(("#" * _len), _repl)
return new_text
def _convert_to_tag_data(self):
""" Convert internal data to tag data.
Populating the tag data into internal variable self.tag_data
"""
# define vertical sync attributes
master_layer = True
self.review_layer = ""
if self.vertical_sync:
# check if track name is not in driving layer
if self.track_name not in self.driving_layer:
# if it is not then define vertical sync as None
master_layer = False
# increasing steps by index of rename iteration
self.count_steps *= self.rename_index
hierarchy_formating_data = dict()
_data = self.track_item_default_data.copy()
if self.ui_inputs:
# adding tag metadata from ui
for _k, _v in self.ui_inputs.items():
if _v["target"] == "tag":
self.tag_data[_k] = _v["value"]
# driving layer is set as positive match
if master_layer or self.vertical_sync:
# mark review layer
if self.review_track and (
self.review_track not in self.review_track_default):
# if review layer is defined and not the same as defalut
self.review_layer = self.review_track
# shot num calculate
if self.rename_index == 0:
self.shot_num = self.count_from
else:
self.shot_num = self.count_from + self.count_steps
# clip name sequence number
_data.update({"shot": self.shot_num})
# solve # in test to pythonic expression
for _k, _v in self.hierarchy_data.items():
if "#" not in _v["value"]:
continue
self.hierarchy_data[
_k]["value"] = self._replace_hash_to_expression(
_k, _v["value"])
# fill up pythonic expresisons in hierarchy data
for k, _v in self.hierarchy_data.items():
hierarchy_formating_data[k] = _v["value"].format(**_data)
else:
# if no gui mode then just pass default data
hierarchy_formating_data = self.hierarchy_data
tag_hierarchy_data = self._solve_tag_hierarchy_data(
hierarchy_formating_data
)
tag_hierarchy_data.update({"masterLayer": True})
if master_layer and self.vertical_sync:
self.vertical_clip_match.update({
(self.clip_in, self.clip_out): tag_hierarchy_data
})
if not master_layer and self.vertical_sync:
# driving layer is set as negative match
for (_in, _out), master_data in self.vertical_clip_match.items():
master_data.update({"masterLayer": False})
if _in == self.clip_in and _out == self.clip_out:
data_subset = master_data["subset"]
# add track index in case duplicity of names in master data
if self.subset in data_subset:
master_data["subset"] = self.subset + str(
self.track_index)
# in case track name and subset name is the same then add
if self.subset_name == self.track_name:
master_data["subset"] = self.subset
# assing data to return hierarchy data to tag
tag_hierarchy_data = master_data
# add data to return data dict
self.tag_data.update(tag_hierarchy_data)
if master_layer and self.review_layer:
self.tag_data.update({"reviewTrack": self.review_layer})
def _solve_tag_hierarchy_data(self, hierarchy_formating_data):
""" Solve tag data from hierarchy data and templates. """
# fill up clip name and hierarchy keys
hierarchy_filled = self.hierarchy.format(**hierarchy_formating_data)
clip_name_filled = self.clip_name.format(**hierarchy_formating_data)
return {
"newClipName": clip_name_filled,
"hierarchy": hierarchy_filled,
"parents": self.parents,
"hierarchyData": hierarchy_formating_data,
"subset": self.subset,
"family": self.subset_family,
"families": [self.data["family"]]
}
def _convert_to_entity(self, key):
""" Converting input key to key with type. """
# convert to entity type
entity_type = self.types.get(key, None)
assert entity_type, "Missing entity type for `{}`".format(
key
)
return {
"entity_type": entity_type,
"entity_name": self.hierarchy_data[key]["value"].format(
**self.track_item_default_data
)
}
def _create_parents(self):
""" Create parents and return it in list. """
self.parents = list()
patern = re.compile(self.parents_search_patern)
par_split = [patern.findall(t).pop()
for t in self.hierarchy.split("/")]
for key in par_split:
parent = self._convert_to_entity(key)
self.parents.append(parent)

View file

@ -0,0 +1,26 @@
QWidget {
font-size: 13px;
}
QSpinBox {
padding: 2;
max-width: 8em;
}
QLineEdit {
padding: 2;
min-width: 15em;
}
QVBoxLayout {
min-width: 15em;
background-color: #201f1f;
}
QComboBox {
min-width: 8em;
}
#sectionContent {
background-color: #2E2D2D;
}

View file

@ -1,10 +1,7 @@
import re
import os
import json
import hiero
from pprint import pformat
from pype.api import Logger
from avalon import io
@ -12,55 +9,99 @@ log = Logger().get_logger(__name__)
def tag_data():
current_dir = os.path.dirname(__file__)
json_path = os.path.join(current_dir, "tags.json")
with open(json_path, "r") as json_stream:
data = json.load(json_stream)
return data
return {
"Retiming": {
"editable": "1",
"note": "Clip has retime or TimeWarp effects (or multiple effects stacked on the clip)", # noqa
"icon": "retiming.png",
"metadata": {
"family": "retiming",
"marginIn": 1,
"marginOut": 1
}
},
"[Lenses]": {
"Set lense here": {
"editable": "1",
"note": "Adjust parameters of your lense and then drop to clip. Remember! You can always overwrite on clip", # noqa
"icon": "lense.png",
"metadata": {
"focalLengthMm": 57
}
}
},
"NukeScript": {
"editable": "1",
"note": "Collecting track items to Nuke scripts.",
"icon": "icons:TagNuke.png",
"metadata": {
"family": "nukescript",
"subset": "main"
}
},
"Comment": {
"editable": "1",
"note": "Comment on a shot.",
"icon": "icons:TagComment.png",
"metadata": {
"family": "comment",
"subset": "main"
}
}
}
def create_tag(key, value):
def create_tag(key, data):
"""
Creating Tag object.
Args:
key (str): name of tag
value (dict): parameters of tag
data (dict): parameters of tag
Returns:
object: Tag object
"""
tag = hiero.core.Tag(str(key))
return update_tag(tag, value)
return update_tag(tag, data)
def update_tag(tag, value):
def update_tag(tag, data):
"""
Fixing Tag object.
Args:
tag (obj): Tag object
value (dict): parameters of tag
data (dict): parameters of tag
"""
tag.setNote(value["note"])
tag.setIcon(str(value["icon"]["path"]))
# set icon if any available in input data
if data.get("icon"):
tag.setIcon(str(data["icon"]))
# set note description of tag
tag.setNote(data["note"])
# get metadata of tag
mtd = tag.metadata()
pres_mtd = value.get("metadata", None)
if pres_mtd:
[mtd.setValue("tag.{}".format(str(k)), str(v))
for k, v in pres_mtd.items()]
# get metadata key from data
data_mtd = data.get("metadata", {})
# set all data metadata to tag metadata
for k, v in data_mtd.items():
mtd.setValue(
"tag.{}".format(str(k)),
str(v)
)
return tag
def add_tags_from_presets():
def add_tags_to_workfile():
"""
Will create default tags from presets.
"""
project = hiero.core.projects()[-1]
from .lib import get_current_project
# get project and root bin object
project = get_current_project()
root_bin = project.tagsBin()
if "Tag Presets" in project.name():
return
@ -73,7 +114,7 @@ def add_tags_from_presets():
# Get project task types.
tasks = io.find_one({"type": "project"})["config"]["tasks"]
nks_pres_tags["[Tasks]"] = {}
log.debug("__ tasks: {}".format(pformat(tasks)))
log.debug("__ tasks: {}".format(tasks))
for task_type in tasks.keys():
nks_pres_tags["[Tasks]"][task_type.lower()] = {
"editable": "1",
@ -104,24 +145,32 @@ def add_tags_from_presets():
}
}
# get project and root bin object
project = hiero.core.projects()[-1]
root_bin = project.tagsBin()
# loop trough tag data dict and create deep tag structure
for _k, _val in nks_pres_tags.items():
# check if key is not decorated with [] so it is defined as bin
bin_find = None
pattern = re.compile(r"\[(.*)\]")
bin_find = pattern.findall(_k)
bin_finds = pattern.findall(_k)
# if there is available any then pop it to string
if bin_finds:
bin_find = bin_finds.pop()
# if bin was found then create or update
if bin_find:
# check what is in root bin
root_add = False
# first check if in root lever is not already created bins
bins = [b for b in root_bin.items()
if b.name() in str(bin_find[0])]
if b.name() in str(bin_find)]
log.debug(">>> bins: {}".format(bins))
if bins:
bin = bins[0]
bin = bins.pop()
else:
# create Bin object
bin = hiero.core.Bin(str(bin_find[0]))
root_add = True
# create Bin object for processing
bin = hiero.core.Bin(str(bin_find))
# update or create tags in the bin
for k, v in _val.items():
tags = [t for t in bin.items()
if str(k) in t.name()
@ -133,13 +182,15 @@ def add_tags_from_presets():
# adding Tag to Bin
bin.addItem(tag)
else:
update_tag(tags[0], v)
update_tag(tags.pop(), v)
if not bins:
# finally add the Bin object to the root level Bin
if root_add:
# adding Tag to Root Bin
root_bin.addItem(bin)
else:
# for Tags to be created in root level Bin
# at first check if any of input data tag is not already created
tags = None
tags = [t for t in root_bin.items()
if str(_k) in t.name()]
@ -151,16 +202,18 @@ def add_tags_from_presets():
# adding Tag to Root Bin
root_bin.addItem(tag)
else:
# check if Hierarchy in name
# update Tag if already exists
# update Tags if they already exists
for _t in tags:
# skip bin objects
if isinstance(_t, hiero.core.Bin):
continue
# check if Hierarchy in name and skip it
# because hierarchy could be edited
if "hierarchy" in _t.name().lower():
continue
# update only non hierarchy tags
# because hierarchy could be edited
update_tag(_t, _val)
log.info("Default Tags were set...")

View file

@ -1,817 +0,0 @@
import os
import re
import sys
import hiero
import pyblish.api
import avalon.api as avalon
import avalon.io
from avalon.vendor.Qt import (QtWidgets, QtGui)
import pype.api as pype
from pype.api import Logger, Anatomy
log = Logger().get_logger(__name__)
cached_process = None
self = sys.modules[__name__]
self._has_been_setup = False
self._has_menu = False
self._registered_gui = None
AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype")
def set_workfiles():
''' Wrapping function for workfiles launcher '''
from avalon.tools import workfiles
workdir = os.environ["AVALON_WORKDIR"]
# show workfile gui
workfiles.show(workdir)
def sync_avalon_data_to_workfile():
# import session to get project dir
project_name = avalon.Session["AVALON_PROJECT"]
anatomy = Anatomy(project_name)
work_template = anatomy.templates["work"]["path"]
work_root = anatomy.root_value_for_template(work_template)
active_project_root = (
os.path.join(work_root, project_name)
).replace("\\", "/")
# getting project
project = hiero.core.projects()[-1]
if "Tag Presets" in project.name():
return
log.debug("Synchronizing Pype metadata to project: {}".format(
project.name()))
# set project root with backward compatibility
try:
project.setProjectDirectory(active_project_root)
except Exception:
# old way of seting it
project.setProjectRoot(active_project_root)
# get project data from avalon db
project_doc = avalon.io.find_one({"type": "project"})
project_data = project_doc["data"]
log.debug("project_data: {}".format(project_data))
# get format and fps property from avalon db on project
width = project_data["resolutionWidth"]
height = project_data["resolutionHeight"]
pixel_aspect = project_data["pixelAspect"]
fps = project_data['fps']
format_name = project_data['code']
# create new format in hiero project
format = hiero.core.Format(width, height, pixel_aspect, format_name)
project.setOutputFormat(format)
# set fps to hiero project
project.setFramerate(fps)
# TODO: add auto colorspace set from project drop
log.info("Project property has been synchronised with Avalon db")
def launch_workfiles_app(event):
"""
Event for launching workfiles after hiero start
Args:
event (obj): required but unused
"""
set_workfiles()
def reload_config():
"""Attempt to reload pipeline at run-time.
CAUTION: This is primarily for development and debugging purposes.
"""
import importlib
for module in (
"avalon",
"avalon.lib",
"avalon.pipeline",
"pyblish",
"pyblish_lite",
"{}.api".format(AVALON_CONFIG),
"{}.templates".format(AVALON_CONFIG),
"{}.hosts.hiero.lib".format(AVALON_CONFIG),
"{}.hosts.hiero.menu".format(AVALON_CONFIG),
"{}.hosts.hiero.tags".format(AVALON_CONFIG)
):
log.info("Reloading module: {}...".format(module))
try:
module = importlib.import_module(module)
reload(module)
except Exception as e:
log.warning("Cannot reload module: {}".format(e))
importlib.reload(module)
def setup(console=False, port=None, menu=True):
"""Setup integration
Registers Pyblish for Hiero plug-ins and appends an item to the File-menu
Arguments:
console (bool): Display console with GUI
port (int, optional): Port from which to start looking for an
available port to connect with Pyblish QML, default
provided by Pyblish Integration.
menu (bool, optional): Display file menu in Hiero.
"""
if self._has_been_setup:
teardown()
add_submission()
if menu:
add_to_filemenu()
self._has_menu = True
self._has_been_setup = True
print("pyblish: Loaded successfully.")
def show():
"""Try showing the most desirable GUI
This function cycles through the currently registered
graphical user interfaces, if any, and presents it to
the user.
"""
return (_discover_gui() or _show_no_gui)()
def _discover_gui():
"""Return the most desirable of the currently registered GUIs"""
# Prefer last registered
guis = reversed(pyblish.api.registered_guis())
for gui in list(guis) + ["pyblish_lite"]:
try:
gui = __import__(gui).show
except (ImportError, AttributeError):
continue
else:
return gui
def teardown():
"""Remove integration"""
if not self._has_been_setup:
return
if self._has_menu:
remove_from_filemenu()
self._has_menu = False
self._has_been_setup = False
print("pyblish: Integration torn down successfully")
def remove_from_filemenu():
raise NotImplementedError("Implement me please.")
def add_to_filemenu():
PublishAction()
class PyblishSubmission(hiero.exporters.FnSubmission.Submission):
def __init__(self):
hiero.exporters.FnSubmission.Submission.__init__(self)
def addToQueue(self):
# Add submission to Hiero module for retrieval in plugins.
hiero.submission = self
show()
def add_submission():
registry = hiero.core.taskRegistry
registry.addSubmission("Pyblish", PyblishSubmission)
class PublishAction(QtWidgets.QAction):
"""
Action with is showing as menu item
"""
def __init__(self):
QtWidgets.QAction.__init__(self, "Publish", None)
self.triggered.connect(self.publish)
for interest in ["kShowContextMenu/kTimeline",
"kShowContextMenukBin",
"kShowContextMenu/kSpreadsheet"]:
hiero.core.events.registerInterest(interest, self.eventHandler)
self.setShortcut("Ctrl+Alt+P")
def publish(self):
# Removing "submission" attribute from hiero module, to prevent tasks
# from getting picked up when not using the "Export" dialog.
if hasattr(hiero, "submission"):
del hiero.submission
show()
def eventHandler(self, event):
# Add the Menu to the right-click menu
event.menu.addAction(self)
def _show_no_gui():
"""
Popup with information about how to register a new GUI
In the event of no GUI being registered or available,
this information dialog will appear to guide the user
through how to get set up with one.
"""
messagebox = QtWidgets.QMessageBox()
messagebox.setIcon(messagebox.Warning)
messagebox.setWindowIcon(QtGui.QIcon(os.path.join(
os.path.dirname(pyblish.__file__),
"icons",
"logo-32x32.svg"))
)
spacer = QtWidgets.QWidget()
spacer.setMinimumSize(400, 0)
spacer.setSizePolicy(QtWidgets.QSizePolicy.Minimum,
QtWidgets.QSizePolicy.Expanding)
layout = messagebox.layout()
layout.addWidget(spacer, layout.rowCount(), 0, 1, layout.columnCount())
messagebox.setWindowTitle("Uh oh")
messagebox.setText("No registered GUI found.")
if not pyblish.api.registered_guis():
messagebox.setInformativeText(
"In order to show you a GUI, one must first be registered. "
"Press \"Show details...\" below for information on how to "
"do that.")
messagebox.setDetailedText(
"Pyblish supports one or more graphical user interfaces "
"to be registered at once, the next acting as a fallback to "
"the previous."
"\n"
"\n"
"For example, to use Pyblish Lite, first install it:"
"\n"
"\n"
"$ pip install pyblish-lite"
"\n"
"\n"
"Then register it, like so:"
"\n"
"\n"
">>> import pyblish.api\n"
">>> pyblish.api.register_gui(\"pyblish_lite\")"
"\n"
"\n"
"The next time you try running this, Lite will appear."
"\n"
"See http://api.pyblish.com/register_gui.html for "
"more information.")
else:
messagebox.setInformativeText(
"None of the registered graphical user interfaces "
"could be found."
"\n"
"\n"
"Press \"Show details\" for more information.")
messagebox.setDetailedText(
"These interfaces are currently registered."
"\n"
"%s" % "\n".join(pyblish.api.registered_guis()))
messagebox.setStandardButtons(messagebox.Ok)
messagebox.exec_()
def CreateNukeWorkfile(nodes=None,
nodes_effects=None,
to_timeline=False,
**kwargs):
''' Creating nuke workfile with particular version with given nodes
Also it is creating timeline track items as precomps.
Arguments:
nodes(list of dict): each key in dict is knob order is important
to_timeline(type): will build trackItem with metadata
Returns:
bool: True if done
Raises:
Exception: with traceback
'''
import hiero.core
from avalon.nuke import imprint
from pype.hosts.nuke import (
lib as nklib
)
# check if the file exists if does then Raise "File exists!"
if os.path.exists(filepath):
raise FileExistsError("File already exists: `{}`".format(filepath))
# if no representations matching then
# Raise "no representations to be build"
if len(representations) == 0:
raise AttributeError("Missing list of `representations`")
# check nodes input
if len(nodes) == 0:
log.warning("Missing list of `nodes`")
# create temp nk file
nuke_script = hiero.core.nuke.ScriptWriter()
# create root node and save all metadata
root_node = hiero.core.nuke.RootNode()
anatomy = Anatomy(os.environ["AVALON_PROJECT"])
work_template = anatomy.templates["work"]["path"]
root_path = anatomy.root_value_for_template(work_template)
nuke_script.addNode(root_node)
# here to call pype.hosts.nuke.lib.BuildWorkfile
script_builder = nklib.BuildWorkfile(
root_node=root_node,
root_path=root_path,
nodes=nuke_script.getNodes(),
**kwargs
)
class ClipLoader:
active_bin = None
def __init__(self, plugin_cls, context, sequence=None, track=None, **kwargs):
""" Initialize object
Arguments:
plugin_cls (api.Loader): plugin object
context (dict): loader plugin context
sequnce (hiero.core.Sequence): sequence object
track (hiero.core.Track): track object
kwargs (dict)[optional]: possible keys:
projectBinPath: "path/to/binItem"
hieroWorkfileName: "name_of_hiero_project_file_no_extension"
"""
self.cls = plugin_cls
self.context = context
self.kwargs = kwargs
self.active_project = self._get_active_project()
self.project_bin = self.active_project.clipsBin()
self.data = dict()
assert self._set_data(), str("Cannot Load selected data, look into "
"database or call your supervisor")
# inject asset data to representation dict
self._get_asset_data()
log.debug("__init__ self.data: `{}`".format(self.data))
# add active components to class
self.active_sequence = self._get_active_sequence(sequence)
self.active_track = self._get_active_track(track)
def _set_data(self):
""" Gets context and convert it to self.data
data structure:
{
"name": "assetName_subsetName_representationName"
"path": "path/to/file/created/by/get_repr..",
"binPath": "projectBinPath",
}
"""
# create name
repr = self.context["representation"]
repr_cntx = repr["context"]
asset = str(repr_cntx["asset"])
subset = str(repr_cntx["subset"])
representation = str(repr_cntx["representation"])
self.data["clip_name"] = "_".join([asset, subset, representation])
self.data["track_name"] = "_".join([subset, representation])
# gets file path
file = self.cls.fname
if not file:
repr_id = repr["_id"]
log.warning(
"Representation id `{}` is failing to load".format(repr_id))
return None
self.data["path"] = file.replace("\\", "/")
# convert to hashed path
if repr_cntx.get("frame"):
self._fix_path_hashes()
# solve project bin structure path
hierarchy = str("/".join((
"Loader",
repr_cntx["hierarchy"].replace("\\", "/"),
asset
)))
self.data["binPath"] = self.kwargs.get(
"projectBinPath",
hierarchy
)
return True
def _fix_path_hashes(self):
""" Convert file path where it is needed padding with hashes
"""
file = self.data["path"]
if "#" not in file:
frame = self.context["representation"]["context"].get("frame")
padding = len(frame)
file = file.replace(frame, "#"*padding)
self.data["path"] = file
def _get_active_project(self):
""" Get hiero active project object
"""
fname = self.kwargs.get("hieroWorkfileName", "")
return next((p for p in hiero.core.projects()
if fname in p.name()),
hiero.core.projects()[-1])
def _get_asset_data(self):
""" Get all available asset data
joint `data` key with asset.data dict into the representaion
"""
asset_name = self.context["representation"]["context"]["asset"]
self.data["assetData"] = pype.get_asset(asset_name)["data"]
def _make_project_bin(self, hierarchy):
""" Creare bins by given hierarchy path
It will also make sure no duplicit bins will be created
Arguments:
hierarchy (str): path devided by slashes "bin0/bin1/bin2"
Returns:
bin (hiero.core.BinItem): with the bin to be used for mediaItem
"""
if self.active_bin:
return self.active_bin
assert hierarchy != "", "Please add hierarchy!"
log.debug("__ hierarchy1: `{}`".format(hierarchy))
if '/' in hierarchy:
hierarchy = hierarchy.split('/')
else:
hierarchy = [hierarchy]
parent_bin = None
for i, name in enumerate(hierarchy):
# if first index and list is more then one long
if i == 0:
bin = next((bin for bin in self.project_bin.bins()
if name in bin.name()), None)
if not bin:
bin = hiero.core.Bin(name)
self.project_bin.addItem(bin)
log.debug("__ bin.name: `{}`".format(bin.name()))
parent_bin = bin
# if second to prelast
elif (i >= 1) and (i <= (len(hierarchy) - 1)):
bin = next((bin for bin in parent_bin.bins()
if name in bin.name()), None)
if not bin:
bin = hiero.core.Bin(name)
parent_bin.addItem(bin)
parent_bin = bin
return parent_bin
def _make_track_item(self):
""" Create track item with """
pass
def _set_clip_color(self, last_version=True):
""" Sets color of clip on clip/track item
Arguments:
last_version (bool): True = green | False = red
"""
pass
def _set_container_tag(self, item, metadata):
""" Sets container tag to given clip/track item
Arguments:
item (hiero.core.BinItem or hiero.core.TrackItem)
metadata (dict): data to be added to tag
"""
pass
def _get_active_sequence(self, sequence):
if not sequence:
return hiero.ui.activeSequence()
else:
return sequence
def _get_active_track(self, track):
if not track:
track_name = self.data["track_name"]
else:
track_name = track.name()
track_pass = next(
(t for t in self.active_sequence.videoTracks()
if t.name() in track_name), None
)
if not track_pass:
track_pass = hiero.core.VideoTrack(track_name)
self.active_sequence.addTrack(track_pass)
return track_pass
def load(self):
log.debug("__ active_project: `{}`".format(self.active_project))
log.debug("__ active_sequence: `{}`".format(self.active_sequence))
# create project bin for the media to be imported into
self.active_bin = self._make_project_bin(self.data["binPath"])
log.debug("__ active_bin: `{}`".format(self.active_bin))
log.debug("__ version.data: `{}`".format(
self.context["version"]["data"]))
# create mediaItem in active project bin
# create clip media
media = hiero.core.MediaSource(self.data["path"])
media_duration = int(media.duration())
handle_start = int(self.data["assetData"]["handleStart"])
handle_end = int(self.data["assetData"]["handleEnd"])
clip_in = int(self.data["assetData"]["clipIn"])
clip_out = int(self.data["assetData"]["clipOut"])
log.debug("__ media_duration: `{}`".format(media_duration))
log.debug("__ handle_start: `{}`".format(handle_start))
log.debug("__ handle_end: `{}`".format(handle_end))
log.debug("__ clip_in: `{}`".format(clip_in))
log.debug("__ clip_out: `{}`".format(clip_out))
# check if slate is included
# either in version data families or by calculating frame diff
slate_on = next(
(f for f in self.context["version"]["data"]["families"]
if "slate" in f),
None) or bool(((
clip_out - clip_in + 1) + handle_start + handle_end
) - media_duration)
log.debug("__ slate_on: `{}`".format(slate_on))
# calculate slate differences
if slate_on:
media_duration -= 1
handle_start += 1
fps = self.data["assetData"]["fps"]
# create Clip from Media
_clip = hiero.core.Clip(media)
_clip.setName(self.data["clip_name"])
# add Clip to bin if not there yet
if self.data["clip_name"] not in [
b.name()
for b in self.active_bin.items()]:
binItem = hiero.core.BinItem(_clip)
self.active_bin.addItem(binItem)
_source = next((item for item in self.active_bin.items()
if self.data["clip_name"] in item.name()), None)
if not _source:
log.warning("Problem with created Source clip: `{}`".format(
self.data["clip_name"]))
version = next((s for s in _source.items()), None)
clip = version.item()
# add to track as clip item
track_item = hiero.core.TrackItem(
self.data["clip_name"], hiero.core.TrackItem.kVideo)
track_item.setSource(clip)
track_item.setSourceIn(handle_start)
track_item.setTimelineIn(clip_in)
track_item.setSourceOut(media_duration - handle_end)
track_item.setTimelineOut(clip_out)
track_item.setPlaybackSpeed(1)
self.active_track.addTrackItem(track_item)
log.info("Loading clips: `{}`".format(self.data["clip_name"]))
def create_nk_workfile_clips(nk_workfiles, seq=None):
'''
nk_workfile is list of dictionaries like:
[{
'path': 'P:/Jakub_testy_pipeline/test_v01.nk',
'name': 'test',
'handleStart': 15, # added asymetrically to handles
'handleEnd': 10, # added asymetrically to handles
"clipIn": 16,
"frameStart": 991,
"frameEnd": 1023,
'task': 'Comp-tracking',
'work_dir': 'VFX_PR',
'shot': '00010'
}]
'''
proj = hiero.core.projects()[-1]
root = proj.clipsBin()
if not seq:
seq = hiero.core.Sequence('NewSequences')
root.addItem(hiero.core.BinItem(seq))
# todo will ned to define this better
# track = seq[1] # lazy example to get a destination# track
clips_lst = []
for nk in nk_workfiles:
task_path = '/'.join([nk['work_dir'], nk['shot'], nk['task']])
bin = create_bin_in_project(task_path, proj)
if nk['task'] not in seq.videoTracks():
track = hiero.core.VideoTrack(nk['task'])
seq.addTrack(track)
else:
track = seq.tracks(nk['task'])
# create clip media
media = hiero.core.MediaSource(nk['path'])
media_in = int(media.startTime() or 0)
media_duration = int(media.duration() or 0)
handle_start = nk.get("handleStart")
handle_end = nk.get("handleEnd")
if media_in:
source_in = media_in + handle_start
else:
source_in = nk["frameStart"] + handle_start
if media_duration:
source_out = (media_in + media_duration - 1) - handle_end
else:
source_out = nk["frameEnd"] - handle_end
source = hiero.core.Clip(media)
name = os.path.basename(os.path.splitext(nk['path'])[0])
split_name = split_by_client_version(name)[0] or name
# add to bin as clip item
items_in_bin = [b.name() for b in bin.items()]
if split_name not in items_in_bin:
binItem = hiero.core.BinItem(source)
bin.addItem(binItem)
new_source = [
item for item in bin.items() if split_name in item.name()
][0].items()[0].item()
# add to track as clip item
trackItem = hiero.core.TrackItem(
split_name, hiero.core.TrackItem.kVideo)
trackItem.setSource(new_source)
trackItem.setSourceIn(source_in)
trackItem.setSourceOut(source_out)
trackItem.setTimelineIn(nk["clipIn"])
trackItem.setTimelineOut(nk["clipIn"] + (source_out - source_in))
track.addTrackItem(trackItem)
clips_lst.append(trackItem)
return clips_lst
def create_bin_in_project(bin_name='', project=''):
'''
create bin in project and
if the bin_name is "bin1/bin2/bin3" it will create whole depth
'''
if not project:
# get the first loaded project
project = hiero.core.projects()[-1]
if not bin_name:
return None
if '/' in bin_name:
bin_name = bin_name.split('/')
else:
bin_name = [bin_name]
clipsBin = project.clipsBin()
done_bin_lst = []
for i, b in enumerate(bin_name):
if i == 0 and len(bin_name) > 1:
if b in [bin.name() for bin in clipsBin.bins()]:
bin = [bin for bin in clipsBin.bins() if b in bin.name()][0]
done_bin_lst.append(bin)
else:
create_bin = hiero.core.Bin(b)
clipsBin.addItem(create_bin)
done_bin_lst.append(create_bin)
elif i >= 1 and i < len(bin_name) - 1:
if b in [bin.name() for bin in done_bin_lst[i - 1].bins()]:
bin = [
bin for bin in done_bin_lst[i - 1].bins()
if b in bin.name()
][0]
done_bin_lst.append(bin)
else:
create_bin = hiero.core.Bin(b)
done_bin_lst[i - 1].addItem(create_bin)
done_bin_lst.append(create_bin)
elif i == len(bin_name) - 1:
if b in [bin.name() for bin in done_bin_lst[i - 1].bins()]:
bin = [
bin for bin in done_bin_lst[i - 1].bins()
if b in bin.name()
][0]
done_bin_lst.append(bin)
else:
create_bin = hiero.core.Bin(b)
done_bin_lst[i - 1].addItem(create_bin)
done_bin_lst.append(create_bin)
# print [bin.name() for bin in clipsBin.bins()]
return done_bin_lst[-1]
def split_by_client_version(string):
regex = r"[/_.]v\d+"
try:
matches = re.findall(regex, string, re.IGNORECASE)
return string.split(matches[0])
except Exception as e:
print(e)
return None
# nk_workfiles = [{
# 'path': 'C:/Users/hubert/_PYPE_testing/projects/D001_projectx/episodes/ep120/ep120sq01/120sh020/publish/plates/platesMain/v023/prjx_120sh020_platesMain_v023.nk',
# 'name': '120sh020_platesMain',
# 'handles': 10,
# 'handleStart': 10,
# 'handleEnd': 10,
# "clipIn": 16,
# "frameStart": 991,
# "frameEnd": 1023,
# 'task': 'platesMain',
# 'work_dir': 'shots',
# 'shot': '120sh020'
# }]

View file

@ -0,0 +1,257 @@
import pype.hosts.hiero.api as phiero
# from pype.hosts.hiero.api import plugin, lib
# reload(lib)
# reload(plugin)
# reload(phiero)
class CreateShotClip(phiero.Creator):
"""Publishable clip"""
label = "Create Publishable Clip"
family = "clip"
icon = "film"
defaults = ["Main"]
gui_tracks = [track.name()
for track in phiero.get_current_sequence().videoTracks()]
gui_name = "Pype publish attributes creator"
gui_info = "Define sequential rename and fill hierarchy data."
gui_inputs = {
"renameHierarchy": {
"type": "section",
"label": "Shot Hierarchy And Rename Settings",
"target": "ui",
"order": 0,
"value": {
"hierarchy": {
"value": "{folder}/{sequence}",
"type": "QLineEdit",
"label": "Shot Parent Hierarchy",
"target": "tag",
"toolTip": "Parents folder for shot root folder, Template filled with `Hierarchy Data` section", # noqa
"order": 0},
"clipRename": {
"value": False,
"type": "QCheckBox",
"label": "Rename clips",
"target": "ui",
"toolTip": "Renaming selected clips on fly", # noqa
"order": 1},
"clipName": {
"value": "{sequence}{shot}",
"type": "QLineEdit",
"label": "Clip Name Template",
"target": "ui",
"toolTip": "template for creating shot namespaused for renaming (use rename: on)", # noqa
"order": 2},
"countFrom": {
"value": 10,
"type": "QSpinBox",
"label": "Count sequence from",
"target": "ui",
"toolTip": "Set when the sequence number stafrom", # noqa
"order": 3},
"countSteps": {
"value": 10,
"type": "QSpinBox",
"label": "Stepping number",
"target": "ui",
"toolTip": "What number is adding every new step", # noqa
"order": 4},
}
},
"hierarchyData": {
"type": "dict",
"label": "Shot Template Keywords",
"target": "tag",
"order": 1,
"value": {
"folder": {
"value": "shots",
"type": "QLineEdit",
"label": "{folder}",
"target": "tag",
"toolTip": "Name of folder used for root of generated shots.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa
"order": 0},
"episode": {
"value": "ep01",
"type": "QLineEdit",
"label": "{episode}",
"target": "tag",
"toolTip": "Name of episode.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa
"order": 1},
"sequence": {
"value": "sq01",
"type": "QLineEdit",
"label": "{sequence}",
"target": "tag",
"toolTip": "Name of sequence of shots.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa
"order": 2},
"track": {
"value": "{_track_}",
"type": "QLineEdit",
"label": "{track}",
"target": "tag",
"toolTip": "Name of sequence of shots.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa
"order": 3},
"shot": {
"value": "sh###",
"type": "QLineEdit",
"label": "{shot}",
"target": "tag",
"toolTip": "Name of shot. `#` is converted to paded number. \nAlso could be used with usable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa
"order": 4}
}
},
"verticalSync": {
"type": "section",
"label": "Vertical Synchronization Of Attributes",
"target": "ui",
"order": 2,
"value": {
"vSyncOn": {
"value": True,
"type": "QCheckBox",
"label": "Enable Vertical Sync",
"target": "ui",
"toolTip": "Switch on if you want clips above each other to share its attributes", # noqa
"order": 0},
"vSyncTrack": {
"value": gui_tracks, # noqa
"type": "QComboBox",
"label": "Master track",
"target": "ui",
"toolTip": "Select driving track name which should be mastering all others", # noqa
"order": 1}
}
},
"publishSettings": {
"type": "section",
"label": "Publish Settings",
"target": "ui",
"order": 3,
"value": {
"subsetName": {
"value": ["<track_name>", "main", "bg", "fg", "bg",
"animatic"],
"type": "QComboBox",
"label": "Subset Name",
"target": "ui",
"toolTip": "chose subset name patern, if <track_name> is selected, name of track layer will be used", # noqa
"order": 0},
"subsetFamily": {
"value": ["plate", "take"],
"type": "QComboBox",
"label": "Subset Family",
"target": "ui", "toolTip": "What use of this subset is for", # noqa
"order": 1},
"reviewTrack": {
"value": ["< none >"] + gui_tracks,
"type": "QComboBox",
"label": "Use Review Track",
"target": "ui",
"toolTip": "Generate preview videos on fly, if `< none >` is defined nothing will be generated.", # noqa
"order": 2},
"audio": {
"value": False,
"type": "QCheckBox",
"label": "Include audio",
"target": "tag",
"toolTip": "Process subsets with corresponding audio", # noqa
"order": 3},
"sourceResolution": {
"value": False,
"type": "QCheckBox",
"label": "Source resolution",
"target": "tag",
"toolTip": "Is resloution taken from timeline or source?", # noqa
"order": 4},
}
},
"frameRangeAttr": {
"type": "section",
"label": "Shot Attributes",
"target": "ui",
"order": 4,
"value": {
"workfileFrameStart": {
"value": 1001,
"type": "QSpinBox",
"label": "Workfiles Start Frame",
"target": "tag",
"toolTip": "Set workfile starting frame number", # noqa
"order": 0
},
"handleStart": {
"value": 0,
"type": "QSpinBox",
"label": "Handle Start",
"target": "tag",
"toolTip": "Handle at start of clip", # noqa
"order": 1
},
"handleEnd": {
"value": 0,
"type": "QSpinBox",
"label": "Handle End",
"target": "tag",
"toolTip": "Handle at end of clip", # noqa
"order": 2
}
}
}
}
presets = None
def process(self):
# get key pares from presets and match it on ui inputs
for k, v in self.gui_inputs.items():
if v["type"] in ("dict", "section"):
# nested dictionary (only one level allowed
# for sections and dict)
for _k, _v in v["value"].items():
if self.presets.get(_k):
self.gui_inputs[k][
"value"][_k]["value"] = self.presets[_k]
if self.presets.get(k):
self.gui_inputs[k]["value"] = self.presets[k]
# open widget for plugins inputs
widget = self.widget(self.gui_name, self.gui_info, self.gui_inputs)
widget.exec_()
if len(self.selected) < 1:
return
if not widget.result:
print("Operation aborted")
return
self.rename_add = 0
# get ui output for track name for vertical sync
v_sync_track = widget.result["vSyncTrack"]["value"]
# sort selected trackItems by
sorted_selected_track_items = list()
unsorted_selected_track_items = list()
for _ti in self.selected:
if _ti.parent().name() in v_sync_track:
sorted_selected_track_items.append(_ti)
else:
unsorted_selected_track_items.append(_ti)
sorted_selected_track_items.extend(unsorted_selected_track_items)
kwargs = {
"ui_inputs": widget.result,
"avalon": self.data
}
for i, track_item in enumerate(sorted_selected_track_items):
self.rename_index = i
# convert track item to timeline media pool item
phiero.PublishClip(self, track_item, **kwargs).convert()

View file

@ -0,0 +1,170 @@
from avalon import io, api
import pype.hosts.hiero.api as phiero
# from pype.hosts.hiero.api import plugin, lib
# reload(lib)
# reload(plugin)
# reload(phiero)
class LoadClip(phiero.SequenceLoader):
"""Load a subset to timeline as clip
Place clip to timeline on its asset origin timings collected
during conforming to project
"""
families = ["render2d", "source", "plate", "render", "review"]
representations = ["exr", "dpx", "jpg", "jpeg", "png", "h264"]
label = "Load as clip"
order = -10
icon = "code-fork"
color = "orange"
# for loader multiselection
sequence = None
track = None
# presets
clip_color_last = "green"
clip_color = "red"
def load(self, context, name, namespace, options):
# in case loader uses multiselection
if self.track and self.sequence:
options.update({
"sequence": self.sequence,
"track": self.track
})
# load clip to timeline and get main variables
track_item = phiero.ClipLoader(self, context, **options).load()
namespace = namespace or track_item.name()
version = context['version']
version_data = version.get("data", {})
version_name = version.get("name", None)
colorspace = version_data.get("colorspace", None)
object_name = "{}_{}".format(name, namespace)
# add additional metadata from the version to imprint Avalon knob
add_keys = [
"frameStart", "frameEnd", "source", "author",
"fps", "handleStart", "handleEnd"
]
# move all version data keys to tag data
data_imprint = {}
for key in add_keys:
data_imprint.update({
key: version_data.get(key, str(None))
})
# add variables related to version context
data_imprint.update({
"version": version_name,
"colorspace": colorspace,
"objectName": object_name
})
# update color of clip regarding the version order
self.set_item_color(track_item, version)
# deal with multiselection
self.multiselection(track_item)
self.log.info("Loader done: `{}`".format(name))
return phiero.containerise(
track_item,
name, namespace, context,
self.__class__.__name__,
data_imprint)
def switch(self, container, representation):
self.update(container, representation)
def update(self, container, representation):
""" Updating previously loaded clips
"""
# load clip to timeline and get main variables
name = container['name']
namespace = container['namespace']
track_item = phiero.get_track_items(
track_item_name=namespace)
version = io.find_one({
"type": "version",
"_id": representation["parent"]
})
version_data = version.get("data", {})
version_name = version.get("name", None)
colorspace = version_data.get("colorspace", None)
object_name = "{}_{}".format(name, namespace)
file = api.get_representation_path(representation).replace("\\", "/")
# reconnect media to new path
track_item.source().reconnectMedia(file)
# add additional metadata from the version to imprint Avalon knob
add_keys = [
"frameStart", "frameEnd", "source", "author",
"fps", "handleStart", "handleEnd"
]
# move all version data keys to tag data
data_imprint = {}
for key in add_keys:
data_imprint.update({
key: version_data.get(key, str(None))
})
# add variables related to version context
data_imprint.update({
"representation": str(representation["_id"]),
"version": version_name,
"colorspace": colorspace,
"objectName": object_name
})
# update color of clip regarding the version order
self.set_item_color(track_item, version)
return phiero.update_container(track_item, data_imprint)
def remove(self, container):
""" Removing previously loaded clips
"""
# load clip to timeline and get main variables
namespace = container['namespace']
track_item = phiero.get_track_items(
track_item_name=namespace)
track = track_item.parent()
# remove track item from track
track.removeItem(track_item)
@classmethod
def multiselection(cls, track_item):
if not cls.track:
cls.track = track_item.parent()
cls.sequence = cls.track.parent()
@classmethod
def set_item_color(cls, track_item, version):
# define version name
version_name = version.get("name", None)
# get all versions in list
versions = io.find({
"type": "version",
"parent": version["parent"]
}).distinct('name')
max_version = max(versions)
# set clip colour
if version_name == max_version:
track_item.source().binItem().setColor(cls.clip_color_last)
else:
track_item.source().binItem().setColor(cls.clip_color)

View file

@ -30,9 +30,12 @@ class CollectAssetBuilds(api.ContextPlugin):
# Exclude non-tagged instances.
tagged = False
asset_names = []
for tag in instance.data["tags"]:
family = dict(tag["metadata"]).get("tag.family", "")
if family.lower() == "assetbuild":
t_metadata = dict(tag.metadata())
t_family = t_metadata.get("tag.family", "")
if t_family.lower() == "assetbuild":
asset_names.append(tag["name"])
tagged = True

View file

@ -0,0 +1,38 @@
import pyblish.api
class CollectClipResolution(pyblish.api.InstancePlugin):
"""Collect clip geometry resolution"""
order = pyblish.api.CollectorOrder - 0.1
label = "Collect Clip Resoluton"
hosts = ["hiero"]
families = ["clip"]
def process(self, instance):
sequence = instance.context.data['activeSequence']
item = instance.data["item"]
source_resolution = instance.data.get("sourceResolution", None)
resolution_width = int(sequence.format().width())
resolution_height = int(sequence.format().height())
pixel_aspect = sequence.format().pixelAspect()
# source exception
if source_resolution:
resolution_width = int(item.source().mediaSource().width())
resolution_height = int(item.source().mediaSource().height())
pixel_aspect = item.source().mediaSource().pixelAspect()
resolution_data = {
"resolutionWidth": resolution_width,
"resolutionHeight": resolution_height,
"pixelAspect": pixel_aspect
}
# add to instacne data
instance.data.update(resolution_data)
self.log.info("Resolution of instance '{}' is: {}".format(
instance,
resolution_data
))

View file

@ -0,0 +1,70 @@
import pyblish.api
class CollectFrameRanges(pyblish.api.InstancePlugin):
""" Collect all framranges.
"""
order = pyblish.api.CollectorOrder
label = "Collect Frame Ranges"
hosts = ["hiero"]
families = ["clip", "effect"]
def process(self, instance):
data = dict()
track_item = instance.data["item"]
# handles
handle_start = instance.data["handleStart"]
handle_end = instance.data["handleEnd"]
# source frame ranges
source_in = int(track_item.sourceIn())
source_out = int(track_item.sourceOut())
source_in_h = int(source_in - handle_start)
source_out_h = int(source_out + handle_end)
# timeline frame ranges
clip_in = int(track_item.timelineIn())
clip_out = int(track_item.timelineOut())
clip_in_h = clip_in - handle_start
clip_out_h = clip_out + handle_end
# durations
clip_duration = (clip_out - clip_in) + 1
clip_duration_h = clip_duration + (handle_start + handle_end)
# set frame start with tag or take it from timeline `startingFrame`
frame_start = instance.data.get("workfileFrameStart")
if not frame_start:
frame_start = clip_in
frame_end = frame_start + (clip_out - clip_in)
data.update({
# media source frame range
"sourceIn": source_in,
"sourceOut": source_out,
"sourceInH": source_in_h,
"sourceOutH": source_out_h,
# timeline frame range
"clipIn": clip_in,
"clipOut": clip_out,
"clipInH": clip_in_h,
"clipOutH": clip_out_h,
# workfile frame range
"frameStart": frame_start,
"frameEnd": frame_end,
"clipDuration": clip_duration,
"clipDurationH": clip_duration_h,
"fps": instance.context.data["fps"]
})
self.log.info("Frame range data for instance `{}` are: {}".format(
instance, data))
instance.data.update(data)

View file

@ -0,0 +1,116 @@
import pyblish.api
import avalon.api as avalon
class CollectHierarchy(pyblish.api.ContextPlugin):
"""Collecting hierarchy from `parents`.
present in `clip` family instances coming from the request json data file
It will add `hierarchical_context` into each instance for integrate
plugins to be able to create needed parents for the context if they
don't exist yet
"""
label = "Collect Hierarchy"
order = pyblish.api.CollectorOrder
families = ["clip"]
def process(self, context):
temp_context = {}
project_name = avalon.Session["AVALON_PROJECT"]
final_context = {}
final_context[project_name] = {}
final_context[project_name]['entity_type'] = 'Project'
for instance in context:
self.log.info("Processing instance: `{}` ...".format(instance))
# shot data dict
shot_data = {}
families = instance.data.get("families")
# filter out all unepropriate instances
if not instance.data["publish"]:
continue
if not families:
continue
# exclude other families then self.families with intersection
if not set(self.families).intersection(families):
continue
# exclude if not masterLayer True
if not instance.data.get("masterLayer"):
continue
# update families to include `shot` for hierarchy integration
instance.data["families"] = families + ["shot"]
# get asset build data if any available
shot_data["inputs"] = [
x["_id"] for x in instance.data.get("assetbuilds", [])
]
# suppose that all instances are Shots
shot_data['entity_type'] = 'Shot'
shot_data['tasks'] = instance.data.get("tasks") or []
shot_data["comments"] = instance.data.get("comments", [])
shot_data['custom_attributes'] = {
"handleStart": instance.data["handleStart"],
"handleEnd": instance.data["handleEnd"],
"frameStart": instance.data["frameStart"],
"frameEnd": instance.data["frameEnd"],
"clipIn": instance.data["clipIn"],
"clipOut": instance.data["clipOut"],
'fps': instance.context.data["fps"],
"resolutionWidth": instance.data["resolutionWidth"],
"resolutionHeight": instance.data["resolutionHeight"],
"pixelAspect": instance.data["pixelAspect"]
}
actual = {instance.data["asset"]: shot_data}
for parent in reversed(instance.data["parents"]):
next_dict = {}
parent_name = parent["entity_name"]
next_dict[parent_name] = {}
next_dict[parent_name]["entity_type"] = parent[
"entity_type"].capitalize()
next_dict[parent_name]["childs"] = actual
actual = next_dict
temp_context = self._update_dict(temp_context, actual)
# skip if nothing for hierarchy available
if not temp_context:
return
final_context[project_name]['childs'] = temp_context
# adding hierarchy context to context
context.data["hierarchyContext"] = final_context
self.log.debug("context.data[hierarchyContext] is: {}".format(
context.data["hierarchyContext"]))
def _update_dict(self, parent_dict, child_dict):
"""
Nesting each children into its parent.
Args:
parent_dict (dict): parent dict wich should be nested with children
child_dict (dict): children dict which should be injested
"""
for key in parent_dict:
if key in child_dict and isinstance(parent_dict[key], dict):
child_dict[key] = self._update_dict(
parent_dict[key], child_dict[key]
)
else:
if parent_dict.get(key) and child_dict.get(key):
continue
else:
child_dict[key] = parent_dict[key]
return child_dict

View file

@ -4,9 +4,12 @@ import pyblish.api
class CollectHostVersion(pyblish.api.ContextPlugin):
"""Inject the hosts version into context"""
order = pyblish.api.CollectorOrder
label = "Collect Host and HostVersion"
order = pyblish.api.CollectorOrder - 0.5
def process(self, context):
import nuke
import pyblish.api
context.set_data("host", pyblish.api.current_host())
context.set_data('hostVersion', value=nuke.NUKE_VERSION_STRING)

View file

@ -0,0 +1,169 @@
from pyblish import api
import os
import re
import clique
class CollectPlates(api.InstancePlugin):
"""Collect plate representations.
"""
# Run just before CollectSubsets
order = api.CollectorOrder + 0.1020
label = "Collect Plates"
hosts = ["hiero"]
families = ["plate"]
def process(self, instance):
# add to representations
if not instance.data.get("representations"):
instance.data["representations"] = list()
self.main_clip = instance.data["item"]
# get plate source attributes
source_media = instance.data["sourceMedia"]
source_path = instance.data["sourcePath"]
source_first = instance.data["sourceFirst"]
frame_start = instance.data["frameStart"]
frame_end = instance.data["frameEnd"]
handle_start = instance.data["handleStart"]
handle_end = instance.data["handleEnd"]
source_in = instance.data["sourceIn"]
source_out = instance.data["sourceOut"]
source_in_h = instance.data["sourceInH"]
source_out_h = instance.data["sourceOutH"]
# define if review media is sequence
is_sequence = bool(not source_media.singleFile())
self.log.debug("is_sequence: {}".format(is_sequence))
file_dir = os.path.dirname(source_path)
file = os.path.basename(source_path)
ext = os.path.splitext(file)[-1]
# detect if sequence
if not is_sequence:
# is video file
files = file
else:
files = list()
spliter, padding = self.detect_sequence(file)
self.log.debug("_ spliter, padding: {}, {}".format(
spliter, padding))
base_name = file.split(spliter)[0]
# define collection and calculate frame range
collection = clique.Collection(
base_name,
ext,
padding,
set(range(
int(source_first + source_in_h),
int(source_first + source_out_h) + 1
))
)
self.log.debug("_ collection: {}".format(collection))
real_files = os.listdir(file_dir)
self.log.debug("_ real_files: {}".format(real_files))
# collect frames to repre files list
self.handle_start_exclude = list()
self.handle_end_exclude = list()
for findex, item in enumerate(collection):
if item not in real_files:
self.log.debug("_ item: {}".format(item))
test_index = findex + int(source_first + source_in_h)
test_start = int(source_first + source_in)
test_end = int(source_first + source_out)
if (test_index < test_start):
self.handle_start_exclude.append(test_index)
elif (test_index > test_end):
self.handle_end_exclude.append(test_index)
continue
files.append(item)
# change label
instance.data["label"] = "{0} - ({1})".format(
instance.data["label"], ext
)
self.log.debug("Instance review: {}".format(instance.data["name"]))
# adding representation for review mov
representation = {
"files": files,
"stagingDir": file_dir,
"frameStart": frame_start - handle_start,
"frameEnd": frame_end + handle_end,
"name": ext[1:],
"ext": ext[1:]
}
instance.data["representations"].append(representation)
self.version_data(instance)
self.log.debug(
"Added representations: {}".format(
instance.data["representations"]))
self.log.debug(
"instance.data: {}".format(instance.data))
def version_data(self, instance):
transfer_data = [
"handleStart", "handleEnd", "sourceIn", "sourceOut",
"frameStart", "frameEnd", "sourceInH", "sourceOutH",
"clipIn", "clipOut", "clipInH", "clipOutH", "asset",
"track"
]
version_data = dict()
# pass data to version
version_data.update({k: instance.data[k] for k in transfer_data})
if 'version' in instance.data:
version_data["version"] = instance.data["version"]
handle_start = instance.data["handleStart"]
handle_end = instance.data["handleEnd"]
if self.handle_start_exclude:
handle_start -= len(self.handle_start_exclude)
if self.handle_end_exclude:
handle_end -= len(self.handle_end_exclude)
# add to data of representation
version_data.update({
"colorspace": self.main_clip.sourceMediaColourTransform(),
"families": instance.data["families"],
"subset": instance.data["subset"],
"fps": instance.data["fps"],
"handleStart": handle_start,
"handleEnd": handle_end
})
instance.data["versionData"] = version_data
def detect_sequence(self, file):
""" Get identificating pater for image sequence
Can find file.0001.ext, file.%02d.ext, file.####.ext
Return:
string: any matching sequence patern
int: padding of sequnce numbering
"""
foundall = re.findall(
r"(#+)|(%\d+d)|(?<=[^a-zA-Z0-9])(\d+)(?=\.\w+$)", file)
if foundall:
found = sorted(list(set(foundall[0])))[-1]
if "%" in found:
padding = int(re.findall(r"\d+", found)[-1])
else:
padding = len(found)
return found, padding
else:
return None, None

View file

@ -0,0 +1,261 @@
from pyblish import api
import os
import clique
from pype.hosts.hiero.api import (
is_overlapping, get_sequence_pattern_and_padding)
class CollectReview(api.InstancePlugin):
"""Collect review representation.
"""
# Run just before CollectSubsets
order = api.CollectorOrder + 0.1022
label = "Collect Review"
hosts = ["hiero"]
families = ["review"]
def get_review_item(self, instance):
"""
Get review clip track item from review track name
Args:
instance (obj): publishing instance
Returns:
hiero.core.TrackItem: corresponding track item
Raises:
Exception: description
"""
review_track = instance.data.get("review")
video_tracks = instance.context.data["videoTracks"]
for track in video_tracks:
if review_track not in track.name():
continue
for item in track.items():
self.log.debug(item)
if is_overlapping(item, self.main_clip):
self.log.debug("Winner is: {}".format(item))
break
# validate the clip is fully converted with review clip
assert is_overlapping(
item, self.main_clip, strict=True), (
"Review clip not cowering fully "
"the clip `{}`").format(self.main_clip.name())
return item
def process(self, instance):
tags = ["review", "ftrackreview"]
# get reviewable item from `review` instance.data attribute
self.main_clip = instance.data.get("item")
self.rw_clip = self.get_review_item(instance)
# let user know there is missing review clip and convert instance
# back as not reviewable
assert self.rw_clip, "Missing reviewable clip for '{}'".format(
self.main_clip.name()
)
# add to representations
if not instance.data.get("representations"):
instance.data["representations"] = list()
# get review media main info
rw_source = self.rw_clip.source().mediaSource()
rw_source_duration = int(rw_source.duration())
self.rw_source_path = rw_source.firstpath()
rw_source_file_info = rw_source.fileinfos().pop()
# define if review media is sequence
is_sequence = bool(not rw_source.singleFile())
self.log.debug("is_sequence: {}".format(is_sequence))
# get handles
handle_start = instance.data["handleStart"]
handle_end = instance.data["handleEnd"]
# review timeline and source frame ranges
rw_clip_in = int(self.rw_clip.timelineIn())
rw_clip_out = int(self.rw_clip.timelineOut())
self.rw_clip_source_in = int(self.rw_clip.sourceIn())
self.rw_clip_source_out = int(self.rw_clip.sourceOut())
rw_source_first = int(rw_source_file_info.startFrame())
# calculate delivery source_in and source_out
# main_clip_timeline_in - review_item_timeline_in + 1
main_clip_in = self.main_clip.timelineIn()
main_clip_out = self.main_clip.timelineOut()
source_in_diff = main_clip_in - rw_clip_in
source_out_diff = main_clip_out - rw_clip_out
if source_in_diff:
self.rw_clip_source_in += source_in_diff
if source_out_diff:
self.rw_clip_source_out += source_out_diff
# review clip durations
rw_clip_duration = (
self.rw_clip_source_out - self.rw_clip_source_in) + 1
rw_clip_duration_h = rw_clip_duration + (
handle_start + handle_end)
# add created data to review item data
instance.data["reviewItemData"] = {
"mediaDuration": rw_source_duration
}
file_dir = os.path.dirname(self.rw_source_path)
file = os.path.basename(self.rw_source_path)
ext = os.path.splitext(file)[-1]
# detect if sequence
if not is_sequence:
# is video file
files = file
else:
files = list()
spliter, padding = get_sequence_pattern_and_padding(file)
self.log.debug("_ spliter, padding: {}, {}".format(
spliter, padding))
base_name = file.split(spliter)[0]
# define collection and calculate frame range
collection = clique.Collection(base_name, ext, padding, set(range(
int(rw_source_first + int(
self.rw_clip_source_in - handle_start)),
int(rw_source_first + int(
self.rw_clip_source_out + handle_end) + 1))))
self.log.debug("_ collection: {}".format(collection))
real_files = os.listdir(file_dir)
self.log.debug("_ real_files: {}".format(real_files))
# collect frames to repre files list
for item in collection:
if item not in real_files:
self.log.debug("_ item: {}".format(item))
continue
files.append(item)
# add prep tag
tags.extend(["prep", "delete"])
# change label
instance.data["label"] = "{0} - ({1})".format(
instance.data["label"], ext
)
self.log.debug("Instance review: {}".format(instance.data["name"]))
# adding representation for review mov
representation = {
"files": files,
"stagingDir": file_dir,
"frameStart": rw_source_first + self.rw_clip_source_in,
"frameEnd": rw_source_first + self.rw_clip_source_out,
"frameStartFtrack": int(
self.rw_clip_source_in - handle_start),
"frameEndFtrack": int(self.rw_clip_source_out + handle_end),
"step": 1,
"fps": instance.data["fps"],
"name": "review",
"tags": tags,
"ext": ext[1:]
}
if rw_source_duration > rw_clip_duration_h:
self.log.debug("Media duration higher: {}".format(
(rw_source_duration - rw_clip_duration_h)))
representation.update({
"frameStart": rw_source_first + int(
self.rw_clip_source_in - handle_start),
"frameEnd": rw_source_first + int(
self.rw_clip_source_out + handle_end),
"tags": ["_cut-bigger", "prep", "delete"]
})
elif rw_source_duration < rw_clip_duration_h:
self.log.debug("Media duration higher: {}".format(
(rw_source_duration - rw_clip_duration_h)))
representation.update({
"frameStart": rw_source_first + int(
self.rw_clip_source_in - handle_start),
"frameEnd": rw_source_first + int(
self.rw_clip_source_out + handle_end),
"tags": ["prep", "delete"]
})
instance.data["representations"].append(representation)
self.create_thumbnail(instance)
self.log.debug(
"Added representations: {}".format(
instance.data["representations"]))
def create_thumbnail(self, instance):
source_file = os.path.basename(self.rw_source_path)
spliter, padding = get_sequence_pattern_and_padding(source_file)
if spliter:
head, ext = source_file.split(spliter)
else:
head, ext = os.path.splitext(source_file)
# staging dir creation
staging_dir = os.path.dirname(
self.rw_source_path)
# get thumbnail frame from the middle
thumb_frame = int(self.rw_clip_source_in + (
(self.rw_clip_source_out - self.rw_clip_source_in) / 2))
thumb_file = "{}thumbnail{}{}".format(head, thumb_frame, ".png")
thumb_path = os.path.join(staging_dir, thumb_file)
thumbnail = self.rw_clip.thumbnail(thumb_frame).save(
thumb_path,
format='png'
)
self.log.debug(
"__ thumbnail: `{}`, frame: `{}`".format(thumbnail, thumb_frame))
self.log.debug("__ thumbnail: {}".format(thumbnail))
thumb_representation = {
'files': thumb_file,
'stagingDir': staging_dir,
'name': "thumbnail",
'thumbnail': True,
'ext': "png"
}
instance.data["representations"].append(
thumb_representation)
def version_data(self, instance):
transfer_data = [
"handleStart", "handleEnd", "sourceIn", "sourceOut",
"frameStart", "frameEnd", "sourceInH", "sourceOutH",
"clipIn", "clipOut", "clipInH", "clipOutH", "asset",
"track"
]
version_data = dict()
# pass data to version
version_data.update({k: instance.data[k] for k in transfer_data})
if 'version' in instance.data:
version_data["version"] = instance.data["version"]
# add to data of representation
version_data.update({
"colorspace": self.rw_clip.sourceMediaColourTransform(),
"families": instance.data["families"],
"subset": instance.data["subset"],
"fps": instance.data["fps"]
})
instance.data["versionData"] = version_data

View file

@ -4,7 +4,7 @@ from pyblish import api
class CollectClipTagTasks(api.InstancePlugin):
"""Collect Tags from selected track items."""
order = api.CollectorOrder + 0.012
order = api.CollectorOrder
label = "Collect Tag Tasks"
hosts = ["hiero"]
families = ['clip']
@ -14,8 +14,8 @@ class CollectClipTagTasks(api.InstancePlugin):
tags = instance.data["tags"]
tasks = dict()
for t in tags:
t_metadata = dict(t["metadata"])
for tag in tags:
t_metadata = dict(tag.metadata())
t_family = t_metadata.get("tag.family", "")
# gets only task family tags and collect labels

View file

@ -1,47 +1,44 @@
from pyblish import api
import os
from hiero.exporters.FnExportUtil import writeSequenceAudioWithHandles
import pyblish
import pype
class ExtractAudioFile(pype.api.Extractor):
"""Extracts audio subset file"""
"""Extracts audio subset file from all active timeline audio tracks"""
order = api.ExtractorOrder
order = pyblish.api.ExtractorOrder
label = "Extract Subset Audio"
hosts = ["hiero"]
families = ["clip", "audio"]
match = api.Intersection
families = ["audio"]
match = pyblish.api.Intersection
def process(self, instance):
import os
from hiero.exporters.FnExportUtil import writeSequenceAudioWithHandles
item = instance.data["item"]
context = instance.context
self.log.debug("creating staging dir")
self.staging_dir(instance)
staging_dir = instance.data["stagingDir"]
# get sequence
sequence = instance.context.data["activeSequence"]
subset = instance.data["subset"]
# get timeline in / out
clip_in = instance.data["clipIn"]
clip_out = instance.data["clipOut"]
# get handles from context
handle_start = instance.data["handleStart"]
handle_end = instance.data["handleEnd"]
# get sequence from context
sequence = context.data["activeSequence"]
staging_dir = self.staging_dir(instance)
self.log.info("Created staging dir: {}...".format(staging_dir))
# path to wav file
audio_file = os.path.join(
staging_dir, "{0}.wav".format(instance.data["subset"])
staging_dir, "{}.wav".format(subset)
)
# export audio to disk
writeSequenceAudioWithHandles(
audio_file,
sequence,
item.timelineIn(),
item.timelineOut(),
clip_in,
clip_out,
handle_start,
handle_end
)

View file

@ -0,0 +1,100 @@
# from pype import plugins
import os
import json
import pyblish.api
import pype
class ExtractClipEffects(pype.api.Extractor):
"""Extract clip effects instances."""
order = pyblish.api.ExtractorOrder
label = "Export Clip Effects"
families = ["effect"]
def process(self, instance):
item = instance.data["item"]
effects = instance.data.get("effects")
# skip any without effects
if not effects:
return
subset = instance.data.get("subset")
family = instance.data["family"]
self.log.debug("creating staging dir")
staging_dir = self.staging_dir(instance)
transfers = list()
if "transfers" not in instance.data:
instance.data["transfers"] = list()
ext = "json"
file = subset + "." + ext
# when instance is created during collection part
resources_dir = instance.data["resourcesDir"]
# change paths in effects to files
for k, effect in effects.items():
if "assignTo" in k:
continue
trn = self.copy_linked_files(effect, resources_dir)
if trn:
transfers.append((trn[0], trn[1]))
instance.data["transfers"].extend(transfers)
self.log.debug("_ transfers: `{}`".format(
instance.data["transfers"]))
# create representations
instance.data["representations"] = list()
transfer_data = [
"handleStart", "handleEnd", "sourceIn", "sourceOut",
"frameStart", "frameEnd", "sourceInH", "sourceOutH",
"clipIn", "clipOut", "clipInH", "clipOutH", "asset", "track",
"version"
]
# pass data to version
version_data = dict()
version_data.update({k: instance.data[k] for k in transfer_data})
# add to data of representation
version_data.update({
"colorspace": item.sourceMediaColourTransform(),
"colorspaceScript": instance.context.data["colorspace"],
"families": [family, "plate"],
"subset": subset,
"fps": instance.context.data["fps"]
})
instance.data["versionData"] = version_data
representation = {
'files': file,
'stagingDir': staging_dir,
'name': family + ext.title(),
'ext': ext
}
instance.data["representations"].append(representation)
self.log.debug("_ representations: `{}`".format(
instance.data["representations"]))
self.log.debug("_ version_data: `{}`".format(
instance.data["versionData"]))
with open(os.path.join(staging_dir, file), "w") as outfile:
outfile.write(json.dumps(effects, indent=4, sort_keys=True))
def copy_linked_files(self, effect, dst_dir):
for k, v in effect["node"].items():
if k in "file" and v != '':
base_name = os.path.basename(v)
dst = os.path.join(dst_dir, base_name).replace("\\", "/")
# add it to the json
effect["node"][k] = dst
return (v, dst)

View file

@ -8,12 +8,11 @@ import clique
from avalon.vendor import filelink
class ExtractReviewCutUp(pype.api.Extractor):
class ExtractReviewPreparation(pype.api.Extractor):
"""Cut up clips from long video file"""
order = api.ExtractorOrder
# order = api.CollectorOrder + 0.1023
label = "Extract Review CutUp"
label = "Extract Review Preparation"
hosts = ["hiero"]
families = ["review"]
@ -22,22 +21,18 @@ class ExtractReviewCutUp(pype.api.Extractor):
def process(self, instance):
inst_data = instance.data
asset = inst_data['asset']
item = inst_data['item']
event_number = int(item.eventNumber())
asset = inst_data["asset"]
review_item_data = instance.data.get("reviewItemData")
# get representation and loop them
representations = inst_data["representations"]
# check if sequence
is_sequence = inst_data["isSequence"]
# get resolution default
resolution_width = inst_data["resolutionWidth"]
resolution_height = inst_data["resolutionHeight"]
# frame range data
media_duration = inst_data["mediaDuration"]
media_duration = review_item_data["mediaDuration"]
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
ffprobe_path = pype.lib.get_ffmpeg_tool_path("ffprobe")
@ -52,7 +47,7 @@ class ExtractReviewCutUp(pype.api.Extractor):
# check if supported tags are in representation for activation
filter_tag = False
for tag in ["_cut-bigger", "_cut-smaller"]:
for tag in ["_cut-bigger", "prep"]:
if tag in tags:
filter_tag = True
break
@ -70,7 +65,7 @@ class ExtractReviewCutUp(pype.api.Extractor):
full_output_dir = os.path.join(
staging_dir, "cuts")
if is_sequence:
if isinstance(files, list):
new_files = list()
# frame range delivery included handles
@ -99,12 +94,7 @@ class ExtractReviewCutUp(pype.api.Extractor):
index = 0
for image in collection:
dst_file_num = frame_start + index
dst_file_name = "".join([
str(event_number),
head,
str(padding % dst_file_num),
tail
])
dst_file_name = head + str(padding % dst_file_num) + tail
src = os.path.join(staging_dir, image)
dst = os.path.join(full_output_dir, dst_file_name)
self.log.info("Creating temp hardlinks: {}".format(dst))
@ -142,7 +132,7 @@ class ExtractReviewCutUp(pype.api.Extractor):
).format(**locals())
self.log.debug("ffprob_cmd: {}".format(ffprob_cmd))
audio_check_output = pype.api.run_subprocess(ffprob_cmd)
audio_check_output = pype.api.subprocess(ffprob_cmd)
self.log.debug(
"audio_check_output: {}".format(audio_check_output))
@ -177,7 +167,7 @@ class ExtractReviewCutUp(pype.api.Extractor):
# try to get video native resolution data
try:
resolution_output = pype.api.run_subprocess((
resolution_output = pype.api.subprocess((
"\"{ffprobe_path}\" -i \"{full_input_path}\""
" -v error "
"-select_streams v:0 -show_entries "
@ -290,7 +280,8 @@ class ExtractReviewCutUp(pype.api.Extractor):
# run subprocess
self.log.debug("Executing: {}".format(subprcs_cmd))
pype.api.run_subprocess(subprcs_cmd, logger=self.log)
output = pype.api.subprocess(subprcs_cmd)
self.log.debug("Output: {}".format(output))
repre_new = {
"files": new_files,
@ -302,7 +293,8 @@ class ExtractReviewCutUp(pype.api.Extractor):
"step": 1,
"fps": fps,
"name": "cut_up_preview",
"tags": ["review"] + self.tags_addition,
"tags": [
"review", "ftrackreview", "delete"] + self.tags_addition,
"ext": ext,
"anatomy_template": "publish"
}

View file

@ -0,0 +1,164 @@
import re
import pyblish.api
class PreCollectClipEffects(pyblish.api.InstancePlugin):
"""Collect soft effects instances."""
order = pyblish.api.CollectorOrder - 0.508
label = "Pre-collect Clip Effects Instances"
families = ["clip"]
def process(self, instance):
family = "effect"
effects = {}
review = instance.data.get("review")
review_track_index = instance.context.data.get("reviewTrackIndex")
item = instance.data["item"]
# frame range
self.handle_start = instance.data["handleStart"]
self.handle_end = instance.data["handleEnd"]
self.clip_in = int(item.timelineIn())
self.clip_out = int(item.timelineOut())
self.clip_in_h = self.clip_in - self.handle_start
self.clip_out_h = self.clip_out + self.handle_end
track = instance.data["trackItem"]
track_index = track.trackIndex()
tracks_effect_items = instance.context.data.get("tracksEffectItems")
clip_effect_items = instance.data.get("clipEffectItems")
# add clips effects to track's:
if clip_effect_items:
tracks_effect_items[track_index] = clip_effect_items
# process all effects and devide them to instance
for _track_index, sub_track_items in tracks_effect_items.items():
# skip if track index is the same as review track index
if review and review_track_index == _track_index:
continue
for sitem in sub_track_items:
if not (track_index <= _track_index):
continue
effect = self.add_effect(_track_index, sitem)
if effect:
effects.update(effect)
# skip any without effects
if not effects:
return
subset = instance.data.get("subset")
effects.update({"assignTo": subset})
subset_split = re.findall(r'[A-Z][^A-Z]*', subset)
if len(subset_split) > 0:
root_name = subset.replace(subset_split[0], "")
subset_split.insert(0, root_name.capitalize())
subset_split.insert(0, "effect")
name = "".join(subset_split)
# create new instance and inherit data
data = {}
for key, value in instance.data.items():
if "clipEffectItems" in key:
continue
data[key] = value
# change names
data["subset"] = name
data["family"] = family
data["families"] = [family]
data["name"] = data["subset"] + "_" + data["asset"]
data["label"] = "{} - {}".format(
data['asset'], data["subset"]
)
data["effects"] = effects
# create new instance
_instance = instance.context.create_instance(**data)
self.log.info("Created instance `{}`".format(_instance))
self.log.debug("instance.data `{}`".format(_instance.data))
def test_overlap(self, effect_t_in, effect_t_out):
covering_exp = bool(
(effect_t_in <= self.clip_in)
and (effect_t_out >= self.clip_out)
)
overlaying_right_exp = bool(
(effect_t_in < self.clip_out)
and (effect_t_out >= self.clip_out)
)
overlaying_left_exp = bool(
(effect_t_out > self.clip_in)
and (effect_t_in <= self.clip_in)
)
return any((
covering_exp,
overlaying_right_exp,
overlaying_left_exp
))
def add_effect(self, track_index, sitem):
track = sitem.parentTrack().name()
# node serialization
node = sitem.node()
node_serialized = self.node_serialisation(node)
node_name = sitem.name()
node_class = re.sub(r"\d+", "", node_name)
# collect timelineIn/Out
effect_t_in = int(sitem.timelineIn())
effect_t_out = int(sitem.timelineOut())
if not self.test_overlap(effect_t_in, effect_t_out):
return
self.log.debug("node_name: `{}`".format(node_name))
return {node_name: {
"class": node_class,
"timelineIn": effect_t_in,
"timelineOut": effect_t_out,
"subTrackIndex": sitem.subTrackIndex(),
"trackIndex": track_index,
"track": track,
"node": node_serialized
}}
def node_serialisation(self, node):
node_serialized = {}
# adding ignoring knob keys
_ignoring_keys = ['invert_mask', 'help', 'mask',
'xpos', 'ypos', 'layer', 'process_mask', 'channel',
'channels', 'maskChannelMask', 'maskChannelInput',
'note_font', 'note_font_size', 'unpremult',
'postage_stamp_frame', 'maskChannel', 'export_cc',
'select_cccid', 'mix', 'version', 'matrix']
# loop trough all knobs and collect not ignored
# and any with any value
for knob in node.knobs().keys():
# skip nodes in ignore keys
if knob in _ignoring_keys:
continue
# get animation if node is animated
if node[knob].isAnimated():
# grab animation including handles
knob_anim = [node[knob].getValueAt(i)
for i in range(
self.clip_in_h, self.clip_in_h + 1)]
node_serialized[knob] = knob_anim
else:
node_serialized[knob] = node[knob].value()
return node_serialized

View file

@ -0,0 +1,221 @@
from compiler.ast import flatten
from pyblish import api
from pype.hosts.hiero import api as phiero
import hiero
# from pype.hosts.hiero.api import lib
# reload(lib)
# reload(phiero)
class PreCollectInstances(api.ContextPlugin):
"""Collect all Track items selection."""
order = api.CollectorOrder - 0.509
label = "Pre-collect Instances"
hosts = ["hiero"]
def process(self, context):
track_items = phiero.get_track_items(
selected=True, check_tagged=True, check_enabled=True)
# only return enabled track items
if not track_items:
track_items = phiero.get_track_items(
check_enabled=True, check_tagged=True)
# get sequence and video tracks
sequence = context.data["activeSequence"]
tracks = sequence.videoTracks()
# add collection to context
tracks_effect_items = self.collect_sub_track_items(tracks)
context.data["tracksEffectItems"] = tracks_effect_items
self.log.info(
"Processing enabled track items: {}".format(len(track_items)))
for _ti in track_items:
data = dict()
clip = _ti.source()
# get clips subtracks and anotations
annotations = self.clip_annotations(clip)
subtracks = self.clip_subtrack(_ti)
self.log.debug("Annotations: {}".format(annotations))
self.log.debug(">> Subtracks: {}".format(subtracks))
# get pype tag data
tag_parsed_data = phiero.get_track_item_pype_data(_ti)
# self.log.debug(pformat(tag_parsed_data))
if not tag_parsed_data:
continue
if tag_parsed_data.get("id") != "pyblish.avalon.instance":
continue
# add tag data to instance data
data.update({
k: v for k, v in tag_parsed_data.items()
if k not in ("id", "applieswhole", "label")
})
asset = tag_parsed_data["asset"]
subset = tag_parsed_data["subset"]
review = tag_parsed_data.get("review")
audio = tag_parsed_data.get("audio")
# remove audio attribute from data
data.pop("audio")
# insert family into families
family = tag_parsed_data["family"]
families = [str(f) for f in tag_parsed_data["families"]]
families.insert(0, str(family))
track = _ti.parent()
media_source = _ti.source().mediaSource()
source_path = media_source.firstpath()
file_head = media_source.filenameHead()
file_info = media_source.fileinfos().pop()
source_first_frame = int(file_info.startFrame())
# apply only for feview and master track instance
if review:
families += ["review", "ftrack"]
data.update({
"name": "{} {} {}".format(asset, subset, families),
"asset": asset,
"item": _ti,
"families": families,
# tags
"tags": _ti.tags(),
# track item attributes
"track": track.name(),
"trackItem": track,
# version data
"versionData": {
"colorspace": _ti.sourceMediaColourTransform()
},
# source attribute
"source": source_path,
"sourceMedia": media_source,
"sourcePath": source_path,
"sourceFileHead": file_head,
"sourceFirst": source_first_frame,
# clip's effect
"clipEffectItems": subtracks
})
instance = context.create_instance(**data)
self.log.info("Creating instance: {}".format(instance))
if audio:
a_data = dict()
# add tag data to instance data
a_data.update({
k: v for k, v in tag_parsed_data.items()
if k not in ("id", "applieswhole", "label")
})
# create main attributes
subset = "audioMain"
family = "audio"
families = ["clip", "ftrack"]
families.insert(0, str(family))
name = "{} {} {}".format(asset, subset, families)
a_data.update({
"name": name,
"subset": subset,
"asset": asset,
"family": family,
"families": families,
"item": _ti,
# tags
"tags": _ti.tags(),
})
a_instance = context.create_instance(**a_data)
self.log.info("Creating audio instance: {}".format(a_instance))
@staticmethod
def clip_annotations(clip):
"""
Returns list of Clip's hiero.core.Annotation
"""
annotations = []
subTrackItems = flatten(clip.subTrackItems())
annotations += [item for item in subTrackItems if isinstance(
item, hiero.core.Annotation)]
return annotations
@staticmethod
def clip_subtrack(clip):
"""
Returns list of Clip's hiero.core.SubTrackItem
"""
subtracks = []
subTrackItems = flatten(clip.parent().subTrackItems())
for item in subTrackItems:
# avoid all anotation
if isinstance(item, hiero.core.Annotation):
continue
# # avoid all not anaibled
if not item.isEnabled():
continue
subtracks.append(item)
return subtracks
@staticmethod
def collect_sub_track_items(tracks):
"""
Returns dictionary with track index as key and list of subtracks
"""
# collect all subtrack items
sub_track_items = dict()
for track in tracks:
items = track.items()
# skip if no clips on track > need track with effect only
if items:
continue
# skip all disabled tracks
if not track.isEnabled():
continue
track_index = track.trackIndex()
_sub_track_items = flatten(track.subTrackItems())
# continue only if any subtrack items are collected
if len(_sub_track_items) < 1:
continue
enabled_sti = list()
# loop all found subtrack items and check if they are enabled
for _sti in _sub_track_items:
# checking if not enabled
if not _sti.isEnabled():
continue
if isinstance(_sti, hiero.core.Annotation):
continue
# collect the subtrack item
enabled_sti.append(_sti)
# continue only if any subtrack items are collected
if len(enabled_sti) < 1:
continue
# add collection of subtrackitems to dict
sub_track_items[track_index] = enabled_sti
return sub_track_items

View file

@ -0,0 +1,74 @@
import os
import pyblish.api
from pype.hosts.hiero import api as phiero
from avalon import api as avalon
class PreCollectWorkfile(pyblish.api.ContextPlugin):
"""Inject the current working file into context"""
label = "Pre-collect Workfile"
order = pyblish.api.CollectorOrder - 0.51
def process(self, context):
asset = avalon.Session["AVALON_ASSET"]
subset = "workfile"
project = phiero.get_current_project()
active_sequence = phiero.get_current_sequence()
video_tracks = active_sequence.videoTracks()
audio_tracks = active_sequence.audioTracks()
current_file = project.path()
staging_dir = os.path.dirname(current_file)
base_name = os.path.basename(current_file)
# get workfile's colorspace properties
_clrs = {}
_clrs["useOCIOEnvironmentOverride"] = project.useOCIOEnvironmentOverride() # noqa
_clrs["lutSetting16Bit"] = project.lutSetting16Bit()
_clrs["lutSetting8Bit"] = project.lutSetting8Bit()
_clrs["lutSettingFloat"] = project.lutSettingFloat()
_clrs["lutSettingLog"] = project.lutSettingLog()
_clrs["lutSettingViewer"] = project.lutSettingViewer()
_clrs["lutSettingWorkingSpace"] = project.lutSettingWorkingSpace()
_clrs["lutUseOCIOForExport"] = project.lutUseOCIOForExport()
_clrs["ocioConfigName"] = project.ocioConfigName()
_clrs["ocioConfigPath"] = project.ocioConfigPath()
# set main project attributes to context
context.data["activeProject"] = project
context.data["activeSequence"] = active_sequence
context.data["videoTracks"] = video_tracks
context.data["audioTracks"] = audio_tracks
context.data["currentFile"] = current_file
context.data["colorspace"] = _clrs
self.log.info("currentFile: {}".format(current_file))
# creating workfile representation
representation = {
'name': 'hrox',
'ext': 'hrox',
'files': base_name,
"stagingDir": staging_dir,
}
instance_data = {
"name": "{}_{}".format(asset, subset),
"asset": asset,
"subset": "{}{}".format(asset, subset.capitalize()),
"item": project,
"family": "workfile",
# version data
"versionData": {
"colorspace": _clrs
},
# source attribute
"sourcePath": current_file,
"representations": [representation]
}
instance = context.create_instance(**instance_data)
self.log.info("Creating instance: {}".format(instance))

View file

@ -0,0 +1,25 @@
import pyblish
from pype.hosts.hiero.api import is_overlapping
class ValidateAudioFile(pyblish.api.InstancePlugin):
"""Validate audio subset has avilable audio track clips"""
order = pyblish.api.ValidatorOrder
label = "Validate Audio Tracks"
hosts = ["hiero"]
families = ["audio"]
def process(self, instance):
clip = instance.data["item"]
audio_tracks = instance.context.data["audioTracks"]
audio_clip = None
for a_track in audio_tracks:
for item in a_track.items():
if is_overlapping(item, clip):
audio_clip = item
assert audio_clip, "Missing relative audio clip for clip {}".format(
clip.name()
)

View file

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Before After
Before After

View file

@ -2,11 +2,11 @@ import traceback
# activate hiero from pype
import avalon.api
import pype.hosts.hiero
avalon.api.install(pype.hosts.hiero)
import pype.hosts.hiero.api as phiero
avalon.api.install(phiero)
try:
__import__("pype.hosts.hiero")
__import__("pype.hosts.hiero.api")
__import__("pyblish")
except ImportError as e:
@ -15,5 +15,5 @@ except ImportError as e:
else:
# Setup integration
import pype.hosts.hiero.lib
pype.hosts.hiero.lib.setup()
import pype.hosts.hiero.api as phiero
phiero.lib.setup()

Some files were not shown because too many files have changed in this diff Show more