mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #258 from tokejepsen/2.x/feature/standalone_editorial
Standalone editorial
This commit is contained in:
commit
5ff939c283
8 changed files with 277 additions and 8 deletions
|
|
@ -44,10 +44,14 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
|
||||
family = instance.data['family'].lower()
|
||||
|
||||
asset_type = ''
|
||||
asset_type = instance.data.get(
|
||||
"ftrackFamily", self.family_mapping[family]
|
||||
)
|
||||
asset_type = instance.data.get("ftrackFamily")
|
||||
if not asset_type and family in self.family_mapping:
|
||||
asset_type = self.family_mapping[family]
|
||||
|
||||
# Ignore this instance if neither "ftrackFamily" or a family mapping is
|
||||
# found.
|
||||
if not asset_type:
|
||||
return
|
||||
|
||||
componentList = []
|
||||
ft_session = instance.context.data["ftrackSession"]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
|
|||
|
||||
order = pyblish.api.ExtractorOrder - 0.01
|
||||
label = "Extract Hierarchy To Avalon"
|
||||
families = ["clip", "shot"]
|
||||
families = ["clip", "shot", "editorial"]
|
||||
|
||||
def process(self, context):
|
||||
if "hierarchyContext" not in context.data:
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
"fbx",
|
||||
"textures",
|
||||
"action",
|
||||
"harmony.template"
|
||||
"harmony.template",
|
||||
"editorial"
|
||||
]
|
||||
exclude_families = ["clip"]
|
||||
db_representation_context_keys = [
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class ExtractRender(pyblish.api.InstancePlugin):
|
|||
"frameEnd": frame_end,
|
||||
"fps": frame_rate,
|
||||
"preview": True,
|
||||
"tags": ["review"]
|
||||
"tags": ["review", "ftrackreview"]
|
||||
}
|
||||
thumbnail = {
|
||||
"name": "thumbnail",
|
||||
|
|
|
|||
141
pype/plugins/standalonepublisher/publish/collect_shots.py
Normal file
141
pype/plugins/standalonepublisher/publish/collect_shots.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
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"]
|
||||
|
||||
# Project specific prefix naming. This needs to be replaced with some
|
||||
# options to be more flexible.
|
||||
asset_name = asset_name.split("_")[0]
|
||||
|
||||
shot_number = 10
|
||||
for track in tracks:
|
||||
self.log.info(track)
|
||||
|
||||
if "audio" in track.name.lower():
|
||||
continue
|
||||
|
||||
instances = []
|
||||
for child in track.each_child():
|
||||
|
||||
# Transitions are ignored, because Clips have the full frame
|
||||
# range.
|
||||
if isinstance(child, otio.schema.transition.Transition):
|
||||
continue
|
||||
|
||||
frame_start = child.range_in_parent().start_time.value
|
||||
frame_end = child.range_in_parent().end_time_inclusive().value
|
||||
|
||||
name = f"{asset_name}_sh{shot_number:04}"
|
||||
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
|
||||
})
|
||||
)
|
||||
|
||||
shot_number += 10
|
||||
|
||||
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)
|
||||
)
|
||||
|
|
@ -42,7 +42,7 @@ class ExtractReviewSP(pyblish.api.InstancePlugin):
|
|||
self.log.debug("Families In: `{}`".format(instance.data["families"]))
|
||||
|
||||
# get specific profile if was defined
|
||||
specific_profiles = instance.data.get("repreProfiles")
|
||||
specific_profiles = instance.data.get("repreProfiles", [])
|
||||
|
||||
new_repres = []
|
||||
# filter out mov and img sequences
|
||||
|
|
|
|||
95
pype/plugins/standalonepublisher/publish/extract_shot.py
Normal file
95
pype/plugins/standalonepublisher/publish/extract_shot.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
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")
|
||||
args = [
|
||||
"ffmpeg",
|
||||
"-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", "-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", "-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
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
import pype.api
|
||||
|
||||
|
||||
class ValidateEditorialResources(pyblish.api.InstancePlugin):
|
||||
"""Validate there is a "mov" next to the editorial file."""
|
||||
|
||||
label = "Validate Editorial Resources"
|
||||
hosts = ["standalonepublisher"]
|
||||
families = ["editorial"]
|
||||
order = pype.api.ValidateContentsOrder
|
||||
|
||||
def process(self, instance):
|
||||
representation = instance.data["representations"][0]
|
||||
staging_dir = representation["stagingDir"]
|
||||
basename = os.path.splitext(
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue