mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 08:24:53 +01:00
Merge branch 'hotfix/invalid-scope' into 2.x/develop
This commit is contained in:
commit
1923d60600
20 changed files with 791 additions and 376 deletions
|
|
@ -228,12 +228,7 @@ class Delivery(BaseAction):
|
||||||
if location_path:
|
if location_path:
|
||||||
location_path = os.path.normpath(location_path)
|
location_path = os.path.normpath(location_path)
|
||||||
if not os.path.exists(location_path):
|
if not os.path.exists(location_path):
|
||||||
return {
|
os.makedirs(location_path)
|
||||||
"success": False,
|
|
||||||
"message": (
|
|
||||||
"Entered location path does not exists. \"{}\""
|
|
||||||
).format(location_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.db_con.install()
|
self.db_con.install()
|
||||||
self.db_con.Session["AVALON_PROJECT"] = project_name
|
self.db_con.Session["AVALON_PROJECT"] = project_name
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ import tempfile
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
from avalon import io, api
|
from avalon import io
|
||||||
from avalon.tools import publish as av_publish
|
|
||||||
|
|
||||||
import pype
|
import pype
|
||||||
from pype.api import execute
|
from pype.api import execute, Logger
|
||||||
|
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
from . import PUBLISH_PATHS
|
|
||||||
|
|
||||||
|
log = Logger().get_logger("standalonepublisher")
|
||||||
|
|
||||||
|
|
||||||
def set_context(project, asset, task, app):
|
def set_context(project, asset, task, app):
|
||||||
|
|
@ -61,105 +61,71 @@ def set_context(project, asset, task, app):
|
||||||
def publish(data, gui=True):
|
def publish(data, gui=True):
|
||||||
# cli pyblish seems like better solution
|
# cli pyblish seems like better solution
|
||||||
return cli_publish(data, gui)
|
return cli_publish(data, gui)
|
||||||
# # this uses avalon pyblish launch tool
|
|
||||||
# avalon_api_publish(data, gui)
|
|
||||||
|
|
||||||
|
|
||||||
def avalon_api_publish(data, gui=True):
|
|
||||||
''' Launches Pyblish (GUI by default)
|
|
||||||
:param data: Should include data for pyblish and standalone collector
|
|
||||||
:type data: dict
|
|
||||||
:param gui: Pyblish will be launched in GUI mode if set to True
|
|
||||||
:type gui: bool
|
|
||||||
'''
|
|
||||||
io.install()
|
|
||||||
|
|
||||||
# Create hash name folder in temp
|
|
||||||
chars = "".join([random.choice(string.ascii_letters) for i in range(15)])
|
|
||||||
staging_dir = tempfile.mkdtemp(chars)
|
|
||||||
|
|
||||||
# create also json and fill with data
|
|
||||||
json_data_path = staging_dir + os.path.basename(staging_dir) + '.json'
|
|
||||||
with open(json_data_path, 'w') as outfile:
|
|
||||||
json.dump(data, outfile)
|
|
||||||
|
|
||||||
args = [
|
|
||||||
"-pp", os.pathsep.join(pyblish.api.registered_paths())
|
|
||||||
]
|
|
||||||
|
|
||||||
envcopy = os.environ.copy()
|
|
||||||
envcopy["PYBLISH_HOSTS"] = "standalonepublisher"
|
|
||||||
envcopy["SAPUBLISH_INPATH"] = json_data_path
|
|
||||||
|
|
||||||
if gui:
|
|
||||||
av_publish.show()
|
|
||||||
else:
|
|
||||||
returncode = execute([
|
|
||||||
sys.executable, "-u", "-m", "pyblish"
|
|
||||||
] + args, env=envcopy)
|
|
||||||
|
|
||||||
io.uninstall()
|
|
||||||
|
|
||||||
|
|
||||||
def cli_publish(data, gui=True):
|
def cli_publish(data, gui=True):
|
||||||
|
from . import PUBLISH_PATHS
|
||||||
|
|
||||||
|
PUBLISH_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "publish.py")
|
||||||
io.install()
|
io.install()
|
||||||
|
|
||||||
pyblish.api.deregister_all_plugins()
|
|
||||||
# Registers Global pyblish plugins
|
|
||||||
pype.install()
|
|
||||||
# Registers Standalone pyblish plugins
|
|
||||||
for path in PUBLISH_PATHS:
|
|
||||||
pyblish.api.register_plugin_path(path)
|
|
||||||
|
|
||||||
project_plugins_paths = os.environ.get("PYPE_PROJECT_PLUGINS")
|
|
||||||
project_name = os.environ["AVALON_PROJECT"]
|
|
||||||
if project_plugins_paths and project_name:
|
|
||||||
for path in project_plugins_paths.split(os.pathsep):
|
|
||||||
if not path:
|
|
||||||
continue
|
|
||||||
plugin_path = os.path.join(path, project_name, "plugins")
|
|
||||||
if os.path.exists(plugin_path):
|
|
||||||
pyblish.api.register_plugin_path(plugin_path)
|
|
||||||
api.register_plugin_path(api.Loader, plugin_path)
|
|
||||||
api.register_plugin_path(api.Creator, plugin_path)
|
|
||||||
|
|
||||||
# Create hash name folder in temp
|
# Create hash name folder in temp
|
||||||
chars = "".join([random.choice(string.ascii_letters) for i in range(15)])
|
chars = "".join([random.choice(string.ascii_letters) for i in range(15)])
|
||||||
staging_dir = tempfile.mkdtemp(chars)
|
staging_dir = tempfile.mkdtemp(chars)
|
||||||
|
|
||||||
# create json for return data
|
|
||||||
return_data_path = (
|
|
||||||
staging_dir + os.path.basename(staging_dir) + 'return.json'
|
|
||||||
)
|
|
||||||
# create also json and fill with data
|
# create also json and fill with data
|
||||||
json_data_path = staging_dir + os.path.basename(staging_dir) + '.json'
|
json_data_path = staging_dir + os.path.basename(staging_dir) + '.json'
|
||||||
with open(json_data_path, 'w') as outfile:
|
with open(json_data_path, 'w') as outfile:
|
||||||
json.dump(data, outfile)
|
json.dump(data, outfile)
|
||||||
|
|
||||||
args = [
|
|
||||||
"-pp", os.pathsep.join(pyblish.api.registered_paths())
|
|
||||||
]
|
|
||||||
|
|
||||||
if gui:
|
|
||||||
args += ["gui"]
|
|
||||||
|
|
||||||
envcopy = os.environ.copy()
|
envcopy = os.environ.copy()
|
||||||
envcopy["PYBLISH_HOSTS"] = "standalonepublisher"
|
envcopy["PYBLISH_HOSTS"] = "standalonepublisher"
|
||||||
envcopy["SAPUBLISH_INPATH"] = json_data_path
|
envcopy["SAPUBLISH_INPATH"] = json_data_path
|
||||||
envcopy["SAPUBLISH_OUTPATH"] = return_data_path
|
envcopy["PYBLISHGUI"] = "pyblish_pype"
|
||||||
envcopy["PYBLISH_GUI"] = "pyblish_pype"
|
envcopy["PUBLISH_PATHS"] = os.pathsep.join(PUBLISH_PATHS)
|
||||||
|
|
||||||
returncode = execute([
|
result = execute(
|
||||||
sys.executable, "-u", "-m", "pyblish"
|
[sys.executable, PUBLISH_SCRIPT_PATH],
|
||||||
] + args, env=envcopy)
|
env=envcopy
|
||||||
|
)
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
if os.path.exists(json_data_path):
|
if os.path.exists(json_data_path):
|
||||||
with open(json_data_path, "r") as f:
|
with open(json_data_path, "r") as f:
|
||||||
result = json.load(f)
|
result = json.load(f)
|
||||||
|
|
||||||
|
log.info(f"Publish result: {result}")
|
||||||
|
|
||||||
io.uninstall()
|
io.uninstall()
|
||||||
# TODO: check if was pyblish successful
|
|
||||||
# if successful return True
|
|
||||||
print('Check result here')
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main(env):
|
||||||
|
from avalon.tools import publish
|
||||||
|
# Registers pype's Global pyblish plugins
|
||||||
|
pype.install()
|
||||||
|
|
||||||
|
# Register additional paths
|
||||||
|
addition_paths_str = env.get("PUBLISH_PATHS") or ""
|
||||||
|
addition_paths = addition_paths_str.split(os.pathsep)
|
||||||
|
for path in addition_paths:
|
||||||
|
path = os.path.normpath(path)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
continue
|
||||||
|
pyblish.api.register_plugin_path(path)
|
||||||
|
|
||||||
|
# Register project specific plugins
|
||||||
|
project_name = os.environ["AVALON_PROJECT"]
|
||||||
|
project_plugins_paths = env.get("PYPE_PROJECT_PLUGINS") or ""
|
||||||
|
for path in project_plugins_paths.split(os.pathsep):
|
||||||
|
plugin_path = os.path.join(path, project_name, "plugins")
|
||||||
|
if os.path.exists(plugin_path):
|
||||||
|
pyblish.api.register_plugin_path(plugin_path)
|
||||||
|
|
||||||
|
return publish.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
result = main(os.environ)
|
||||||
|
sys.exit(not bool(result))
|
||||||
|
|
|
||||||
|
|
@ -357,7 +357,7 @@ class DropDataFrame(QtWidgets.QFrame):
|
||||||
if data['name'] == item.in_data['name']:
|
if data['name'] == item.in_data['name']:
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
paths = data['files']
|
paths = list(data['files'])
|
||||||
paths.extend(item.in_data['files'])
|
paths.extend(item.in_data['files'])
|
||||||
c, r = clique.assemble(paths)
|
c, r = clique.assemble(paths)
|
||||||
if len(c) == 0:
|
if len(c) == 0:
|
||||||
|
|
@ -392,7 +392,7 @@ class DropDataFrame(QtWidgets.QFrame):
|
||||||
else:
|
else:
|
||||||
if data['name'] != item.in_data['name']:
|
if data['name'] != item.in_data['name']:
|
||||||
continue
|
continue
|
||||||
if data['files'] == item.in_data['files']:
|
if data['files'] == list(item.in_data['files']):
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
a_name = 'merge'
|
a_name = 'merge'
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
|
||||||
|
|
||||||
order = pyblish.api.IntegratorOrder - 0.04
|
order = pyblish.api.IntegratorOrder - 0.04
|
||||||
label = 'Integrate Hierarchy To Ftrack'
|
label = 'Integrate Hierarchy To Ftrack'
|
||||||
families = ["clip", "shot"]
|
families = ["shot"]
|
||||||
optional = False
|
optional = False
|
||||||
|
|
||||||
def process(self, context):
|
def process(self, context):
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,14 @@ class ExtractBurnin(pype.api.Extractor):
|
||||||
label = "Extract burnins"
|
label = "Extract burnins"
|
||||||
order = pyblish.api.ExtractorOrder + 0.03
|
order = pyblish.api.ExtractorOrder + 0.03
|
||||||
families = ["review", "burnin"]
|
families = ["review", "burnin"]
|
||||||
hosts = ["nuke", "maya", "shell", "nukestudio", "premiere"]
|
hosts = [
|
||||||
|
"nuke",
|
||||||
|
"maya",
|
||||||
|
"shell",
|
||||||
|
"nukestudio",
|
||||||
|
"premiere",
|
||||||
|
"standalonepublisher"
|
||||||
|
]
|
||||||
optional = True
|
optional = True
|
||||||
|
|
||||||
positions = [
|
positions = [
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
|
||||||
|
|
||||||
order = pyblish.api.ExtractorOrder - 0.01
|
order = pyblish.api.ExtractorOrder - 0.01
|
||||||
label = "Extract Hierarchy To Avalon"
|
label = "Extract Hierarchy To Avalon"
|
||||||
families = ["clip", "shot", "editorial"]
|
families = ["clip", "shot"]
|
||||||
|
|
||||||
def process(self, context):
|
def process(self, context):
|
||||||
if "hierarchyContext" not in context.data:
|
if "hierarchyContext" not in context.data:
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,15 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
||||||
label = "Extract Review"
|
label = "Extract Review"
|
||||||
order = pyblish.api.ExtractorOrder + 0.02
|
order = pyblish.api.ExtractorOrder + 0.02
|
||||||
families = ["review"]
|
families = ["review"]
|
||||||
hosts = ["nuke", "maya", "shell", "nukestudio", "premiere", "harmony"]
|
hosts = [
|
||||||
|
"nuke",
|
||||||
|
"maya",
|
||||||
|
"shell",
|
||||||
|
"nukestudio",
|
||||||
|
"premiere",
|
||||||
|
"harmony",
|
||||||
|
"standalonepublisher"
|
||||||
|
]
|
||||||
|
|
||||||
# Supported extensions
|
# Supported extensions
|
||||||
image_exts = ["exr", "jpg", "jpeg", "png", "dpx"]
|
image_exts = ["exr", "jpg", "jpeg", "png", "dpx"]
|
||||||
|
|
|
||||||
|
|
@ -40,13 +40,11 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
||||||
expected_settings["frameEnd"] = frame_end - frame_start + 1
|
expected_settings["frameEnd"] = frame_end - frame_start + 1
|
||||||
expected_settings["frameStart"] = 1
|
expected_settings["frameStart"] = 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
self.log.info(instance.context.data['anatomyData']['asset'])
|
self.log.info(instance.context.data['anatomyData']['asset'])
|
||||||
|
|
||||||
if any(string in instance.context.data['anatomyData']['asset']
|
if any(string in instance.context.data['anatomyData']['asset']
|
||||||
for string in frame_check_filter):
|
for string in self.frame_check_filter):
|
||||||
expected_settings.pop("frameEnd")
|
expected_settings.pop("frameEnd")
|
||||||
|
|
||||||
func = """function func()
|
func = """function func()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,7 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin):
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"assets_shared: {assets_shared}".format(**locals()))
|
"assets_shared: {assets_shared}".format(**locals()))
|
||||||
|
|
||||||
|
|
||||||
class CollectHierarchyContext(pyblish.api.ContextPlugin):
|
class CollectHierarchyContext(pyblish.api.ContextPlugin):
|
||||||
'''Collecting Hierarchy from instaces and building
|
'''Collecting Hierarchy from instaces and building
|
||||||
context hierarchy tree
|
context hierarchy tree
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
import os
|
||||||
|
import opentimelineio as otio
|
||||||
|
import tempfile
|
||||||
|
import pyblish.api
|
||||||
|
from pype import lib as plib
|
||||||
|
|
||||||
|
|
||||||
|
class CollectClipInstances(pyblish.api.InstancePlugin):
|
||||||
|
"""Collect Clips instances from editorial's OTIO sequence"""
|
||||||
|
|
||||||
|
order = pyblish.api.CollectorOrder + 0.01
|
||||||
|
label = "Collect Clips"
|
||||||
|
hosts = ["standalonepublisher"]
|
||||||
|
families = ["editorial"]
|
||||||
|
|
||||||
|
# presets
|
||||||
|
subsets = {
|
||||||
|
"referenceMain": {
|
||||||
|
"family": "review",
|
||||||
|
"families": ["review", "ftrack"],
|
||||||
|
"ftrackFamily": "review",
|
||||||
|
"extension": ".mp4"
|
||||||
|
},
|
||||||
|
"audioMain": {
|
||||||
|
"family": "audio",
|
||||||
|
"families": ["ftrack"],
|
||||||
|
"ftrackFamily": "audio",
|
||||||
|
"extension": ".wav",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
"shotMain": {
|
||||||
|
"family": "shot",
|
||||||
|
"families": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timeline_frame_offset = None # if 900000 for edl default then -900000
|
||||||
|
custom_start_frame = None
|
||||||
|
|
||||||
|
def process(self, instance):
|
||||||
|
staging_dir = os.path.normpath(
|
||||||
|
tempfile.mkdtemp(prefix="pyblish_tmp_")
|
||||||
|
)
|
||||||
|
# get context
|
||||||
|
context = instance.context
|
||||||
|
|
||||||
|
# attribute for checking duplicity during creation
|
||||||
|
if not context.data.get("assetNameCheck"):
|
||||||
|
context.data["assetNameCheck"] = list()
|
||||||
|
|
||||||
|
# create asset_names conversion table
|
||||||
|
if not context.data.get("assetsShared"):
|
||||||
|
context.data["assetsShared"] = dict()
|
||||||
|
|
||||||
|
# get timeline otio data
|
||||||
|
timeline = instance.data["otio_timeline"]
|
||||||
|
fps = plib.get_asset()["data"]["fps"]
|
||||||
|
|
||||||
|
tracks = timeline.each_child(
|
||||||
|
descended_from_type=otio.schema.track.Track
|
||||||
|
)
|
||||||
|
|
||||||
|
# get data from avalon
|
||||||
|
asset_entity = instance.context.data["assetEntity"]
|
||||||
|
asset_data = asset_entity["data"]
|
||||||
|
asset_name = asset_entity["name"]
|
||||||
|
|
||||||
|
# Timeline data.
|
||||||
|
handle_start = int(asset_data["handleStart"])
|
||||||
|
handle_end = int(asset_data["handleEnd"])
|
||||||
|
|
||||||
|
instances = []
|
||||||
|
for track in tracks:
|
||||||
|
try:
|
||||||
|
track_start_frame = (
|
||||||
|
abs(track.source_range.start_time.value)
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
track_start_frame = 0
|
||||||
|
|
||||||
|
for clip in track.each_child():
|
||||||
|
if clip.name is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# skip all generators like black ampty
|
||||||
|
if isinstance(
|
||||||
|
clip.media_reference,
|
||||||
|
otio.schema.GeneratorReference):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Transitions are ignored, because Clips have the full frame
|
||||||
|
# range.
|
||||||
|
if isinstance(clip, otio.schema.transition.Transition):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# basic unique asset name
|
||||||
|
clip_name = os.path.splitext(clip.name)[0].lower()
|
||||||
|
name = f"{asset_name.split('_')[0]}_{clip_name}"
|
||||||
|
|
||||||
|
if name not in context.data["assetNameCheck"]:
|
||||||
|
context.data["assetNameCheck"].append(name)
|
||||||
|
else:
|
||||||
|
self.log.warning(f"duplicate shot name: {name}")
|
||||||
|
|
||||||
|
# frame ranges data
|
||||||
|
clip_in = clip.range_in_parent().start_time.value
|
||||||
|
clip_out = clip.range_in_parent().end_time_inclusive().value
|
||||||
|
|
||||||
|
# add offset in case there is any
|
||||||
|
if self.timeline_frame_offset:
|
||||||
|
clip_in += self.timeline_frame_offset
|
||||||
|
clip_out += self.timeline_frame_offset
|
||||||
|
|
||||||
|
clip_duration = clip.duration().value
|
||||||
|
self.log.info(f"clip duration: {clip_duration}")
|
||||||
|
|
||||||
|
source_in = clip.trimmed_range().start_time.value
|
||||||
|
source_out = source_in + clip_duration
|
||||||
|
source_in_h = source_in - handle_start
|
||||||
|
source_out_h = source_out + handle_end
|
||||||
|
|
||||||
|
clip_in_h = clip_in - handle_start
|
||||||
|
clip_out_h = clip_out + handle_end
|
||||||
|
|
||||||
|
# define starting frame for future shot
|
||||||
|
if self.custom_start_frame is not None:
|
||||||
|
frame_start = self.custom_start_frame
|
||||||
|
else:
|
||||||
|
frame_start = clip_in
|
||||||
|
|
||||||
|
frame_end = frame_start + (clip_duration - 1)
|
||||||
|
|
||||||
|
# create shared new instance data
|
||||||
|
instance_data = {
|
||||||
|
"stagingDir": staging_dir,
|
||||||
|
|
||||||
|
# shared attributes
|
||||||
|
"asset": name,
|
||||||
|
"assetShareName": name,
|
||||||
|
"editorialVideoPath": instance.data[
|
||||||
|
"editorialVideoPath"],
|
||||||
|
"item": clip,
|
||||||
|
|
||||||
|
# parent time properities
|
||||||
|
"trackStartFrame": track_start_frame,
|
||||||
|
"handleStart": handle_start,
|
||||||
|
"handleEnd": handle_end,
|
||||||
|
"fps": fps,
|
||||||
|
|
||||||
|
# media source
|
||||||
|
"sourceIn": source_in,
|
||||||
|
"sourceOut": source_out,
|
||||||
|
"sourceInH": source_in_h,
|
||||||
|
"sourceOutH": source_out_h,
|
||||||
|
|
||||||
|
# timeline
|
||||||
|
"clipIn": clip_in,
|
||||||
|
"clipOut": clip_out,
|
||||||
|
"clipDuration": clip_duration,
|
||||||
|
"clipInH": clip_in_h,
|
||||||
|
"clipOutH": clip_out_h,
|
||||||
|
"clipDurationH": clip_duration + handle_start + handle_end,
|
||||||
|
|
||||||
|
# task
|
||||||
|
"frameStart": frame_start,
|
||||||
|
"frameEnd": frame_end,
|
||||||
|
"frameStartH": frame_start - handle_start,
|
||||||
|
"frameEndH": frame_end + handle_end
|
||||||
|
}
|
||||||
|
|
||||||
|
# adding subsets to context as instances
|
||||||
|
for subset, properities in self.subsets.items():
|
||||||
|
# adding Review-able instance
|
||||||
|
subset_instance_data = instance_data.copy()
|
||||||
|
subset_instance_data.update(properities)
|
||||||
|
subset_instance_data.update({
|
||||||
|
# unique attributes
|
||||||
|
"name": f"{subset}_{name}",
|
||||||
|
"label": f"{subset} {name} ({clip_in}-{clip_out})",
|
||||||
|
"subset": subset
|
||||||
|
})
|
||||||
|
instances.append(instance.context.create_instance(
|
||||||
|
**subset_instance_data))
|
||||||
|
|
||||||
|
context.data["assetsShared"][name] = {
|
||||||
|
"_clipIn": clip_in,
|
||||||
|
"_clipOut": clip_out
|
||||||
|
}
|
||||||
|
|
@ -36,18 +36,6 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin):
|
||||||
def process(self, context):
|
def process(self, context):
|
||||||
# get json paths from os and load them
|
# get json paths from os and load them
|
||||||
io.install()
|
io.install()
|
||||||
input_json_path = os.environ.get("SAPUBLISH_INPATH")
|
|
||||||
output_json_path = os.environ.get("SAPUBLISH_OUTPATH")
|
|
||||||
|
|
||||||
# context.data["stagingDir"] = os.path.dirname(input_json_path)
|
|
||||||
context.data["returnJsonPath"] = output_json_path
|
|
||||||
|
|
||||||
with open(input_json_path, "r") as f:
|
|
||||||
in_data = json.load(f)
|
|
||||||
|
|
||||||
asset_name = in_data["asset"]
|
|
||||||
family = in_data["family"]
|
|
||||||
subset = in_data["subset"]
|
|
||||||
|
|
||||||
# Load presets
|
# Load presets
|
||||||
presets = context.data.get("presets")
|
presets = context.data.get("presets")
|
||||||
|
|
@ -57,19 +45,92 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin):
|
||||||
presets = config.get_presets()
|
presets = config.get_presets()
|
||||||
|
|
||||||
project = io.find_one({"type": "project"})
|
project = io.find_one({"type": "project"})
|
||||||
asset = io.find_one({"type": "asset", "name": asset_name})
|
|
||||||
context.data["project"] = project
|
context.data["project"] = project
|
||||||
|
|
||||||
|
# get json file context
|
||||||
|
input_json_path = os.environ.get("SAPUBLISH_INPATH")
|
||||||
|
|
||||||
|
with open(input_json_path, "r") as f:
|
||||||
|
in_data = json.load(f)
|
||||||
|
self.log.debug(f"_ in_data: {in_data}")
|
||||||
|
|
||||||
|
self.asset_name = in_data["asset"]
|
||||||
|
self.family = in_data["family"]
|
||||||
|
asset = io.find_one({"type": "asset", "name": self.asset_name})
|
||||||
context.data["asset"] = asset
|
context.data["asset"] = asset
|
||||||
|
|
||||||
|
# exception for editorial
|
||||||
|
if "editorial" in self.family:
|
||||||
|
# avoid subset name duplicity
|
||||||
|
if not context.data.get("subsetNamesCheck"):
|
||||||
|
context.data["subsetNamesCheck"] = list()
|
||||||
|
|
||||||
|
in_data_list = list()
|
||||||
|
representations = in_data.pop("representations")
|
||||||
|
for repr in representations:
|
||||||
|
in_data_copy = in_data.copy()
|
||||||
|
ext = repr["ext"][1:]
|
||||||
|
subset = in_data_copy["subset"]
|
||||||
|
# filter out non editorial files
|
||||||
|
if ext not in ["edl", "xml"]:
|
||||||
|
in_data_copy["representations"] = [repr]
|
||||||
|
in_data_copy["subset"] = f"{ext}{subset}"
|
||||||
|
in_data_list.append(in_data_copy)
|
||||||
|
|
||||||
|
files = repr.pop("files")
|
||||||
|
|
||||||
|
# delete unneeded keys
|
||||||
|
delete_repr_keys = ["frameStart", "frameEnd"]
|
||||||
|
for k in delete_repr_keys:
|
||||||
|
if repr.get(k):
|
||||||
|
repr.pop(k)
|
||||||
|
|
||||||
|
# convert files to list if it isnt
|
||||||
|
if not isinstance(files, list):
|
||||||
|
files = [files]
|
||||||
|
|
||||||
|
self.log.debug(f"_ files: {files}")
|
||||||
|
for index, f in enumerate(files):
|
||||||
|
index += 1
|
||||||
|
# copy dictionaries
|
||||||
|
in_data_copy = in_data_copy.copy()
|
||||||
|
repr_new = repr.copy()
|
||||||
|
|
||||||
|
repr_new["files"] = f
|
||||||
|
repr_new["name"] = ext
|
||||||
|
in_data_copy["representations"] = [repr_new]
|
||||||
|
|
||||||
|
# create subset Name
|
||||||
|
new_subset = f"{ext}{index}{subset}"
|
||||||
|
while new_subset in context.data["subsetNamesCheck"]:
|
||||||
|
index += 1
|
||||||
|
new_subset = f"{ext}{index}{subset}"
|
||||||
|
|
||||||
|
context.data["subsetNamesCheck"].append(new_subset)
|
||||||
|
in_data_copy["subset"] = new_subset
|
||||||
|
in_data_list.append(in_data_copy)
|
||||||
|
self.log.info(f"Creating subset: {ext}{index}{subset}")
|
||||||
|
else:
|
||||||
|
in_data_list = [in_data]
|
||||||
|
|
||||||
|
self.log.debug(f"_ in_data_list: {in_data_list}")
|
||||||
|
|
||||||
|
for in_data in in_data_list:
|
||||||
|
# create instance
|
||||||
|
self.create_instance(context, in_data)
|
||||||
|
|
||||||
|
def create_instance(self, context, in_data):
|
||||||
|
subset = in_data["subset"]
|
||||||
|
|
||||||
instance = context.create_instance(subset)
|
instance = context.create_instance(subset)
|
||||||
|
|
||||||
instance.data.update(
|
instance.data.update(
|
||||||
{
|
{
|
||||||
"subset": subset,
|
"subset": subset,
|
||||||
"asset": asset_name,
|
"asset": self.asset_name,
|
||||||
"label": subset,
|
"label": subset,
|
||||||
"name": subset,
|
"name": subset,
|
||||||
"family": family,
|
"family": self.family,
|
||||||
"version": in_data.get("version", 1),
|
"version": in_data.get("version", 1),
|
||||||
"frameStart": in_data.get("representations", [None])[0].get(
|
"frameStart": in_data.get("representations", [None])[0].get(
|
||||||
"frameStart", None
|
"frameStart", None
|
||||||
|
|
@ -77,7 +138,7 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin):
|
||||||
"frameEnd": in_data.get("representations", [None])[0].get(
|
"frameEnd": in_data.get("representations", [None])[0].get(
|
||||||
"frameEnd", None
|
"frameEnd", None
|
||||||
),
|
),
|
||||||
"families": [family, "ftrack"],
|
"families": [self.family, "ftrack"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.log.info("collected instance: {}".format(instance.data))
|
self.log.info("collected instance: {}".format(instance.data))
|
||||||
|
|
@ -105,5 +166,3 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin):
|
||||||
self.log.debug("Adding review family")
|
self.log.debug("Adding review family")
|
||||||
|
|
||||||
instance.data["representations"].append(component)
|
instance.data["representations"].append(component)
|
||||||
|
|
||||||
self.log.info(in_data)
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import os
|
||||||
|
import opentimelineio as otio
|
||||||
|
import pyblish.api
|
||||||
|
from pype import lib as plib
|
||||||
|
|
||||||
|
|
||||||
|
class OTIO_View(pyblish.api.Action):
|
||||||
|
"""Currently disabled because OTIO requires PySide2. Issue on Qt.py:
|
||||||
|
https://github.com/PixarAnimationStudios/OpenTimelineIO/issues/289
|
||||||
|
"""
|
||||||
|
|
||||||
|
label = "OTIO View"
|
||||||
|
icon = "wrench"
|
||||||
|
on = "failed"
|
||||||
|
|
||||||
|
def process(self, context, plugin):
|
||||||
|
instance = context[0]
|
||||||
|
representation = instance.data["representations"][0]
|
||||||
|
file_path = os.path.join(
|
||||||
|
representation["stagingDir"], representation["files"]
|
||||||
|
)
|
||||||
|
plib._subprocess(["otioview", file_path])
|
||||||
|
|
||||||
|
|
||||||
|
class CollectEditorial(pyblish.api.InstancePlugin):
|
||||||
|
"""Collect Editorial OTIO timeline"""
|
||||||
|
|
||||||
|
order = pyblish.api.CollectorOrder
|
||||||
|
label = "Collect Editorial"
|
||||||
|
hosts = ["standalonepublisher"]
|
||||||
|
families = ["editorial"]
|
||||||
|
actions = []
|
||||||
|
|
||||||
|
# presets
|
||||||
|
extensions = [".mov"]
|
||||||
|
|
||||||
|
def process(self, instance):
|
||||||
|
# remove context test attribute
|
||||||
|
if instance.context.data.get("subsetNamesCheck"):
|
||||||
|
instance.context.data.pop("subsetNamesCheck")
|
||||||
|
|
||||||
|
self.log.debug(f"__ instance: `{instance}`")
|
||||||
|
# get representation with editorial file
|
||||||
|
for representation in instance.data["representations"]:
|
||||||
|
self.log.debug(f"__ representation: `{representation}`")
|
||||||
|
# make editorial sequence file path
|
||||||
|
staging_dir = representation["stagingDir"]
|
||||||
|
file_path = os.path.join(
|
||||||
|
staging_dir, str(representation["files"])
|
||||||
|
)
|
||||||
|
instance.context.data["currentFile"] = file_path
|
||||||
|
|
||||||
|
# get video file path
|
||||||
|
video_path = None
|
||||||
|
basename = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
|
for f in os.listdir(staging_dir):
|
||||||
|
self.log.debug(f"__ test file: `{f}`")
|
||||||
|
# filter out by not sharing the same name
|
||||||
|
if os.path.splitext(f)[0] not in basename:
|
||||||
|
continue
|
||||||
|
# filter out by respected extensions
|
||||||
|
if os.path.splitext(f)[1] not in self.extensions:
|
||||||
|
continue
|
||||||
|
video_path = os.path.join(
|
||||||
|
staging_dir, f
|
||||||
|
)
|
||||||
|
self.log.debug(f"__ video_path: `{video_path}`")
|
||||||
|
instance.data["editorialVideoPath"] = video_path
|
||||||
|
instance.data["stagingDir"] = staging_dir
|
||||||
|
|
||||||
|
# get editorial sequence file into otio timeline object
|
||||||
|
extension = os.path.splitext(file_path)[1]
|
||||||
|
kwargs = {}
|
||||||
|
if extension == ".edl":
|
||||||
|
# EDL has no frame rate embedded so needs explicit
|
||||||
|
# frame rate else 24 is asssumed.
|
||||||
|
kwargs["rate"] = plib.get_asset()["data"]["fps"]
|
||||||
|
|
||||||
|
instance.data["otio_timeline"] = otio.adapters.read_from_file(
|
||||||
|
file_path, **kwargs)
|
||||||
|
|
||||||
|
self.log.info(f"Added OTIO timeline from: `{file_path}`")
|
||||||
242
pype/plugins/standalonepublisher/publish/collect_hierarchy.py
Normal file
242
pype/plugins/standalonepublisher/publish/collect_hierarchy.py
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
import pyblish.api
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
from avalon import io
|
||||||
|
|
||||||
|
|
||||||
|
class CollectHierarchyInstance(pyblish.api.ContextPlugin):
|
||||||
|
"""Collecting hierarchy context from `parents` and `hierarchy` data
|
||||||
|
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 Clip"
|
||||||
|
order = pyblish.api.CollectorOrder + 0.101
|
||||||
|
hosts = ["standalonepublisher"]
|
||||||
|
families = ["shot"]
|
||||||
|
|
||||||
|
# presets
|
||||||
|
shot_rename_template = None
|
||||||
|
shot_rename_search_patterns = None
|
||||||
|
shot_add_hierarchy = None
|
||||||
|
shot_add_tasks = None
|
||||||
|
|
||||||
|
def convert_to_entity(self, key, value):
|
||||||
|
# ftrack compatible entity types
|
||||||
|
types = {"shot": "Shot",
|
||||||
|
"folder": "Folder",
|
||||||
|
"episode": "Episode",
|
||||||
|
"sequence": "Sequence",
|
||||||
|
"track": "Sequence",
|
||||||
|
}
|
||||||
|
# convert to entity type
|
||||||
|
entity_type = types.get(key, None)
|
||||||
|
|
||||||
|
# return if any
|
||||||
|
if entity_type:
|
||||||
|
return {"entityType": entity_type, "entityName": value}
|
||||||
|
|
||||||
|
def rename_with_hierarchy(self, instance):
|
||||||
|
search_text = ""
|
||||||
|
parent_name = instance.context.data["assetEntity"]["name"]
|
||||||
|
clip = instance.data["item"]
|
||||||
|
clip_name = os.path.splitext(clip.name)[0].lower()
|
||||||
|
if self.shot_rename_search_patterns:
|
||||||
|
search_text += parent_name + clip_name
|
||||||
|
instance.data["anatomyData"].update({"clip_name": clip_name})
|
||||||
|
for type, pattern in self.shot_rename_search_patterns.items():
|
||||||
|
p = re.compile(pattern)
|
||||||
|
match = p.findall(search_text)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
instance.data["anatomyData"][type] = match[-1]
|
||||||
|
|
||||||
|
# format to new shot name
|
||||||
|
instance.data["asset"] = self.shot_rename_template.format(
|
||||||
|
**instance.data["anatomyData"])
|
||||||
|
|
||||||
|
def create_hierarchy(self, instance):
|
||||||
|
parents = list()
|
||||||
|
hierarchy = ""
|
||||||
|
visual_hierarchy = [instance.context.data["assetEntity"]]
|
||||||
|
while True:
|
||||||
|
visual_parent = io.find_one(
|
||||||
|
{"_id": visual_hierarchy[-1]["data"]["visualParent"]}
|
||||||
|
)
|
||||||
|
if visual_parent:
|
||||||
|
visual_hierarchy.append(visual_parent)
|
||||||
|
else:
|
||||||
|
visual_hierarchy.append(
|
||||||
|
instance.context.data["projectEntity"])
|
||||||
|
break
|
||||||
|
|
||||||
|
# add current selection context hierarchy from standalonepublisher
|
||||||
|
for entity in reversed(visual_hierarchy):
|
||||||
|
parents.append({
|
||||||
|
"entityType": entity["data"]["entityType"],
|
||||||
|
"entityName": entity["name"]
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.shot_add_hierarchy:
|
||||||
|
# fill the parents parts from presets
|
||||||
|
shot_add_hierarchy = self.shot_add_hierarchy.copy()
|
||||||
|
hierarchy_parents = shot_add_hierarchy["parents"].copy()
|
||||||
|
for parent in hierarchy_parents:
|
||||||
|
hierarchy_parents[parent] = hierarchy_parents[parent].format(
|
||||||
|
**instance.data["anatomyData"])
|
||||||
|
prnt = self.convert_to_entity(
|
||||||
|
parent, hierarchy_parents[parent])
|
||||||
|
parents.append(prnt)
|
||||||
|
|
||||||
|
hierarchy = shot_add_hierarchy[
|
||||||
|
"parents_path"].format(**hierarchy_parents)
|
||||||
|
|
||||||
|
instance.data["hierarchy"] = hierarchy
|
||||||
|
instance.data["parents"] = parents
|
||||||
|
self.log.debug(f"Hierarchy: {hierarchy}")
|
||||||
|
|
||||||
|
if self.shot_add_tasks:
|
||||||
|
instance.data["tasks"] = self.shot_add_tasks
|
||||||
|
else:
|
||||||
|
instance.data["tasks"] = list()
|
||||||
|
|
||||||
|
# updating hierarchy data
|
||||||
|
instance.data["anatomyData"].update({
|
||||||
|
"asset": instance.data["asset"],
|
||||||
|
"task": "conform"
|
||||||
|
})
|
||||||
|
|
||||||
|
def process(self, context):
|
||||||
|
for instance in context:
|
||||||
|
if instance.data["family"] in self.families:
|
||||||
|
self.processing_instance(instance)
|
||||||
|
|
||||||
|
def processing_instance(self, instance):
|
||||||
|
self.log.info(f"_ instance: {instance}")
|
||||||
|
# adding anatomyData for burnins
|
||||||
|
instance.data["anatomyData"] = instance.context.data["anatomyData"]
|
||||||
|
|
||||||
|
asset = instance.data["asset"]
|
||||||
|
assets_shared = instance.context.data.get("assetsShared")
|
||||||
|
|
||||||
|
frame_start = instance.data["frameStart"]
|
||||||
|
frame_end = instance.data["frameEnd"]
|
||||||
|
|
||||||
|
if self.shot_rename_template:
|
||||||
|
self.rename_with_hierarchy(instance)
|
||||||
|
|
||||||
|
self.create_hierarchy(instance)
|
||||||
|
|
||||||
|
shot_name = instance.data["asset"]
|
||||||
|
self.log.debug(f"Shot Name: {shot_name}")
|
||||||
|
|
||||||
|
if instance.data["hierarchy"] not in shot_name:
|
||||||
|
self.log.warning("wrong parent")
|
||||||
|
|
||||||
|
label = f"{shot_name} ({frame_start}-{frame_end})"
|
||||||
|
instance.data["label"] = label
|
||||||
|
|
||||||
|
# dealing with shared attributes across instances
|
||||||
|
# with the same asset name
|
||||||
|
if assets_shared.get(asset):
|
||||||
|
asset_shared = assets_shared.get(asset)
|
||||||
|
else:
|
||||||
|
asset_shared = assets_shared[asset]
|
||||||
|
|
||||||
|
asset_shared.update({
|
||||||
|
"asset": instance.data["asset"],
|
||||||
|
"hierarchy": instance.data["hierarchy"],
|
||||||
|
"parents": instance.data["parents"],
|
||||||
|
"tasks": instance.data["tasks"]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class CollectHierarchyContext(pyblish.api.ContextPlugin):
|
||||||
|
'''Collecting Hierarchy from instaces and building
|
||||||
|
context hierarchy tree
|
||||||
|
'''
|
||||||
|
|
||||||
|
label = "Collect Hierarchy Context"
|
||||||
|
order = pyblish.api.CollectorOrder + 0.102
|
||||||
|
hosts = ["standalonepublisher"]
|
||||||
|
|
||||||
|
def update_dict(self, ex_dict, new_dict):
|
||||||
|
for key in ex_dict:
|
||||||
|
if key in new_dict and isinstance(ex_dict[key], dict):
|
||||||
|
new_dict[key] = self.update_dict(ex_dict[key], new_dict[key])
|
||||||
|
else:
|
||||||
|
if ex_dict.get(key) and new_dict.get(key):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
new_dict[key] = ex_dict[key]
|
||||||
|
|
||||||
|
return new_dict
|
||||||
|
|
||||||
|
def process(self, context):
|
||||||
|
instances = context
|
||||||
|
# create hierarchyContext attr if context has none
|
||||||
|
assets_shared = context.data.get("assetsShared")
|
||||||
|
final_context = {}
|
||||||
|
for instance in instances:
|
||||||
|
if 'editorial' in instance.data.get('family', ''):
|
||||||
|
continue
|
||||||
|
# inject assetsShared to other instances with
|
||||||
|
# the same `assetShareName` attribute in data
|
||||||
|
asset_shared_name = instance.data.get("assetShareName")
|
||||||
|
|
||||||
|
s_asset_data = assets_shared.get(asset_shared_name)
|
||||||
|
if s_asset_data:
|
||||||
|
instance.data["asset"] = s_asset_data["asset"]
|
||||||
|
instance.data["parents"] = s_asset_data["parents"]
|
||||||
|
instance.data["hierarchy"] = s_asset_data["hierarchy"]
|
||||||
|
instance.data["tasks"] = s_asset_data["tasks"]
|
||||||
|
|
||||||
|
# generate hierarchy data only on shot instances
|
||||||
|
if 'shot' not in instance.data.get('family', ''):
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = instance.data["asset"]
|
||||||
|
|
||||||
|
# get handles
|
||||||
|
handle_start = int(instance.data["handleStart"])
|
||||||
|
handle_end = int(instance.data["handleEnd"])
|
||||||
|
|
||||||
|
in_info = {}
|
||||||
|
|
||||||
|
# suppose that all instances are Shots
|
||||||
|
in_info['entity_type'] = 'Shot'
|
||||||
|
|
||||||
|
# get custom attributes of the shot
|
||||||
|
|
||||||
|
in_info['custom_attributes'] = {
|
||||||
|
"handleStart": handle_start,
|
||||||
|
"handleEnd": handle_end,
|
||||||
|
"frameStart": instance.data["frameStart"],
|
||||||
|
"frameEnd": instance.data["frameEnd"],
|
||||||
|
"clipIn": instance.data["clipIn"],
|
||||||
|
"clipOut": instance.data["clipOut"],
|
||||||
|
'fps': instance.data["fps"]
|
||||||
|
}
|
||||||
|
|
||||||
|
in_info['tasks'] = instance.data['tasks']
|
||||||
|
|
||||||
|
parents = instance.data.get('parents', [])
|
||||||
|
|
||||||
|
actual = {name: in_info}
|
||||||
|
|
||||||
|
for parent in reversed(parents):
|
||||||
|
next_dict = {}
|
||||||
|
parent_name = parent["entityName"]
|
||||||
|
next_dict[parent_name] = {}
|
||||||
|
next_dict[parent_name]["entity_type"] = parent["entityType"]
|
||||||
|
next_dict[parent_name]["childs"] = actual
|
||||||
|
actual = next_dict
|
||||||
|
|
||||||
|
final_context = self.update_dict(final_context, actual)
|
||||||
|
|
||||||
|
# adding hierarchy context to instance
|
||||||
|
context.data["hierarchyContext"] = final_context
|
||||||
|
self.log.info("Hierarchy instance collected")
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
import opentimelineio as otio
|
|
||||||
from bson import json_util
|
|
||||||
|
|
||||||
import pyblish.api
|
|
||||||
from pype import lib
|
|
||||||
from avalon import io
|
|
||||||
|
|
||||||
|
|
||||||
class OTIO_View(pyblish.api.Action):
|
|
||||||
"""Currently disabled because OTIO requires PySide2. Issue on Qt.py:
|
|
||||||
https://github.com/PixarAnimationStudios/OpenTimelineIO/issues/289
|
|
||||||
"""
|
|
||||||
|
|
||||||
label = "OTIO View"
|
|
||||||
icon = "wrench"
|
|
||||||
on = "failed"
|
|
||||||
|
|
||||||
def process(self, context, plugin):
|
|
||||||
instance = context[0]
|
|
||||||
representation = instance.data["representations"][0]
|
|
||||||
file_path = os.path.join(
|
|
||||||
representation["stagingDir"], representation["files"]
|
|
||||||
)
|
|
||||||
lib._subprocess(["otioview", file_path])
|
|
||||||
|
|
||||||
|
|
||||||
class CollectShots(pyblish.api.InstancePlugin):
|
|
||||||
"""Collect Anatomy object into Context"""
|
|
||||||
|
|
||||||
order = pyblish.api.CollectorOrder
|
|
||||||
label = "Collect Shots"
|
|
||||||
hosts = ["standalonepublisher"]
|
|
||||||
families = ["editorial"]
|
|
||||||
actions = []
|
|
||||||
|
|
||||||
def process(self, instance):
|
|
||||||
representation = instance.data["representations"][0]
|
|
||||||
file_path = os.path.join(
|
|
||||||
representation["stagingDir"], representation["files"]
|
|
||||||
)
|
|
||||||
instance.context.data["editorialPath"] = file_path
|
|
||||||
|
|
||||||
extension = os.path.splitext(file_path)[1][1:]
|
|
||||||
kwargs = {}
|
|
||||||
if extension == "edl":
|
|
||||||
# EDL has no frame rate embedded so needs explicit frame rate else
|
|
||||||
# 24 is asssumed.
|
|
||||||
kwargs["rate"] = lib.get_asset()["data"]["fps"]
|
|
||||||
|
|
||||||
timeline = otio.adapters.read_from_file(file_path, **kwargs)
|
|
||||||
tracks = timeline.each_child(
|
|
||||||
descended_from_type=otio.schema.track.Track
|
|
||||||
)
|
|
||||||
asset_entity = instance.context.data["assetEntity"]
|
|
||||||
asset_name = asset_entity["name"]
|
|
||||||
|
|
||||||
# Ask user for sequence start. Usually 10:00:00:00.
|
|
||||||
sequence_start_frame = 900000
|
|
||||||
|
|
||||||
# Project specific prefix naming. This needs to be replaced with some
|
|
||||||
# options to be more flexible.
|
|
||||||
asset_name = asset_name.split("_")[0]
|
|
||||||
|
|
||||||
instances = []
|
|
||||||
for track in tracks:
|
|
||||||
track_start_frame = (
|
|
||||||
abs(track.source_range.start_time.value) - sequence_start_frame
|
|
||||||
)
|
|
||||||
for child in track.each_child():
|
|
||||||
|
|
||||||
# Transitions are ignored, because Clips have the full frame
|
|
||||||
# range.
|
|
||||||
if isinstance(child, otio.schema.transition.Transition):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if child.name is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Hardcoded to expect a shot name of "[name].[extension]"
|
|
||||||
child_name = os.path.splitext(child.name)[0].lower()
|
|
||||||
name = f"{asset_name}_{child_name}"
|
|
||||||
|
|
||||||
frame_start = track_start_frame
|
|
||||||
frame_start += child.range_in_parent().start_time.value
|
|
||||||
frame_end = track_start_frame
|
|
||||||
frame_end += child.range_in_parent().end_time_inclusive().value
|
|
||||||
|
|
||||||
label = f"{name} (framerange: {frame_start}-{frame_end})"
|
|
||||||
instances.append(
|
|
||||||
instance.context.create_instance(**{
|
|
||||||
"name": name,
|
|
||||||
"label": label,
|
|
||||||
"frameStart": frame_start,
|
|
||||||
"frameEnd": frame_end,
|
|
||||||
"family": "shot",
|
|
||||||
"families": ["review", "ftrack"],
|
|
||||||
"ftrackFamily": "review",
|
|
||||||
"asset": name,
|
|
||||||
"subset": "shotMain",
|
|
||||||
"representations": [],
|
|
||||||
"source": file_path
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
visual_hierarchy = [asset_entity]
|
|
||||||
while True:
|
|
||||||
visual_parent = io.find_one(
|
|
||||||
{"_id": visual_hierarchy[-1]["data"]["visualParent"]}
|
|
||||||
)
|
|
||||||
if visual_parent:
|
|
||||||
visual_hierarchy.append(visual_parent)
|
|
||||||
else:
|
|
||||||
visual_hierarchy.append(instance.context.data["projectEntity"])
|
|
||||||
break
|
|
||||||
|
|
||||||
context_hierarchy = None
|
|
||||||
for entity in visual_hierarchy:
|
|
||||||
childs = {}
|
|
||||||
if context_hierarchy:
|
|
||||||
name = context_hierarchy.pop("name")
|
|
||||||
childs = {name: context_hierarchy}
|
|
||||||
else:
|
|
||||||
for instance in instances:
|
|
||||||
childs[instance.data["name"]] = {
|
|
||||||
"childs": {},
|
|
||||||
"entity_type": "Shot",
|
|
||||||
"custom_attributes": {
|
|
||||||
"frameStart": instance.data["frameStart"],
|
|
||||||
"frameEnd": instance.data["frameEnd"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context_hierarchy = {
|
|
||||||
"entity_type": entity["data"]["entityType"],
|
|
||||||
"childs": childs,
|
|
||||||
"name": entity["name"]
|
|
||||||
}
|
|
||||||
|
|
||||||
name = context_hierarchy.pop("name")
|
|
||||||
context_hierarchy = {name: context_hierarchy}
|
|
||||||
instance.context.data["hierarchyContext"] = context_hierarchy
|
|
||||||
self.log.info(
|
|
||||||
"Hierarchy:\n" +
|
|
||||||
json_util.dumps(context_hierarchy, sort_keys=True, indent=4)
|
|
||||||
)
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
import clique
|
|
||||||
|
|
||||||
import pype.api
|
|
||||||
import pype.lib
|
|
||||||
|
|
||||||
|
|
||||||
class ExtractShot(pype.api.Extractor):
|
|
||||||
"""Extract shot "mov" and "wav" files."""
|
|
||||||
|
|
||||||
label = "Extract Shot"
|
|
||||||
hosts = ["standalonepublisher"]
|
|
||||||
families = ["shot"]
|
|
||||||
|
|
||||||
def process(self, instance):
|
|
||||||
staging_dir = self.staging_dir(instance)
|
|
||||||
self.log.info("Outputting shot to {}".format(staging_dir))
|
|
||||||
|
|
||||||
editorial_path = instance.context.data["editorialPath"]
|
|
||||||
basename = os.path.splitext(os.path.basename(editorial_path))[0]
|
|
||||||
|
|
||||||
# Generate mov file.
|
|
||||||
fps = pype.lib.get_asset()["data"]["fps"]
|
|
||||||
input_path = os.path.join(
|
|
||||||
os.path.dirname(editorial_path), basename + ".mov"
|
|
||||||
)
|
|
||||||
shot_mov = os.path.join(staging_dir, instance.data["name"] + ".mov")
|
|
||||||
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
|
|
||||||
args = [
|
|
||||||
ffmpeg_path,
|
|
||||||
"-ss", str(instance.data["frameStart"] / fps),
|
|
||||||
"-i", input_path,
|
|
||||||
"-t", str(
|
|
||||||
(instance.data["frameEnd"] - instance.data["frameStart"] + 1) /
|
|
||||||
fps
|
|
||||||
),
|
|
||||||
"-crf", "18",
|
|
||||||
"-pix_fmt", "yuv420p",
|
|
||||||
shot_mov
|
|
||||||
]
|
|
||||||
self.log.info(f"Processing: {args}")
|
|
||||||
output = pype.lib._subprocess(args)
|
|
||||||
self.log.info(output)
|
|
||||||
|
|
||||||
instance.data["representations"].append({
|
|
||||||
"name": "mov",
|
|
||||||
"ext": "mov",
|
|
||||||
"files": os.path.basename(shot_mov),
|
|
||||||
"stagingDir": staging_dir,
|
|
||||||
"frameStart": instance.data["frameStart"],
|
|
||||||
"frameEnd": instance.data["frameEnd"],
|
|
||||||
"fps": fps,
|
|
||||||
"thumbnail": True,
|
|
||||||
"tags": ["review", "ftrackreview"]
|
|
||||||
})
|
|
||||||
|
|
||||||
# Generate jpegs.
|
|
||||||
shot_jpegs = os.path.join(
|
|
||||||
staging_dir, instance.data["name"] + ".%04d.jpeg"
|
|
||||||
)
|
|
||||||
args = [ffmpeg_path, "-i", shot_mov, shot_jpegs]
|
|
||||||
self.log.info(f"Processing: {args}")
|
|
||||||
output = pype.lib._subprocess(args)
|
|
||||||
self.log.info(output)
|
|
||||||
|
|
||||||
collection = clique.Collection(
|
|
||||||
head=instance.data["name"] + ".", tail='.jpeg', padding=4
|
|
||||||
)
|
|
||||||
for f in os.listdir(staging_dir):
|
|
||||||
if collection.match(f):
|
|
||||||
collection.add(f)
|
|
||||||
|
|
||||||
instance.data["representations"].append({
|
|
||||||
"name": "jpeg",
|
|
||||||
"ext": "jpeg",
|
|
||||||
"files": list(collection),
|
|
||||||
"stagingDir": staging_dir
|
|
||||||
})
|
|
||||||
|
|
||||||
# Generate wav file.
|
|
||||||
shot_wav = os.path.join(staging_dir, instance.data["name"] + ".wav")
|
|
||||||
args = [ffmpeg_path, "-i", shot_mov, shot_wav]
|
|
||||||
self.log.info(f"Processing: {args}")
|
|
||||||
output = pype.lib._subprocess(args)
|
|
||||||
self.log.info(output)
|
|
||||||
|
|
||||||
instance.data["representations"].append({
|
|
||||||
"name": "wav",
|
|
||||||
"ext": "wav",
|
|
||||||
"files": os.path.basename(shot_wav),
|
|
||||||
"stagingDir": staging_dir
|
|
||||||
})
|
|
||||||
|
|
||||||
# Required for extract_review plugin (L222 onwards).
|
|
||||||
instance.data["fps"] = fps
|
|
||||||
123
pype/plugins/standalonepublisher/publish/extract_shot_data.py
Normal file
123
pype/plugins/standalonepublisher/publish/extract_shot_data.py
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
import os
|
||||||
|
import clique
|
||||||
|
import pype.api
|
||||||
|
|
||||||
|
from pprint import pformat
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractShotData(pype.api.Extractor):
|
||||||
|
"""Extract shot "mov" and "wav" files."""
|
||||||
|
|
||||||
|
label = "Extract Shot Data"
|
||||||
|
hosts = ["standalonepublisher"]
|
||||||
|
families = ["review", "audio"]
|
||||||
|
|
||||||
|
# presets
|
||||||
|
add_representation = None # ".jpeg"
|
||||||
|
|
||||||
|
def process(self, instance):
|
||||||
|
representation = instance.data.get("representations")
|
||||||
|
self.log.debug(f"_ representation: {representation}")
|
||||||
|
|
||||||
|
if not representation:
|
||||||
|
instance.data["representations"] = list()
|
||||||
|
|
||||||
|
# get ffmpet path
|
||||||
|
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
|
||||||
|
|
||||||
|
# get staging dir
|
||||||
|
staging_dir = self.staging_dir(instance)
|
||||||
|
self.log.info("Staging dir set to: `{}`".format(staging_dir))
|
||||||
|
|
||||||
|
# Generate mov file.
|
||||||
|
fps = instance.data["fps"]
|
||||||
|
video_file_path = instance.data["editorialVideoPath"]
|
||||||
|
ext = instance.data.get("extension", ".mov")
|
||||||
|
|
||||||
|
clip_trimed_path = os.path.join(
|
||||||
|
staging_dir, instance.data["name"] + ext)
|
||||||
|
#
|
||||||
|
# # check video file metadata
|
||||||
|
# input_data = plib.ffprobe_streams(video_file_path)[0]
|
||||||
|
# self.log.debug(f"__ input_data: `{input_data}`")
|
||||||
|
|
||||||
|
start = float(instance.data["clipInH"])
|
||||||
|
dur = float(instance.data["clipDurationH"])
|
||||||
|
|
||||||
|
if ext in ".wav":
|
||||||
|
start += 0.5
|
||||||
|
|
||||||
|
args = [
|
||||||
|
ffmpeg_path,
|
||||||
|
"-ss", str(start / fps),
|
||||||
|
"-i", f"\"{video_file_path}\"",
|
||||||
|
"-t", str(dur / fps)
|
||||||
|
]
|
||||||
|
if ext in [".mov", ".mp4"]:
|
||||||
|
args.extend([
|
||||||
|
"-crf", "18",
|
||||||
|
"-pix_fmt", "yuv420p"])
|
||||||
|
elif ext in ".wav":
|
||||||
|
args.extend([
|
||||||
|
"-vn -acodec pcm_s16le",
|
||||||
|
"-ar 48000 -ac 2"
|
||||||
|
])
|
||||||
|
|
||||||
|
# add output path
|
||||||
|
args.append(f"\"{clip_trimed_path}\"")
|
||||||
|
|
||||||
|
self.log.info(f"Processing: {args}")
|
||||||
|
ffmpeg_args = " ".join(args)
|
||||||
|
output = pype.api.subprocess(ffmpeg_args)
|
||||||
|
self.log.info(output)
|
||||||
|
|
||||||
|
repr = {
|
||||||
|
"name": ext[1:],
|
||||||
|
"ext": ext[1:],
|
||||||
|
"files": os.path.basename(clip_trimed_path),
|
||||||
|
"stagingDir": staging_dir,
|
||||||
|
"frameStart": int(instance.data["frameStart"]),
|
||||||
|
"frameEnd": int(instance.data["frameEnd"]),
|
||||||
|
"frameStartFtrack": int(instance.data["frameStartH"]),
|
||||||
|
"frameEndFtrack": int(instance.data["frameEndH"]),
|
||||||
|
"fps": fps,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ext[1:] in ["mov", "mp4"]:
|
||||||
|
repr.update({
|
||||||
|
"thumbnail": True,
|
||||||
|
"tags": ["review", "ftrackreview", "delete"]})
|
||||||
|
|
||||||
|
instance.data["representations"].append(repr)
|
||||||
|
|
||||||
|
if self.add_representation:
|
||||||
|
# Generate jpegs.
|
||||||
|
clip_img_sequence = os.path.join(
|
||||||
|
staging_dir, instance.data["name"] + ".%04d.jpeg"
|
||||||
|
)
|
||||||
|
args = [
|
||||||
|
ffmpeg_path, "-i",
|
||||||
|
f"\"{clip_trimed_path}\"",
|
||||||
|
f"\"{clip_img_sequence}\""
|
||||||
|
]
|
||||||
|
self.log.info(f"Processing: {args}")
|
||||||
|
output = pype.lib._subprocess(args)
|
||||||
|
self.log.info(output)
|
||||||
|
|
||||||
|
# collect jpeg sequence if editorial data for publish
|
||||||
|
# are image sequence
|
||||||
|
collection = clique.Collection(
|
||||||
|
head=instance.data["name"] + ".", tail='.jpeg', padding=4
|
||||||
|
)
|
||||||
|
for f in os.listdir(staging_dir):
|
||||||
|
if collection.match(f):
|
||||||
|
collection.add(f)
|
||||||
|
|
||||||
|
instance.data["representations"].append({
|
||||||
|
"name": "jpeg",
|
||||||
|
"ext": "jpeg",
|
||||||
|
"files": list(collection),
|
||||||
|
"stagingDir": staging_dir
|
||||||
|
})
|
||||||
|
|
||||||
|
self.log.debug(f"Instance data: {pformat(instance.data)}")
|
||||||
|
|
@ -9,20 +9,10 @@ class ValidateEditorialResources(pyblish.api.InstancePlugin):
|
||||||
|
|
||||||
label = "Validate Editorial Resources"
|
label = "Validate Editorial Resources"
|
||||||
hosts = ["standalonepublisher"]
|
hosts = ["standalonepublisher"]
|
||||||
families = ["editorial"]
|
families = ["audio", "review"]
|
||||||
order = pype.api.ValidateContentsOrder
|
order = pype.api.ValidateContentsOrder
|
||||||
|
|
||||||
def process(self, instance):
|
def process(self, instance):
|
||||||
representation = instance.data["representations"][0]
|
check_file = instance.data["editorialVideoPath"]
|
||||||
staging_dir = representation["stagingDir"]
|
msg = f"Missing \"{check_file}\"."
|
||||||
basename = os.path.splitext(
|
assert check_file, msg
|
||||||
os.path.basename(representation["files"])
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
files = [x for x in os.listdir(staging_dir)]
|
|
||||||
|
|
||||||
# Check for "mov" file.
|
|
||||||
filename = basename + ".mov"
|
|
||||||
filepath = os.path.join(staging_dir, filename)
|
|
||||||
msg = f"Missing \"{filepath}\"."
|
|
||||||
assert filename in files, msg
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import pyblish.api
|
||||||
import pype.api
|
import pype.api
|
||||||
|
|
||||||
|
|
||||||
class ValidateShots(pyblish.api.ContextPlugin):
|
class ValidateShotDuplicates(pyblish.api.ContextPlugin):
|
||||||
"""Validate there is a "mov" next to the editorial file."""
|
"""Validating no duplicate names are in context."""
|
||||||
|
|
||||||
label = "Validate Shots"
|
label = "Validate Shot Duplicates"
|
||||||
hosts = ["standalonepublisher"]
|
hosts = ["standalonepublisher"]
|
||||||
order = pype.api.ValidateContentsOrder
|
order = pype.api.ValidateContentsOrder
|
||||||
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
__version__ = "2.11.3"
|
__version__ = "2.11.4"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue