From 90da9ca89613a3bdf1d3baea634741ab871e5766 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Nov 2020 11:26:26 +0100 Subject: [PATCH 01/72] fix(resolve): improving bits --- pype/hosts/resolve/plugin.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index 72eec04896..4be17870cc 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -7,6 +7,7 @@ from pype.api import config from Qt import QtWidgets, QtCore + class CreatorWidget(QtWidgets.QDialog): # output items @@ -132,8 +133,8 @@ class CreatorWidget(QtWidgets.QDialog): return item def add_presets_to_layout(self, content_layout, data): - for k, v in data.items(): - if isinstance(v, dict): + for _key, _val in data.items(): + if isinstance(_val, dict): # adding spacer between sections self.content_widget.append(QtWidgets.QWidget(self)) devider = QtWidgets.QVBoxLayout(self.content_widget[-1]) @@ -147,18 +148,19 @@ class CreatorWidget(QtWidgets.QDialog): nested_content_layout.setObjectName("NestedContentLayout") # add nested key as label - self.create_row(nested_content_layout, "QLabel", k) - data[k] = self.add_presets_to_layout(nested_content_layout, v) - elif isinstance(v, str): - print(f"layout.str: {k}") - print(f"content_layout: {content_layout}") - data[k] = self.create_row( - content_layout, "QLineEdit", k, setText=v) - elif isinstance(v, int): - print(f"layout.int: {k}") - print(f"content_layout: {content_layout}") - data[k] = self.create_row( - content_layout, "QSpinBox", k, setValue=v) + self.create_row(nested_content_layout, "QLabel", _key) + data[_key] = self.add_presets_to_layout( + nested_content_layout, _val) + elif isinstance(_val, str): + log.debug("layout.str: {}".format(_key)) + log.debug("content_layout: {}".format(content_layout)) + data[_key] = self.create_row( + content_layout, "QLineEdit", _key, setText=_val) + elif isinstance(_val, int): + log.debug("layout.int: {}".format(_key)) + log.debug("content_layout: {}".format(content_layout)) + data[_key] = self.create_row( + content_layout, "QSpinBox", _key, setValue=_val) return data From 8eed57908d477e894443da85f780454a64d2e585 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Nov 2020 14:03:09 +0100 Subject: [PATCH 02/72] feat(resolve): adding otio and sync util script --- .../resolve/utility_scripts/OTIO_export.py | 185 ++++++++++++++++++ .../utility_scripts/PYPE_sync_util_scripts.py | 16 ++ 2 files changed, 201 insertions(+) create mode 100644 pype/hosts/resolve/utility_scripts/OTIO_export.py create mode 100644 pype/hosts/resolve/utility_scripts/PYPE_sync_util_scripts.py diff --git a/pype/hosts/resolve/utility_scripts/OTIO_export.py b/pype/hosts/resolve/utility_scripts/OTIO_export.py new file mode 100644 index 0000000000..a0c8e80bc7 --- /dev/null +++ b/pype/hosts/resolve/utility_scripts/OTIO_export.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python +import os +import sys +import opentimelineio as otio +print(otio) +resolve = bmd.scriptapp("Resolve") +fu = resolve.Fusion() + +ui = fu.UIManager +disp = bmd.UIDispatcher(fu.UIManager) + +TRACK_TYPES = { + "video": otio.schema.TrackKind.Video, + "audio": otio.schema.TrackKind.Audio +} + +print(resolve) + +def _create_rational_time(frame, fps): + return otio.opentime.RationalTime( + float(frame), + float(fps) + ) + + +def _create_time_range(start, duration, fps): + return otio.opentime.TimeRange( + start_time=_create_rational_time(start, fps), + duration=_create_rational_time(duration, fps) + ) + + +def _create_reference(mp_item): + return otio.schema.ExternalReference( + target_url=mp_item.GetClipProperty("File Path").get("File Path"), + available_range=_create_time_range( + mp_item.GetClipProperty("Start").get("Start"), + mp_item.GetClipProperty("Frames").get("Frames"), + mp_item.GetClipProperty("FPS").get("FPS") + ) + ) + + +def _create_markers(tl_item, frame_rate): + tl_markers = tl_item.GetMarkers() + markers = [] + for m_frame in tl_markers: + markers.append( + otio.schema.Marker( + name=tl_markers[m_frame]["name"], + marked_range=_create_time_range( + m_frame, + tl_markers[m_frame]["duration"], + frame_rate + ), + color=tl_markers[m_frame]["color"].upper(), + metadata={"Resolve": {"note": tl_markers[m_frame]["note"]}} + ) + ) + return markers + + +def _create_clip(tl_item): + mp_item = tl_item.GetMediaPoolItem() + frame_rate = mp_item.GetClipProperty("FPS").get("FPS") + clip = otio.schema.Clip( + name=tl_item.GetName(), + source_range=_create_time_range( + tl_item.GetLeftOffset(), + tl_item.GetDuration(), + frame_rate + ), + media_reference=_create_reference(mp_item) + ) + for marker in _create_markers(tl_item, frame_rate): + clip.markers.append(marker) + return clip + + +def _create_gap(gap_start, clip_start, tl_start_frame, frame_rate): + return otio.schema.Gap( + source_range=_create_time_range( + gap_start, + (clip_start - tl_start_frame) - gap_start, + frame_rate + ) + ) + + +def _create_ot_timeline(output_path): + if not output_path: + return + project_manager = resolve.GetProjectManager() + current_project = project_manager.GetCurrentProject() + dr_timeline = current_project.GetCurrentTimeline() + ot_timeline = otio.schema.Timeline(name=dr_timeline.GetName()) + for track_type in list(TRACK_TYPES.keys()): + track_count = dr_timeline.GetTrackCount(track_type) + for track_index in range(1, int(track_count) + 1): + ot_track = otio.schema.Track( + name="{}{}".format(track_type[0].upper(), track_index), + kind=TRACK_TYPES[track_type] + ) + tl_items = dr_timeline.GetItemListInTrack(track_type, track_index) + for tl_item in tl_items: + if tl_item.GetMediaPoolItem() is None: + continue + clip_start = tl_item.GetStart() - dr_timeline.GetStartFrame() + if clip_start > ot_track.available_range().duration.value: + ot_track.append( + _create_gap( + ot_track.available_range().duration.value, + tl_item.GetStart(), + dr_timeline.GetStartFrame(), + current_project.GetSetting("timelineFrameRate") + ) + ) + ot_track.append(_create_clip(tl_item)) + ot_timeline.tracks.append(ot_track) + otio.adapters.write_to_file( + ot_timeline, "{}/{}.otio".format(output_path, dr_timeline.GetName())) + + +title_font = ui.Font({"PixelSize": 18}) +dlg = disp.AddWindow( + { + "WindowTitle": "Export OTIO", + "ID": "OTIOwin", + "Geometry": [250, 250, 250, 100], + "Spacing": 0, + "Margin": 10 + }, + [ + ui.VGroup( + { + "Spacing": 2 + }, + [ + ui.Button( + { + "ID": "exportfilebttn", + "Text": "Select Destination", + "Weight": 1.25, + "ToolTip": "Choose where to save the otio", + "Flat": False + } + ), + ui.VGap(), + ui.Button( + { + "ID": "exportbttn", + "Text": "Export", + "Weight": 2, + "ToolTip": "Export the current timeline", + "Flat": False + } + ) + ] + ) + ] +) + +itm = dlg.GetItems() + + +def _close_window(event): + disp.ExitLoop() + + +def _export_button(event): + _create_ot_timeline(itm["exportfilebttn"].Text) + _close_window(None) + + +def _export_file_pressed(event): + selectedPath = fu.RequestDir(os.path.expanduser("~/Documents")) + itm["exportfilebttn"].Text = selectedPath + + +dlg.On.OTIOwin.Close = _close_window +dlg.On.exportfilebttn.Clicked = _export_file_pressed +dlg.On.exportbttn.Clicked = _export_button +dlg.Show() +disp.RunLoop() +dlg.Hide() diff --git a/pype/hosts/resolve/utility_scripts/PYPE_sync_util_scripts.py b/pype/hosts/resolve/utility_scripts/PYPE_sync_util_scripts.py new file mode 100644 index 0000000000..ee4905033b --- /dev/null +++ b/pype/hosts/resolve/utility_scripts/PYPE_sync_util_scripts.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +import os +import sys +import pype +import pype.hosts.resolve as bmdvr + + +def main(env): + # Registers pype's Global pyblish plugins + pype.install() + bmdvr.setup(env) + + +if __name__ == "__main__": + result = main(os.environ) + sys.exit(not bool(result)) From 09f175afab35473ee0d63a3a1a5ea8bd80acb66e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Nov 2020 15:00:30 +0100 Subject: [PATCH 03/72] fix(resolve): code improvemenets --- pype/hosts/resolve/plugin.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index 4be17870cc..c5d4c1d3e5 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -7,7 +7,6 @@ from pype.api import config from Qt import QtWidgets, QtCore - class CreatorWidget(QtWidgets.QDialog): # output items @@ -87,12 +86,10 @@ class CreatorWidget(QtWidgets.QDialog): data[k] = self.value(v) elif getattr(v, "value", None): print(f"normal int: {k}") - result = v.value() - data[k] = result() + data[k] = v.value() else: print(f"normal text: {k}") - result = v.text() - data[k] = result() + data[k] = v.text() return data def camel_case_split(self, text): @@ -152,13 +149,13 @@ class CreatorWidget(QtWidgets.QDialog): data[_key] = self.add_presets_to_layout( nested_content_layout, _val) elif isinstance(_val, str): - log.debug("layout.str: {}".format(_key)) - log.debug("content_layout: {}".format(content_layout)) + print("layout.str: {}".format(_key)) + print("content_layout: {}".format(content_layout)) data[_key] = self.create_row( content_layout, "QLineEdit", _key, setText=_val) elif isinstance(_val, int): - log.debug("layout.int: {}".format(_key)) - log.debug("content_layout: {}".format(content_layout)) + print("layout.int: {}".format(_key)) + print("content_layout: {}".format(content_layout)) data[_key] = self.create_row( content_layout, "QSpinBox", _key, setValue=_val) return data From 6f9c56d03188bb02a0775acd9f06c198f5cdfe2b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Nov 2020 17:01:04 +0100 Subject: [PATCH 04/72] feat(resolve): wip new creator --- pype/hosts/resolve/__init__.py | 2 + pype/hosts/resolve/lib.py | 16 ++ pype/hosts/resolve/plugin.py | 187 +++++++++---- .../utility_scripts/PYPE_sync_util_scripts.py | 2 +- .../utility_scripts/resolve_dev_scriping.py | 22 ++ .../resolve/create/create_shot_clip_new.py | 252 ++++++++++++++++++ 6 files changed, 427 insertions(+), 54 deletions(-) create mode 100644 pype/hosts/resolve/utility_scripts/resolve_dev_scriping.py create mode 100644 pype/plugins/resolve/create/create_shot_clip_new.py diff --git a/pype/hosts/resolve/__init__.py b/pype/hosts/resolve/__init__.py index c8f45259ff..8b0ca774b5 100644 --- a/pype/hosts/resolve/__init__.py +++ b/pype/hosts/resolve/__init__.py @@ -17,6 +17,7 @@ from .lib import ( get_project_manager, get_current_project, get_current_sequence, + get_video_track_names, get_current_track_items, create_current_sequence_media_bin, create_compound_clip, @@ -60,6 +61,7 @@ __all__ = [ "get_project_manager", "get_current_project", "get_current_sequence", + "get_video_track_names", "get_current_track_items", "create_current_sequence_media_bin", "create_compound_clip", diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index deb4fa6339..e36dc1bb15 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -35,6 +35,22 @@ def get_current_sequence(): return project.GetCurrentTimeline() +def get_video_track_names(): + tracks = list() + track_type = "video" + sequence = get_current_sequence() + + # get all tracks count filtered by track type + selected_track_count = sequence.GetTrackCount(track_type) + + # loop all tracks and get items + for track_index in range(1, (int(selected_track_count) + 1)): + track_name = sequence.GetTrackName("video", track_index) + tracks.append(track_name) + + return tracks + + def get_current_track_items( filter=False, track_type=None, diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index c5d4c1d3e5..a652fbfe64 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -12,7 +12,7 @@ class CreatorWidget(QtWidgets.QDialog): # output items items = dict() - def __init__(self, name, info, presets, parent=None): + def __init__(self, name, info, ui_inputs, parent=None): super(CreatorWidget, self).__init__(parent) self.setObjectName(name) @@ -25,6 +25,7 @@ class CreatorWidget(QtWidgets.QDialog): | 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)] @@ -35,14 +36,25 @@ class CreatorWidget(QtWidgets.QDialog): # first add widget tag line top_layout.addWidget(QtWidgets.QLabel(info)) - top_layout.addWidget(Spacer(5, self)) - # main dynamic layout - self.content_widget.append(QtWidgets.QWidget(self)) - content_layout = QtWidgets.QFormLayout(self.content_widget[-1]) + 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.add_presets_to_layout(content_layout, presets) + self.items = self.populate_widgets(ui_inputs) + self.scroll_area.setWidget(scroll_widget) # Confirmation buttons btns_widget = QtWidgets.QWidget(self) @@ -79,18 +91,33 @@ class CreatorWidget(QtWidgets.QDialog): self.result = None self.close() - def value(self, data): + def value(self, data, new_data=None): + new_data = new_data or dict() for k, v in data.items(): - if isinstance(v, dict): - print(f"nested: {k}") - data[k] = self.value(v) - elif getattr(v, "value", None): - print(f"normal int: {k}") - data[k] = v.value() - else: - print(f"normal text: {k}") - data[k] = v.text() - return data + 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( @@ -129,35 +156,103 @@ class CreatorWidget(QtWidgets.QDialog): return item - def add_presets_to_layout(self, content_layout, data): - for _key, _val in data.items(): - if isinstance(_val, dict): + 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 = list(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_widget.append(QtWidgets.QWidget(self)) - devider = QtWidgets.QVBoxLayout(self.content_widget[-1]) - devider.addWidget(Spacer(5, self)) - devider.setObjectName("Devider") + 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_widget.append(QtWidgets.QWidget(self)) + self.content_layout.append(QtWidgets.QWidget(self)) + self.content_layout[-1].setObjectName("sectionContent") + nested_content_layout = QtWidgets.QFormLayout( - self.content_widget[-1]) + self.content_layout[-1]) nested_content_layout.setObjectName("NestedContentLayout") + content_layout.addWidget(self.content_layout[-1]) # add nested key as label - self.create_row(nested_content_layout, "QLabel", _key) - data[_key] = self.add_presets_to_layout( - nested_content_layout, _val) - elif isinstance(_val, str): - print("layout.str: {}".format(_key)) - print("content_layout: {}".format(content_layout)) - data[_key] = self.create_row( - content_layout, "QLineEdit", _key, setText=_val) - elif isinstance(_val, int): - print("layout.int: {}".format(_key)) - print("content_layout: {}".format(content_layout)) - data[_key] = self.create_row( - content_layout, "QSpinBox", _key, setValue=_val) + 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 @@ -178,20 +273,6 @@ class Spacer(QtWidgets.QWidget): 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(api.Loader): """A basic SequenceLoader for Resolve diff --git a/pype/hosts/resolve/utility_scripts/PYPE_sync_util_scripts.py b/pype/hosts/resolve/utility_scripts/PYPE_sync_util_scripts.py index ee4905033b..753bddc1da 100644 --- a/pype/hosts/resolve/utility_scripts/PYPE_sync_util_scripts.py +++ b/pype/hosts/resolve/utility_scripts/PYPE_sync_util_scripts.py @@ -2,10 +2,10 @@ import os import sys import pype -import pype.hosts.resolve as bmdvr def main(env): + import pype.hosts.resolve as bmdvr # Registers pype's Global pyblish plugins pype.install() bmdvr.setup(env) diff --git a/pype/hosts/resolve/utility_scripts/resolve_dev_scriping.py b/pype/hosts/resolve/utility_scripts/resolve_dev_scriping.py new file mode 100644 index 0000000000..bd9fe593e0 --- /dev/null +++ b/pype/hosts/resolve/utility_scripts/resolve_dev_scriping.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + + +def main(): + import pype.hosts.resolve as bmdvr + bmdvr.utils.get_resolve_module() + + tracks = list() + track_type = "video" + sequence = bmdvr.get_current_sequence() + + # get all tracks count filtered by track type + selected_track_count = sequence.GetTrackCount(track_type) + + # loop all tracks and get items + for track_index in range(1, (int(selected_track_count) + 1)): + track_name = sequence.GetTrackName("video", track_index) + tracks.append(track_name) + + +if __name__ == "__main__": + main() diff --git a/pype/plugins/resolve/create/create_shot_clip_new.py b/pype/plugins/resolve/create/create_shot_clip_new.py new file mode 100644 index 0000000000..03a3041089 --- /dev/null +++ b/pype/plugins/resolve/create/create_shot_clip_new.py @@ -0,0 +1,252 @@ +from pprint import pformat +from pype.hosts import resolve +from pype.hosts.resolve import lib + + +class CreateShotClipNew(resolve.Creator): + """Publishable clip""" + + label = "Create Publishable Clip [New]" + family = "clip" + icon = "film" + defaults = ["Main"] + + gui_tracks = resolve.get_video_track_names() + 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": ["", "main", "bg", "fg", "bg", + "animatic"], + "type": "QComboBox", + "label": "Subset Name", + "target": "ui", + "toolTip": "chose subset name patern, if 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] + + print(pformat(self.gui_inputs)) + # 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() From 6448380312eea365b376774f09c458e739a5c0b5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Nov 2020 18:29:29 +0100 Subject: [PATCH 05/72] feat(hiero): wip new crator --- pype/hosts/resolve/__init__.py | 16 +- pype/hosts/resolve/lib.py | 123 +++++++- pype/hosts/resolve/pipeline.py | 100 +++++- pype/hosts/resolve/plugin.py | 293 ++++++++++++++++++ .../resolve/create/create_shot_clip_new.py | 31 +- 5 files changed, 538 insertions(+), 25 deletions(-) diff --git a/pype/hosts/resolve/__init__.py b/pype/hosts/resolve/__init__.py index 8b0ca774b5..b8457438c6 100644 --- a/pype/hosts/resolve/__init__.py +++ b/pype/hosts/resolve/__init__.py @@ -19,6 +19,11 @@ from .lib import ( get_current_sequence, get_video_track_names, get_current_track_items, + get_track_item_pype_tag, + set_track_item_pype_tag, + imprint, + set_publish_attribute, + get_publish_attribute, create_current_sequence_media_bin, create_compound_clip, swap_clips, @@ -28,7 +33,10 @@ from .lib import ( from .menu import launch_pype_menu -from .plugin import Creator +from .plugin import ( + Creator, + PublishClip +) from .workio import ( open_file, @@ -63,6 +71,11 @@ __all__ = [ "get_current_sequence", "get_video_track_names", "get_current_track_items", + "get_track_item_pype_tag", + "set_track_item_pype_tag", + "imprint", + "set_publish_attribute", + "get_publish_attribute", "create_current_sequence_media_bin", "create_compound_clip", "swap_clips", @@ -74,6 +87,7 @@ __all__ = [ # plugin "Creator", + "PublishClip", # workio "open_file", diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index e36dc1bb15..4052fa74fd 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -1,5 +1,6 @@ import sys import json +import ast from opentimelineio import opentime from pprint import pformat @@ -11,7 +12,7 @@ self = sys.modules[__name__] self.pm = None self.rename_index = 0 self.rename_add = 0 -self.pype_metadata_key = "VFX Notes" +self.pype_tag_name = "VFX Notes" def get_project_manager(): @@ -93,13 +94,121 @@ def get_current_track_items( if filter is True: if selecting_color in ti_color: selected_clips.append(data) - # ti.ClearClipColor() else: selected_clips.append(data) return selected_clips +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 + """ + return_tag = None + # get all tags from track item + _tags = track_item.GetMetadata() + if not _tags: + return None + for key, data in _tags.items(): + # return only correct tag defined by global name + if key in self.pype_tag_name: + return_tag = json.loads(data) + + return 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.update(tag_data) + track_item.SetMetadata(self.pype_tag_name, json.dumps(_tag)) + return _tag + else: + # if pype tag available then update with input data + # add it to the input track item + track_item.SetMetadata(self.pype_tag_name, json.dumps(tag_data)) + return tag_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 {} + + set_track_item_pype_tag(track_item, data) + + # add publish attribute + set_publish_attribute(track_item, True) + + +def set_publish_attribute(track_item, value): + """ Set Publish attribute in input Tag object + + Attribute: + tag (hiero.core.Tag): a tag object + value (bool): True or False + """ + tag_data = get_track_item_pype_tag(track_item) + tag_data["publish"] = str(value) + # set data to the publish attribute + set_track_item_pype_tag(track_item, tag_data) + + +def get_publish_attribute(track_item): + """ Get Publish attribute from input Tag object + + Attribute: + tag (hiero.core.Tag): a tag object + value (bool): True or False + """ + tag_data = get_track_item_pype_tag(track_item) + value = tag_data["publish"] + + # return value converted to bool value. Atring is stored in tag. + return ast.literal_eval(value) + + def create_current_sequence_media_bin(sequence): seq_name = sequence.GetName() media_pool = get_current_project().GetMediaPool() @@ -299,9 +408,9 @@ def create_compound_clip(clip_data, folder, rename=False, **kwargs): project.SetCurrentTimeline(sq_origin) # Add collected metadata and attributes to the comound clip: - if mp_item.GetMetadata(self.pype_metadata_key): - clip_attributes[self.pype_metadata_key] = mp_item.GetMetadata( - self.pype_metadata_key)[self.pype_metadata_key] + if mp_item.GetMetadata(self.pype_tag_name): + clip_attributes[self.pype_tag_name] = mp_item.GetMetadata( + self.pype_tag_name)[self.pype_tag_name] # stringify clip_attributes = json.dumps(clip_attributes) @@ -311,7 +420,7 @@ def create_compound_clip(clip_data, folder, rename=False, **kwargs): cct.SetMetadata(k, v) # add metadata to cct - cct.SetMetadata(self.pype_metadata_key, clip_attributes) + cct.SetMetadata(self.pype_tag_name, clip_attributes) # reset start timecode of the compound clip cct.SetClipProperty("Start TC", mp_props["Start TC"]) @@ -389,7 +498,7 @@ def get_pype_clip_metadata(clip): mp_item = clip.GetMediaPoolItem() metadata = mp_item.GetMetadata() - return metadata.get(self.pype_metadata_key) + return metadata.get(self.pype_tag_name) def get_clip_attributes(clip): diff --git a/pype/hosts/resolve/pipeline.py b/pype/hosts/resolve/pipeline.py index 92bef2e13b..22437980e7 100644 --- a/pype/hosts/resolve/pipeline.py +++ b/pype/hosts/resolve/pipeline.py @@ -3,11 +3,15 @@ Basic avalon integration """ import os import contextlib +from collections import OrderedDict from avalon.tools import workfiles from avalon import api as avalon +from avalon import schema +from avalon.pipeline import AVALON_CONTAINER_ID from pyblish import api as pyblish import pype from pype.api import Logger +from . import lib log = Logger().get_logger(__name__, "resolve") @@ -80,29 +84,46 @@ def uninstall(): avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH) -def containerise(obj, +def containerise(track_item, name, namespace, context, loader=None, data=None): - """Bundle Resolve's object into an assembly and imprint it with metadata + """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: - obj (obj): Resolve's object to imprint as container + 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: - obj (obj): containerised object + track_item (hiero.core.TrackItem): containerised object """ - pass + + 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}) + + print("_ data_imprint: {}".format(data_imprint)) + lib.set_track_item_pype_tag(track_item, data_imprint) + + return track_item def ls(): @@ -115,20 +136,77 @@ def ls(): See the `container.json` schema for details on how it should look, and the Maya equivalent, which is in `avalon.maya.pipeline` """ - pass + + # get all track items from current timeline + all_track_items = lib.get_current_track_items(filter=False) + + for track_item_data in all_track_items: + track_item = track_item_data["clip"]["item"] + container = parse_container(track_item) + if container: + yield container -def parse_container(container): - """Return the container node's full container data. +def parse_container(track_item, validate=True): + """Return container data from track_item's pype tag. Args: - container (str): A container node name. + track_item (hiero.core.TrackItem): A containerised track item. + validate (bool)[optional]: validating with avalon scheme Returns: - dict: The container schema data for this container node. + dict: The container schema data for input containerized track item. """ - pass + # convert tag metadata to normal keys names + data = lib.get_track_item_pype_tag(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_tag(track_item) + + for _key, _value in container.items(): + try: + container[_key] = data[_key] + except KeyError: + pass + + log.info("Updating container: `{}`".format(track_item)) + return bool(lib.set_track_item_pype_tag(track_item, container)) def launch_workfiles_app(*args): diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index a652fbfe64..b465d77950 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -3,6 +3,7 @@ from avalon import api from pype.hosts import resolve from avalon.vendor import qargparse from pype.api import config +from . import lib from Qt import QtWidgets, QtCore @@ -351,3 +352,295 @@ class Creator(api.Creator): self.selected = resolve.get_current_track_items(filter=False) 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 = "" + 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["clip"]["item"] + sequence_name = track_item["sequence"].GetName() + self.sequence_name = str(sequence_name).replace(" ", "_") + + # track item (clip) main attributes + self.ti_name = self.track_item.GetName() + self.ti_index = int(track_item["clip"]["index"]) + + # get track name and index + track_name = track_item["track"]["name"] + self.track_name = str(track_name).replace(" ", "_") + self.track_index = int(track_item["track"]["index"]) + + # 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.GetStart()) + self.clip_out = int(self.track_item.GetEnd()) + + # define ui inputs if non gui mode was used + self.shot_num = self.ti_index + print( + "____ 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 + + # build subset name from layer name + if self.subset_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 + ) + + if master_layer and self.vertical_sync: + tag_hierarchy_data.update({"masterLayer": True}) + 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({"review": self.review_layer}) + else: + self.tag_data.update({"review": False}) + + 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, + "families": [self.subset_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) diff --git a/pype/plugins/resolve/create/create_shot_clip_new.py b/pype/plugins/resolve/create/create_shot_clip_new.py index 03a3041089..5d6c0a2e79 100644 --- a/pype/plugins/resolve/create/create_shot_clip_new.py +++ b/pype/plugins/resolve/create/create_shot_clip_new.py @@ -232,11 +232,11 @@ class CreateShotClipNew(resolve.Creator): # 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) + for track_item_data in self.selected: + if track_item_data["track"]["name"] in v_sync_track: + sorted_selected_track_items.append(track_item_data) else: - unsorted_selected_track_items.append(_ti) + unsorted_selected_track_items.append(track_item_data) sorted_selected_track_items.extend(unsorted_selected_track_items) @@ -245,8 +245,27 @@ class CreateShotClipNew(resolve.Creator): "avalon": self.data } - for i, track_item in enumerate(sorted_selected_track_items): + # sequence attrs + sq_frame_start = self.sequence.GetStartFrame() + sq_markers = self.sequence.GetMarkers() + + # create media bin for compound clips (trackItems) + mp_folder = resolve.create_current_sequence_media_bin(self.sequence) + + for i, track_item_data 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() + resolve.PublishClip(self, track_item_data, **kwargs).convert() + + # clear color after it is done + track_item_data["clip"]["item"].ClearClipColor() + + # convert track item to timeline media pool item + resolve.create_compound_clip( + track_item_data, + mp_folder, + rename=True, + **dict( + {"presets": widget.result}) + ) From f59f7bb3985458fa557be3d303fc548522d39ea8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 30 Nov 2020 12:02:07 +0100 Subject: [PATCH 06/72] feat(resolve): refactory Creator for clips --- pype/hosts/resolve/lib.py | 27 ++----------------- pype/hosts/resolve/menu_style.qss | 11 ++++++++ pype/hosts/resolve/plugin.py | 24 +++++++++++------ .../resolve/create/create_shot_clip_new.py | 22 ++++++--------- 4 files changed, 37 insertions(+), 47 deletions(-) diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index 4052fa74fd..74f105a130 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -303,7 +303,7 @@ def get_name_with_data(clip_data, presets): }) -def create_compound_clip(clip_data, folder, rename=False, **kwargs): +def create_compound_clip(clip_data, name, folder): """ Convert timeline object into nested timeline object @@ -311,8 +311,7 @@ def create_compound_clip(clip_data, folder, rename=False, **kwargs): clip_data (dict): timeline item object packed into dict with project, timeline (sequence) folder (resolve.MediaPool.Folder): media pool folder object, - rename (bool)[optional]: renaming in sequence or not - kwargs (optional): additional data needed for rename=True (presets) + name (str): name for compound clip Returns: resolve.MediaPoolItem: media pool item with compound clip timeline(cct) @@ -324,34 +323,12 @@ def create_compound_clip(clip_data, folder, rename=False, **kwargs): # get details of objects clip_item = clip["item"] - track = clip_data["track"] mp = project.GetMediaPool() # get clip attributes clip_attributes = get_clip_attributes(clip_item) - print(f"_ clip_attributes: {pformat(clip_attributes)}") - if rename: - presets = kwargs.get("presets") - if presets: - name, data = get_name_with_data(clip_data, presets) - # add hirarchy data to clip attributes - clip_attributes.update(data) - else: - name = "{:0>3}_{:0>4}".format( - int(track["index"]), int(clip["index"])) - else: - # build name - clip_name_split = clip_item.GetName().split(".") - name = "_".join([ - track["name"], - str(track["index"]), - clip_name_split[0], - str(clip["index"])] - ) - - # get metadata mp_item = clip_item.GetMediaPoolItem() mp_props = mp_item.GetClipProperty() diff --git a/pype/hosts/resolve/menu_style.qss b/pype/hosts/resolve/menu_style.qss index ea11c4ca2e..5a1d39fe79 100644 --- a/pype/hosts/resolve/menu_style.qss +++ b/pype/hosts/resolve/menu_style.qss @@ -4,6 +4,17 @@ QWidget { font-size: 13px; } +QComboBox { + border: 1px solid #090909; + background-color: #201f1f; + color: #ffffff; +} + +QComboBox QAbstractItemView +{ + color: white; +} + QPushButton { border: 1px solid #090909; background-color: #201f1f; diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index b465d77950..c816735be2 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -390,23 +390,24 @@ class PublishClip: vertical_sync_default = False driving_layer_default = "" - def __init__(self, cls, track_item, **kwargs): + def __init__(self, cls, track_item_data, **kwargs): # populate input cls attribute onto self.[attr] self.__dict__.update(cls.__dict__) # get main parent objects - self.track_item = track_item["clip"]["item"] - sequence_name = track_item["sequence"].GetName() + self.track_item_data = track_item_data + self.track_item = track_item_data["clip"]["item"] + sequence_name = track_item_data["sequence"].GetName() self.sequence_name = str(sequence_name).replace(" ", "_") # track item (clip) main attributes self.ti_name = self.track_item.GetName() - self.ti_index = int(track_item["clip"]["index"]) + self.ti_index = int(track_item_data["clip"]["index"]) # get track name and index - track_name = track_item["track"]["name"] + track_name = track_item_data["track"]["name"] self.track_name = str(track_name).replace(" ", "_") - self.track_index = int(track_item["track"]["index"]) + self.track_index = int(track_item_data["track"]["index"]) # adding tag.family into tag if kwargs.get("avalon"): @@ -415,6 +416,9 @@ class PublishClip: # adding ui inputs if any self.ui_inputs = kwargs.get("ui_inputs", {}) + # adding media pool folder if any + self.mp_folder = kwargs.get("mp_folder") + # populate default data before we get other attributes self._populate_track_item_default_data() @@ -438,12 +442,16 @@ class PublishClip: 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 + self.track_item = lib.create_compound_clip( + self.track_item_data, + self.tag_data["asset"], + self.mp_folder + ) + # create pype tag on track_item and add data lib.imprint(self.track_item, self.tag_data) diff --git a/pype/plugins/resolve/create/create_shot_clip_new.py b/pype/plugins/resolve/create/create_shot_clip_new.py index 5d6c0a2e79..a94e30ed73 100644 --- a/pype/plugins/resolve/create/create_shot_clip_new.py +++ b/pype/plugins/resolve/create/create_shot_clip_new.py @@ -240,11 +240,6 @@ class CreateShotClipNew(resolve.Creator): sorted_selected_track_items.extend(unsorted_selected_track_items) - kwargs = { - "ui_inputs": widget.result, - "avalon": self.data - } - # sequence attrs sq_frame_start = self.sequence.GetStartFrame() sq_markers = self.sequence.GetMarkers() @@ -252,6 +247,14 @@ class CreateShotClipNew(resolve.Creator): # create media bin for compound clips (trackItems) mp_folder = resolve.create_current_sequence_media_bin(self.sequence) + kwargs = { + "ui_inputs": widget.result, + "avalon": self.data, + "mp_folder": mp_folder, + "sq_frame_start": sq_frame_start, + "sq_markers": sq_markers + } + for i, track_item_data in enumerate(sorted_selected_track_items): self.rename_index = i @@ -260,12 +263,3 @@ class CreateShotClipNew(resolve.Creator): # clear color after it is done track_item_data["clip"]["item"].ClearClipColor() - - # convert track item to timeline media pool item - resolve.create_compound_clip( - track_item_data, - mp_folder, - rename=True, - **dict( - {"presets": widget.result}) - ) From 1f65c27c1b0489d35ea088448ad2239757aadc86 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 30 Nov 2020 18:17:53 +0100 Subject: [PATCH 07/72] feat(resolve): publishing wip --- pype/hosts/resolve/lib.py | 27 +++--- pype/hosts/resolve/plugin.py | 7 +- .../{publish => _publish}/collect_clips.py | 0 .../resolve/create/collect_clip_resolution.py | 38 ++++++++ .../resolve/publish/collect_instances.py | 97 +++++++++++++++++++ .../resolve/publish/collect_project.py | 29 ------ .../resolve/publish/collect_workfile.py | 58 +++++++++++ 7 files changed, 211 insertions(+), 45 deletions(-) rename pype/plugins/resolve/{publish => _publish}/collect_clips.py (100%) create mode 100644 pype/plugins/resolve/create/collect_clip_resolution.py create mode 100644 pype/plugins/resolve/publish/collect_instances.py delete mode 100644 pype/plugins/resolve/publish/collect_project.py create mode 100644 pype/plugins/resolve/publish/collect_workfile.py diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index 74f105a130..8dd9566c44 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -105,14 +105,16 @@ 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 + trackItem (resolve.TimelineItem): hiero object Returns: hiero.core.Tag: hierarchy, orig clip attributes """ return_tag = None + media_pool_item = track_item.GetMediaPoolItem() + # get all tags from track item - _tags = track_item.GetMetadata() + _tags = media_pool_item.GetMetadata() if not _tags: return None for key, data in _tags.items(): @@ -135,22 +137,17 @@ def set_track_item_pype_tag(track_item, data=None): """ 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) + tag_data = get_track_item_pype_tag(track_item) - if _tag: + if tag_data: + media_pool_item = track_item.GetMediaPoolItem() # it not tag then create one - _tag.update(tag_data) - track_item.SetMetadata(self.pype_tag_name, json.dumps(_tag)) - return _tag + tag_data.update(data) + media_pool_item.SetMetadata(self.pype_tag_name, json.dumps(tag_data)) + return tag_data else: + tag_data = data # if pype tag available then update with input data # add it to the input track item track_item.SetMetadata(self.pype_tag_name, json.dumps(tag_data)) @@ -416,7 +413,7 @@ def swap_clips(from_clip, to_clip, to_clip_name, to_in_frame, to_out_frame): It will add take and activate it to the frame range which is inputted Args: - from_clip (resolve.mediaPoolItem) + from_clip (resolve.TimelineItem) to_clip (resolve.mediaPoolItem) to_clip_name (str): name of to_clip to_in_frame (float): cut in frame, usually `GetLeftOffset()` diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index c816735be2..95097434f8 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -446,12 +446,17 @@ class PublishClip: else: self.tag_data["asset"] = self.ti_name - self.track_item = lib.create_compound_clip( + lib.create_compound_clip( self.track_item_data, self.tag_data["asset"], self.mp_folder ) + # add track_item_data selection to tag + self.tag_data.update({ + "track_data": self.track_item_data["track"] + }) + # create pype tag on track_item and add data lib.imprint(self.track_item, self.tag_data) diff --git a/pype/plugins/resolve/publish/collect_clips.py b/pype/plugins/resolve/_publish/collect_clips.py similarity index 100% rename from pype/plugins/resolve/publish/collect_clips.py rename to pype/plugins/resolve/_publish/collect_clips.py diff --git a/pype/plugins/resolve/create/collect_clip_resolution.py b/pype/plugins/resolve/create/collect_clip_resolution.py new file mode 100644 index 0000000000..3bea68c677 --- /dev/null +++ b/pype/plugins/resolve/create/collect_clip_resolution.py @@ -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 = ["resolve"] + 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 + )) diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py new file mode 100644 index 0000000000..a2c7fea0e0 --- /dev/null +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -0,0 +1,97 @@ +import os +import pyblish +from pype.hosts import resolve + +# # developer reload modules +from pprint import pformat + + +class CollectInstances(pyblish.api.ContextPlugin): + """Collect all Track items selection.""" + + order = pyblish.api.CollectorOrder - 0.5 + label = "Collect Instances" + hosts = ["resolve"] + + def process(self, context): + selected_track_items = resolve.get_current_track_items( + filter=True, selecting_color="Pink") + + self.log.info( + "Processing enabled track items: {}".format( + len(selected_track_items))) + + for track_item_data in selected_track_items: + self.log.debug(pformat(track_item_data)) + data = dict() + track_item = track_item_data["clip"]["item"] + self.log.debug(track_item) + # get pype tag data + tag_parsed_data = resolve.get_track_item_pype_tag(track_item) + self.log.debug(pformat(tag_parsed_data)) + + if not tag_parsed_data: + continue + + if tag_parsed_data.get("id") != "pyblish.avalon.instance": + continue + + compound_source_prop = tag_parsed_data["sourceProperties"] + self.log.debug(f"compound_source_prop: {compound_source_prop}") + + # source = track_item_data.GetMediaPoolItem() + + source_path = os.path.normpath( + compound_source_prop["File Path"]) + source_name = compound_source_prop["File Name"] + source_id = tag_parsed_data["sourceId"] + self.log.debug(f"source_path: {source_path}") + self.log.debug(f"source_name: {source_name}") + self.log.debug(f"source_id: {source_id}") + + # 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["review"] + + # 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 = tag_parsed_data["track_data"]["name"] + base_name = os.path.basename(source_path) + file_head = os.path.splitext(base_name)[0] + # 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": track_item, + "families": families, + + # tags + "tags": tag_parsed_data, + + # track item attributes + "track": track, + + # source attribute + "source": source_path, + "sourcePath": source_path, + "sourceFileHead": file_head, + # "sourceFirst": source_first_frame, + }) + + instance = context.create_instance(**data) + + self.log.info("Creating instance: {}".format(instance)) diff --git a/pype/plugins/resolve/publish/collect_project.py b/pype/plugins/resolve/publish/collect_project.py deleted file mode 100644 index aa57f93619..0000000000 --- a/pype/plugins/resolve/publish/collect_project.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -import pyblish.api -from pype.hosts.resolve.utils import get_resolve_module - - -class CollectProject(pyblish.api.ContextPlugin): - """Collect Project object""" - - order = pyblish.api.CollectorOrder - 0.1 - label = "Collect Project" - hosts = ["resolve"] - - def process(self, context): - exported_projet_ext = ".drp" - current_dir = os.getenv("AVALON_WORKDIR") - resolve = get_resolve_module() - PM = resolve.GetProjectManager() - P = PM.GetCurrentProject() - name = P.GetName() - - fname = name + exported_projet_ext - current_file = os.path.join(current_dir, fname) - normalised = os.path.normpath(current_file) - - context.data["project"] = P - context.data["currentFile"] = normalised - - self.log.info(name) - self.log.debug(normalised) diff --git a/pype/plugins/resolve/publish/collect_workfile.py b/pype/plugins/resolve/publish/collect_workfile.py new file mode 100644 index 0000000000..a8b09573db --- /dev/null +++ b/pype/plugins/resolve/publish/collect_workfile.py @@ -0,0 +1,58 @@ +import os +import pyblish.api +from pype.hosts import resolve +from avalon import api as avalon + + +class CollectWorkfile(pyblish.api.ContextPlugin): + """Inject the current working file into context""" + + label = "Collect Workfile" + order = pyblish.api.CollectorOrder - 0.501 + + def process(self, context): + exported_projet_ext = ".drp" + asset = avalon.Session["AVALON_ASSET"] + staging_dir = os.getenv("AVALON_WORKDIR") + subset = "workfile" + + project = resolve.get_current_project() + name = project.GetName() + + base_name = name + exported_projet_ext + current_file = os.path.join(staging_dir, base_name) + current_file = os.path.normpath(current_file) + + active_sequence = resolve.get_current_sequence() + video_tracks = resolve.get_video_track_names() + + # set main project attributes to context + context.data["activeProject"] = project + context.data["activeSequence"] = active_sequence + context.data["videoTracks"] = video_tracks + context.data["currentFile"] = current_file + + 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", + + # source attribute + "sourcePath": current_file, + "representations": [representation] + } + + instance = context.create_instance(**instance_data) + self.log.info("Creating instance: {}".format(instance)) From dbf0438f36e26ba57ef403f015e61d78fb30373f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 30 Nov 2020 18:33:19 +0100 Subject: [PATCH 08/72] feat(resolve): activating publish on instances --- pype/hosts/resolve/pipeline.py | 21 ++++++++++++ .../resolve/publish/collect_instances.py | 34 +++++++++---------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/pype/hosts/resolve/pipeline.py b/pype/hosts/resolve/pipeline.py index 22437980e7..23cf042a13 100644 --- a/pype/hosts/resolve/pipeline.py +++ b/pype/hosts/resolve/pipeline.py @@ -61,6 +61,9 @@ def install(): 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) + get_resolve_module() @@ -83,6 +86,9 @@ def uninstall(): avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH) + # register callback for switching publishable + pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) + def containerise(track_item, name, @@ -241,3 +247,18 @@ def reset_selection(): """Deselect all selected nodes """ pass + + +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.resolve import ( + set_publish_attribute + ) + + # Whether instances should be passthrough based on new value + track_item = instance.data["item"] + set_publish_attribute(track_item, new_value) diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py index a2c7fea0e0..b556117e65 100644 --- a/pype/plugins/resolve/publish/collect_instances.py +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -22,21 +22,21 @@ class CollectInstances(pyblish.api.ContextPlugin): len(selected_track_items))) for track_item_data in selected_track_items: - self.log.debug(pformat(track_item_data)) + data = dict() track_item = track_item_data["clip"]["item"] - self.log.debug(track_item) + # get pype tag data - tag_parsed_data = resolve.get_track_item_pype_tag(track_item) - self.log.debug(pformat(tag_parsed_data)) + tag_data = resolve.get_track_item_pype_tag(track_item) + self.log.debug(f"__ tag_data: {pformat(tag_data)}") - if not tag_parsed_data: + if not tag_data: continue - if tag_parsed_data.get("id") != "pyblish.avalon.instance": + if tag_data.get("id") != "pyblish.avalon.instance": continue - compound_source_prop = tag_parsed_data["sourceProperties"] + compound_source_prop = tag_data["sourceProperties"] self.log.debug(f"compound_source_prop: {compound_source_prop}") # source = track_item_data.GetMediaPoolItem() @@ -44,27 +44,27 @@ class CollectInstances(pyblish.api.ContextPlugin): source_path = os.path.normpath( compound_source_prop["File Path"]) source_name = compound_source_prop["File Name"] - source_id = tag_parsed_data["sourceId"] + source_id = tag_data["sourceId"] self.log.debug(f"source_path: {source_path}") self.log.debug(f"source_name: {source_name}") self.log.debug(f"source_id: {source_id}") # add tag data to instance data data.update({ - k: v for k, v in tag_parsed_data.items() + k: v for k, v in tag_data.items() if k not in ("id", "applieswhole", "label") }) - asset = tag_parsed_data["asset"] - subset = tag_parsed_data["subset"] - review = tag_parsed_data["review"] + asset = tag_data["asset"] + subset = tag_data["subset"] + review = tag_data["review"] # insert family into families - family = tag_parsed_data["family"] - families = [str(f) for f in tag_parsed_data["families"]] + family = tag_data["family"] + families = [str(f) for f in tag_data["families"]] families.insert(0, str(family)) - track = tag_parsed_data["track_data"]["name"] + track = tag_data["track_data"]["name"] base_name = os.path.basename(source_path) file_head = os.path.splitext(base_name)[0] # source_first_frame = int(file_info.startFrame()) @@ -78,9 +78,9 @@ class CollectInstances(pyblish.api.ContextPlugin): "asset": asset, "item": track_item, "families": families, - + "publish": resolve.get_publish_attribute(track_item), # tags - "tags": tag_parsed_data, + "tags": tag_data, # track item attributes "track": track, From 727ce8b0f7319957e56405000aa84d9901e4a1bd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Dec 2020 14:55:59 +0100 Subject: [PATCH 09/72] feat(resolve): otio host modul for publishing and injesting now only works for exporting from resolve --- pype/hosts/resolve/otio.py | 192 +++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 pype/hosts/resolve/otio.py diff --git a/pype/hosts/resolve/otio.py b/pype/hosts/resolve/otio.py new file mode 100644 index 0000000000..3edb6b08c2 --- /dev/null +++ b/pype/hosts/resolve/otio.py @@ -0,0 +1,192 @@ +import opentimelineio as otio + + +TRACK_TYPES = { + "video": otio.schema.TrackKind.Video, + "audio": otio.schema.TrackKind.Audio +} + + +def create_rational_time(frame, fps): + return otio.opentime.RationalTime( + float(frame), + float(fps) + ) + + +def create_time_range(start_frame, frame_duration, fps): + return otio.opentime.TimeRange( + start_time=create_rational_time(start_frame, fps), + duration=create_rational_time(frame_duration, fps) + ) + + +def create_reference(media_pool_item): + return otio.schema.ExternalReference( + target_url=media_pool_item.GetClipProperty( + "File Path").get("File Path"), + available_range=create_time_range( + media_pool_item.GetClipProperty("Start").get("Start"), + media_pool_item.GetClipProperty("Frames").get("Frames"), + media_pool_item.GetClipProperty("FPS").get("FPS") + ) + ) + + +def create_markers(track_item, frame_rate): + track_item_markers = track_item.GetMarkers() + markers = [] + for m_frame in track_item_markers: + markers.append( + otio.schema.Marker( + name=track_item_markers[m_frame]["name"], + marked_range=create_time_range( + m_frame, + track_item_markers[m_frame]["duration"], + frame_rate + ), + color=track_item_markers[m_frame]["color"].upper(), + metadata={ + "Resolve": { + "note": track_item_markers[m_frame]["note"] + } + } + ) + ) + return markers + + +def create_clip(track_item): + media_pool_item = track_item.GetMediaPoolItem() + frame_rate = media_pool_item.GetClipProperty("FPS").get("FPS") + clip = otio.schema.Clip( + name=track_item.GetName(), + source_range=create_time_range( + track_item.GetLeftOffset(), + track_item.GetDuration(), + frame_rate + ), + media_reference=create_reference(media_pool_item) + ) + for marker in create_markers(track_item, frame_rate): + clip.markers.append(marker) + return clip + + +def create_gap(gap_start, clip_start, tl_start_frame, frame_rate): + return otio.schema.Gap( + source_range=create_time_range( + gap_start, + (clip_start - tl_start_frame) - gap_start, + frame_rate + ) + ) + + +def create_timeline(timeline): + return otio.schema.Timeline(name=timeline.GetName()) + + +def create_track(track_type, track_name): + return otio.schema.Track( + name=track_name, + kind=TRACK_TYPES[track_type] + ) + + +def create_complete_otio_timeline(project): + # get current timeline + timeline = project.GetCurrentTimeline() + + # convert timeline to otio + otio_timeline = create_timeline(timeline) + + # loop all defined track types + for track_type in list(TRACK_TYPES.keys()): + # get total track count + track_count = timeline.GetTrackCount(track_type) + + # loop all tracks by track indexes + for track_index in range(1, int(track_count) + 1): + # get current track name + track_name = timeline.GetTrackName(track_type, track_index) + + # convert track to otio + otio_track = create_track( + track_type, "{}{}".format(track_name, track_index)) + + # get all track items in current track + current_track_items = timeline.GetItemListInTrack( + track_type, track_index) + + # loop available track items in current track items + for track_item in current_track_items: + # skip offline track items + if track_item.GetMediaPoolItem() is None: + continue + + # calculate real clip start + clip_start = track_item.GetStart() - timeline.GetStartFrame() + + # if gap between track start and clip start + if clip_start > otio_track.available_range().duration.value: + # create gap and add it to track + otio_track.append( + create_gap( + otio_track.available_range().duration.value, + track_item.GetStart(), + timeline.GetStartFrame(), + project.GetSetting("timelineFrameRate") + ) + ) + + # create otio clip and add it to track + otio_track.append(create_clip(track_item)) + + # add track to otio timeline + otio_timeline.tracks.append(otio_track) + + +def get_clip_with_parents(track_item_data): + """ + Return otio objects for timeline, track and clip + + Args: + track_item_data (dict): track_item_data from list returned by + resolve.get_current_track_items() + + Returns: + dict: otio clip with parent objects + + """ + + track_item = track_item_data["clip"]["item"] + timeline = track_item_data["timeline"] + track_type = track_item_data["track"]["type"] + track_name = track_item_data["track"]["name"] + track_index = track_item_data["track"]["index"] + + # convert timeline to otio + otio_timeline = create_timeline(timeline) + # convert track to otio + otio_track = create_track( + track_type, "{}{}".format(track_name, track_index)) + + # create otio clip + otio_clip = create_clip(track_item) + + # add it to track + otio_track.append(otio_clip) + + # add track to otio timeline + otio_timeline.tracks.append(otio_track) + + return { + "otioTimeline": otio_timeline, + "otioTrack": otio_track, + "otioClip": otio_clip + } + + +def save(otio_timeline, path): + otio.adapters.write_to_file(otio_timeline, path) From 60213ecc5dd4e384170da38a7735fd4efdb4826e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Dec 2020 15:03:06 +0100 Subject: [PATCH 10/72] feat(resolve): collect workfile --- .../resolve/publish/collect_workfile.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pype/plugins/resolve/publish/collect_workfile.py b/pype/plugins/resolve/publish/collect_workfile.py index a8b09573db..d1b45117c9 100644 --- a/pype/plugins/resolve/publish/collect_workfile.py +++ b/pype/plugins/resolve/publish/collect_workfile.py @@ -2,6 +2,7 @@ import os import pyblish.api from pype.hosts import resolve from avalon import api as avalon +from pprint import pformat class CollectWorkfile(pyblish.api.ContextPlugin): @@ -18,6 +19,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): project = resolve.get_current_project() name = project.GetName() + fps = project.GetSetting("timelineFrameRate") base_name = name + exported_projet_ext current_file = os.path.join(staging_dir, base_name) @@ -27,17 +29,18 @@ class CollectWorkfile(pyblish.api.ContextPlugin): video_tracks = resolve.get_video_track_names() # set main project attributes to context - context.data["activeProject"] = project - context.data["activeSequence"] = active_sequence - context.data["videoTracks"] = video_tracks - context.data["currentFile"] = current_file - - self.log.info("currentFile: {}".format(current_file)) + context.data.update({ + "activeProject": project, + "activeSequence": active_sequence, + "videoTracks": video_tracks, + "currentFile": current_file, + "fps": fps, + }) # creating workfile representation representation = { - 'name': 'hrox', - 'ext': 'hrox', + 'name': exported_projet_ext[1:], + 'ext': exported_projet_ext[1:], 'files': base_name, "stagingDir": staging_dir, } @@ -56,3 +59,4 @@ class CollectWorkfile(pyblish.api.ContextPlugin): instance = context.create_instance(**instance_data) self.log.info("Creating instance: {}".format(instance)) + self.log.debug("__ instance.data: {}".format(pformat(instance.data))) From 4d6b9fd33ff21cdb891a0000b4f7f7b0c51c29d1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Dec 2020 15:04:03 +0100 Subject: [PATCH 11/72] fix(resolve): moving file to correct folder --- .../resolve/{create => _publish}/collect_clip_resolution.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pype/plugins/resolve/{create => _publish}/collect_clip_resolution.py (100%) diff --git a/pype/plugins/resolve/create/collect_clip_resolution.py b/pype/plugins/resolve/_publish/collect_clip_resolution.py similarity index 100% rename from pype/plugins/resolve/create/collect_clip_resolution.py rename to pype/plugins/resolve/_publish/collect_clip_resolution.py From 78946d04baa08b7b4a9c781a07a66177f9469351 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Dec 2020 11:28:14 +0100 Subject: [PATCH 12/72] feat(resolve): create with markers and publishing via markers --- pype/hosts/resolve/__init__.py | 2 + pype/hosts/resolve/lib.py | 136 +++++++++++++----- pype/hosts/resolve/otio.py | 12 +- pype/hosts/resolve/plugin.py | 20 +-- .../resolve/create/create_shot_clip_new.py | 7 +- .../resolve/publish/collect_instances.py | 24 ++-- 6 files changed, 132 insertions(+), 69 deletions(-) diff --git a/pype/hosts/resolve/__init__.py b/pype/hosts/resolve/__init__.py index b8457438c6..83f8e3a720 100644 --- a/pype/hosts/resolve/__init__.py +++ b/pype/hosts/resolve/__init__.py @@ -14,6 +14,7 @@ from .pipeline import ( ) from .lib import ( + publish_clip_color, get_project_manager, get_current_project, get_current_sequence, @@ -66,6 +67,7 @@ __all__ = [ "get_resolve_module", # lib + "publish_clip_color", "get_project_manager", "get_current_project", "get_current_sequence", diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index 8dd9566c44..2ade558d89 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -2,31 +2,41 @@ import sys import json import ast from opentimelineio import opentime -from pprint import pformat - from pype.api import Logger log = Logger().get_logger(__name__, "resolve") self = sys.modules[__name__] -self.pm = None +self.project_manager = None + +# Pype sequencial rename variables self.rename_index = 0 self.rename_add = 0 + +self.publish_clip_color = "Pink" +self.pype_marker_workflow = True + +# Pype compound clip workflow variable self.pype_tag_name = "VFX Notes" +# Pype marker workflow variables +self.pype_marker_name = "PYPEDATA" +self.pype_marker_duration = 1 +self.pype_marker_color = "Mint" +self.temp_marker_frame = None def get_project_manager(): from . import bmdvr - if not self.pm: - self.pm = bmdvr.GetProjectManager() - return self.pm + if not self.project_manager: + self.project_manager = bmdvr.GetProjectManager() + return self.project_manager def get_current_project(): # initialize project manager get_project_manager() - return self.pm.GetCurrentProject() + return self.project_manager.GetCurrentProject() def get_current_sequence(): @@ -111,16 +121,20 @@ def get_track_item_pype_tag(track_item): hiero.core.Tag: hierarchy, orig clip attributes """ return_tag = None - media_pool_item = track_item.GetMediaPoolItem() - # get all tags from track item - _tags = media_pool_item.GetMetadata() - if not _tags: - return None - for key, data in _tags.items(): - # return only correct tag defined by global name - if key in self.pype_tag_name: - return_tag = json.loads(data) + if self.pype_marker_workflow: + return_tag = get_pype_marker(track_item) + else: + media_pool_item = track_item.GetMediaPoolItem() + + # get all tags from track item + _tags = media_pool_item.GetMetadata() + if not _tags: + return None + for key, data in _tags.items(): + # return only correct tag defined by global name + if key in self.pype_tag_name: + return_tag = json.loads(data) return return_tag @@ -130,28 +144,37 @@ 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 + trackItem (resolve.TimelineItem): resolve api object Returns: - hiero.core.Tag + dict: json loaded data """ data = data or dict() # get available pype tag if any tag_data = get_track_item_pype_tag(track_item) - if tag_data: - media_pool_item = track_item.GetMediaPoolItem() - # it not tag then create one + if self.pype_marker_workflow: + # delete tag as it is not updatable + if tag_data: + delete_pype_marker(track_item) + tag_data.update(data) - media_pool_item.SetMetadata(self.pype_tag_name, json.dumps(tag_data)) - return tag_data + set_pype_marker(track_item, tag_data) else: - tag_data = data - # if pype tag available then update with input data - # add it to the input track item - track_item.SetMetadata(self.pype_tag_name, json.dumps(tag_data)) - return tag_data + if tag_data: + media_pool_item = track_item.GetMediaPoolItem() + # it not tag then create one + tag_data.update(data) + media_pool_item.SetMetadata( + self.pype_tag_name, json.dumps(tag_data)) + else: + tag_data = data + # if pype tag available then update with input data + # add it to the input track item + track_item.SetMetadata(self.pype_tag_name, json.dumps(tag_data)) + + return tag_data def imprint(track_item, data=None): @@ -187,7 +210,7 @@ def set_publish_attribute(track_item, value): value (bool): True or False """ tag_data = get_track_item_pype_tag(track_item) - tag_data["publish"] = str(value) + tag_data["publish"] = value # set data to the publish attribute set_track_item_pype_tag(track_item, tag_data) @@ -200,10 +223,47 @@ def get_publish_attribute(track_item): value (bool): True or False """ tag_data = get_track_item_pype_tag(track_item) - value = tag_data["publish"] + return tag_data["publish"] - # return value converted to bool value. Atring is stored in tag. - return ast.literal_eval(value) + +def set_pype_marker(track_item, tag_data): + source_start = track_item.GetLeftOffset() + item_duration = track_item.GetDuration() + frame = int(source_start + (item_duration / 2)) + + # marker attributes + frameId = (frame / 10) * 10 + color = self.pype_marker_color + name = self.pype_marker_name + note = json.dumps(tag_data) + duration = (self.pype_marker_duration / 10) * 10 + + track_item.AddMarker( + frameId, + color, + name, + note, + duration + ) + + +def get_pype_marker(track_item): + track_item_markers = track_item.GetMarkers() + for marker_frame in track_item_markers: + note = track_item_markers[marker_frame]["note"] + color = track_item_markers[marker_frame]["color"] + name = track_item_markers[marker_frame]["name"] + print(f"_ marker data: {marker_frame} | {name} | {color} | {note}") + if name == self.pype_marker_name and color == self.pype_marker_color: + self.temp_marker_frame = marker_frame + return json.loads(note) + + return dict() + + +def delete_pype_marker(track_item): + track_item.DeleteMarkerAtFrame(self.temp_marker_frame) + self.temp_marker_frame = None def create_current_sequence_media_bin(sequence): @@ -523,16 +583,16 @@ def set_project_manager_to_folder_name(folder_name): set_folder = False # go back to root folder - if self.pm.GotoRootFolder(): + if self.project_manager.GotoRootFolder(): log.info(f"Testing existing folder: {folder_name}") folders = convert_resolve_list_type( - self.pm.GetFoldersInCurrentFolder()) + self.project_manager.GetFoldersInCurrentFolder()) log.info(f"Testing existing folders: {folders}") # get me first available folder object # with the same name as in `folder_name` else return False if next((f for f in folders if f in folder_name), False): log.info(f"Found existing folder: {folder_name}") - set_folder = self.pm.OpenFolder(folder_name) + set_folder = self.project_manager.OpenFolder(folder_name) if set_folder: return True @@ -540,11 +600,11 @@ def set_project_manager_to_folder_name(folder_name): # if folder by name is not existent then create one # go back to root folder log.info(f"Folder `{folder_name}` not found and will be created") - if self.pm.GotoRootFolder(): + if self.project_manager.GotoRootFolder(): try: # create folder by given name - self.pm.CreateFolder(folder_name) - self.pm.OpenFolder(folder_name) + self.project_manager.CreateFolder(folder_name) + self.project_manager.OpenFolder(folder_name) return True except NameError as e: log.error((f"Folder with name `{folder_name}` cannot be created!" diff --git a/pype/hosts/resolve/otio.py b/pype/hosts/resolve/otio.py index 3edb6b08c2..7a6d142a10 100644 --- a/pype/hosts/resolve/otio.py +++ b/pype/hosts/resolve/otio.py @@ -36,19 +36,19 @@ def create_reference(media_pool_item): def create_markers(track_item, frame_rate): track_item_markers = track_item.GetMarkers() markers = [] - for m_frame in track_item_markers: + for marker_frame in track_item_markers: markers.append( otio.schema.Marker( - name=track_item_markers[m_frame]["name"], + name=track_item_markers[marker_frame]["name"], marked_range=create_time_range( - m_frame, - track_item_markers[m_frame]["duration"], + marker_frame, + track_item_markers[marker_frame]["duration"], frame_rate ), - color=track_item_markers[m_frame]["color"].upper(), + color=track_item_markers[marker_frame]["color"].upper(), metadata={ "Resolve": { - "note": track_item_markers[m_frame]["note"] + "note": track_item_markers[marker_frame]["note"] } } ) diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index 95097434f8..be666358ae 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -446,16 +446,18 @@ class PublishClip: else: self.tag_data["asset"] = self.ti_name - lib.create_compound_clip( - self.track_item_data, - self.tag_data["asset"], - self.mp_folder - ) + if not lib.pype_marker_workflow: + # create compound clip workflow + lib.create_compound_clip( + self.track_item_data, + self.tag_data["asset"], + self.mp_folder + ) - # add track_item_data selection to tag - self.tag_data.update({ - "track_data": self.track_item_data["track"] - }) + # add track_item_data selection to tag + self.tag_data.update({ + "track_data": self.track_item_data["track"] + }) # create pype tag on track_item and add data lib.imprint(self.track_item, self.tag_data) diff --git a/pype/plugins/resolve/create/create_shot_clip_new.py b/pype/plugins/resolve/create/create_shot_clip_new.py index a94e30ed73..5f6790394b 100644 --- a/pype/plugins/resolve/create/create_shot_clip_new.py +++ b/pype/plugins/resolve/create/create_shot_clip_new.py @@ -259,7 +259,6 @@ class CreateShotClipNew(resolve.Creator): self.rename_index = i # convert track item to timeline media pool item - resolve.PublishClip(self, track_item_data, **kwargs).convert() - - # clear color after it is done - track_item_data["clip"]["item"].ClearClipColor() + track_item = resolve.PublishClip( + self, track_item_data, **kwargs).convert() + track_item.SetClipColor(lib.publish_clip_color) diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py index b556117e65..b8c929f3d6 100644 --- a/pype/plugins/resolve/publish/collect_instances.py +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -15,7 +15,7 @@ class CollectInstances(pyblish.api.ContextPlugin): def process(self, context): selected_track_items = resolve.get_current_track_items( - filter=True, selecting_color="Pink") + filter=True, selecting_color=resolve.publish_clip_color) self.log.info( "Processing enabled track items: {}".format( @@ -36,18 +36,15 @@ class CollectInstances(pyblish.api.ContextPlugin): if tag_data.get("id") != "pyblish.avalon.instance": continue - compound_source_prop = tag_data["sourceProperties"] - self.log.debug(f"compound_source_prop: {compound_source_prop}") - - # source = track_item_data.GetMediaPoolItem() + media_pool_item = track_item.GetMediaPoolItem() + clip_property = media_pool_item.GetClipProperty() + self.log.debug(f"clip_property: {clip_property}") source_path = os.path.normpath( - compound_source_prop["File Path"]) - source_name = compound_source_prop["File Name"] - source_id = tag_data["sourceId"] + clip_property["File Path"]) + source_name = clip_property["File Name"] self.log.debug(f"source_path: {source_path}") self.log.debug(f"source_name: {source_name}") - self.log.debug(f"source_id: {source_id}") # add tag data to instance data data.update({ @@ -64,10 +61,13 @@ class CollectInstances(pyblish.api.ContextPlugin): families = [str(f) for f in tag_data["families"]] families.insert(0, str(family)) - track = tag_data["track_data"]["name"] + track = track_item_data["track"]["name"] base_name = os.path.basename(source_path) file_head = os.path.splitext(base_name)[0] - # source_first_frame = int(file_info.startFrame()) + source_first_frame = int( + track_item.GetStart() + - track_item.GetLeftOffset() + ) # apply only for feview and master track instance if review: @@ -89,7 +89,7 @@ class CollectInstances(pyblish.api.ContextPlugin): "source": source_path, "sourcePath": source_path, "sourceFileHead": file_head, - # "sourceFirst": source_first_frame, + "sourceFirst": source_first_frame, }) instance = context.create_instance(**data) From c4864b11e3451901c8a4d045028712927525619d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 3 Dec 2020 13:00:44 +0100 Subject: [PATCH 13/72] feat(resolve): otio publishing wip --- pype/hosts/resolve/__init__.py | 17 +- pype/hosts/resolve/lib.py | 28 +- pype/hosts/resolve/lib_hiero.py | 838 ++++++++++++++++++ pype/hosts/resolve/otio.py | 112 ++- pype/hosts/resolve/pipeline_hiero.py | 302 +++++++ pype/hosts/resolve/plugin.py | 9 +- .../resolve/publish/collect_instances.py | 36 +- .../StartupUI/otioimporter/OTIOImport.py | 24 +- 8 files changed, 1286 insertions(+), 80 deletions(-) create mode 100644 pype/hosts/resolve/lib_hiero.py create mode 100644 pype/hosts/resolve/pipeline_hiero.py diff --git a/pype/hosts/resolve/__init__.py b/pype/hosts/resolve/__init__.py index 83f8e3a720..45aa5502cc 100644 --- a/pype/hosts/resolve/__init__.py +++ b/pype/hosts/resolve/__init__.py @@ -29,7 +29,8 @@ from .lib import ( create_compound_clip, swap_clips, get_pype_clip_metadata, - set_project_manager_to_folder_name + set_project_manager_to_folder_name, + get_reformated_path ) from .menu import launch_pype_menu @@ -48,6 +49,12 @@ from .workio import ( work_root ) +from .otio import ( + get_otio_clip_instance_data, + get_otio_complete_timeline, + save_otio +) + bmdvr = None bmdvf = None @@ -83,6 +90,7 @@ __all__ = [ "swap_clips", "get_pype_clip_metadata", "set_project_manager_to_folder_name", + "get_reformated_path", # menu "launch_pype_menu", @@ -101,5 +109,10 @@ __all__ = [ # singleton with black magic resolve module "bmdvr", - "bmdvf" + "bmdvf", + + # open color io integration + "get_otio_clip_instance_data", + "get_otio_complete_timeline", + "save_otio" ] diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index 2ade558d89..777cae0eb2 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -1,6 +1,6 @@ import sys import json -import ast +import re from opentimelineio import opentime from pype.api import Logger @@ -25,6 +25,7 @@ self.pype_marker_duration = 1 self.pype_marker_color = "Mint" self.temp_marker_frame = None + def get_project_manager(): from . import bmdvr if not self.project_manager: @@ -621,3 +622,28 @@ def convert_resolve_list_type(resolve_list): "Input argument should be dict() type") return [resolve_list[i] for i in sorted(resolve_list.keys())] + + +def get_reformated_path(path, padded=True): + """ + Return fixed python expression path + + Args: + path (str): path url or simple file name + + Returns: + type: string with reformated path + + Example: + get_reformated_path("plate.[0001-1008].exr") > plate.%04d.exr + + """ + num_pattern = "(\\[\\d+\\-\\d+\\])" + padding_pattern = "(\\d+)(?=-)" + if "[" in path: + padding = len(re.findall(padding_pattern, path).pop()) + if padded: + path = re.sub(num_pattern, f"%0{padding}d", path) + else: + path = re.sub(num_pattern, f"%d", path) + return path diff --git a/pype/hosts/resolve/lib_hiero.py b/pype/hosts/resolve/lib_hiero.py new file mode 100644 index 0000000000..891ca3905c --- /dev/null +++ b/pype/hosts/resolve/lib_hiero.py @@ -0,0 +1,838 @@ +""" +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__, "hiero") + +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() diff --git a/pype/hosts/resolve/otio.py b/pype/hosts/resolve/otio.py index 7a6d142a10..782775253e 100644 --- a/pype/hosts/resolve/otio.py +++ b/pype/hosts/resolve/otio.py @@ -1,5 +1,6 @@ +import json import opentimelineio as otio - +from . import lib TRACK_TYPES = { "video": otio.schema.TrackKind.Video, @@ -7,75 +8,85 @@ TRACK_TYPES = { } -def create_rational_time(frame, fps): +def create_otio_rational_time(frame, fps): return otio.opentime.RationalTime( float(frame), float(fps) ) -def create_time_range(start_frame, frame_duration, fps): +def create_otio_time_range(start_frame, frame_duration, fps): return otio.opentime.TimeRange( - start_time=create_rational_time(start_frame, fps), - duration=create_rational_time(frame_duration, fps) + start_time=create_otio_rational_time(start_frame, fps), + duration=create_otio_rational_time(frame_duration, fps) ) -def create_reference(media_pool_item): +def create_otio_reference(media_pool_item): + path = media_pool_item.GetClipProperty( + "File Path").get("File Path") + reformat_path = lib.get_reformated_path(path, padded=False) + frame_start = int(media_pool_item.GetClipProperty( + "Start").get("Start")) + frame_duration = int(media_pool_item.GetClipProperty( + "Frames").get("Frames")) + fps = media_pool_item.GetClipProperty("FPS").get("FPS") + return otio.schema.ExternalReference( - target_url=media_pool_item.GetClipProperty( - "File Path").get("File Path"), - available_range=create_time_range( - media_pool_item.GetClipProperty("Start").get("Start"), - media_pool_item.GetClipProperty("Frames").get("Frames"), - media_pool_item.GetClipProperty("FPS").get("FPS") + target_url=reformat_path, + available_range=create_otio_time_range( + frame_start, + frame_duration, + fps ) ) -def create_markers(track_item, frame_rate): +def create_otio_markers(track_item, frame_rate): track_item_markers = track_item.GetMarkers() markers = [] for marker_frame in track_item_markers: + note = track_item_markers[marker_frame]["note"] + if "{" in note and "}" in note: + metadata = json.loads(note) + else: + metadata = {"note": note} markers.append( otio.schema.Marker( name=track_item_markers[marker_frame]["name"], - marked_range=create_time_range( + marked_range=create_otio_time_range( marker_frame, track_item_markers[marker_frame]["duration"], frame_rate ), color=track_item_markers[marker_frame]["color"].upper(), - metadata={ - "Resolve": { - "note": track_item_markers[marker_frame]["note"] - } - } + metadata=metadata ) ) return markers -def create_clip(track_item): +def create_otio_clip(track_item): media_pool_item = track_item.GetMediaPoolItem() frame_rate = media_pool_item.GetClipProperty("FPS").get("FPS") + name = lib.get_reformated_path(track_item.GetName()) clip = otio.schema.Clip( - name=track_item.GetName(), - source_range=create_time_range( - track_item.GetLeftOffset(), - track_item.GetDuration(), + name=name, + source_range=create_otio_time_range( + int(track_item.GetLeftOffset()), + int(track_item.GetDuration()), frame_rate ), - media_reference=create_reference(media_pool_item) + media_reference=create_otio_reference(media_pool_item) ) - for marker in create_markers(track_item, frame_rate): + for marker in create_otio_markers(track_item, frame_rate): clip.markers.append(marker) return clip -def create_gap(gap_start, clip_start, tl_start_frame, frame_rate): +def create_otio_gap(gap_start, clip_start, tl_start_frame, frame_rate): return otio.schema.Gap( - source_range=create_time_range( + source_range=create_otio_time_range( gap_start, (clip_start - tl_start_frame) - gap_start, frame_rate @@ -83,23 +94,30 @@ def create_gap(gap_start, clip_start, tl_start_frame, frame_rate): ) -def create_timeline(timeline): - return otio.schema.Timeline(name=timeline.GetName()) +def create_otio_timeline(timeline, fps): + start_time = create_otio_rational_time( + timeline.GetStartFrame(), fps) + otio_timeline = otio.schema.Timeline( + name=timeline.GetName(), + global_start_time=start_time + ) + return otio_timeline -def create_track(track_type, track_name): +def create_otio_track(track_type, track_name): return otio.schema.Track( name=track_name, kind=TRACK_TYPES[track_type] ) -def create_complete_otio_timeline(project): +def get_otio_complete_timeline(project): # get current timeline timeline = project.GetCurrentTimeline() + fps = project.GetSetting("timelineFrameRate") # convert timeline to otio - otio_timeline = create_timeline(timeline) + otio_timeline = create_otio_timeline(timeline, fps) # loop all defined track types for track_type in list(TRACK_TYPES.keys()): @@ -112,7 +130,7 @@ def create_complete_otio_timeline(project): track_name = timeline.GetTrackName(track_type, track_index) # convert track to otio - otio_track = create_track( + otio_track = create_otio_track( track_type, "{}{}".format(track_name, track_index)) # get all track items in current track @@ -132,7 +150,7 @@ def create_complete_otio_timeline(project): if clip_start > otio_track.available_range().duration.value: # create gap and add it to track otio_track.append( - create_gap( + create_otio_gap( otio_track.available_range().duration.value, track_item.GetStart(), timeline.GetStartFrame(), @@ -141,13 +159,13 @@ def create_complete_otio_timeline(project): ) # create otio clip and add it to track - otio_track.append(create_clip(track_item)) + otio_track.append(create_otio_clip(track_item)) # add track to otio timeline otio_timeline.tracks.append(otio_track) -def get_clip_with_parents(track_item_data): +def get_otio_clip_instance_data(track_item_data): """ Return otio objects for timeline, track and clip @@ -161,19 +179,26 @@ def get_clip_with_parents(track_item_data): """ track_item = track_item_data["clip"]["item"] - timeline = track_item_data["timeline"] + project = track_item_data["project"] + timeline = track_item_data["sequence"] track_type = track_item_data["track"]["type"] track_name = track_item_data["track"]["name"] track_index = track_item_data["track"]["index"] + frame_start = track_item.GetStart() + frame_duration = track_item.GetDuration() + project_fps = project.GetSetting("timelineFrameRate") + + otio_clip_range = create_otio_time_range( + frame_start, frame_duration, project_fps) # convert timeline to otio - otio_timeline = create_timeline(timeline) + otio_timeline = create_otio_timeline(timeline, project_fps) # convert track to otio - otio_track = create_track( + otio_track = create_otio_track( track_type, "{}{}".format(track_name, track_index)) # create otio clip - otio_clip = create_clip(track_item) + otio_clip = create_otio_clip(track_item) # add it to track otio_track.append(otio_clip) @@ -184,9 +209,10 @@ def get_clip_with_parents(track_item_data): return { "otioTimeline": otio_timeline, "otioTrack": otio_track, - "otioClip": otio_clip + "otioClip": otio_clip, + "otioClipRange": otio_clip_range } -def save(otio_timeline, path): +def save_otio(otio_timeline, path): otio.adapters.write_to_file(otio_timeline, path) diff --git a/pype/hosts/resolve/pipeline_hiero.py b/pype/hosts/resolve/pipeline_hiero.py new file mode 100644 index 0000000000..73025e790f --- /dev/null +++ b/pype/hosts/resolve/pipeline_hiero.py @@ -0,0 +1,302 @@ +""" +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 +import pype +from pype.api import Logger + +from . import lib, menu, events + +log = Logger().get_logger(__name__, "hiero") + +AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") + +# plugin paths +LOAD_PATH = os.path.join(pype.PLUGINS_DIR, "hiero", "load") +CREATE_PATH = os.path.join(pype.PLUGINS_DIR, "hiero", "create") +INVENTORY_PATH = os.path.join(pype.PLUGINS_DIR, "hiero", "inventory") + +PUBLISH_PATH = os.path.join( + pype.PLUGINS_DIR, "hiero", "publish" +).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 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) diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index be666358ae..1b7e6fc051 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -150,7 +150,10 @@ class CreatorWidget(QtWidgets.QDialog): for func, val in kwargs.items(): if getattr(item, func): func_attr = getattr(item, func) - func_attr(val) + if isinstance(val, tuple): + func_attr(*val) + else: + func_attr(val) # add to layout layout.addRow(label, item) @@ -253,7 +256,9 @@ class CreatorWidget(QtWidgets.QDialog): elif v["type"] == "QSpinBox": data[k]["value"] = self.create_row( content_layout, "QSpinBox", v["label"], - setValue=v["value"], setMaximum=10000, setToolTip=tool_tip) + setRange=(1, 99999), + setValue=v["value"], + setToolTip=tool_tip) return data diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py index b8c929f3d6..7f874a3281 100644 --- a/pype/plugins/resolve/publish/collect_instances.py +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -40,12 +40,6 @@ class CollectInstances(pyblish.api.ContextPlugin): clip_property = media_pool_item.GetClipProperty() self.log.debug(f"clip_property: {clip_property}") - source_path = os.path.normpath( - clip_property["File Path"]) - source_name = clip_property["File Name"] - self.log.debug(f"source_path: {source_path}") - self.log.debug(f"source_name: {source_name}") - # add tag data to instance data data.update({ k: v for k, v in tag_data.items() @@ -61,14 +55,6 @@ class CollectInstances(pyblish.api.ContextPlugin): families = [str(f) for f in tag_data["families"]] families.insert(0, str(family)) - track = track_item_data["track"]["name"] - base_name = os.path.basename(source_path) - file_head = os.path.splitext(base_name)[0] - source_first_frame = int( - track_item.GetStart() - - track_item.GetLeftOffset() - ) - # apply only for feview and master track instance if review: families += ["review", "ftrack"] @@ -81,17 +67,21 @@ class CollectInstances(pyblish.api.ContextPlugin): "publish": resolve.get_publish_attribute(track_item), # tags "tags": tag_data, - - # track item attributes - "track": track, - - # source attribute - "source": source_path, - "sourcePath": source_path, - "sourceFileHead": file_head, - "sourceFirst": source_first_frame, }) + # otio + otio_data = resolve.get_otio_clip_instance_data(track_item_data) + data.update(otio_data) + + file_name = "".join([asset, "_", subset, ".otio"]) + file_dir = os.path.dirname(context.data["currentFile"]) + file_path = os.path.join(file_dir, "otio", file_name) + + resolve.save_otio(otio_data["otioTimeline"], file_path) + + # create instance instance = context.create_instance(**data) self.log.info("Creating instance: {}".format(instance)) + self.log.debug( + "_ instance.data: {}".format(pformat(instance.data))) diff --git a/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py b/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py index f506333a67..ddb57def00 100644 --- a/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py +++ b/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py @@ -202,7 +202,8 @@ marker_color_map = { "PURPLE": "Magenta", "MAGENTA": "Magenta", "BLACK": "Blue", - "WHITE": "Green" + "WHITE": "Green", + "MINT": "Cyan" } @@ -259,7 +260,7 @@ def add_markers(otio_item, hiero_item, tagsbin): marker.marked_range.duration.value ) - tag = hiero_item.addTagToRange(_tag, start, end) + tag = hiero_item.addTag(_tag) tag.setName(marker.name or marker_color_map[marker_color]) # Add metadata @@ -285,7 +286,7 @@ def create_track(otio_track, tracknum, track_kind): return track -def create_clip(otio_clip, tagsbin): +def create_clip(otio_clip): # Create MediaSource otio_media = otio_clip.media_reference if isinstance(otio_media, otio.schema.ExternalReference): @@ -300,13 +301,10 @@ def create_clip(otio_clip, tagsbin): # Create Clip clip = hiero.core.Clip(media) - # Add markers - add_markers(otio_clip, clip, tagsbin) - return clip -def create_trackitem(playhead, track, otio_clip, clip): +def create_trackitem(playhead, track, otio_clip, clip, tagsbin): source_range = otio_clip.source_range trackitem = track.createTrackItem(otio_clip.name) @@ -352,6 +350,9 @@ def create_trackitem(playhead, track, otio_clip, clip): trackitem.setTimelineIn(timeline_in) trackitem.setTimelineOut(timeline_out) + # Add markers + add_markers(otio_clip, trackitem, tagsbin) + return trackitem @@ -362,6 +363,10 @@ def build_sequence(otio_timeline, project=None, track_kind=None): # Create a Sequence sequence = hiero.core.Sequence(otio_timeline.name or 'OTIOSequence') + global_start_time = otio_timeline.global_start_time.value + global_rate = otio_timeline.global_start_time.rate + sequence.setFramerate(global_rate) + sequence.setTimecodeStart(global_start_time) # Create a Bin to hold clips projectbin = project.clipsBin() @@ -403,7 +408,7 @@ def build_sequence(otio_timeline, project=None, track_kind=None): elif isinstance(otio_clip, otio.schema.Clip): # Create a Clip - clip = create_clip(otio_clip, tagsbin) + clip = create_clip(otio_clip) # Add Clip to a Bin sequencebin.addItem(hiero.core.BinItem(clip)) @@ -413,7 +418,8 @@ def build_sequence(otio_timeline, project=None, track_kind=None): playhead, track, otio_clip, - clip + clip, + tagsbin ) # Add trackitem to track From 9b8d34ed177962199bd445082d34929690df29f7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 3 Dec 2020 13:00:44 +0100 Subject: [PATCH 14/72] feat(resolve): otio publishing wip hiero otio import fixes --- pype/hosts/resolve/__init__.py | 17 +- pype/hosts/resolve/lib.py | 28 +- pype/hosts/resolve/lib_hiero.py | 838 ++++++++++++++++++ pype/hosts/resolve/otio.py | 112 ++- pype/hosts/resolve/pipeline_hiero.py | 302 +++++++ pype/hosts/resolve/plugin.py | 9 +- .../resolve/publish/collect_instances.py | 36 +- .../StartupUI/otioimporter/OTIOImport.py | 24 +- 8 files changed, 1286 insertions(+), 80 deletions(-) create mode 100644 pype/hosts/resolve/lib_hiero.py create mode 100644 pype/hosts/resolve/pipeline_hiero.py diff --git a/pype/hosts/resolve/__init__.py b/pype/hosts/resolve/__init__.py index 83f8e3a720..45aa5502cc 100644 --- a/pype/hosts/resolve/__init__.py +++ b/pype/hosts/resolve/__init__.py @@ -29,7 +29,8 @@ from .lib import ( create_compound_clip, swap_clips, get_pype_clip_metadata, - set_project_manager_to_folder_name + set_project_manager_to_folder_name, + get_reformated_path ) from .menu import launch_pype_menu @@ -48,6 +49,12 @@ from .workio import ( work_root ) +from .otio import ( + get_otio_clip_instance_data, + get_otio_complete_timeline, + save_otio +) + bmdvr = None bmdvf = None @@ -83,6 +90,7 @@ __all__ = [ "swap_clips", "get_pype_clip_metadata", "set_project_manager_to_folder_name", + "get_reformated_path", # menu "launch_pype_menu", @@ -101,5 +109,10 @@ __all__ = [ # singleton with black magic resolve module "bmdvr", - "bmdvf" + "bmdvf", + + # open color io integration + "get_otio_clip_instance_data", + "get_otio_complete_timeline", + "save_otio" ] diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index 2ade558d89..777cae0eb2 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -1,6 +1,6 @@ import sys import json -import ast +import re from opentimelineio import opentime from pype.api import Logger @@ -25,6 +25,7 @@ self.pype_marker_duration = 1 self.pype_marker_color = "Mint" self.temp_marker_frame = None + def get_project_manager(): from . import bmdvr if not self.project_manager: @@ -621,3 +622,28 @@ def convert_resolve_list_type(resolve_list): "Input argument should be dict() type") return [resolve_list[i] for i in sorted(resolve_list.keys())] + + +def get_reformated_path(path, padded=True): + """ + Return fixed python expression path + + Args: + path (str): path url or simple file name + + Returns: + type: string with reformated path + + Example: + get_reformated_path("plate.[0001-1008].exr") > plate.%04d.exr + + """ + num_pattern = "(\\[\\d+\\-\\d+\\])" + padding_pattern = "(\\d+)(?=-)" + if "[" in path: + padding = len(re.findall(padding_pattern, path).pop()) + if padded: + path = re.sub(num_pattern, f"%0{padding}d", path) + else: + path = re.sub(num_pattern, f"%d", path) + return path diff --git a/pype/hosts/resolve/lib_hiero.py b/pype/hosts/resolve/lib_hiero.py new file mode 100644 index 0000000000..891ca3905c --- /dev/null +++ b/pype/hosts/resolve/lib_hiero.py @@ -0,0 +1,838 @@ +""" +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__, "hiero") + +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() diff --git a/pype/hosts/resolve/otio.py b/pype/hosts/resolve/otio.py index 7a6d142a10..782775253e 100644 --- a/pype/hosts/resolve/otio.py +++ b/pype/hosts/resolve/otio.py @@ -1,5 +1,6 @@ +import json import opentimelineio as otio - +from . import lib TRACK_TYPES = { "video": otio.schema.TrackKind.Video, @@ -7,75 +8,85 @@ TRACK_TYPES = { } -def create_rational_time(frame, fps): +def create_otio_rational_time(frame, fps): return otio.opentime.RationalTime( float(frame), float(fps) ) -def create_time_range(start_frame, frame_duration, fps): +def create_otio_time_range(start_frame, frame_duration, fps): return otio.opentime.TimeRange( - start_time=create_rational_time(start_frame, fps), - duration=create_rational_time(frame_duration, fps) + start_time=create_otio_rational_time(start_frame, fps), + duration=create_otio_rational_time(frame_duration, fps) ) -def create_reference(media_pool_item): +def create_otio_reference(media_pool_item): + path = media_pool_item.GetClipProperty( + "File Path").get("File Path") + reformat_path = lib.get_reformated_path(path, padded=False) + frame_start = int(media_pool_item.GetClipProperty( + "Start").get("Start")) + frame_duration = int(media_pool_item.GetClipProperty( + "Frames").get("Frames")) + fps = media_pool_item.GetClipProperty("FPS").get("FPS") + return otio.schema.ExternalReference( - target_url=media_pool_item.GetClipProperty( - "File Path").get("File Path"), - available_range=create_time_range( - media_pool_item.GetClipProperty("Start").get("Start"), - media_pool_item.GetClipProperty("Frames").get("Frames"), - media_pool_item.GetClipProperty("FPS").get("FPS") + target_url=reformat_path, + available_range=create_otio_time_range( + frame_start, + frame_duration, + fps ) ) -def create_markers(track_item, frame_rate): +def create_otio_markers(track_item, frame_rate): track_item_markers = track_item.GetMarkers() markers = [] for marker_frame in track_item_markers: + note = track_item_markers[marker_frame]["note"] + if "{" in note and "}" in note: + metadata = json.loads(note) + else: + metadata = {"note": note} markers.append( otio.schema.Marker( name=track_item_markers[marker_frame]["name"], - marked_range=create_time_range( + marked_range=create_otio_time_range( marker_frame, track_item_markers[marker_frame]["duration"], frame_rate ), color=track_item_markers[marker_frame]["color"].upper(), - metadata={ - "Resolve": { - "note": track_item_markers[marker_frame]["note"] - } - } + metadata=metadata ) ) return markers -def create_clip(track_item): +def create_otio_clip(track_item): media_pool_item = track_item.GetMediaPoolItem() frame_rate = media_pool_item.GetClipProperty("FPS").get("FPS") + name = lib.get_reformated_path(track_item.GetName()) clip = otio.schema.Clip( - name=track_item.GetName(), - source_range=create_time_range( - track_item.GetLeftOffset(), - track_item.GetDuration(), + name=name, + source_range=create_otio_time_range( + int(track_item.GetLeftOffset()), + int(track_item.GetDuration()), frame_rate ), - media_reference=create_reference(media_pool_item) + media_reference=create_otio_reference(media_pool_item) ) - for marker in create_markers(track_item, frame_rate): + for marker in create_otio_markers(track_item, frame_rate): clip.markers.append(marker) return clip -def create_gap(gap_start, clip_start, tl_start_frame, frame_rate): +def create_otio_gap(gap_start, clip_start, tl_start_frame, frame_rate): return otio.schema.Gap( - source_range=create_time_range( + source_range=create_otio_time_range( gap_start, (clip_start - tl_start_frame) - gap_start, frame_rate @@ -83,23 +94,30 @@ def create_gap(gap_start, clip_start, tl_start_frame, frame_rate): ) -def create_timeline(timeline): - return otio.schema.Timeline(name=timeline.GetName()) +def create_otio_timeline(timeline, fps): + start_time = create_otio_rational_time( + timeline.GetStartFrame(), fps) + otio_timeline = otio.schema.Timeline( + name=timeline.GetName(), + global_start_time=start_time + ) + return otio_timeline -def create_track(track_type, track_name): +def create_otio_track(track_type, track_name): return otio.schema.Track( name=track_name, kind=TRACK_TYPES[track_type] ) -def create_complete_otio_timeline(project): +def get_otio_complete_timeline(project): # get current timeline timeline = project.GetCurrentTimeline() + fps = project.GetSetting("timelineFrameRate") # convert timeline to otio - otio_timeline = create_timeline(timeline) + otio_timeline = create_otio_timeline(timeline, fps) # loop all defined track types for track_type in list(TRACK_TYPES.keys()): @@ -112,7 +130,7 @@ def create_complete_otio_timeline(project): track_name = timeline.GetTrackName(track_type, track_index) # convert track to otio - otio_track = create_track( + otio_track = create_otio_track( track_type, "{}{}".format(track_name, track_index)) # get all track items in current track @@ -132,7 +150,7 @@ def create_complete_otio_timeline(project): if clip_start > otio_track.available_range().duration.value: # create gap and add it to track otio_track.append( - create_gap( + create_otio_gap( otio_track.available_range().duration.value, track_item.GetStart(), timeline.GetStartFrame(), @@ -141,13 +159,13 @@ def create_complete_otio_timeline(project): ) # create otio clip and add it to track - otio_track.append(create_clip(track_item)) + otio_track.append(create_otio_clip(track_item)) # add track to otio timeline otio_timeline.tracks.append(otio_track) -def get_clip_with_parents(track_item_data): +def get_otio_clip_instance_data(track_item_data): """ Return otio objects for timeline, track and clip @@ -161,19 +179,26 @@ def get_clip_with_parents(track_item_data): """ track_item = track_item_data["clip"]["item"] - timeline = track_item_data["timeline"] + project = track_item_data["project"] + timeline = track_item_data["sequence"] track_type = track_item_data["track"]["type"] track_name = track_item_data["track"]["name"] track_index = track_item_data["track"]["index"] + frame_start = track_item.GetStart() + frame_duration = track_item.GetDuration() + project_fps = project.GetSetting("timelineFrameRate") + + otio_clip_range = create_otio_time_range( + frame_start, frame_duration, project_fps) # convert timeline to otio - otio_timeline = create_timeline(timeline) + otio_timeline = create_otio_timeline(timeline, project_fps) # convert track to otio - otio_track = create_track( + otio_track = create_otio_track( track_type, "{}{}".format(track_name, track_index)) # create otio clip - otio_clip = create_clip(track_item) + otio_clip = create_otio_clip(track_item) # add it to track otio_track.append(otio_clip) @@ -184,9 +209,10 @@ def get_clip_with_parents(track_item_data): return { "otioTimeline": otio_timeline, "otioTrack": otio_track, - "otioClip": otio_clip + "otioClip": otio_clip, + "otioClipRange": otio_clip_range } -def save(otio_timeline, path): +def save_otio(otio_timeline, path): otio.adapters.write_to_file(otio_timeline, path) diff --git a/pype/hosts/resolve/pipeline_hiero.py b/pype/hosts/resolve/pipeline_hiero.py new file mode 100644 index 0000000000..73025e790f --- /dev/null +++ b/pype/hosts/resolve/pipeline_hiero.py @@ -0,0 +1,302 @@ +""" +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 +import pype +from pype.api import Logger + +from . import lib, menu, events + +log = Logger().get_logger(__name__, "hiero") + +AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") + +# plugin paths +LOAD_PATH = os.path.join(pype.PLUGINS_DIR, "hiero", "load") +CREATE_PATH = os.path.join(pype.PLUGINS_DIR, "hiero", "create") +INVENTORY_PATH = os.path.join(pype.PLUGINS_DIR, "hiero", "inventory") + +PUBLISH_PATH = os.path.join( + pype.PLUGINS_DIR, "hiero", "publish" +).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 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) diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index be666358ae..1b7e6fc051 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -150,7 +150,10 @@ class CreatorWidget(QtWidgets.QDialog): for func, val in kwargs.items(): if getattr(item, func): func_attr = getattr(item, func) - func_attr(val) + if isinstance(val, tuple): + func_attr(*val) + else: + func_attr(val) # add to layout layout.addRow(label, item) @@ -253,7 +256,9 @@ class CreatorWidget(QtWidgets.QDialog): elif v["type"] == "QSpinBox": data[k]["value"] = self.create_row( content_layout, "QSpinBox", v["label"], - setValue=v["value"], setMaximum=10000, setToolTip=tool_tip) + setRange=(1, 99999), + setValue=v["value"], + setToolTip=tool_tip) return data diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py index b8c929f3d6..7f874a3281 100644 --- a/pype/plugins/resolve/publish/collect_instances.py +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -40,12 +40,6 @@ class CollectInstances(pyblish.api.ContextPlugin): clip_property = media_pool_item.GetClipProperty() self.log.debug(f"clip_property: {clip_property}") - source_path = os.path.normpath( - clip_property["File Path"]) - source_name = clip_property["File Name"] - self.log.debug(f"source_path: {source_path}") - self.log.debug(f"source_name: {source_name}") - # add tag data to instance data data.update({ k: v for k, v in tag_data.items() @@ -61,14 +55,6 @@ class CollectInstances(pyblish.api.ContextPlugin): families = [str(f) for f in tag_data["families"]] families.insert(0, str(family)) - track = track_item_data["track"]["name"] - base_name = os.path.basename(source_path) - file_head = os.path.splitext(base_name)[0] - source_first_frame = int( - track_item.GetStart() - - track_item.GetLeftOffset() - ) - # apply only for feview and master track instance if review: families += ["review", "ftrack"] @@ -81,17 +67,21 @@ class CollectInstances(pyblish.api.ContextPlugin): "publish": resolve.get_publish_attribute(track_item), # tags "tags": tag_data, - - # track item attributes - "track": track, - - # source attribute - "source": source_path, - "sourcePath": source_path, - "sourceFileHead": file_head, - "sourceFirst": source_first_frame, }) + # otio + otio_data = resolve.get_otio_clip_instance_data(track_item_data) + data.update(otio_data) + + file_name = "".join([asset, "_", subset, ".otio"]) + file_dir = os.path.dirname(context.data["currentFile"]) + file_path = os.path.join(file_dir, "otio", file_name) + + resolve.save_otio(otio_data["otioTimeline"], file_path) + + # create instance instance = context.create_instance(**data) self.log.info("Creating instance: {}".format(instance)) + self.log.debug( + "_ instance.data: {}".format(pformat(instance.data))) diff --git a/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py b/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py index f506333a67..ddb57def00 100644 --- a/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py +++ b/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py @@ -202,7 +202,8 @@ marker_color_map = { "PURPLE": "Magenta", "MAGENTA": "Magenta", "BLACK": "Blue", - "WHITE": "Green" + "WHITE": "Green", + "MINT": "Cyan" } @@ -259,7 +260,7 @@ def add_markers(otio_item, hiero_item, tagsbin): marker.marked_range.duration.value ) - tag = hiero_item.addTagToRange(_tag, start, end) + tag = hiero_item.addTag(_tag) tag.setName(marker.name or marker_color_map[marker_color]) # Add metadata @@ -285,7 +286,7 @@ def create_track(otio_track, tracknum, track_kind): return track -def create_clip(otio_clip, tagsbin): +def create_clip(otio_clip): # Create MediaSource otio_media = otio_clip.media_reference if isinstance(otio_media, otio.schema.ExternalReference): @@ -300,13 +301,10 @@ def create_clip(otio_clip, tagsbin): # Create Clip clip = hiero.core.Clip(media) - # Add markers - add_markers(otio_clip, clip, tagsbin) - return clip -def create_trackitem(playhead, track, otio_clip, clip): +def create_trackitem(playhead, track, otio_clip, clip, tagsbin): source_range = otio_clip.source_range trackitem = track.createTrackItem(otio_clip.name) @@ -352,6 +350,9 @@ def create_trackitem(playhead, track, otio_clip, clip): trackitem.setTimelineIn(timeline_in) trackitem.setTimelineOut(timeline_out) + # Add markers + add_markers(otio_clip, trackitem, tagsbin) + return trackitem @@ -362,6 +363,10 @@ def build_sequence(otio_timeline, project=None, track_kind=None): # Create a Sequence sequence = hiero.core.Sequence(otio_timeline.name or 'OTIOSequence') + global_start_time = otio_timeline.global_start_time.value + global_rate = otio_timeline.global_start_time.rate + sequence.setFramerate(global_rate) + sequence.setTimecodeStart(global_start_time) # Create a Bin to hold clips projectbin = project.clipsBin() @@ -403,7 +408,7 @@ def build_sequence(otio_timeline, project=None, track_kind=None): elif isinstance(otio_clip, otio.schema.Clip): # Create a Clip - clip = create_clip(otio_clip, tagsbin) + clip = create_clip(otio_clip) # Add Clip to a Bin sequencebin.addItem(hiero.core.BinItem(clip)) @@ -413,7 +418,8 @@ def build_sequence(otio_timeline, project=None, track_kind=None): playhead, track, otio_clip, - clip + clip, + tagsbin ) # Add trackitem to track From 6e618c6f10f9070384ea023e87f8939f41217fec Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 3 Dec 2020 13:16:22 +0100 Subject: [PATCH 15/72] feat(resolve): adding before if timeline start lower then clip start --- pype/hosts/resolve/otio.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pype/hosts/resolve/otio.py b/pype/hosts/resolve/otio.py index 782775253e..71cdbcc97b 100644 --- a/pype/hosts/resolve/otio.py +++ b/pype/hosts/resolve/otio.py @@ -185,6 +185,7 @@ def get_otio_clip_instance_data(track_item_data): track_name = track_item_data["track"]["name"] track_index = track_item_data["track"]["index"] + timeline_start = timeline.GetStartFrame() frame_start = track_item.GetStart() frame_duration = track_item.GetDuration() project_fps = project.GetSetting("timelineFrameRate") @@ -197,6 +198,19 @@ def get_otio_clip_instance_data(track_item_data): otio_track = create_otio_track( track_type, "{}{}".format(track_name, track_index)) + # add gap if track item is not starting from timeline start + # if gap between track start and clip start + if frame_start > timeline_start: + # create gap and add it to track + otio_track.append( + create_otio_gap( + 0, + frame_start, + timeline_start, + project_fps + ) + ) + # create otio clip otio_clip = create_otio_clip(track_item) From e94523f1c192da9a69257c2fd54ab59aa92db82b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 3 Dec 2020 18:15:11 +0100 Subject: [PATCH 16/72] feat(resolve): otio export full timeline fix(heiro): import otio timeline start and rate --- pype/hosts/resolve/otio.py | 141 +++++++++++++----- .../resolve/publish/collect_workfile.py | 19 ++- .../StartupUI/otioimporter/OTIOImport.py | 10 +- 3 files changed, 122 insertions(+), 48 deletions(-) diff --git a/pype/hosts/resolve/otio.py b/pype/hosts/resolve/otio.py index 71cdbcc97b..c1dab6defe 100644 --- a/pype/hosts/resolve/otio.py +++ b/pype/hosts/resolve/otio.py @@ -8,6 +8,17 @@ TRACK_TYPES = { } +def timecode_to_frames(timecode, framerate): + parts = zip(( + 3600 * framerate, + 60 * framerate, + framerate, 1 + ), timecode.split(":")) + return sum( + f * int(t) for f, t in parts + ) + + def create_otio_rational_time(frame, fps): return otio.opentime.RationalTime( float(frame), @@ -23,14 +34,21 @@ def create_otio_time_range(start_frame, frame_duration, fps): def create_otio_reference(media_pool_item): - path = media_pool_item.GetClipProperty( - "File Path").get("File Path") + mp_clip_property = media_pool_item.GetClipProperty() + path = mp_clip_property["File Path"] reformat_path = lib.get_reformated_path(path, padded=False) - frame_start = int(media_pool_item.GetClipProperty( - "Start").get("Start")) - frame_duration = int(media_pool_item.GetClipProperty( - "Frames").get("Frames")) - fps = media_pool_item.GetClipProperty("FPS").get("FPS") + + # get clip property regarding to type + mp_clip_property = media_pool_item.GetClipProperty() + fps = mp_clip_property["FPS"] + if mp_clip_property["Type"] == "Video": + frame_start = int(mp_clip_property["Start"]) + frame_duration = int(mp_clip_property["Frames"]) + else: + audio_duration = str(mp_clip_property["Duration"]) + frame_start = 0 + frame_duration = int(timecode_to_frames( + audio_duration, float(fps))) return otio.schema.ExternalReference( target_url=reformat_path, @@ -42,7 +60,7 @@ def create_otio_reference(media_pool_item): ) -def create_otio_markers(track_item, frame_rate): +def create_otio_markers(track_item, fps): track_item_markers = track_item.GetMarkers() markers = [] for marker_frame in track_item_markers: @@ -57,7 +75,7 @@ def create_otio_markers(track_item, frame_rate): marked_range=create_otio_time_range( marker_frame, track_item_markers[marker_frame]["duration"], - frame_rate + fps ), color=track_item_markers[marker_frame]["color"].upper(), metadata=metadata @@ -68,28 +86,48 @@ def create_otio_markers(track_item, frame_rate): def create_otio_clip(track_item): media_pool_item = track_item.GetMediaPoolItem() - frame_rate = media_pool_item.GetClipProperty("FPS").get("FPS") + mp_clip_property = media_pool_item.GetClipProperty() + fps = mp_clip_property["FPS"] name = lib.get_reformated_path(track_item.GetName()) - clip = otio.schema.Clip( - name=name, - source_range=create_otio_time_range( - int(track_item.GetLeftOffset()), - int(track_item.GetDuration()), - frame_rate - ), - media_reference=create_otio_reference(media_pool_item) + + media_reference = create_otio_reference(media_pool_item) + source_range = create_otio_time_range( + int(track_item.GetLeftOffset()), + int(track_item.GetDuration()), + fps ) - for marker in create_otio_markers(track_item, frame_rate): - clip.markers.append(marker) - return clip + + if mp_clip_property["Type"] == "Audio": + return_clips = list() + audio_chanels = mp_clip_property["Audio Ch"] + for channel in range(0, int(audio_chanels)): + clip = otio.schema.Clip( + name=f"{name}_{channel}", + source_range=source_range, + media_reference=media_reference + ) + for marker in create_otio_markers(track_item, fps): + clip.markers.append(marker) + return_clips.append(clip) + return return_clips + else: + clip = otio.schema.Clip( + name=name, + source_range=source_range, + media_reference=media_reference + ) + for marker in create_otio_markers(track_item, fps): + clip.markers.append(marker) + + return clip -def create_otio_gap(gap_start, clip_start, tl_start_frame, frame_rate): +def create_otio_gap(gap_start, clip_start, tl_start_frame, fps): return otio.schema.Gap( source_range=create_otio_time_range( gap_start, (clip_start - tl_start_frame) - gap_start, - frame_rate + fps ) ) @@ -111,6 +149,20 @@ def create_otio_track(track_type, track_name): ) +def add_otio_gap(clip_start, otio_track, track_item, timeline, project): + # if gap between track start and clip start + if clip_start > otio_track.available_range().duration.value: + # create gap and add it to track + otio_track.append( + create_otio_gap( + otio_track.available_range().duration.value, + track_item.GetStart(), + timeline.GetStartFrame(), + project.GetSetting("timelineFrameRate") + ) + ) + + def get_otio_complete_timeline(project): # get current timeline timeline = project.GetCurrentTimeline() @@ -131,7 +183,7 @@ def get_otio_complete_timeline(project): # convert track to otio otio_track = create_otio_track( - track_type, "{}{}".format(track_name, track_index)) + track_type, track_name) # get all track items in current track current_track_items = timeline.GetItemListInTrack( @@ -146,24 +198,34 @@ def get_otio_complete_timeline(project): # calculate real clip start clip_start = track_item.GetStart() - timeline.GetStartFrame() - # if gap between track start and clip start - if clip_start > otio_track.available_range().duration.value: - # create gap and add it to track - otio_track.append( - create_otio_gap( - otio_track.available_range().duration.value, - track_item.GetStart(), - timeline.GetStartFrame(), - project.GetSetting("timelineFrameRate") - ) - ) + add_otio_gap( + clip_start, otio_track, track_item, timeline, project) # create otio clip and add it to track - otio_track.append(create_otio_clip(track_item)) + otio_clip = create_otio_clip(track_item) + + if not isinstance(otio_clip, list): + otio_track.append(otio_clip) + else: + for index, clip in enumerate(otio_clip): + if index == 0: + otio_track.append(clip) + else: + # add previouse otio track to timeline + otio_timeline.tracks.append(otio_track) + # convert track to otio + otio_track = create_otio_track( + track_type, track_name) + add_otio_gap( + clip_start, otio_track, + track_item, timeline, project) + otio_track.append(clip) # add track to otio timeline otio_timeline.tracks.append(otio_track) + return otio_timeline + def get_otio_clip_instance_data(track_item_data): """ @@ -211,19 +273,16 @@ def get_otio_clip_instance_data(track_item_data): ) ) - # create otio clip + # create otio clip and add it to track otio_clip = create_otio_clip(track_item) - # add it to track - otio_track.append(otio_clip) - # add track to otio timeline otio_timeline.tracks.append(otio_track) return { "otioTimeline": otio_timeline, "otioTrack": otio_track, - "otioClip": otio_clip, + "otioClips": otio_clip, "otioClipRange": otio_clip_range } diff --git a/pype/plugins/resolve/publish/collect_workfile.py b/pype/plugins/resolve/publish/collect_workfile.py index d1b45117c9..cbbb1936c6 100644 --- a/pype/plugins/resolve/publish/collect_workfile.py +++ b/pype/plugins/resolve/publish/collect_workfile.py @@ -4,6 +4,11 @@ from pype.hosts import resolve from avalon import api as avalon from pprint import pformat +# dev +from importlib import reload +from pype.hosts.resolve import otio +reload(otio) + class CollectWorkfile(pyblish.api.ContextPlugin): """Inject the current working file into context""" @@ -21,6 +26,9 @@ class CollectWorkfile(pyblish.api.ContextPlugin): name = project.GetName() fps = project.GetSetting("timelineFrameRate") + # adding otio timeline to context + otio_timeline = resolve.get_otio_complete_timeline(project) + base_name = name + exported_projet_ext current_file = os.path.join(staging_dir, base_name) current_file = os.path.normpath(current_file) @@ -29,13 +37,16 @@ class CollectWorkfile(pyblish.api.ContextPlugin): video_tracks = resolve.get_video_track_names() # set main project attributes to context - context.data.update({ + context_data = { "activeProject": project, "activeSequence": active_sequence, + "otioTimeline": otio_timeline, "videoTracks": video_tracks, "currentFile": current_file, "fps": fps, - }) + } + self.log.debug("__ context_data: {}".format(pformat(context_data))) + context.data.update(context_data) # creating workfile representation representation = { @@ -60,3 +71,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): instance = context.create_instance(**instance_data) self.log.info("Creating instance: {}".format(instance)) self.log.debug("__ instance.data: {}".format(pformat(instance.data))) + + file_name = "".join([asset, "_", subset, ".otio"]) + file_path = os.path.join(staging_dir, file_name) + resolve.save_otio(otio_timeline, file_path) diff --git a/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py b/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py index ddb57def00..8884ecf806 100644 --- a/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py +++ b/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py @@ -363,10 +363,6 @@ def build_sequence(otio_timeline, project=None, track_kind=None): # Create a Sequence sequence = hiero.core.Sequence(otio_timeline.name or 'OTIOSequence') - global_start_time = otio_timeline.global_start_time.value - global_rate = otio_timeline.global_start_time.rate - sequence.setFramerate(global_rate) - sequence.setTimecodeStart(global_start_time) # Create a Bin to hold clips projectbin = project.clipsBin() @@ -380,7 +376,11 @@ def build_sequence(otio_timeline, project=None, track_kind=None): # Add timeline markers add_markers(otio_timeline, sequence, tagsbin) - # TODO: Set sequence settings from otio timeline if available + # add sequence attributes form otio timeline + if otio_timeline.global_start_time: + sequence.setFramerate(otio_timeline.global_start_time.rate) + sequence.setTimecodeStart(otio_timeline.global_start_time.value) + if isinstance(otio_timeline, otio.schema.Timeline): tracks = otio_timeline.tracks From adcbf74f542be7bcd047cc7d71b3298464818911 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 3 Dec 2020 18:15:22 +0100 Subject: [PATCH 17/72] feat(resolve): wip rendering --- pype/hosts/resolve/rendering.py | 111 ++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 pype/hosts/resolve/rendering.py diff --git a/pype/hosts/resolve/rendering.py b/pype/hosts/resolve/rendering.py new file mode 100644 index 0000000000..e38466e5d4 --- /dev/null +++ b/pype/hosts/resolve/rendering.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python + +""" +Example DaVinci Resolve script: +Load a still from DRX file, apply the still to all clips in all timelines. Set render format and codec, add render jobs for all timelines, render to specified path and wait for rendering completion. +Once render is complete, delete all jobs +""" + +from python_get_resolve import GetResolve +import sys +import time + +def AddTimelineToRender( project, timeline, presetName, targetDirectory, renderFormat, renderCodec ): + project.SetCurrentTimeline(timeline) + project.LoadRenderPreset(presetName) + + if not project.SetCurrentRenderFormatAndCodec(renderFormat, renderCodec): + return False + + project.SetRenderSettings({"SelectAllFrames" : 1, "TargetDir" : targetDirectory}) + return project.AddRenderJob() + +def RenderAllTimelines( resolve, presetName, targetDirectory, renderFormat, renderCodec ): + projectManager = resolve.GetProjectManager() + project = projectManager.GetCurrentProject() + if not project: + return False + + resolve.OpenPage("Deliver") + timelineCount = project.GetTimelineCount() + + for index in range (0, int(timelineCount)): + if not AddTimelineToRender(project, project.GetTimelineByIndex(index + 1), presetName, targetDirectory, renderFormat, renderCodec): + return False + return project.StartRendering() + +def IsRenderingInProgress( resolve ): + projectManager = resolve.GetProjectManager() + project = projectManager.GetCurrentProject() + if not project: + return False + + return project.IsRenderingInProgress() + +def WaitForRenderingCompletion( resolve ): + while IsRenderingInProgress(resolve): + time.sleep(1) + return + +def ApplyDRXToAllTimelineClips( timeline, path, gradeMode = 0 ): + trackCount = timeline.GetTrackCount("video") + + clips = {} + for index in range (1, int(trackCount) + 1): + clips.update( timeline.GetItemsInTrack("video", index) ) + return timeline.ApplyGradeFromDRX(path, int(gradeMode), clips) + +def ApplyDRXToAllTimelines( resolve, path, gradeMode = 0 ): + projectManager = resolve.GetProjectManager() + project = projectManager.GetCurrentProject() + if not project: + return False + timelineCount = project.GetTimelineCount() + + for index in range (0, int(timelineCount)): + timeline = project.GetTimelineByIndex(index + 1) + project.SetCurrentTimeline( timeline ) + if not ApplyDRXToAllTimelineClips(timeline, path, gradeMode): + return False + return True + +def DeleteAllRenderJobs( resolve ): + projectManager = resolve.GetProjectManager() + project = projectManager.GetCurrentProject() + project.DeleteAllRenderJobs() + return + +# Inputs: +# - DRX file to import grade still and apply it for clips +# - grade mode (0, 1 or 2) +# - preset name for rendering +# - render path +# - render format +# - render codec +if len(sys.argv) < 7: + print("input parameters for scripts are [drx file path] [grade mode] [render preset name] [render path] [render format] [render codec]") + sys.exit() + +drxPath = sys.argv[1] +gradeMode = sys.argv[2] +renderPresetName = sys.argv[3] +renderPath = sys.argv[4] +renderFormat = sys.argv[5] +renderCodec = sys.argv[6] + +# Get currently open project +resolve = GetResolve() + +if not ApplyDRXToAllTimelines(resolve, drxPath, gradeMode): + print("Unable to apply a still from drx file to all timelines") + sys.exit() + +if not RenderAllTimelines(resolve, renderPresetName, renderPath, renderFormat, renderCodec): + print("Unable to set all timelines for rendering") + sys.exit() + +WaitForRenderingCompletion(resolve) + +DeleteAllRenderJobs(resolve) + +print("Rendering is completed.") From 027f19f9f6524a4898083b704b6422b47585396f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 3 Dec 2020 18:19:53 +0100 Subject: [PATCH 18/72] fix(resolve): clip was not added to track --- pype/hosts/resolve/otio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/hosts/resolve/otio.py b/pype/hosts/resolve/otio.py index c1dab6defe..c4de1160c6 100644 --- a/pype/hosts/resolve/otio.py +++ b/pype/hosts/resolve/otio.py @@ -275,6 +275,7 @@ def get_otio_clip_instance_data(track_item_data): # create otio clip and add it to track otio_clip = create_otio_clip(track_item) + otio_track.append(otio_clip) # add track to otio timeline otio_timeline.tracks.append(otio_track) From 7dfc6a56d8a6343762fc6144e145c11b8ff11fad Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Dec 2020 13:04:33 +0100 Subject: [PATCH 19/72] feat(hiero): update otio export import --- .../Startup/otioexporter/OTIOExportTask.py | 87 ++++++++++++------- .../StartupUI/otioimporter/OTIOImport.py | 45 ++++++---- 2 files changed, 83 insertions(+), 49 deletions(-) diff --git a/setup/hiero/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportTask.py b/setup/hiero/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportTask.py index 77dc9c45b3..90504ccd18 100644 --- a/setup/hiero/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportTask.py +++ b/setup/hiero/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportTask.py @@ -49,6 +49,9 @@ class OTIOExportTask(hiero.core.TaskBase): return str(type(self)) def get_rate(self, item): + if not hasattr(item, 'framerate'): + item = item.sequence() + num, den = item.framerate().toRational() rate = float(num) / float(den) @@ -58,12 +61,12 @@ class OTIOExportTask(hiero.core.TaskBase): return round(rate, 2) def get_clip_ranges(self, trackitem): - # Is clip an audio file? Use sequence frame rate - if not trackitem.source().mediaSource().hasVideo(): - rate_item = trackitem.sequence() + # Get rate from source or sequence + if trackitem.source().mediaSource().hasVideo(): + rate_item = trackitem.source() else: - rate_item = trackitem.source() + rate_item = trackitem.sequence() source_rate = self.get_rate(rate_item) @@ -88,9 +91,10 @@ class OTIOExportTask(hiero.core.TaskBase): duration=source_duration ) - available_range = None hiero_clip = trackitem.source() - if not hiero_clip.mediaSource().isOffline(): + + available_range = None + if hiero_clip.mediaSource().isMediaPresent(): start_time = otio.opentime.RationalTime( hiero_clip.mediaSource().startTime(), source_rate @@ -123,7 +127,7 @@ class OTIOExportTask(hiero.core.TaskBase): def get_marker_color(self, tag): icon = tag.icon() - pat = 'icons:Tag(?P\w+)\.\w+' + pat = r'icons:Tag(?P\w+)\.\w+' res = re.search(pat, icon) if res: @@ -155,13 +159,17 @@ class OTIOExportTask(hiero.core.TaskBase): ) ) + metadata = dict( + Hiero=tag.metadata().dict() + ) + # Store the source item for future import assignment + metadata['Hiero']['source_type'] = hiero_item.__class__.__name__ + marker = otio.schema.Marker( name=tag.name(), color=self.get_marker_color(tag), marked_range=marked_range, - metadata={ - 'Hiero': tag.metadata().dict() - } + metadata=metadata ) otio_item.markers.append(marker) @@ -170,37 +178,44 @@ class OTIOExportTask(hiero.core.TaskBase): hiero_clip = trackitem.source() # Add Gap if needed - prev_item = ( - itemindex and trackitem.parent().items()[itemindex - 1] or - trackitem - ) + if itemindex == 0: + prev_item = trackitem - if prev_item == trackitem and trackitem.timelineIn() > 0: + else: + prev_item = trackitem.parent().items()[itemindex - 1] + + clip_diff = trackitem.timelineIn() - prev_item.timelineOut() + + if itemindex == 0 and trackitem.timelineIn() > 0: self.add_gap(trackitem, otio_track, 0) - elif ( - prev_item != trackitem and - prev_item.timelineOut() != trackitem.timelineIn() - ): + elif itemindex and clip_diff != 1: self.add_gap(trackitem, otio_track, prev_item.timelineOut()) # Create Clip source_range, available_range = self.get_clip_ranges(trackitem) - otio_clip = otio.schema.Clip() - otio_clip.name = trackitem.name() - otio_clip.source_range = source_range + otio_clip = otio.schema.Clip( + name=trackitem.name(), + source_range=source_range + ) # Add media reference media_reference = otio.schema.MissingReference() - if not hiero_clip.mediaSource().isOffline(): + if hiero_clip.mediaSource().isMediaPresent(): source = hiero_clip.mediaSource() - media_reference = otio.schema.ExternalReference() - media_reference.available_range = available_range + first_file = source.fileinfos()[0] + path = first_file.filename() - path, name = os.path.split(source.fileinfos()[0].filename()) - media_reference.target_url = os.path.join(path, name) - media_reference.name = name + if "%" in path: + path = re.sub(r"%\d+d", "%d", path) + if "#" in path: + path = re.sub(r"#+", "%d", path) + + media_reference = otio.schema.ExternalReference( + target_url=u'{}'.format(path), + available_range=available_range + ) otio_clip.media_reference = media_reference @@ -218,6 +233,7 @@ class OTIOExportTask(hiero.core.TaskBase): # Add tags as markers if self._preset.properties()["includeTags"]: + self.add_markers(trackitem, otio_clip) self.add_markers(trackitem.source(), otio_clip) otio_track.append(otio_clip) @@ -273,16 +289,16 @@ class OTIOExportTask(hiero.core.TaskBase): name=alignment, # Consider placing Hiero name in metadata transition_type=otio.schema.TransitionTypes.SMPTE_Dissolve, in_offset=in_time, - out_offset=out_time, - metadata={} + out_offset=out_time ) if alignment == 'kFadeIn': - otio_track.insert(-2, otio_transition) + otio_track.insert(-1, otio_transition) else: otio_track.append(otio_transition) + def add_tracks(self): for track in self._sequence.items(): if isinstance(track, hiero.core.AudioTrack): @@ -291,8 +307,7 @@ class OTIOExportTask(hiero.core.TaskBase): else: kind = otio.schema.TrackKind.Video - otio_track = otio.schema.Track(kind=kind) - otio_track.name = track.name() + otio_track = otio.schema.Track(name=track.name(), kind=kind) for itemindex, trackitem in enumerate(track): if isinstance(trackitem.source(), hiero.core.Clip): @@ -306,6 +321,12 @@ class OTIOExportTask(hiero.core.TaskBase): def create_OTIO(self): self.otio_timeline = otio.schema.Timeline() + + # Set global start time based on sequence + self.otio_timeline.global_start_time = otio.opentime.RationalTime( + self._sequence.timecodeStart(), + self._sequence.framerate().toFloat() + ) self.otio_timeline.name = self._sequence.name() self.add_tracks() diff --git a/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py b/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py index 8884ecf806..7efb352ed2 100644 --- a/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py +++ b/setup/hiero/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py @@ -356,19 +356,38 @@ def create_trackitem(playhead, track, otio_clip, clip, tagsbin): return trackitem -def build_sequence(otio_timeline, project=None, track_kind=None): +def build_sequence( + otio_timeline, project=None, sequence=None, track_kind=None): + if project is None: - # TODO: Find a proper way for active project - project = hiero.core.projects(hiero.core.Project.kUserProjects)[-1] + if sequence: + project = sequence.project() - # Create a Sequence - sequence = hiero.core.Sequence(otio_timeline.name or 'OTIOSequence') + else: + # Per version 12.1v2 there is no way of getting active project + project = hiero.core.projects(hiero.core.Project.kUserProjects)[-1] - # Create a Bin to hold clips projectbin = project.clipsBin() - projectbin.addItem(hiero.core.BinItem(sequence)) - sequencebin = hiero.core.Bin(sequence.name()) - projectbin.addItem(sequencebin) + + if not sequence: + # Create a Sequence + sequence = hiero.core.Sequence(otio_timeline.name or 'OTIOSequence') + + # Set sequence settings from otio timeline if available + if hasattr(otio_timeline, 'global_start_time'): + if otio_timeline.global_start_time: + start_time = otio_timeline.global_start_time + sequence.setFramerate(start_time.rate) + sequence.setTimecodeStart(start_time.value) + + # Create a Bin to hold clips + projectbin.addItem(hiero.core.BinItem(sequence)) + + sequencebin = hiero.core.Bin(sequence.name()) + projectbin.addItem(sequencebin) + + else: + sequencebin = projectbin # Get tagsBin tagsbin = hiero.core.project("Tag Presets").tagsBin() @@ -376,17 +395,11 @@ def build_sequence(otio_timeline, project=None, track_kind=None): # Add timeline markers add_markers(otio_timeline, sequence, tagsbin) - # add sequence attributes form otio timeline - if otio_timeline.global_start_time: - sequence.setFramerate(otio_timeline.global_start_time.rate) - sequence.setTimecodeStart(otio_timeline.global_start_time.value) - if isinstance(otio_timeline, otio.schema.Timeline): tracks = otio_timeline.tracks else: - # otio.schema.Stack - tracks = otio_timeline + tracks = [otio_timeline] for tracknum, otio_track in enumerate(tracks): playhead = 0 From 724c2e902324c609a9eb6bbbb38f46ac5f70a181 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Dec 2020 14:57:54 +0100 Subject: [PATCH 20/72] feat(global): extract otio timeline file --- .../global/publish/extract_otio_file.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 pype/plugins/global/publish/extract_otio_file.py diff --git a/pype/plugins/global/publish/extract_otio_file.py b/pype/plugins/global/publish/extract_otio_file.py new file mode 100644 index 0000000000..c93cf34c79 --- /dev/null +++ b/pype/plugins/global/publish/extract_otio_file.py @@ -0,0 +1,41 @@ +import os +import pyblish.api +import pype.api +import opentimelineio as otio + + +class ExtractOTIOFile(pype.api.Extractor): + """ + Extractor export OTIO file + """ + + label = "Extract OTIO file" + order = pyblish.api.ExtractorOrder + families = ["workfile"] + hosts = ["resolve"] + + def process(self, instance): + # create representation data + if "representations" not in instance.data: + instance.data["representations"] = [] + + name = instance.data["name"] + staging_dir = self.staging_dir(instance) + + otio_timeline = instance.context.data["otioTimeline"] + # create otio timeline representation + otio_file_name = name + ".otio" + otio_file_path = os.path.join(staging_dir, otio_file_name) + otio.adapters.write_to_file(otio_timeline, otio_file_path) + + representation_otio = { + 'name': "otio", + 'ext': "otio", + 'files': otio_file_name, + "stagingDir": staging_dir, + } + + instance.data["representations"].append(representation_otio) + + self.log.info("Added OTIO file representation: {}".format( + representation_otio)) From eeb635dab57a2a2a88c5964b6475ff16b3dd30cb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Dec 2020 14:58:34 +0100 Subject: [PATCH 21/72] feat(resove): wip publishing otio editorial --- .../resolve/publish/collect_instances.py | 6 -- .../resolve/publish/collect_workfile.py | 55 ++++++------------- .../resolve/publish/extract_workfile.py | 49 +++++++++++++++++ 3 files changed, 66 insertions(+), 44 deletions(-) create mode 100644 pype/plugins/resolve/publish/extract_workfile.py diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py index 7f874a3281..d8dac70a8f 100644 --- a/pype/plugins/resolve/publish/collect_instances.py +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -73,12 +73,6 @@ class CollectInstances(pyblish.api.ContextPlugin): otio_data = resolve.get_otio_clip_instance_data(track_item_data) data.update(otio_data) - file_name = "".join([asset, "_", subset, ".otio"]) - file_dir = os.path.dirname(context.data["currentFile"]) - file_path = os.path.join(file_dir, "otio", file_name) - - resolve.save_otio(otio_data["otioTimeline"], file_path) - # create instance instance = context.create_instance(**data) diff --git a/pype/plugins/resolve/publish/collect_workfile.py b/pype/plugins/resolve/publish/collect_workfile.py index cbbb1936c6..0bd1a24a46 100644 --- a/pype/plugins/resolve/publish/collect_workfile.py +++ b/pype/plugins/resolve/publish/collect_workfile.py @@ -17,61 +17,40 @@ class CollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.501 def process(self, context): - exported_projet_ext = ".drp" - asset = avalon.Session["AVALON_ASSET"] - staging_dir = os.getenv("AVALON_WORKDIR") - subset = "workfile" + asset = avalon.Session["AVALON_ASSET"] + subset = "workfile" project = resolve.get_current_project() - name = project.GetName() fps = project.GetSetting("timelineFrameRate") # adding otio timeline to context otio_timeline = resolve.get_otio_complete_timeline(project) - base_name = name + exported_projet_ext - current_file = os.path.join(staging_dir, base_name) - current_file = os.path.normpath(current_file) - active_sequence = resolve.get_current_sequence() video_tracks = resolve.get_video_track_names() - # set main project attributes to context - context_data = { - "activeProject": project, - "activeSequence": active_sequence, - "otioTimeline": otio_timeline, - "videoTracks": video_tracks, - "currentFile": current_file, - "fps": fps, - } - self.log.debug("__ context_data: {}".format(pformat(context_data))) - context.data.update(context_data) - - # creating workfile representation - representation = { - 'name': exported_projet_ext[1:], - 'ext': exported_projet_ext[1:], - 'files': base_name, - "stagingDir": staging_dir, - } - instance_data = { "name": "{}_{}".format(asset, subset), "asset": asset, "subset": "{}{}".format(asset, subset.capitalize()), "item": project, - "family": "workfile", - - # source attribute - "sourcePath": current_file, - "representations": [representation] + "family": "workfile" } + # create instance with workfile instance = context.create_instance(**instance_data) + + # update context with main project attributes + context_data = { + "activeProject": project, + "activeSequence": active_sequence, + "otioTimeline": otio_timeline, + "videoTracks": video_tracks, + "currentFile": project.GetName(), + "fps": fps, + } + context.data.update(context_data) + self.log.info("Creating instance: {}".format(instance)) self.log.debug("__ instance.data: {}".format(pformat(instance.data))) - - file_name = "".join([asset, "_", subset, ".otio"]) - file_path = os.path.join(staging_dir, file_name) - resolve.save_otio(otio_timeline, file_path) + self.log.debug("__ context_data: {}".format(pformat(context_data))) diff --git a/pype/plugins/resolve/publish/extract_workfile.py b/pype/plugins/resolve/publish/extract_workfile.py new file mode 100644 index 0000000000..a88794841b --- /dev/null +++ b/pype/plugins/resolve/publish/extract_workfile.py @@ -0,0 +1,49 @@ +import os +import pyblish.api +import pype.api +from pype.hosts import resolve + +class ExtractWorkfile(pype.api.Extractor): + """ + Extractor export DRP workfile file representation + """ + + label = "Extract Workfile" + order = pyblish.api.ExtractorOrder + families = ["workfile"] + hosts = ["resolve"] + + def process(self, instance): + # create representation data + if "representations" not in instance.data: + instance.data["representations"] = [] + + name = instance.data["name"] + project = instance.context.data["activeProject"] + staging_dir = self.staging_dir(instance) + + resolve_workfile_ext = ".drp" + drp_file_name = name + resolve_workfile_ext + drp_file_path = os.path.normpath( + os.path.join(staging_dir, drp_file_name)) + + # write out the drp workfile + resolve.get_project_manager().ExportProject( + project.GetName(), drp_file_path) + + # create drp workfile representation + representation_drp = { + 'name': resolve_workfile_ext[1:], + 'ext': resolve_workfile_ext[1:], + 'files': drp_file_name, + "stagingDir": staging_dir, + } + + instance.data["representations"].append(representation_drp) + + # add sourcePath attribute to instance + if not instance.data.get("sourcePath"): + instance.data["sourcePath"] = drp_file_path + + self.log.info("Added Resolve file representation: {}".format( + representation_drp)) From 882183356a13d567893467ee4e46e8094fd392c1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Dec 2020 17:47:59 +0100 Subject: [PATCH 22/72] fix(resolve, otio): source range with project fps rather then source --- pype/hosts/resolve/otio.py | 39 +++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/pype/hosts/resolve/otio.py b/pype/hosts/resolve/otio.py index c4de1160c6..acb669196f 100644 --- a/pype/hosts/resolve/otio.py +++ b/pype/hosts/resolve/otio.py @@ -1,11 +1,15 @@ +import sys import json import opentimelineio as otio from . import lib -TRACK_TYPES = { + +self = sys.modules[__name__] +self.track_types = { "video": otio.schema.TrackKind.Video, "audio": otio.schema.TrackKind.Audio } +self.project_fps = None def timecode_to_frames(timecode, framerate): @@ -87,7 +91,12 @@ def create_otio_markers(track_item, fps): def create_otio_clip(track_item): media_pool_item = track_item.GetMediaPoolItem() mp_clip_property = media_pool_item.GetClipProperty() - fps = mp_clip_property["FPS"] + + if not self.project_fps: + fps = mp_clip_property["FPS"] + else: + fps = self.project_fps + name = lib.get_reformated_path(track_item.GetName()) media_reference = create_otio_reference(media_pool_item) @@ -145,11 +154,11 @@ def create_otio_timeline(timeline, fps): def create_otio_track(track_type, track_name): return otio.schema.Track( name=track_name, - kind=TRACK_TYPES[track_type] + kind=self.track_types[track_type] ) -def add_otio_gap(clip_start, otio_track, track_item, timeline, project): +def add_otio_gap(clip_start, otio_track, track_item, timeline): # if gap between track start and clip start if clip_start > otio_track.available_range().duration.value: # create gap and add it to track @@ -158,7 +167,7 @@ def add_otio_gap(clip_start, otio_track, track_item, timeline, project): otio_track.available_range().duration.value, track_item.GetStart(), timeline.GetStartFrame(), - project.GetSetting("timelineFrameRate") + self.project_fps ) ) @@ -166,13 +175,13 @@ def add_otio_gap(clip_start, otio_track, track_item, timeline, project): def get_otio_complete_timeline(project): # get current timeline timeline = project.GetCurrentTimeline() - fps = project.GetSetting("timelineFrameRate") + self.project_fps = project.GetSetting("timelineFrameRate") # convert timeline to otio - otio_timeline = create_otio_timeline(timeline, fps) + otio_timeline = create_otio_timeline(timeline, self.project_fps) # loop all defined track types - for track_type in list(TRACK_TYPES.keys()): + for track_type in list(self.track_types.keys()): # get total track count track_count = timeline.GetTrackCount(track_type) @@ -199,7 +208,7 @@ def get_otio_complete_timeline(project): clip_start = track_item.GetStart() - timeline.GetStartFrame() add_otio_gap( - clip_start, otio_track, track_item, timeline, project) + clip_start, otio_track, track_item, timeline) # create otio clip and add it to track otio_clip = create_otio_clip(track_item) @@ -218,7 +227,7 @@ def get_otio_complete_timeline(project): track_type, track_name) add_otio_gap( clip_start, otio_track, - track_item, timeline, project) + track_item, timeline) otio_track.append(clip) # add track to otio timeline @@ -250,12 +259,12 @@ def get_otio_clip_instance_data(track_item_data): timeline_start = timeline.GetStartFrame() frame_start = track_item.GetStart() frame_duration = track_item.GetDuration() - project_fps = project.GetSetting("timelineFrameRate") + self.project_fps = project.GetSetting("timelineFrameRate") otio_clip_range = create_otio_time_range( - frame_start, frame_duration, project_fps) + frame_start, frame_duration, self.project_fps) # convert timeline to otio - otio_timeline = create_otio_timeline(timeline, project_fps) + otio_timeline = create_otio_timeline(timeline, self.project_fps) # convert track to otio otio_track = create_otio_track( track_type, "{}{}".format(track_name, track_index)) @@ -269,7 +278,7 @@ def get_otio_clip_instance_data(track_item_data): 0, frame_start, timeline_start, - project_fps + self.project_fps ) ) @@ -283,7 +292,7 @@ def get_otio_clip_instance_data(track_item_data): return { "otioTimeline": otio_timeline, "otioTrack": otio_track, - "otioClips": otio_clip, + "otioClip": otio_clip, "otioClipRange": otio_clip_range } From ee46ca58338b3426bf74ef92a52cd72cde2fad99 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Dec 2020 17:48:30 +0100 Subject: [PATCH 23/72] clean(resolve): removing os import --- pype/plugins/resolve/publish/collect_instances.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py index d8dac70a8f..561b1b6198 100644 --- a/pype/plugins/resolve/publish/collect_instances.py +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -1,4 +1,3 @@ -import os import pyblish from pype.hosts import resolve From 70e760981ec510fdcefa8d5345de453540bf56f2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Dec 2020 17:54:25 +0100 Subject: [PATCH 24/72] feat(global): otio review clip collector --- .../global/publish/collect_otio_review.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 pype/plugins/global/publish/collect_otio_review.py diff --git a/pype/plugins/global/publish/collect_otio_review.py b/pype/plugins/global/publish/collect_otio_review.py new file mode 100644 index 0000000000..2943cc9ba5 --- /dev/null +++ b/pype/plugins/global/publish/collect_otio_review.py @@ -0,0 +1,89 @@ +""" +Requires: + otioTimeline -> context data attribute + review -> instance data attribute + masterLayer -> instance data attribute + otioClip -> instance data attribute + otioClipRange -> instance data attribute +""" +import opentimelineio as otio +from opentimelineio.opentime import to_frames +import pyblish.api + + +class CollectOcioReview(pyblish.api.InstancePlugin): + """Get matching otio from defined review layer""" + + label = "Collect OTIO review" + order = pyblish.api.CollectorOrder + families = ["clip"] + hosts = ["resolve"] + + def process(self, instance): + # get basic variables + review_track_name = instance.data["review"] + master_layer = instance.data["masterLayer"] + otio_timeline_context = instance.context.data.get("otioTimeline") + otio_clip = instance.data["otioClip"] + otio_clip_range = instance.data["otioClipRange"] + + # skip if master layer is False + if not master_layer: + return + + # get timeline time values + start_time = otio_timeline_context.global_start_time + timeline_fps = start_time.rate + playhead = start_time.value + + # get matching review track as defined in instance data `review` + review_otio_track = None + for track in otio_timeline_context.video_tracks(): + if track.name == review_track_name: + review_otio_track = track + + frame_start = to_frames( + otio_clip_range.start_time, timeline_fps) + frame_duration = to_frames( + otio_clip_range.duration, timeline_fps) + self.log.debug( + ("name: {} | " + "timeline_in: {} | timeline_out: {}").format( + otio_clip.name, frame_start, + (frame_start + frame_duration - 1))) + + orwc_fps = timeline_fps + for clip_index, otio_rw_clip in enumerate(review_otio_track): + if isinstance(otio_rw_clip, otio.schema.Clip): + orwc_source_range = otio_rw_clip.source_range + orwc_fps = orwc_source_range.start_time.rate + orwc_start = to_frames(orwc_source_range.start_time, orwc_fps) + orwc_duration = to_frames(orwc_source_range.duration, orwc_fps) + source_in = orwc_start + source_out = (orwc_start + orwc_duration) - 1 + timeline_in = playhead + timeline_out = (timeline_in + orwc_duration) - 1 + self.log.debug( + ("name: {} | source_in: {} | source_out: {} | " + "timeline_in: {} | timeline_out: {} " + "| orwc_fps: {}").format( + otio_rw_clip.name, source_in, source_out, + timeline_in, timeline_out, orwc_fps)) + + # move plyhead to next available frame + playhead = timeline_out + 1 + + elif isinstance(otio_rw_clip, otio.schema.Gap): + gap_source_range = otio_rw_clip.source_range + gap_fps = gap_source_range.start_time.rate + gap_start = to_frames( + gap_source_range.start_time, gap_fps) + gap_duration = to_frames( + gap_source_range.duration, gap_fps) + if gap_fps != orwc_fps: + gap_duration += 1 + self.log.debug( + ("name: Gap | gap_start: {} | gap_fps: {}" + "| gap_duration: {} | timeline_fps: {}").format( + gap_start, gap_fps, gap_duration, timeline_fps)) + playhead += gap_duration From a3b11ad30925ec5072b7f961f5b387aef7b34017 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 7 Dec 2020 12:32:49 +0100 Subject: [PATCH 25/72] feat(resolve): wip otio import and publishing --- pype/hosts/resolve/__init__.py | 17 +--- pype/hosts/resolve/lib.py | 33 +++++++ pype/hosts/resolve/otio/__init__.py | 0 .../{otio.py => otio/davinci_export.py} | 92 ++----------------- pype/hosts/resolve/otio/davinci_import.py | 48 ++++++++++ pype/hosts/resolve/otio/utils.py | 37 ++++++++ .../resolve/utility_scripts/OTIO_export.py | 2 +- .../resolve/utility_scripts/OTIO_import.py | 73 +++++++++++++++ .../resolve/publish/collect_workfile.py | 12 +-- 9 files changed, 211 insertions(+), 103 deletions(-) create mode 100644 pype/hosts/resolve/otio/__init__.py rename pype/hosts/resolve/{otio.py => otio/davinci_export.py} (72%) create mode 100644 pype/hosts/resolve/otio/davinci_import.py create mode 100644 pype/hosts/resolve/otio/utils.py create mode 100644 pype/hosts/resolve/utility_scripts/OTIO_import.py diff --git a/pype/hosts/resolve/__init__.py b/pype/hosts/resolve/__init__.py index 45aa5502cc..b6c43a58c2 100644 --- a/pype/hosts/resolve/__init__.py +++ b/pype/hosts/resolve/__init__.py @@ -30,7 +30,8 @@ from .lib import ( swap_clips, get_pype_clip_metadata, set_project_manager_to_folder_name, - get_reformated_path + get_reformated_path, + get_otio_clip_instance_data ) from .menu import launch_pype_menu @@ -49,12 +50,6 @@ from .workio import ( work_root ) -from .otio import ( - get_otio_clip_instance_data, - get_otio_complete_timeline, - save_otio -) - bmdvr = None bmdvf = None @@ -91,6 +86,7 @@ __all__ = [ "get_pype_clip_metadata", "set_project_manager_to_folder_name", "get_reformated_path", + "get_otio_clip_instance_data", # menu "launch_pype_menu", @@ -109,10 +105,5 @@ __all__ = [ # singleton with black magic resolve module "bmdvr", - "bmdvf", - - # open color io integration - "get_otio_clip_instance_data", - "get_otio_complete_timeline", - "save_otio" + "bmdvf" ] diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index 777cae0eb2..6b44f97172 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -647,3 +647,36 @@ def get_reformated_path(path, padded=True): else: path = re.sub(num_pattern, f"%d", path) return path + + +def get_otio_clip_instance_data(track_item_data): + """ + Return otio objects for timeline, track and clip + + Args: + track_item_data (dict): track_item_data from list returned by + resolve.get_current_track_items() + + Returns: + dict: otio clip with parent objects + + """ + from .otio import davinci_export as otio_export + + track_item = track_item_data["clip"]["item"] + project = track_item_data["project"] + + frame_start = track_item.GetStart() + frame_duration = track_item.GetDuration() + self.project_fps = project.GetSetting("timelineFrameRate") + + otio_clip_range = otio_export.create_otio_time_range( + frame_start, frame_duration, self.project_fps) + + # create otio clip and add it to track + otio_clip = otio_export.create_otio_clip(track_item) + + return { + "otioClip": otio_clip, + "otioClipRange": otio_clip_range + } diff --git a/pype/hosts/resolve/otio/__init__.py b/pype/hosts/resolve/otio/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/hosts/resolve/otio.py b/pype/hosts/resolve/otio/davinci_export.py similarity index 72% rename from pype/hosts/resolve/otio.py rename to pype/hosts/resolve/otio/davinci_export.py index acb669196f..25c578e0a7 100644 --- a/pype/hosts/resolve/otio.py +++ b/pype/hosts/resolve/otio/davinci_export.py @@ -1,8 +1,7 @@ import sys import json import opentimelineio as otio -from . import lib - +from . import utils self = sys.modules[__name__] self.track_types = { @@ -12,17 +11,6 @@ self.track_types = { self.project_fps = None -def timecode_to_frames(timecode, framerate): - parts = zip(( - 3600 * framerate, - 60 * framerate, - framerate, 1 - ), timecode.split(":")) - return sum( - f * int(t) for f, t in parts - ) - - def create_otio_rational_time(frame, fps): return otio.opentime.RationalTime( float(frame), @@ -40,7 +28,7 @@ def create_otio_time_range(start_frame, frame_duration, fps): def create_otio_reference(media_pool_item): mp_clip_property = media_pool_item.GetClipProperty() path = mp_clip_property["File Path"] - reformat_path = lib.get_reformated_path(path, padded=False) + reformat_path = utils.get_reformated_path(path, padded=False) # get clip property regarding to type mp_clip_property = media_pool_item.GetClipProperty() @@ -51,7 +39,7 @@ def create_otio_reference(media_pool_item): else: audio_duration = str(mp_clip_property["Duration"]) frame_start = 0 - frame_duration = int(timecode_to_frames( + frame_duration = int(utils.timecode_to_frames( audio_duration, float(fps))) return otio.schema.ExternalReference( @@ -97,7 +85,7 @@ def create_otio_clip(track_item): else: fps = self.project_fps - name = lib.get_reformated_path(track_item.GetName()) + name = utils.get_reformated_path(track_item.GetName()) media_reference = create_otio_reference(media_pool_item) source_range = create_otio_time_range( @@ -141,7 +129,7 @@ def create_otio_gap(gap_start, clip_start, tl_start_frame, fps): ) -def create_otio_timeline(timeline, fps): +def _create_otio_timeline(timeline, fps): start_time = create_otio_rational_time( timeline.GetStartFrame(), fps) otio_timeline = otio.schema.Timeline( @@ -172,13 +160,12 @@ def add_otio_gap(clip_start, otio_track, track_item, timeline): ) -def get_otio_complete_timeline(project): +def create_otio_timeline(timeline, fps): # get current timeline - timeline = project.GetCurrentTimeline() - self.project_fps = project.GetSetting("timelineFrameRate") + self.project_fps = fps # convert timeline to otio - otio_timeline = create_otio_timeline(timeline, self.project_fps) + otio_timeline = _create_otio_timeline(timeline, self.project_fps) # loop all defined track types for track_type in list(self.track_types.keys()): @@ -236,66 +223,5 @@ def get_otio_complete_timeline(project): return otio_timeline -def get_otio_clip_instance_data(track_item_data): - """ - Return otio objects for timeline, track and clip - - Args: - track_item_data (dict): track_item_data from list returned by - resolve.get_current_track_items() - - Returns: - dict: otio clip with parent objects - - """ - - track_item = track_item_data["clip"]["item"] - project = track_item_data["project"] - timeline = track_item_data["sequence"] - track_type = track_item_data["track"]["type"] - track_name = track_item_data["track"]["name"] - track_index = track_item_data["track"]["index"] - - timeline_start = timeline.GetStartFrame() - frame_start = track_item.GetStart() - frame_duration = track_item.GetDuration() - self.project_fps = project.GetSetting("timelineFrameRate") - - otio_clip_range = create_otio_time_range( - frame_start, frame_duration, self.project_fps) - # convert timeline to otio - otio_timeline = create_otio_timeline(timeline, self.project_fps) - # convert track to otio - otio_track = create_otio_track( - track_type, "{}{}".format(track_name, track_index)) - - # add gap if track item is not starting from timeline start - # if gap between track start and clip start - if frame_start > timeline_start: - # create gap and add it to track - otio_track.append( - create_otio_gap( - 0, - frame_start, - timeline_start, - self.project_fps - ) - ) - - # create otio clip and add it to track - otio_clip = create_otio_clip(track_item) - otio_track.append(otio_clip) - - # add track to otio timeline - otio_timeline.tracks.append(otio_track) - - return { - "otioTimeline": otio_timeline, - "otioTrack": otio_track, - "otioClip": otio_clip, - "otioClipRange": otio_clip_range - } - - -def save_otio(otio_timeline, path): +def write_to_file(otio_timeline, path): otio.adapters.write_to_file(otio_timeline, path) diff --git a/pype/hosts/resolve/otio/davinci_import.py b/pype/hosts/resolve/otio/davinci_import.py new file mode 100644 index 0000000000..19133279bb --- /dev/null +++ b/pype/hosts/resolve/otio/davinci_import.py @@ -0,0 +1,48 @@ +import sys +import DaVinciResolveScript +import opentimelineio as otio + + +self = sys.modules[__name__] +self.resolve = DaVinciResolveScript.scriptapp('Resolve') +self.fusion = DaVinciResolveScript.scriptapp('Fusion') +self.project_manager = self.resolve.GetProjectManager() +self.current_project = self.project_manager.GetCurrentProject() +self.media_pool = self.current_project.GetMediaPool() +self.track_types = { + "video": otio.schema.TrackKind.Video, + "audio": otio.schema.TrackKind.Audio +} +self.project_fps = None + + +def build_timeline(otio_timeline): + for clip in otio_timeline.each_clip(): + print(clip.name) + print(clip.parent().name) + print(clip.range_in_parent()) + + +def _build_track(otio_track): + pass + + +def _build_media_pool_item(otio_media_reference): + pass + + +def _build_track_item(otio_clip): + pass + + +def _build_gap(otio_clip): + pass + + +def _build_marker(otio_marker): + pass + + +def read_from_file(otio_file): + otio_timeline = otio.adapters.read_from_file(otio_file) + build_timeline(otio_timeline) diff --git a/pype/hosts/resolve/otio/utils.py b/pype/hosts/resolve/otio/utils.py new file mode 100644 index 0000000000..22619d4172 --- /dev/null +++ b/pype/hosts/resolve/otio/utils.py @@ -0,0 +1,37 @@ +import re + + +def timecode_to_frames(timecode, framerate): + parts = zip(( + 3600 * framerate, + 60 * framerate, + framerate, 1 + ), timecode.split(":")) + return sum( + f * int(t) for f, t in parts + ) + + +def get_reformated_path(path, padded=True): + """ + Return fixed python expression path + + Args: + path (str): path url or simple file name + + Returns: + type: string with reformated path + + Example: + get_reformated_path("plate.[0001-1008].exr") > plate.%04d.exr + + """ + num_pattern = "(\\[\\d+\\-\\d+\\])" + padding_pattern = "(\\d+)(?=-)" + if "[" in path: + padding = len(re.findall(padding_pattern, path).pop()) + if padded: + path = re.sub(num_pattern, f"%0{padding}d", path) + else: + path = re.sub(num_pattern, f"%d", path) + return path diff --git a/pype/hosts/resolve/utility_scripts/OTIO_export.py b/pype/hosts/resolve/utility_scripts/OTIO_export.py index a0c8e80bc7..7569ba4c42 100644 --- a/pype/hosts/resolve/utility_scripts/OTIO_export.py +++ b/pype/hosts/resolve/utility_scripts/OTIO_export.py @@ -2,7 +2,7 @@ import os import sys import opentimelineio as otio -print(otio) + resolve = bmd.scriptapp("Resolve") fu = resolve.Fusion() diff --git a/pype/hosts/resolve/utility_scripts/OTIO_import.py b/pype/hosts/resolve/utility_scripts/OTIO_import.py new file mode 100644 index 0000000000..2266fd4b2b --- /dev/null +++ b/pype/hosts/resolve/utility_scripts/OTIO_import.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +import os +import sys +from pype.hosts.resolve.otio import davinci_resolve_import as otio_import + +resolve = bmd.scriptapp("Resolve") +fu = resolve.Fusion() +ui = fu.UIManager +disp = bmd.UIDispatcher(fu.UIManager) + + +title_font = ui.Font({"PixelSize": 18}) +dlg = disp.AddWindow( + { + "WindowTitle": "Import OTIO", + "ID": "OTIOwin", + "Geometry": [250, 250, 250, 100], + "Spacing": 0, + "Margin": 10 + }, + [ + ui.VGroup( + { + "Spacing": 2 + }, + [ + ui.Button( + { + "ID": "importOTIOfileButton", + "Text": "Select OTIO File Path", + "Weight": 1.25, + "ToolTip": "Choose otio file to import from", + "Flat": False + } + ), + ui.VGap(), + ui.Button( + { + "ID": "importButton", + "Text": "Import", + "Weight": 2, + "ToolTip": "Import otio to new timeline", + "Flat": False + } + ) + ] + ) + ] +) + +itm = dlg.GetItems() + + +def _close_window(event): + disp.ExitLoop() + + +def _import_button(event): + otio_import.read_from_file(itm["importOTIOfileButton"].Text) + _close_window(None) + + +def _import_file_pressed(event): + selected_path = fu.RequestFile(os.path.expanduser("~/Documents")) + itm["importOTIOfileButton"].Text = selected_path + + +dlg.On.OTIOwin.Close = _close_window +dlg.On.importOTIOfileButton.Clicked = _import_file_pressed +dlg.On.importButton.Clicked = _import_button +dlg.Show() +disp.RunLoop() +dlg.Hide() diff --git a/pype/plugins/resolve/publish/collect_workfile.py b/pype/plugins/resolve/publish/collect_workfile.py index 0bd1a24a46..9873e1ca97 100644 --- a/pype/plugins/resolve/publish/collect_workfile.py +++ b/pype/plugins/resolve/publish/collect_workfile.py @@ -1,4 +1,3 @@ -import os import pyblish.api from pype.hosts import resolve from avalon import api as avalon @@ -6,8 +5,8 @@ from pprint import pformat # dev from importlib import reload -from pype.hosts.resolve import otio -reload(otio) +from pype.hosts.resolve.otio import davinci_export +reload(davinci_export) class CollectWorkfile(pyblish.api.ContextPlugin): @@ -23,12 +22,13 @@ class CollectWorkfile(pyblish.api.ContextPlugin): project = resolve.get_current_project() fps = project.GetSetting("timelineFrameRate") - # adding otio timeline to context - otio_timeline = resolve.get_otio_complete_timeline(project) - active_sequence = resolve.get_current_sequence() video_tracks = resolve.get_video_track_names() + # adding otio timeline to context + otio_timeline = davinci_export.create_otio_timeline( + active_sequence, fps) + instance_data = { "name": "{}_{}".format(asset, subset), "asset": asset, From cf89dbab48cec08dcfd446eb6cbd222602590175 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 7 Dec 2020 12:38:06 +0100 Subject: [PATCH 26/72] feat(global): wip collect_otio_review --- .../global/publish/collect_otio_review.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/pype/plugins/global/publish/collect_otio_review.py b/pype/plugins/global/publish/collect_otio_review.py index 2943cc9ba5..a7097b84d0 100644 --- a/pype/plugins/global/publish/collect_otio_review.py +++ b/pype/plugins/global/publish/collect_otio_review.py @@ -36,12 +36,6 @@ class CollectOcioReview(pyblish.api.InstancePlugin): timeline_fps = start_time.rate playhead = start_time.value - # get matching review track as defined in instance data `review` - review_otio_track = None - for track in otio_timeline_context.video_tracks(): - if track.name == review_track_name: - review_otio_track = track - frame_start = to_frames( otio_clip_range.start_time, timeline_fps) frame_duration = to_frames( @@ -53,9 +47,12 @@ class CollectOcioReview(pyblish.api.InstancePlugin): (frame_start + frame_duration - 1))) orwc_fps = timeline_fps - for clip_index, otio_rw_clip in enumerate(review_otio_track): - if isinstance(otio_rw_clip, otio.schema.Clip): - orwc_source_range = otio_rw_clip.source_range + for otio_clip in otio_timeline_context.each_clip(): + track_name = otio_clip.parent().name + if track_name not in review_track_name: + continue + if isinstance(otio_clip, otio.schema.Clip): + orwc_source_range = otio_clip.source_range orwc_fps = orwc_source_range.start_time.rate orwc_start = to_frames(orwc_source_range.start_time, orwc_fps) orwc_duration = to_frames(orwc_source_range.duration, orwc_fps) @@ -67,14 +64,14 @@ class CollectOcioReview(pyblish.api.InstancePlugin): ("name: {} | source_in: {} | source_out: {} | " "timeline_in: {} | timeline_out: {} " "| orwc_fps: {}").format( - otio_rw_clip.name, source_in, source_out, + otio_clip.name, source_in, source_out, timeline_in, timeline_out, orwc_fps)) # move plyhead to next available frame playhead = timeline_out + 1 - elif isinstance(otio_rw_clip, otio.schema.Gap): - gap_source_range = otio_rw_clip.source_range + elif isinstance(otio_clip, otio.schema.Gap): + gap_source_range = otio_clip.source_range gap_fps = gap_source_range.start_time.rate gap_start = to_frames( gap_source_range.start_time, gap_fps) From f7f7b657bb838ba2fdd7f2f312b0eb90cf422ece Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 7 Dec 2020 17:10:03 +0100 Subject: [PATCH 27/72] feat(global, resolve): otio publishing wip --- pype/hosts/resolve/lib.py | 6 +- pype/hosts/resolve/otio/davinci_export.py | 28 ++++++- pype/hosts/resolve/otio/utils.py | 21 +++++ pype/lib/__init__.py | 10 ++- pype/lib/editorial.py | 36 +++++++++ .../global/publish/collect_otio_review.py | 76 ++++++------------- 6 files changed, 121 insertions(+), 56 deletions(-) create mode 100644 pype/lib/editorial.py diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index 6b44f97172..5f186b7a98 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -665,9 +665,11 @@ def get_otio_clip_instance_data(track_item_data): track_item = track_item_data["clip"]["item"] project = track_item_data["project"] + timeline = track_item_data["sequence"] + timeline_start = timeline.GetStartFrame() - frame_start = track_item.GetStart() - frame_duration = track_item.GetDuration() + frame_start = int(track_item.GetStart() - timeline_start) + frame_duration = int(track_item.GetDuration()) self.project_fps = project.GetSetting("timelineFrameRate") otio_clip_range = otio_export.create_otio_time_range( diff --git a/pype/hosts/resolve/otio/davinci_export.py b/pype/hosts/resolve/otio/davinci_export.py index 25c578e0a7..cffb58f960 100644 --- a/pype/hosts/resolve/otio/davinci_export.py +++ b/pype/hosts/resolve/otio/davinci_export.py @@ -26,9 +26,17 @@ def create_otio_time_range(start_frame, frame_duration, fps): def create_otio_reference(media_pool_item): + metadata = dict() mp_clip_property = media_pool_item.GetClipProperty() path = mp_clip_property["File Path"] reformat_path = utils.get_reformated_path(path, padded=False) + padding = utils.get_padding_from_path(path) + + if padding: + metadata.update({ + "isSequence": True, + "padding": padding + }) # get clip property regarding to type mp_clip_property = media_pool_item.GetClipProperty() @@ -42,7 +50,7 @@ def create_otio_reference(media_pool_item): frame_duration = int(utils.timecode_to_frames( audio_duration, float(fps))) - return otio.schema.ExternalReference( + otio_ex_ref_item = otio.schema.ExternalReference( target_url=reformat_path, available_range=create_otio_time_range( frame_start, @@ -51,6 +59,11 @@ def create_otio_reference(media_pool_item): ) ) + # add metadata to otio item + add_otio_metadata(otio_ex_ref_item, media_pool_item, **metadata) + + return otio_ex_ref_item + def create_otio_markers(track_item, fps): track_item_markers = track_item.GetMarkers() @@ -85,7 +98,7 @@ def create_otio_clip(track_item): else: fps = self.project_fps - name = utils.get_reformated_path(track_item.GetName()) + name = track_item.GetName() media_reference = create_otio_reference(media_pool_item) source_range = create_otio_time_range( @@ -160,6 +173,17 @@ def add_otio_gap(clip_start, otio_track, track_item, timeline): ) +def add_otio_metadata(otio_item, media_pool_item, **kwargs): + mp_metadata = media_pool_item.GetMetadata() + # add additional metadata from kwargs + if kwargs: + mp_metadata.update(kwargs) + + # add metadata to otio item metadata + for key, value in mp_metadata.items(): + otio_item.metadata.update({key: value}) + + def create_otio_timeline(timeline, fps): # get current timeline self.project_fps = fps diff --git a/pype/hosts/resolve/otio/utils.py b/pype/hosts/resolve/otio/utils.py index 22619d4172..88e0b3d3b4 100644 --- a/pype/hosts/resolve/otio/utils.py +++ b/pype/hosts/resolve/otio/utils.py @@ -35,3 +35,24 @@ def get_reformated_path(path, padded=True): else: path = re.sub(num_pattern, f"%d", path) return path + + +def get_padding_from_path(path): + """ + Return padding number from DaVinci Resolve sequence path style + + Args: + path (str): path url or simple file name + + Returns: + int: padding number + + Example: + get_padding_from_path("plate.[0001-1008].exr") > 4 + + """ + padding_pattern = "(\\d+)(?=-)" + if "[" in path: + return len(re.findall(padding_pattern, path).pop()) + + return None diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 188dd68039..8cc0384032 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -46,6 +46,11 @@ from .ffmpeg_utils import ( ffprobe_streams ) +from .editorial import ( + is_overlapping, + convert_otio_range_to_frame_range +) + __all__ = [ "get_avalon_database", "set_io_database", @@ -81,5 +86,8 @@ __all__ = [ "get_ffmpeg_tool_path", "source_hash", - "_subprocess" + "_subprocess", + + "is_overlapping", + "convert_otio_range_to_frame_range" ] diff --git a/pype/lib/editorial.py b/pype/lib/editorial.py new file mode 100644 index 0000000000..41a92165c3 --- /dev/null +++ b/pype/lib/editorial.py @@ -0,0 +1,36 @@ +from opentimelineio.opentime import to_frames + + +def convert_otio_range_to_frame_range(otio_range): + start = to_frames( + otio_range.start_time, otio_range.start_time.rate) + end = start + to_frames( + otio_range.duration, otio_range.duration.rate) - 1 + return start, end + + +def is_overlapping(test_range, main_range, strict=False): + test_start, test_end = convert_otio_range_to_frame_range(test_range) + main_start, main_end = convert_otio_range_to_frame_range(main_range) + covering_exp = bool( + (test_start <= main_start) and (test_end >= main_end) + ) + inside_exp = bool( + (test_start >= main_start) and (test_end <= main_end) + ) + overlaying_right_exp = bool( + (test_start < main_end) and (test_end >= main_end) + ) + overlaying_left_exp = bool( + (test_end > main_start) and (test_start <= main_start) + ) + + if not strict: + return any(( + covering_exp, + inside_exp, + overlaying_right_exp, + overlaying_left_exp + )) + else: + return covering_exp diff --git a/pype/plugins/global/publish/collect_otio_review.py b/pype/plugins/global/publish/collect_otio_review.py index a7097b84d0..9daea4d30f 100644 --- a/pype/plugins/global/publish/collect_otio_review.py +++ b/pype/plugins/global/publish/collect_otio_review.py @@ -3,12 +3,14 @@ Requires: otioTimeline -> context data attribute review -> instance data attribute masterLayer -> instance data attribute - otioClip -> instance data attribute otioClipRange -> instance data attribute """ import opentimelineio as otio -from opentimelineio.opentime import to_frames import pyblish.api +from pype.lib import ( + is_overlapping, + convert_otio_range_to_frame_range +) class CollectOcioReview(pyblish.api.InstancePlugin): @@ -23,64 +25,36 @@ class CollectOcioReview(pyblish.api.InstancePlugin): # get basic variables review_track_name = instance.data["review"] master_layer = instance.data["masterLayer"] - otio_timeline_context = instance.context.data.get("otioTimeline") - otio_clip = instance.data["otioClip"] + otio_timeline_context = instance.context.data["otioTimeline"] otio_clip_range = instance.data["otioClipRange"] # skip if master layer is False if not master_layer: return - # get timeline time values - start_time = otio_timeline_context.global_start_time - timeline_fps = start_time.rate - playhead = start_time.value - - frame_start = to_frames( - otio_clip_range.start_time, timeline_fps) - frame_duration = to_frames( - otio_clip_range.duration, timeline_fps) - self.log.debug( - ("name: {} | " - "timeline_in: {} | timeline_out: {}").format( - otio_clip.name, frame_start, - (frame_start + frame_duration - 1))) - - orwc_fps = timeline_fps for otio_clip in otio_timeline_context.each_clip(): track_name = otio_clip.parent().name + parent_range = otio_clip.range_in_parent() if track_name not in review_track_name: continue if isinstance(otio_clip, otio.schema.Clip): - orwc_source_range = otio_clip.source_range - orwc_fps = orwc_source_range.start_time.rate - orwc_start = to_frames(orwc_source_range.start_time, orwc_fps) - orwc_duration = to_frames(orwc_source_range.duration, orwc_fps) - source_in = orwc_start - source_out = (orwc_start + orwc_duration) - 1 - timeline_in = playhead - timeline_out = (timeline_in + orwc_duration) - 1 - self.log.debug( - ("name: {} | source_in: {} | source_out: {} | " - "timeline_in: {} | timeline_out: {} " - "| orwc_fps: {}").format( - otio_clip.name, source_in, source_out, - timeline_in, timeline_out, orwc_fps)) + if is_overlapping(parent_range, otio_clip_range, strict=False): + self.create_representation( + otio_clip, otio_clip_range, instance) - # move plyhead to next available frame - playhead = timeline_out + 1 - - elif isinstance(otio_clip, otio.schema.Gap): - gap_source_range = otio_clip.source_range - gap_fps = gap_source_range.start_time.rate - gap_start = to_frames( - gap_source_range.start_time, gap_fps) - gap_duration = to_frames( - gap_source_range.duration, gap_fps) - if gap_fps != orwc_fps: - gap_duration += 1 - self.log.debug( - ("name: Gap | gap_start: {} | gap_fps: {}" - "| gap_duration: {} | timeline_fps: {}").format( - gap_start, gap_fps, gap_duration, timeline_fps)) - playhead += gap_duration + def create_representation(self, otio_clip, to_otio_range, instance): + to_timeline_start, to_timeline_end = convert_otio_range_to_frame_range( + to_otio_range) + timeline_start, timeline_end = convert_otio_range_to_frame_range( + otio_clip.range_in_parent()) + source_start, source_end = convert_otio_range_to_frame_range( + otio_clip.source_range) + media_reference = otio_clip.media_reference + available_start, available_end = convert_otio_range_to_frame_range( + media_reference.available_range) + path = media_reference.target_url + self.log.debug(path) + self.log.debug((available_start, available_end)) + self.log.debug((source_start, source_end)) + self.log.debug((timeline_start, timeline_end)) + self.log.debug((to_timeline_start, to_timeline_end)) From eeeef0d33d7683a48fbd4ed5adc2d0e478659878 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 7 Dec 2020 17:53:43 +0100 Subject: [PATCH 28/72] feat(global, resolve): otio publishing wip --- pype/hosts/resolve/lib.py | 62 ++++++++++++------- pype/lib/__init__.py | 4 +- pype/lib/editorial.py | 6 +- .../global/publish/collect_otio_review.py | 21 ++++--- .../resolve/publish/collect_instances.py | 10 ++- 5 files changed, 63 insertions(+), 40 deletions(-) diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index 5f186b7a98..5324f868d6 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -2,6 +2,10 @@ import sys import json import re from opentimelineio import opentime +import pype + +from .otio import davinci_export as otio_export + from pype.api import Logger log = Logger().get_logger(__name__, "resolve") @@ -649,20 +653,7 @@ def get_reformated_path(path, padded=True): return path -def get_otio_clip_instance_data(track_item_data): - """ - Return otio objects for timeline, track and clip - - Args: - track_item_data (dict): track_item_data from list returned by - resolve.get_current_track_items() - - Returns: - dict: otio clip with parent objects - - """ - from .otio import davinci_export as otio_export - +def create_otio_time_range_from_track_item_data(track_item_data): track_item = track_item_data["clip"]["item"] project = track_item_data["project"] timeline = track_item_data["sequence"] @@ -670,15 +661,40 @@ def get_otio_clip_instance_data(track_item_data): frame_start = int(track_item.GetStart() - timeline_start) frame_duration = int(track_item.GetDuration()) - self.project_fps = project.GetSetting("timelineFrameRate") + fps = project.GetSetting("timelineFrameRate") - otio_clip_range = otio_export.create_otio_time_range( - frame_start, frame_duration, self.project_fps) + return otio_export.create_otio_time_range( + frame_start, frame_duration, fps) - # create otio clip and add it to track - otio_clip = otio_export.create_otio_clip(track_item) - return { - "otioClip": otio_clip, - "otioClipRange": otio_clip_range - } +def get_otio_clip_instance_data(otio_timeline, track_item_data): + """ + Return otio objects for timeline, track and clip + + Args: + track_item_data (dict): track_item_data from list returned by + resolve.get_current_track_items() + otio_timeline (otio.schema.Timeline): otio object + + Returns: + dict: otio clip object + + """ + + track_item = track_item_data["clip"]["item"] + track_name = track_item_data["track"]["name"] + timeline_range = create_otio_time_range_from_track_item_data( + track_item_data) + + for otio_clip in otio_timeline.each_clip(): + track_name = otio_clip.parent().name + parent_range = otio_clip.range_in_parent() + if track_name not in track_name: + continue + if otio_clip.name not in track_item.GetName(): + continue + if pype.lib.is_overlapping_otio_ranges( + parent_range, timeline_range, strict=True): + return {"otioClip": otio_clip} + + return None diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 8cc0384032..fc66504456 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -47,7 +47,7 @@ from .ffmpeg_utils import ( ) from .editorial import ( - is_overlapping, + is_overlapping_otio_ranges, convert_otio_range_to_frame_range ) @@ -88,6 +88,6 @@ __all__ = [ "source_hash", "_subprocess", - "is_overlapping", + "is_overlapping_otio_ranges", "convert_otio_range_to_frame_range" ] diff --git a/pype/lib/editorial.py b/pype/lib/editorial.py index 41a92165c3..89f534b143 100644 --- a/pype/lib/editorial.py +++ b/pype/lib/editorial.py @@ -9,9 +9,9 @@ def convert_otio_range_to_frame_range(otio_range): return start, end -def is_overlapping(test_range, main_range, strict=False): - test_start, test_end = convert_otio_range_to_frame_range(test_range) - main_start, main_end = convert_otio_range_to_frame_range(main_range) +def is_overlapping_otio_ranges(test_otio_range, main_otio_range, strict=False): + test_start, test_end = convert_otio_range_to_frame_range(test_otio_range) + main_start, main_end = convert_otio_range_to_frame_range(main_otio_range) covering_exp = bool( (test_start <= main_start) and (test_end >= main_end) ) diff --git a/pype/plugins/global/publish/collect_otio_review.py b/pype/plugins/global/publish/collect_otio_review.py index 9daea4d30f..0ab3cf8b8b 100644 --- a/pype/plugins/global/publish/collect_otio_review.py +++ b/pype/plugins/global/publish/collect_otio_review.py @@ -8,7 +8,7 @@ Requires: import opentimelineio as otio import pyblish.api from pype.lib import ( - is_overlapping, + is_overlapping_otio_ranges, convert_otio_range_to_frame_range ) @@ -26,21 +26,22 @@ class CollectOcioReview(pyblish.api.InstancePlugin): review_track_name = instance.data["review"] master_layer = instance.data["masterLayer"] otio_timeline_context = instance.context.data["otioTimeline"] - otio_clip_range = instance.data["otioClipRange"] - + otio_clip = instance.data["otioClip"] + otio_clip_range = otio_clip.range_in_parent() # skip if master layer is False if not master_layer: return - for otio_clip in otio_timeline_context.each_clip(): - track_name = otio_clip.parent().name - parent_range = otio_clip.range_in_parent() + for _otio_clip in otio_timeline_context.each_clip(): + track_name = _otio_clip.parent().name + parent_range = _otio_clip.range_in_parent() if track_name not in review_track_name: continue - if isinstance(otio_clip, otio.schema.Clip): - if is_overlapping(parent_range, otio_clip_range, strict=False): + if isinstance(_otio_clip, otio.schema.Clip): + if is_overlapping_otio_ranges( + parent_range, otio_clip_range, strict=False): self.create_representation( - otio_clip, otio_clip_range, instance) + _otio_clip, otio_clip_range, instance) def create_representation(self, otio_clip, to_otio_range, instance): to_timeline_start, to_timeline_end = convert_otio_range_to_frame_range( @@ -50,10 +51,12 @@ class CollectOcioReview(pyblish.api.InstancePlugin): source_start, source_end = convert_otio_range_to_frame_range( otio_clip.source_range) media_reference = otio_clip.media_reference + metadata = media_reference.metadata available_start, available_end = convert_otio_range_to_frame_range( media_reference.available_range) path = media_reference.target_url self.log.debug(path) + self.log.debug(metadata) self.log.debug((available_start, available_end)) self.log.debug((source_start, source_end)) self.log.debug((timeline_start, timeline_end)) diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py index 561b1b6198..9283a7b1a6 100644 --- a/pype/plugins/resolve/publish/collect_instances.py +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -13,6 +13,7 @@ class CollectInstances(pyblish.api.ContextPlugin): hosts = ["resolve"] def process(self, context): + otio_timeline = context.data["otioTimeline"] selected_track_items = resolve.get_current_track_items( filter=True, selecting_color=resolve.publish_clip_color) @@ -68,9 +69,12 @@ class CollectInstances(pyblish.api.ContextPlugin): "tags": tag_data, }) - # otio - otio_data = resolve.get_otio_clip_instance_data(track_item_data) - data.update(otio_data) + # otio clip data + otio_data = resolve.get_otio_clip_instance_data( + otio_timeline, track_item_data) + + if otio_data: + data.update(otio_data) # create instance instance = context.create_instance(**data) From e22aceab4b4a19ea6df0a0bf330b07fae009bf49 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 7 Dec 2020 18:47:39 +0100 Subject: [PATCH 29/72] feat(global): review otio add representation --- pype/lib/__init__.py | 6 +- pype/lib/editorial.py | 21 +++++++ .../global/publish/collect_otio_review.py | 55 ++++++++++++++----- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 6f8434d43e..93799d0232 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -61,7 +61,8 @@ from .ffmpeg_utils import ( from .editorial import ( is_overlapping_otio_ranges, - convert_otio_range_to_frame_range + convert_otio_range_to_frame_range, + convert_to_padded_path ) __all__ = [ @@ -110,5 +111,6 @@ __all__ = [ "_subprocess", "is_overlapping_otio_ranges", - "convert_otio_range_to_frame_range" + "convert_otio_range_to_frame_range", + "convert_to_padded_path" ] diff --git a/pype/lib/editorial.py b/pype/lib/editorial.py index 89f534b143..2381d4b518 100644 --- a/pype/lib/editorial.py +++ b/pype/lib/editorial.py @@ -1,3 +1,4 @@ +import re from opentimelineio.opentime import to_frames @@ -34,3 +35,23 @@ def is_overlapping_otio_ranges(test_otio_range, main_otio_range, strict=False): )) else: return covering_exp + + +def convert_to_padded_path(path, padding): + """ + Return correct padding in sequence string + + Args: + path (str): path url or simple file name + padding (int): number of padding + + Returns: + type: string with reformated path + + Example: + convert_to_padded_path("plate.%d.exr") > plate.%04d.exr + + """ + if "%d" in path: + path = re.sub("%d", "%0{padding}d".format(padding=padding), path) + return path diff --git a/pype/plugins/global/publish/collect_otio_review.py b/pype/plugins/global/publish/collect_otio_review.py index 0ab3cf8b8b..cf80445f8d 100644 --- a/pype/plugins/global/publish/collect_otio_review.py +++ b/pype/plugins/global/publish/collect_otio_review.py @@ -5,12 +5,10 @@ Requires: masterLayer -> instance data attribute otioClipRange -> instance data attribute """ +import os import opentimelineio as otio import pyblish.api -from pype.lib import ( - is_overlapping_otio_ranges, - convert_otio_range_to_frame_range -) +import pype.lib class CollectOcioReview(pyblish.api.InstancePlugin): @@ -38,26 +36,53 @@ class CollectOcioReview(pyblish.api.InstancePlugin): if track_name not in review_track_name: continue if isinstance(_otio_clip, otio.schema.Clip): - if is_overlapping_otio_ranges( + if pype.lib.is_overlapping_otio_ranges( parent_range, otio_clip_range, strict=False): self.create_representation( _otio_clip, otio_clip_range, instance) def create_representation(self, otio_clip, to_otio_range, instance): - to_timeline_start, to_timeline_end = convert_otio_range_to_frame_range( + to_tl_start, to_tl_end = pype.lib.convert_otio_range_to_frame_range( to_otio_range) - timeline_start, timeline_end = convert_otio_range_to_frame_range( + tl_start, tl_end = pype.lib.convert_otio_range_to_frame_range( otio_clip.range_in_parent()) - source_start, source_end = convert_otio_range_to_frame_range( + source_start, source_end = pype.lib.convert_otio_range_to_frame_range( otio_clip.source_range) media_reference = otio_clip.media_reference metadata = media_reference.metadata - available_start, available_end = convert_otio_range_to_frame_range( + mr_start, mr_end = pype.lib.convert_otio_range_to_frame_range( media_reference.available_range) path = media_reference.target_url - self.log.debug(path) - self.log.debug(metadata) - self.log.debug((available_start, available_end)) - self.log.debug((source_start, source_end)) - self.log.debug((timeline_start, timeline_end)) - self.log.debug((to_timeline_start, to_timeline_end)) + reference_frame_start = (mr_start + source_start) + ( + to_tl_start - tl_start) + reference_frame_end = (mr_start + source_end) - ( + tl_end - to_tl_end) + + base_name = os.path.basename(path) + staging_dir = os.path.dirname(path) + ext = os.path.splitext(base_name)[1][1:] + + if metadata.get("isSequence"): + files = list() + padding = metadata["padding"] + base_name = pype.lib.convert_to_padded_path(base_name, padding) + for index in range( + reference_frame_start, (reference_frame_end + 1)): + file_name = base_name % index + path_test = os.path.join(staging_dir, file_name) + if os.path.exists(path_test): + files.append(file_name) + + self.log.debug(files) + else: + files = base_name + + representation = { + "ext": ext, + "name": ext, + "files": files, + "frameStart": reference_frame_start, + "frameEnd": reference_frame_end, + "stagingDir": staging_dir + } + self.log.debug(representation) From 042a4e643b36cae6f30e7017d57458a00a75f111 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Dec 2020 16:03:33 +0100 Subject: [PATCH 30/72] feat(global, resolve): publishing with otio wip --- pype/hosts/resolve/otio/utils.py | 8 + pype/lib/__init__.py | 6 +- pype/lib/editorial.py | 27 +- .../publish/collect_otio_frame_ranges.py | 72 ++++ .../global/publish/collect_otio_review.py | 63 +-- .../global/publish/extract_otio_review.py | 403 ++++++++++++++++++ .../resolve/publish/collect_instances.py | 6 +- .../resolve/publish/collect_workfile.py | 2 +- 8 files changed, 526 insertions(+), 61 deletions(-) create mode 100644 pype/plugins/global/publish/collect_otio_frame_ranges.py create mode 100644 pype/plugins/global/publish/extract_otio_review.py diff --git a/pype/hosts/resolve/otio/utils.py b/pype/hosts/resolve/otio/utils.py index 88e0b3d3b4..54a052bb56 100644 --- a/pype/hosts/resolve/otio/utils.py +++ b/pype/hosts/resolve/otio/utils.py @@ -12,6 +12,14 @@ def timecode_to_frames(timecode, framerate): ) +def frames_to_timecode(frames, framerate): + return '{0:02d}:{1:02d}:{2:02d}:{3:02d}'.format( + int(frames / (3600 * framerate)), + int(frames / (60 * framerate) % 60), + int(frames / framerate % 60), + int(frames % framerate)) + + def get_reformated_path(path, padded=True): """ Return fixed python expression path diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 93799d0232..cfc94ec97d 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -61,7 +61,8 @@ from .ffmpeg_utils import ( from .editorial import ( is_overlapping_otio_ranges, - convert_otio_range_to_frame_range, + otio_range_to_frame_range, + otio_range_with_handles, convert_to_padded_path ) @@ -111,6 +112,7 @@ __all__ = [ "_subprocess", "is_overlapping_otio_ranges", - "convert_otio_range_to_frame_range", + "otio_range_to_frame_range", + "otio_range_with_handles", "convert_to_padded_path" ] diff --git a/pype/lib/editorial.py b/pype/lib/editorial.py index 2381d4b518..c0ad4ace00 100644 --- a/pype/lib/editorial.py +++ b/pype/lib/editorial.py @@ -1,8 +1,9 @@ import re -from opentimelineio.opentime import to_frames +from opentimelineio.opentime import ( + to_frames, RationalTime, TimeRange) -def convert_otio_range_to_frame_range(otio_range): +def otio_range_to_frame_range(otio_range): start = to_frames( otio_range.start_time, otio_range.start_time.rate) end = start + to_frames( @@ -10,9 +11,23 @@ def convert_otio_range_to_frame_range(otio_range): return start, end +def otio_range_with_handles(otio_range, instance): + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] + handles_duration = handle_start + handle_end + fps = float(otio_range.start_time.rate) + start = to_frames(otio_range.start_time, fps) + duration = to_frames(otio_range.duration, fps) + + return TimeRange( + start_time=RationalTime((start - handle_start), fps), + duration=RationalTime((duration + handles_duration), fps) + ) + + def is_overlapping_otio_ranges(test_otio_range, main_otio_range, strict=False): - test_start, test_end = convert_otio_range_to_frame_range(test_otio_range) - main_start, main_end = convert_otio_range_to_frame_range(main_otio_range) + test_start, test_end = otio_range_to_frame_range(test_otio_range) + main_start, main_end = otio_range_to_frame_range(main_otio_range) covering_exp = bool( (test_start <= main_start) and (test_end >= main_end) ) @@ -20,10 +35,10 @@ def is_overlapping_otio_ranges(test_otio_range, main_otio_range, strict=False): (test_start >= main_start) and (test_end <= main_end) ) overlaying_right_exp = bool( - (test_start < main_end) and (test_end >= main_end) + (test_start <= main_end) and (test_end >= main_end) ) overlaying_left_exp = bool( - (test_end > main_start) and (test_start <= main_start) + (test_end >= main_start) and (test_start <= main_start) ) if not strict: diff --git a/pype/plugins/global/publish/collect_otio_frame_ranges.py b/pype/plugins/global/publish/collect_otio_frame_ranges.py new file mode 100644 index 0000000000..5d1370850f --- /dev/null +++ b/pype/plugins/global/publish/collect_otio_frame_ranges.py @@ -0,0 +1,72 @@ +""" +Requires: + otioTimeline -> context data attribute + review -> instance data attribute + masterLayer -> instance data attribute + otioClipRange -> instance data attribute +""" +# import os +import opentimelineio as otio +import pyblish.api +import pype.lib +from pprint import pformat + + +class CollectOcioFrameRanges(pyblish.api.InstancePlugin): + """Getting otio ranges from otio_clip + + Adding timeline and source ranges to instance data""" + + label = "Collect OTIO Frame Ranges" + order = pyblish.api.CollectorOrder - 0.58 + families = ["clip"] + hosts = ["resolve"] + + def process(self, instance): + # get basic variables + otio_clip = instance.data["otioClip"] + workfile_start = instance.data["workfileFrameStart"] + + # get ranges + otio_tl_range = otio_clip.range_in_parent() + self.log.debug(otio_tl_range) + otio_src_range = otio_clip.source_range + otio_avalable_range = otio_clip.available_range() + self.log.debug(otio_avalable_range) + otio_tl_range_handles = pype.lib.otio_range_with_handles( + otio_tl_range, instance) + self.log.debug(otio_tl_range_handles) + otio_src_range_handles = pype.lib.otio_range_with_handles( + otio_src_range, instance) + + # get source avalable start frame + src_starting_from = otio.opentime.to_frames( + otio_avalable_range.start_time, + otio_avalable_range.start_time.rate) + # convert to frames + range_convert = pype.lib.otio_range_to_frame_range + tl_start, tl_end = range_convert(otio_tl_range) + tl_start_h, tl_end_h = range_convert(otio_tl_range_handles) + src_start, src_end = range_convert(otio_src_range) + src_start_h, src_end_h = range_convert(otio_src_range_handles) + frame_start = workfile_start + frame_end = frame_start + otio.opentime.to_frames( + otio_tl_range.duration, otio_tl_range.duration.rate) - 1 + + data = { + "frameStart": frame_start, + "frameEnd": frame_end, + "clipStart": tl_start, + "clipEnd": tl_end, + "clipStartH": tl_start_h, + "clipEndH": tl_end_h, + "sourceStart": src_starting_from + src_start, + "sourceEnd": src_starting_from + src_end, + "sourceStartH": src_starting_from + src_start_h, + "sourceEndH": src_starting_from + src_end_h, + } + instance.data.update(data) + self.log.debug( + "_ data: {}".format(pformat(data))) + self.log.debug( + "_ instance.data: {}".format(pformat(instance.data))) diff --git a/pype/plugins/global/publish/collect_otio_review.py b/pype/plugins/global/publish/collect_otio_review.py index cf80445f8d..86ef469b71 100644 --- a/pype/plugins/global/publish/collect_otio_review.py +++ b/pype/plugins/global/publish/collect_otio_review.py @@ -5,21 +5,23 @@ Requires: masterLayer -> instance data attribute otioClipRange -> instance data attribute """ -import os +# import os import opentimelineio as otio import pyblish.api import pype.lib +from pprint import pformat class CollectOcioReview(pyblish.api.InstancePlugin): """Get matching otio from defined review layer""" label = "Collect OTIO review" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder - 0.57 families = ["clip"] hosts = ["resolve"] def process(self, instance): + otio_review_clips = list() # get basic variables review_track_name = instance.data["review"] master_layer = instance.data["masterLayer"] @@ -36,53 +38,18 @@ class CollectOcioReview(pyblish.api.InstancePlugin): if track_name not in review_track_name: continue if isinstance(_otio_clip, otio.schema.Clip): + test_start, test_end = pype.lib.otio_range_to_frame_range( + parent_range) + main_start, main_end = pype.lib.otio_range_to_frame_range( + otio_clip_range) if pype.lib.is_overlapping_otio_ranges( parent_range, otio_clip_range, strict=False): - self.create_representation( - _otio_clip, otio_clip_range, instance) + # add found clips to list + otio_review_clips.append(_otio_clip) - def create_representation(self, otio_clip, to_otio_range, instance): - to_tl_start, to_tl_end = pype.lib.convert_otio_range_to_frame_range( - to_otio_range) - tl_start, tl_end = pype.lib.convert_otio_range_to_frame_range( - otio_clip.range_in_parent()) - source_start, source_end = pype.lib.convert_otio_range_to_frame_range( - otio_clip.source_range) - media_reference = otio_clip.media_reference - metadata = media_reference.metadata - mr_start, mr_end = pype.lib.convert_otio_range_to_frame_range( - media_reference.available_range) - path = media_reference.target_url - reference_frame_start = (mr_start + source_start) + ( - to_tl_start - tl_start) - reference_frame_end = (mr_start + source_end) - ( - tl_end - to_tl_end) + instance.data["otioReviewClip"] = otio_review_clips + self.log.debug( + "_ otio_review_clips: {}".format(otio_review_clips)) - base_name = os.path.basename(path) - staging_dir = os.path.dirname(path) - ext = os.path.splitext(base_name)[1][1:] - - if metadata.get("isSequence"): - files = list() - padding = metadata["padding"] - base_name = pype.lib.convert_to_padded_path(base_name, padding) - for index in range( - reference_frame_start, (reference_frame_end + 1)): - file_name = base_name % index - path_test = os.path.join(staging_dir, file_name) - if os.path.exists(path_test): - files.append(file_name) - - self.log.debug(files) - else: - files = base_name - - representation = { - "ext": ext, - "name": ext, - "files": files, - "frameStart": reference_frame_start, - "frameEnd": reference_frame_end, - "stagingDir": staging_dir - } - self.log.debug(representation) + self.log.debug( + "_ instance.data: {}".format(pformat(instance.data))) diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py new file mode 100644 index 0000000000..9d43e9a9a8 --- /dev/null +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -0,0 +1,403 @@ +import os +import sys +import six +import errno +from pyblish import api +import pype +import clique +from avalon.vendor import filelink + + +class ExtractOTIOReview(pype.api.Extractor): + """Extract OTIO timeline into one concuted video file""" + + # order = api.ExtractorOrder + order = api.CollectorOrder + 0.1023 + label = "Extract OTIO review" + hosts = ["resolve"] + families = ["review_otio"] + + # presets + tags_addition = [] + + def process(self, instance): + # self.create_representation( + # _otio_clip, otio_clip_range, instance) + """" + Expecting (instance.data): + otioClip (otio.schema.clip): clip from otio timeline + otioReviewClips (list): list with instances of otio.schema.clip + or otio.schema.gap + + Process description: + Comparing `otioClip` parent range with `otioReviewClip` parent range will result in frame range witch is the trimmed cut. In case more otio clips or otio gaps are found in otioReviewClips then ffmpeg will generate multiple clips and those are then concuted together to one video file or image sequence. Resulting files are then added to instance as representation ready for review family plugins. + """" + + + + inst_data = instance.data + asset = inst_data['asset'] + item = inst_data['item'] + event_number = int(item.eventNumber()) + + # 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"] + + ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") + ffprobe_path = pype.lib.get_ffmpeg_tool_path("ffprobe") + + # filter out mov and img sequences + representations_new = representations[:] + for repre in representations: + input_args = list() + output_args = list() + + tags = repre.get("tags", []) + + # check if supported tags are in representation for activation + filter_tag = False + for tag in ["_cut-bigger", "_cut-smaller"]: + if tag in tags: + filter_tag = True + break + if not filter_tag: + continue + + self.log.debug("__ repre: {}".format(repre)) + + files = repre.get("files") + staging_dir = repre.get("stagingDir") + fps = repre.get("fps") + ext = repre.get("ext") + + # make paths + full_output_dir = os.path.join( + staging_dir, "cuts") + + if is_sequence: + new_files = list() + + # frame range delivery included handles + frame_start = ( + inst_data["frameStart"] - inst_data["handleStart"]) + frame_end = ( + inst_data["frameEnd"] + inst_data["handleEnd"]) + self.log.debug("_ frame_start: {}".format(frame_start)) + self.log.debug("_ frame_end: {}".format(frame_end)) + + # make collection from input files list + collections, remainder = clique.assemble(files) + collection = collections.pop() + self.log.debug("_ collection: {}".format(collection)) + + # name components + head = collection.format("{head}") + padding = collection.format("{padding}") + tail = collection.format("{tail}") + self.log.debug("_ head: {}".format(head)) + self.log.debug("_ padding: {}".format(padding)) + self.log.debug("_ tail: {}".format(tail)) + + # make destination file with instance data + # frame start and end range + 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 + ]) + 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)) + self.hardlink_file(src, dst) + new_files.append(dst_file_name) + index += 1 + + self.log.debug("_ new_files: {}".format(new_files)) + + else: + # ffmpeg when single file + new_files = "{}_{}".format(asset, files) + + # frame range + frame_start = repre.get("frameStart") + frame_end = repre.get("frameEnd") + + full_input_path = os.path.join( + staging_dir, files) + + os.path.isdir(full_output_dir) or os.makedirs(full_output_dir) + + full_output_path = os.path.join( + full_output_dir, new_files) + + self.log.debug( + "__ full_input_path: {}".format(full_input_path)) + self.log.debug( + "__ full_output_path: {}".format(full_output_path)) + + # check if audio stream is in input video file + ffprob_cmd = ( + "\"{ffprobe_path}\" -i \"{full_input_path}\" -show_streams" + " -select_streams a -loglevel error" + ).format(**locals()) + + self.log.debug("ffprob_cmd: {}".format(ffprob_cmd)) + audio_check_output = pype.api.subprocess(ffprob_cmd) + self.log.debug( + "audio_check_output: {}".format(audio_check_output)) + + # Fix one frame difference + """ TODO: this is just work-around for issue: + https://github.com/pypeclub/pype/issues/659 + """ + frame_duration_extend = 1 + if audio_check_output: + frame_duration_extend = 0 + + # translate frame to sec + start_sec = float(frame_start) / fps + duration_sec = float( + (frame_end - frame_start) + frame_duration_extend) / fps + + empty_add = None + + # check if not missing frames at start + if (start_sec < 0) or (media_duration < frame_end): + # for later swithing off `-c:v copy` output arg + empty_add = True + + # init empty variables + video_empty_start = video_layer_start = "" + audio_empty_start = audio_layer_start = "" + video_empty_end = video_layer_end = "" + audio_empty_end = audio_layer_end = "" + audio_input = audio_output = "" + v_inp_idx = 0 + concat_n = 1 + + # try to get video native resolution data + try: + resolution_output = pype.api.subprocess(( + "\"{ffprobe_path}\" -i \"{full_input_path}\"" + " -v error " + "-select_streams v:0 -show_entries " + "stream=width,height -of csv=s=x:p=0" + ).format(**locals())) + + x, y = resolution_output.split("x") + resolution_width = int(x) + resolution_height = int(y) + except Exception as _ex: + self.log.warning( + "Video native resolution is untracable: {}".format( + _ex)) + + if audio_check_output: + # adding input for empty audio + input_args.append("-f lavfi -i anullsrc") + + # define audio empty concat variables + audio_input = "[1:a]" + audio_output = ":a=1" + v_inp_idx = 1 + + # adding input for video black frame + input_args.append(( + "-f lavfi -i \"color=c=black:" + "s={resolution_width}x{resolution_height}:r={fps}\"" + ).format(**locals())) + + if (start_sec < 0): + # recalculate input video timing + empty_start_dur = abs(start_sec) + start_sec = 0 + duration_sec = float(frame_end - ( + frame_start + (empty_start_dur * fps)) + 1) / fps + + # define starting empty video concat variables + video_empty_start = ( + "[{v_inp_idx}]trim=duration={empty_start_dur}[gv0];" # noqa + ).format(**locals()) + video_layer_start = "[gv0]" + + if audio_check_output: + # define starting empty audio concat variables + audio_empty_start = ( + "[0]atrim=duration={empty_start_dur}[ga0];" + ).format(**locals()) + audio_layer_start = "[ga0]" + + # alter concat number of clips + concat_n += 1 + + # check if not missing frames at the end + if (media_duration < frame_end): + # recalculate timing + empty_end_dur = float( + frame_end - media_duration + 1) / fps + duration_sec = float( + media_duration - frame_start) / fps + + # define ending empty video concat variables + video_empty_end = ( + "[{v_inp_idx}]trim=duration={empty_end_dur}[gv1];" + ).format(**locals()) + video_layer_end = "[gv1]" + + if audio_check_output: + # define ending empty audio concat variables + audio_empty_end = ( + "[0]atrim=duration={empty_end_dur}[ga1];" + ).format(**locals()) + audio_layer_end = "[ga0]" + + # alter concat number of clips + concat_n += 1 + + # concatting black frame togather + output_args.append(( + "-filter_complex \"" + "{audio_empty_start}" + "{video_empty_start}" + "{audio_empty_end}" + "{video_empty_end}" + "{video_layer_start}{audio_layer_start}[1:v]{audio_input}" # noqa + "{video_layer_end}{audio_layer_end}" + "concat=n={concat_n}:v=1{audio_output}\"" + ).format(**locals())) + + # append ffmpeg input video clip + input_args.append("-ss {:0.2f}".format(start_sec)) + input_args.append("-t {:0.2f}".format(duration_sec)) + input_args.append("-i \"{}\"".format(full_input_path)) + + # add copy audio video codec if only shortening clip + if ("_cut-bigger" in tags) and (not empty_add): + output_args.append("-c:v copy") + + # make sure it is having no frame to frame comprassion + output_args.append("-intra") + + # output filename + output_args.append("-y \"{}\"".format(full_output_path)) + + mov_args = [ + "\"{}\"".format(ffmpeg_path), + " ".join(input_args), + " ".join(output_args) + ] + subprcs_cmd = " ".join(mov_args) + + # run subprocess + self.log.debug("Executing: {}".format(subprcs_cmd)) + output = pype.api.subprocess(subprcs_cmd) + self.log.debug("Output: {}".format(output)) + + repre_new = { + "files": new_files, + "stagingDir": full_output_dir, + "frameStart": frame_start, + "frameEnd": frame_end, + "frameStartFtrack": frame_start, + "frameEndFtrack": frame_end, + "step": 1, + "fps": fps, + "name": "cut_up_preview", + "tags": ["review"] + self.tags_addition, + "ext": ext, + "anatomy_template": "publish" + } + + representations_new.append(repre_new) + + for repre in representations_new: + if ("delete" in repre.get("tags", [])) and ( + "cut_up_preview" not in repre["name"]): + representations_new.remove(repre) + + self.log.debug( + "Representations: {}".format(representations_new)) + instance.data["representations"] = representations_new + + def hardlink_file(self, src, dst): + dirname = os.path.dirname(dst) + + # make sure the destination folder exist + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + six.reraise(*sys.exc_info()) + + # create hardlined file + try: + filelink.create(src, dst, filelink.HARDLINK) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + six.reraise(*sys.exc_info()) + + def create_representation(self, otio_clip, to_otio_range, instance): + to_tl_start, to_tl_end = pype.lib.otio_range_to_frame_range( + to_otio_range) + tl_start, tl_end = pype.lib.otio_range_to_frame_range( + otio_clip.range_in_parent()) + source_start, source_end = pype.lib.otio_range_to_frame_range( + otio_clip.source_range) + media_reference = otio_clip.media_reference + metadata = media_reference.metadata + mr_start, mr_end = pype.lib.otio_range_to_frame_range( + media_reference.available_range) + path = media_reference.target_url + reference_frame_start = (mr_start + source_start) + ( + to_tl_start - tl_start) + reference_frame_end = (mr_start + source_end) - ( + tl_end - to_tl_end) + + base_name = os.path.basename(path) + staging_dir = os.path.dirname(path) + ext = os.path.splitext(base_name)[1][1:] + + if metadata.get("isSequence"): + files = list() + padding = metadata["padding"] + base_name = pype.lib.convert_to_padded_path(base_name, padding) + for index in range( + reference_frame_start, (reference_frame_end + 1)): + file_name = base_name % index + path_test = os.path.join(staging_dir, file_name) + if os.path.exists(path_test): + files.append(file_name) + + self.log.debug(files) + else: + files = base_name + + representation = { + "ext": ext, + "name": ext, + "files": files, + "frameStart": reference_frame_start, + "frameEnd": reference_frame_end, + "stagingDir": staging_dir + } + self.log.debug(representation) diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py index 9283a7b1a6..ee32eac09e 100644 --- a/pype/plugins/resolve/publish/collect_instances.py +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -8,7 +8,7 @@ from pprint import pformat class CollectInstances(pyblish.api.ContextPlugin): """Collect all Track items selection.""" - order = pyblish.api.CollectorOrder - 0.5 + order = pyblish.api.CollectorOrder - 0.59 label = "Collect Instances" hosts = ["resolve"] @@ -64,9 +64,7 @@ class CollectInstances(pyblish.api.ContextPlugin): "asset": asset, "item": track_item, "families": families, - "publish": resolve.get_publish_attribute(track_item), - # tags - "tags": tag_data, + "publish": resolve.get_publish_attribute(track_item) }) # otio clip data diff --git a/pype/plugins/resolve/publish/collect_workfile.py b/pype/plugins/resolve/publish/collect_workfile.py index 9873e1ca97..1c6d682f3f 100644 --- a/pype/plugins/resolve/publish/collect_workfile.py +++ b/pype/plugins/resolve/publish/collect_workfile.py @@ -13,7 +13,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): """Inject the current working file into context""" label = "Collect Workfile" - order = pyblish.api.CollectorOrder - 0.501 + order = pyblish.api.CollectorOrder - 0.6 def process(self, context): From 7c0a0de6d173ab6380f7c3289d64ef667514e5ad Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Dec 2020 16:34:22 +0100 Subject: [PATCH 31/72] feat(resolve): fixing export import otio --- .../resolve/utility_scripts/OTIO_export.py | 192 +++++------------- .../resolve/utility_scripts/OTIO_import.py | 2 +- 2 files changed, 47 insertions(+), 147 deletions(-) diff --git a/pype/hosts/resolve/utility_scripts/OTIO_export.py b/pype/hosts/resolve/utility_scripts/OTIO_export.py index 7569ba4c42..3e08cb370d 100644 --- a/pype/hosts/resolve/utility_scripts/OTIO_export.py +++ b/pype/hosts/resolve/utility_scripts/OTIO_export.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import os import sys -import opentimelineio as otio +from pype.hosts.resolve.otio import davinci_export as otio_export resolve = bmd.scriptapp("Resolve") fu = resolve.Fusion() @@ -9,155 +9,44 @@ fu = resolve.Fusion() ui = fu.UIManager disp = bmd.UIDispatcher(fu.UIManager) -TRACK_TYPES = { - "video": otio.schema.TrackKind.Video, - "audio": otio.schema.TrackKind.Audio -} - -print(resolve) - -def _create_rational_time(frame, fps): - return otio.opentime.RationalTime( - float(frame), - float(fps) - ) - - -def _create_time_range(start, duration, fps): - return otio.opentime.TimeRange( - start_time=_create_rational_time(start, fps), - duration=_create_rational_time(duration, fps) - ) - - -def _create_reference(mp_item): - return otio.schema.ExternalReference( - target_url=mp_item.GetClipProperty("File Path").get("File Path"), - available_range=_create_time_range( - mp_item.GetClipProperty("Start").get("Start"), - mp_item.GetClipProperty("Frames").get("Frames"), - mp_item.GetClipProperty("FPS").get("FPS") - ) - ) - - -def _create_markers(tl_item, frame_rate): - tl_markers = tl_item.GetMarkers() - markers = [] - for m_frame in tl_markers: - markers.append( - otio.schema.Marker( - name=tl_markers[m_frame]["name"], - marked_range=_create_time_range( - m_frame, - tl_markers[m_frame]["duration"], - frame_rate - ), - color=tl_markers[m_frame]["color"].upper(), - metadata={"Resolve": {"note": tl_markers[m_frame]["note"]}} - ) - ) - return markers - - -def _create_clip(tl_item): - mp_item = tl_item.GetMediaPoolItem() - frame_rate = mp_item.GetClipProperty("FPS").get("FPS") - clip = otio.schema.Clip( - name=tl_item.GetName(), - source_range=_create_time_range( - tl_item.GetLeftOffset(), - tl_item.GetDuration(), - frame_rate - ), - media_reference=_create_reference(mp_item) - ) - for marker in _create_markers(tl_item, frame_rate): - clip.markers.append(marker) - return clip - - -def _create_gap(gap_start, clip_start, tl_start_frame, frame_rate): - return otio.schema.Gap( - source_range=_create_time_range( - gap_start, - (clip_start - tl_start_frame) - gap_start, - frame_rate - ) - ) - - -def _create_ot_timeline(output_path): - if not output_path: - return - project_manager = resolve.GetProjectManager() - current_project = project_manager.GetCurrentProject() - dr_timeline = current_project.GetCurrentTimeline() - ot_timeline = otio.schema.Timeline(name=dr_timeline.GetName()) - for track_type in list(TRACK_TYPES.keys()): - track_count = dr_timeline.GetTrackCount(track_type) - for track_index in range(1, int(track_count) + 1): - ot_track = otio.schema.Track( - name="{}{}".format(track_type[0].upper(), track_index), - kind=TRACK_TYPES[track_type] - ) - tl_items = dr_timeline.GetItemListInTrack(track_type, track_index) - for tl_item in tl_items: - if tl_item.GetMediaPoolItem() is None: - continue - clip_start = tl_item.GetStart() - dr_timeline.GetStartFrame() - if clip_start > ot_track.available_range().duration.value: - ot_track.append( - _create_gap( - ot_track.available_range().duration.value, - tl_item.GetStart(), - dr_timeline.GetStartFrame(), - current_project.GetSetting("timelineFrameRate") - ) - ) - ot_track.append(_create_clip(tl_item)) - ot_timeline.tracks.append(ot_track) - otio.adapters.write_to_file( - ot_timeline, "{}/{}.otio".format(output_path, dr_timeline.GetName())) - title_font = ui.Font({"PixelSize": 18}) dlg = disp.AddWindow( - { - "WindowTitle": "Export OTIO", - "ID": "OTIOwin", - "Geometry": [250, 250, 250, 100], - "Spacing": 0, - "Margin": 10 - }, - [ - ui.VGroup( - { - "Spacing": 2 - }, - [ - ui.Button( + { + "WindowTitle": "Export OTIO", + "ID": "OTIOwin", + "Geometry": [250, 250, 250, 100], + "Spacing": 0, + "Margin": 10 + }, + [ + ui.VGroup( { - "ID": "exportfilebttn", - "Text": "Select Destination", - "Weight": 1.25, - "ToolTip": "Choose where to save the otio", - "Flat": False - } - ), - ui.VGap(), - ui.Button( - { - "ID": "exportbttn", - "Text": "Export", - "Weight": 2, - "ToolTip": "Export the current timeline", - "Flat": False - } + "Spacing": 2 + }, + [ + ui.Button( + { + "ID": "exportfilebttn", + "Text": "Select Destination", + "Weight": 1.25, + "ToolTip": "Choose where to save the otio", + "Flat": False + } + ), + ui.VGap(), + ui.Button( + { + "ID": "exportbttn", + "Text": "Export", + "Weight": 2, + "ToolTip": "Export the current timeline", + "Flat": False + } + ) + ] ) - ] - ) - ] + ] ) itm = dlg.GetItems() @@ -168,7 +57,18 @@ def _close_window(event): def _export_button(event): - _create_ot_timeline(itm["exportfilebttn"].Text) + pm = resolve.GetProjectManager() + project = pm.GetCurrentProject() + fps = project.GetSetting("timelineFrameRate") + timeline = project.GetCurrentTimeline() + otio_timeline = otio_export.create_otio_timeline(timeline, fps) + otio_path = os.path.join( + itm["exportfilebttn"].Text, + timeline.GetName() + ".otio") + print(otio_path) + otio_export.write_to_file( + otio_timeline, + otio_path) _close_window(None) diff --git a/pype/hosts/resolve/utility_scripts/OTIO_import.py b/pype/hosts/resolve/utility_scripts/OTIO_import.py index 2266fd4b2b..879f7eb0b5 100644 --- a/pype/hosts/resolve/utility_scripts/OTIO_import.py +++ b/pype/hosts/resolve/utility_scripts/OTIO_import.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import os import sys -from pype.hosts.resolve.otio import davinci_resolve_import as otio_import +from pype.hosts.resolve.otio import davinci_import as otio_import resolve = bmd.scriptapp("Resolve") fu = resolve.Fusion() From 57c595371f1fd851dec25116acce1ea7689fd4a4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 9 Dec 2020 17:20:37 +0100 Subject: [PATCH 32/72] feat(resolve): update otio export modul - refactory timeline creation from project - adding metadata particularly widht, height --- pype/hosts/resolve/otio/davinci_export.py | 43 +++++++++++++++++++---- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/pype/hosts/resolve/otio/davinci_export.py b/pype/hosts/resolve/otio/davinci_export.py index cffb58f960..7244544183 100644 --- a/pype/hosts/resolve/otio/davinci_export.py +++ b/pype/hosts/resolve/otio/davinci_export.py @@ -26,7 +26,7 @@ def create_otio_time_range(start_frame, frame_duration, fps): def create_otio_reference(media_pool_item): - metadata = dict() + metadata = _get_metadata_media_pool_item(media_pool_item) mp_clip_property = media_pool_item.GetClipProperty() path = mp_clip_property["File Path"] reformat_path = utils.get_reformated_path(path, padded=False) @@ -142,16 +142,44 @@ def create_otio_gap(gap_start, clip_start, tl_start_frame, fps): ) -def _create_otio_timeline(timeline, fps): +def _create_otio_timeline(project, timeline, fps): + metadata = _get_timeline_metadata(project, timeline) start_time = create_otio_rational_time( timeline.GetStartFrame(), fps) otio_timeline = otio.schema.Timeline( name=timeline.GetName(), - global_start_time=start_time + global_start_time=start_time, + metadata=metadata ) return otio_timeline +def _get_timeline_metadata(project, timeline): + media_pool = project.GetMediaPool() + root_folder = media_pool.GetRootFolder() + ls_folder = root_folder.GetClipList() + timeline = project.GetCurrentTimeline() + timeline_name = timeline.GetName() + for tl in ls_folder: + if tl.GetName() not in timeline_name: + continue + return _get_metadata_media_pool_item(tl) + + +def _get_metadata_media_pool_item(media_pool_item): + data = dict() + data.update({k: v for k, v in media_pool_item.GetMetadata().items()}) + property = media_pool_item.GetClipProperty() or {} + for name, value in property.items(): + if "Resolution" in name and "" != value: + width, height = value.split("x") + data.update({ + "width": int(width), + "height": int(height) + }) + return data + + def create_otio_track(track_type, track_name): return otio.schema.Track( name=track_name, @@ -184,12 +212,15 @@ def add_otio_metadata(otio_item, media_pool_item, **kwargs): otio_item.metadata.update({key: value}) -def create_otio_timeline(timeline, fps): +def create_otio_timeline(resolve_project): + # get current timeline - self.project_fps = fps + self.project_fps = resolve_project.GetSetting("timelineFrameRate") + timeline = resolve_project.GetCurrentTimeline() # convert timeline to otio - otio_timeline = _create_otio_timeline(timeline, self.project_fps) + otio_timeline = _create_otio_timeline( + resolve_project, timeline, self.project_fps) # loop all defined track types for track_type in list(self.track_types.keys()): From 7ea9080eb82f5828cc7e1359c7638f09a77f8032 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 9 Dec 2020 17:25:53 +0100 Subject: [PATCH 33/72] feat(resolve): publishing otio wip --- .../{Pype_menu.py => __PYPE__MENU__.py} | 0 pype/hosts/resolve/utility_scripts/test.py | 25 +- .../global/publish/collect_otio_review.py | 2 +- .../global/publish/extract_otio_review.py | 761 +++++++++--------- .../resolve/publish/collect_instances.py | 25 +- .../resolve/publish/collect_workfile.py | 3 +- 6 files changed, 420 insertions(+), 396 deletions(-) rename pype/hosts/resolve/utility_scripts/{Pype_menu.py => __PYPE__MENU__.py} (100%) diff --git a/pype/hosts/resolve/utility_scripts/Pype_menu.py b/pype/hosts/resolve/utility_scripts/__PYPE__MENU__.py similarity index 100% rename from pype/hosts/resolve/utility_scripts/Pype_menu.py rename to pype/hosts/resolve/utility_scripts/__PYPE__MENU__.py diff --git a/pype/hosts/resolve/utility_scripts/test.py b/pype/hosts/resolve/utility_scripts/test.py index 69dc4768bd..a76e4dc501 100644 --- a/pype/hosts/resolve/utility_scripts/test.py +++ b/pype/hosts/resolve/utility_scripts/test.py @@ -1,19 +1,24 @@ #! python3 import sys -from pype.api import Logger import DaVinciResolveScript as bmdvr -log = Logger().get_logger(__name__) - - def main(): - import pype.hosts.resolve as bmdvr - bm = bmdvr.utils.get_resolve_module() - log.info(f"blackmagicmodule: {bm}") - - -print(f"_>> bmdvr.scriptapp(Resolve): {bmdvr.scriptapp('Resolve')}") + resolve = bmdvr.scriptapp('Resolve') + print(f"resolve: {resolve}") + project_manager = resolve.GetProjectManager() + project = project_manager.GetCurrentProject() + media_pool = project.GetMediaPool() + root_folder = media_pool.GetRootFolder() + ls_folder = root_folder.GetClipList() + timeline = project.GetCurrentTimeline() + timeline_name = timeline.GetName() + for tl in ls_folder: + if tl.GetName() not in timeline_name: + continue + print(tl.GetName()) + print(tl.GetMetadata()) + print(tl.GetClipProperty()) if __name__ == "__main__": diff --git a/pype/plugins/global/publish/collect_otio_review.py b/pype/plugins/global/publish/collect_otio_review.py index 86ef469b71..30240f456e 100644 --- a/pype/plugins/global/publish/collect_otio_review.py +++ b/pype/plugins/global/publish/collect_otio_review.py @@ -47,7 +47,7 @@ class CollectOcioReview(pyblish.api.InstancePlugin): # add found clips to list otio_review_clips.append(_otio_clip) - instance.data["otioReviewClip"] = otio_review_clips + instance.data["otioReviewClips"] = otio_review_clips self.log.debug( "_ otio_review_clips: {}".format(otio_review_clips)) diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py index 9d43e9a9a8..f829659dff 100644 --- a/pype/plugins/global/publish/extract_otio_review.py +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -12,392 +12,393 @@ class ExtractOTIOReview(pype.api.Extractor): """Extract OTIO timeline into one concuted video file""" # order = api.ExtractorOrder - order = api.CollectorOrder + 0.1023 + order = api.CollectorOrder label = "Extract OTIO review" hosts = ["resolve"] - families = ["review_otio"] - - # presets - tags_addition = [] + families = ["review"] def process(self, instance): # self.create_representation( # _otio_clip, otio_clip_range, instance) - """" - Expecting (instance.data): - otioClip (otio.schema.clip): clip from otio timeline - otioReviewClips (list): list with instances of otio.schema.clip - or otio.schema.gap + # """ + # Expecting (instance.data): + # otioClip (otio.schema.clip): clip from otio timeline + # otioReviewClips (list): list with instances of otio.schema.clip + # or otio.schema.gap + # + # Process description: + # Comparing `otioClip` parent range with `otioReviewClip` parent range will result in frame range witch is the trimmed cut. In case more otio clips or otio gaps are found in otioReviewClips then ffmpeg will generate multiple clips and those are then concuted together to one video file or image sequence. Resulting files are then added to instance as representation ready for review family plugins. + # """" - Process description: - Comparing `otioClip` parent range with `otioReviewClip` parent range will result in frame range witch is the trimmed cut. In case more otio clips or otio gaps are found in otioReviewClips then ffmpeg will generate multiple clips and those are then concuted together to one video file or image sequence. Resulting files are then added to instance as representation ready for review family plugins. - """" - - - - inst_data = instance.data - asset = inst_data['asset'] - item = inst_data['item'] - event_number = int(item.eventNumber()) - - # 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"] - - ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") - ffprobe_path = pype.lib.get_ffmpeg_tool_path("ffprobe") - - # filter out mov and img sequences - representations_new = representations[:] - for repre in representations: - input_args = list() - output_args = list() - - tags = repre.get("tags", []) - - # check if supported tags are in representation for activation - filter_tag = False - for tag in ["_cut-bigger", "_cut-smaller"]: - if tag in tags: - filter_tag = True - break - if not filter_tag: - continue - - self.log.debug("__ repre: {}".format(repre)) - - files = repre.get("files") - staging_dir = repre.get("stagingDir") - fps = repre.get("fps") - ext = repre.get("ext") - - # make paths - full_output_dir = os.path.join( - staging_dir, "cuts") - - if is_sequence: - new_files = list() - - # frame range delivery included handles - frame_start = ( - inst_data["frameStart"] - inst_data["handleStart"]) - frame_end = ( - inst_data["frameEnd"] + inst_data["handleEnd"]) - self.log.debug("_ frame_start: {}".format(frame_start)) - self.log.debug("_ frame_end: {}".format(frame_end)) - - # make collection from input files list - collections, remainder = clique.assemble(files) - collection = collections.pop() - self.log.debug("_ collection: {}".format(collection)) - - # name components - head = collection.format("{head}") - padding = collection.format("{padding}") - tail = collection.format("{tail}") - self.log.debug("_ head: {}".format(head)) - self.log.debug("_ padding: {}".format(padding)) - self.log.debug("_ tail: {}".format(tail)) - - # make destination file with instance data - # frame start and end range - 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 - ]) - 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)) - self.hardlink_file(src, dst) - new_files.append(dst_file_name) - index += 1 - - self.log.debug("_ new_files: {}".format(new_files)) - - else: - # ffmpeg when single file - new_files = "{}_{}".format(asset, files) - - # frame range - frame_start = repre.get("frameStart") - frame_end = repre.get("frameEnd") - - full_input_path = os.path.join( - staging_dir, files) - - os.path.isdir(full_output_dir) or os.makedirs(full_output_dir) - - full_output_path = os.path.join( - full_output_dir, new_files) - - self.log.debug( - "__ full_input_path: {}".format(full_input_path)) - self.log.debug( - "__ full_output_path: {}".format(full_output_path)) - - # check if audio stream is in input video file - ffprob_cmd = ( - "\"{ffprobe_path}\" -i \"{full_input_path}\" -show_streams" - " -select_streams a -loglevel error" - ).format(**locals()) - - self.log.debug("ffprob_cmd: {}".format(ffprob_cmd)) - audio_check_output = pype.api.subprocess(ffprob_cmd) - self.log.debug( - "audio_check_output: {}".format(audio_check_output)) - - # Fix one frame difference - """ TODO: this is just work-around for issue: - https://github.com/pypeclub/pype/issues/659 - """ - frame_duration_extend = 1 - if audio_check_output: - frame_duration_extend = 0 - - # translate frame to sec - start_sec = float(frame_start) / fps - duration_sec = float( - (frame_end - frame_start) + frame_duration_extend) / fps - - empty_add = None - - # check if not missing frames at start - if (start_sec < 0) or (media_duration < frame_end): - # for later swithing off `-c:v copy` output arg - empty_add = True - - # init empty variables - video_empty_start = video_layer_start = "" - audio_empty_start = audio_layer_start = "" - video_empty_end = video_layer_end = "" - audio_empty_end = audio_layer_end = "" - audio_input = audio_output = "" - v_inp_idx = 0 - concat_n = 1 - - # try to get video native resolution data - try: - resolution_output = pype.api.subprocess(( - "\"{ffprobe_path}\" -i \"{full_input_path}\"" - " -v error " - "-select_streams v:0 -show_entries " - "stream=width,height -of csv=s=x:p=0" - ).format(**locals())) - - x, y = resolution_output.split("x") - resolution_width = int(x) - resolution_height = int(y) - except Exception as _ex: - self.log.warning( - "Video native resolution is untracable: {}".format( - _ex)) - - if audio_check_output: - # adding input for empty audio - input_args.append("-f lavfi -i anullsrc") - - # define audio empty concat variables - audio_input = "[1:a]" - audio_output = ":a=1" - v_inp_idx = 1 - - # adding input for video black frame - input_args.append(( - "-f lavfi -i \"color=c=black:" - "s={resolution_width}x{resolution_height}:r={fps}\"" - ).format(**locals())) - - if (start_sec < 0): - # recalculate input video timing - empty_start_dur = abs(start_sec) - start_sec = 0 - duration_sec = float(frame_end - ( - frame_start + (empty_start_dur * fps)) + 1) / fps - - # define starting empty video concat variables - video_empty_start = ( - "[{v_inp_idx}]trim=duration={empty_start_dur}[gv0];" # noqa - ).format(**locals()) - video_layer_start = "[gv0]" - - if audio_check_output: - # define starting empty audio concat variables - audio_empty_start = ( - "[0]atrim=duration={empty_start_dur}[ga0];" - ).format(**locals()) - audio_layer_start = "[ga0]" - - # alter concat number of clips - concat_n += 1 - - # check if not missing frames at the end - if (media_duration < frame_end): - # recalculate timing - empty_end_dur = float( - frame_end - media_duration + 1) / fps - duration_sec = float( - media_duration - frame_start) / fps - - # define ending empty video concat variables - video_empty_end = ( - "[{v_inp_idx}]trim=duration={empty_end_dur}[gv1];" - ).format(**locals()) - video_layer_end = "[gv1]" - - if audio_check_output: - # define ending empty audio concat variables - audio_empty_end = ( - "[0]atrim=duration={empty_end_dur}[ga1];" - ).format(**locals()) - audio_layer_end = "[ga0]" - - # alter concat number of clips - concat_n += 1 - - # concatting black frame togather - output_args.append(( - "-filter_complex \"" - "{audio_empty_start}" - "{video_empty_start}" - "{audio_empty_end}" - "{video_empty_end}" - "{video_layer_start}{audio_layer_start}[1:v]{audio_input}" # noqa - "{video_layer_end}{audio_layer_end}" - "concat=n={concat_n}:v=1{audio_output}\"" - ).format(**locals())) - - # append ffmpeg input video clip - input_args.append("-ss {:0.2f}".format(start_sec)) - input_args.append("-t {:0.2f}".format(duration_sec)) - input_args.append("-i \"{}\"".format(full_input_path)) - - # add copy audio video codec if only shortening clip - if ("_cut-bigger" in tags) and (not empty_add): - output_args.append("-c:v copy") - - # make sure it is having no frame to frame comprassion - output_args.append("-intra") - - # output filename - output_args.append("-y \"{}\"".format(full_output_path)) - - mov_args = [ - "\"{}\"".format(ffmpeg_path), - " ".join(input_args), - " ".join(output_args) - ] - subprcs_cmd = " ".join(mov_args) - - # run subprocess - self.log.debug("Executing: {}".format(subprcs_cmd)) - output = pype.api.subprocess(subprcs_cmd) - self.log.debug("Output: {}".format(output)) - - repre_new = { - "files": new_files, - "stagingDir": full_output_dir, - "frameStart": frame_start, - "frameEnd": frame_end, - "frameStartFtrack": frame_start, - "frameEndFtrack": frame_end, - "step": 1, - "fps": fps, - "name": "cut_up_preview", - "tags": ["review"] + self.tags_addition, - "ext": ext, - "anatomy_template": "publish" - } - - representations_new.append(repre_new) - - for repre in representations_new: - if ("delete" in repre.get("tags", [])) and ( - "cut_up_preview" not in repre["name"]): - representations_new.remove(repre) - - self.log.debug( - "Representations: {}".format(representations_new)) - instance.data["representations"] = representations_new - - def hardlink_file(self, src, dst): - dirname = os.path.dirname(dst) - - # make sure the destination folder exist - try: - os.makedirs(dirname) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) - - # create hardlined file - try: - filelink.create(src, dst, filelink.HARDLINK) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) - - def create_representation(self, otio_clip, to_otio_range, instance): - to_tl_start, to_tl_end = pype.lib.otio_range_to_frame_range( - to_otio_range) - tl_start, tl_end = pype.lib.otio_range_to_frame_range( - otio_clip.range_in_parent()) - source_start, source_end = pype.lib.otio_range_to_frame_range( - otio_clip.source_range) + otio_clip = instance.data["otioClip"] media_reference = otio_clip.media_reference - metadata = media_reference.metadata - mr_start, mr_end = pype.lib.otio_range_to_frame_range( - media_reference.available_range) - path = media_reference.target_url - reference_frame_start = (mr_start + source_start) + ( - to_tl_start - tl_start) - reference_frame_end = (mr_start + source_end) - ( - tl_end - to_tl_end) + self.log.debug(media_reference.metadata) + otio_review_clips = instance.data["otioReviewClips"] + self.log.debug(otio_review_clips) - base_name = os.path.basename(path) - staging_dir = os.path.dirname(path) - ext = os.path.splitext(base_name)[1][1:] - - if metadata.get("isSequence"): - files = list() - padding = metadata["padding"] - base_name = pype.lib.convert_to_padded_path(base_name, padding) - for index in range( - reference_frame_start, (reference_frame_end + 1)): - file_name = base_name % index - path_test = os.path.join(staging_dir, file_name) - if os.path.exists(path_test): - files.append(file_name) - - self.log.debug(files) - else: - files = base_name - - representation = { - "ext": ext, - "name": ext, - "files": files, - "frameStart": reference_frame_start, - "frameEnd": reference_frame_end, - "stagingDir": staging_dir - } - self.log.debug(representation) + # inst_data = instance.data + # asset = inst_data['asset'] + # item = inst_data['item'] + # event_number = int(item.eventNumber()) + # + # # 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"] + # + # ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") + # ffprobe_path = pype.lib.get_ffmpeg_tool_path("ffprobe") + # + # # filter out mov and img sequences + # representations_new = representations[:] + # for repre in representations: + # input_args = list() + # output_args = list() + # + # tags = repre.get("tags", []) + # + # # check if supported tags are in representation for activation + # filter_tag = False + # for tag in ["_cut-bigger", "_cut-smaller"]: + # if tag in tags: + # filter_tag = True + # break + # if not filter_tag: + # continue + # + # self.log.debug("__ repre: {}".format(repre)) + # + # files = repre.get("files") + # staging_dir = repre.get("stagingDir") + # fps = repre.get("fps") + # ext = repre.get("ext") + # + # # make paths + # full_output_dir = os.path.join( + # staging_dir, "cuts") + # + # if is_sequence: + # new_files = list() + # + # # frame range delivery included handles + # frame_start = ( + # inst_data["frameStart"] - inst_data["handleStart"]) + # frame_end = ( + # inst_data["frameEnd"] + inst_data["handleEnd"]) + # self.log.debug("_ frame_start: {}".format(frame_start)) + # self.log.debug("_ frame_end: {}".format(frame_end)) + # + # # make collection from input files list + # collections, remainder = clique.assemble(files) + # collection = collections.pop() + # self.log.debug("_ collection: {}".format(collection)) + # + # # name components + # head = collection.format("{head}") + # padding = collection.format("{padding}") + # tail = collection.format("{tail}") + # self.log.debug("_ head: {}".format(head)) + # self.log.debug("_ padding: {}".format(padding)) + # self.log.debug("_ tail: {}".format(tail)) + # + # # make destination file with instance data + # # frame start and end range + # 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 + # ]) + # 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)) + # self.hardlink_file(src, dst) + # new_files.append(dst_file_name) + # index += 1 + # + # self.log.debug("_ new_files: {}".format(new_files)) + # + # else: + # # ffmpeg when single file + # new_files = "{}_{}".format(asset, files) + # + # # frame range + # frame_start = repre.get("frameStart") + # frame_end = repre.get("frameEnd") + # + # full_input_path = os.path.join( + # staging_dir, files) + # + # os.path.isdir(full_output_dir) or os.makedirs(full_output_dir) + # + # full_output_path = os.path.join( + # full_output_dir, new_files) + # + # self.log.debug( + # "__ full_input_path: {}".format(full_input_path)) + # self.log.debug( + # "__ full_output_path: {}".format(full_output_path)) + # + # # check if audio stream is in input video file + # ffprob_cmd = ( + # "\"{ffprobe_path}\" -i \"{full_input_path}\" -show_streams" + # " -select_streams a -loglevel error" + # ).format(**locals()) + # + # self.log.debug("ffprob_cmd: {}".format(ffprob_cmd)) + # audio_check_output = pype.api.subprocess(ffprob_cmd) + # self.log.debug( + # "audio_check_output: {}".format(audio_check_output)) + # + # # Fix one frame difference + # """ TODO: this is just work-around for issue: + # https://github.com/pypeclub/pype/issues/659 + # """ + # frame_duration_extend = 1 + # if audio_check_output: + # frame_duration_extend = 0 + # + # # translate frame to sec + # start_sec = float(frame_start) / fps + # duration_sec = float( + # (frame_end - frame_start) + frame_duration_extend) / fps + # + # empty_add = None + # + # # check if not missing frames at start + # if (start_sec < 0) or (media_duration < frame_end): + # # for later swithing off `-c:v copy` output arg + # empty_add = True + # + # # init empty variables + # video_empty_start = video_layer_start = "" + # audio_empty_start = audio_layer_start = "" + # video_empty_end = video_layer_end = "" + # audio_empty_end = audio_layer_end = "" + # audio_input = audio_output = "" + # v_inp_idx = 0 + # concat_n = 1 + # + # # try to get video native resolution data + # try: + # resolution_output = pype.api.subprocess(( + # "\"{ffprobe_path}\" -i \"{full_input_path}\"" + # " -v error " + # "-select_streams v:0 -show_entries " + # "stream=width,height -of csv=s=x:p=0" + # ).format(**locals())) + # + # x, y = resolution_output.split("x") + # resolution_width = int(x) + # resolution_height = int(y) + # except Exception as _ex: + # self.log.warning( + # "Video native resolution is untracable: {}".format( + # _ex)) + # + # if audio_check_output: + # # adding input for empty audio + # input_args.append("-f lavfi -i anullsrc") + # + # # define audio empty concat variables + # audio_input = "[1:a]" + # audio_output = ":a=1" + # v_inp_idx = 1 + # + # # adding input for video black frame + # input_args.append(( + # "-f lavfi -i \"color=c=black:" + # "s={resolution_width}x{resolution_height}:r={fps}\"" + # ).format(**locals())) + # + # if (start_sec < 0): + # # recalculate input video timing + # empty_start_dur = abs(start_sec) + # start_sec = 0 + # duration_sec = float(frame_end - ( + # frame_start + (empty_start_dur * fps)) + 1) / fps + # + # # define starting empty video concat variables + # video_empty_start = ( + # "[{v_inp_idx}]trim=duration={empty_start_dur}[gv0];" # noqa + # ).format(**locals()) + # video_layer_start = "[gv0]" + # + # if audio_check_output: + # # define starting empty audio concat variables + # audio_empty_start = ( + # "[0]atrim=duration={empty_start_dur}[ga0];" + # ).format(**locals()) + # audio_layer_start = "[ga0]" + # + # # alter concat number of clips + # concat_n += 1 + # + # # check if not missing frames at the end + # if (media_duration < frame_end): + # # recalculate timing + # empty_end_dur = float( + # frame_end - media_duration + 1) / fps + # duration_sec = float( + # media_duration - frame_start) / fps + # + # # define ending empty video concat variables + # video_empty_end = ( + # "[{v_inp_idx}]trim=duration={empty_end_dur}[gv1];" + # ).format(**locals()) + # video_layer_end = "[gv1]" + # + # if audio_check_output: + # # define ending empty audio concat variables + # audio_empty_end = ( + # "[0]atrim=duration={empty_end_dur}[ga1];" + # ).format(**locals()) + # audio_layer_end = "[ga0]" + # + # # alter concat number of clips + # concat_n += 1 + # + # # concatting black frame togather + # output_args.append(( + # "-filter_complex \"" + # "{audio_empty_start}" + # "{video_empty_start}" + # "{audio_empty_end}" + # "{video_empty_end}" + # "{video_layer_start}{audio_layer_start}[1:v]{audio_input}" # noqa + # "{video_layer_end}{audio_layer_end}" + # "concat=n={concat_n}:v=1{audio_output}\"" + # ).format(**locals())) + # + # # append ffmpeg input video clip + # input_args.append("-ss {:0.2f}".format(start_sec)) + # input_args.append("-t {:0.2f}".format(duration_sec)) + # input_args.append("-i \"{}\"".format(full_input_path)) + # + # # add copy audio video codec if only shortening clip + # if ("_cut-bigger" in tags) and (not empty_add): + # output_args.append("-c:v copy") + # + # # make sure it is having no frame to frame comprassion + # output_args.append("-intra") + # + # # output filename + # output_args.append("-y \"{}\"".format(full_output_path)) + # + # mov_args = [ + # "\"{}\"".format(ffmpeg_path), + # " ".join(input_args), + # " ".join(output_args) + # ] + # subprcs_cmd = " ".join(mov_args) + # + # # run subprocess + # self.log.debug("Executing: {}".format(subprcs_cmd)) + # output = pype.api.subprocess(subprcs_cmd) + # self.log.debug("Output: {}".format(output)) + # + # repre_new = { + # "files": new_files, + # "stagingDir": full_output_dir, + # "frameStart": frame_start, + # "frameEnd": frame_end, + # "frameStartFtrack": frame_start, + # "frameEndFtrack": frame_end, + # "step": 1, + # "fps": fps, + # "name": "cut_up_preview", + # "tags": ["review"] + self.tags_addition, + # "ext": ext, + # "anatomy_template": "publish" + # } + # + # representations_new.append(repre_new) + # + # for repre in representations_new: + # if ("delete" in repre.get("tags", [])) and ( + # "cut_up_preview" not in repre["name"]): + # representations_new.remove(repre) + # + # self.log.debug( + # "Representations: {}".format(representations_new)) + # instance.data["representations"] = representations_new + # + # def hardlink_file(self, src, dst): + # dirname = os.path.dirname(dst) + # + # # make sure the destination folder exist + # try: + # os.makedirs(dirname) + # except OSError as e: + # if e.errno == errno.EEXIST: + # pass + # else: + # self.log.critical("An unexpected error occurred.") + # six.reraise(*sys.exc_info()) + # + # # create hardlined file + # try: + # filelink.create(src, dst, filelink.HARDLINK) + # except OSError as e: + # if e.errno == errno.EEXIST: + # pass + # else: + # self.log.critical("An unexpected error occurred.") + # six.reraise(*sys.exc_info()) + # + # def create_representation(self, otio_clip, to_otio_range, instance): + # to_tl_start, to_tl_end = pype.lib.otio_range_to_frame_range( + # to_otio_range) + # tl_start, tl_end = pype.lib.otio_range_to_frame_range( + # otio_clip.range_in_parent()) + # source_start, source_end = pype.lib.otio_range_to_frame_range( + # otio_clip.source_range) + # media_reference = otio_clip.media_reference + # metadata = media_reference.metadata + # mr_start, mr_end = pype.lib.otio_range_to_frame_range( + # media_reference.available_range) + # path = media_reference.target_url + # reference_frame_start = (mr_start + source_start) + ( + # to_tl_start - tl_start) + # reference_frame_end = (mr_start + source_end) - ( + # tl_end - to_tl_end) + # + # base_name = os.path.basename(path) + # staging_dir = os.path.dirname(path) + # ext = os.path.splitext(base_name)[1][1:] + # + # if metadata.get("isSequence"): + # files = list() + # padding = metadata["padding"] + # base_name = pype.lib.convert_to_padded_path(base_name, padding) + # for index in range( + # reference_frame_start, (reference_frame_end + 1)): + # file_name = base_name % index + # path_test = os.path.join(staging_dir, file_name) + # if os.path.exists(path_test): + # files.append(file_name) + # + # self.log.debug(files) + # else: + # files = base_name + # + # representation = { + # "ext": ext, + # "name": ext, + # "files": files, + # "frameStart": reference_frame_start, + # "frameEnd": reference_frame_end, + # "stagingDir": staging_dir + # } + # self.log.debug(representation) diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py index ee32eac09e..4693b94e4b 100644 --- a/pype/plugins/resolve/publish/collect_instances.py +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -69,10 +69,11 @@ class CollectInstances(pyblish.api.ContextPlugin): # otio clip data otio_data = resolve.get_otio_clip_instance_data( - otio_timeline, track_item_data) + otio_timeline, track_item_data) or {} + data.update(otio_data) - if otio_data: - data.update(otio_data) + # add resolution + self.get_resolution_to_data(data, context) # create instance instance = context.create_instance(**data) @@ -80,3 +81,21 @@ class CollectInstances(pyblish.api.ContextPlugin): self.log.info("Creating instance: {}".format(instance)) self.log.debug( "_ instance.data: {}".format(pformat(instance.data))) + + def get_resolution_to_data(self, data, context): + assert data.get("otioClip"), "Missing `otioClip` data" + + # solve source resolution option + if data.get("sourceResolution", None): + otio_clip_metadata = data[ + "otioClip"].media_reference.metadata + data.update({ + "resolutionWidth": otio_clip_metadata["width"], + "resolutionHeight": otio_clip_metadata["height"] + }) + else: + otio_tl_metadata = context.data["otioTimeline"].metadata + data.update({ + "resolutionWidth": otio_tl_metadata["width"], + "resolutionHeight": otio_tl_metadata["height"] + }) diff --git a/pype/plugins/resolve/publish/collect_workfile.py b/pype/plugins/resolve/publish/collect_workfile.py index 1c6d682f3f..8c8e2b66c8 100644 --- a/pype/plugins/resolve/publish/collect_workfile.py +++ b/pype/plugins/resolve/publish/collect_workfile.py @@ -26,8 +26,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): video_tracks = resolve.get_video_track_names() # adding otio timeline to context - otio_timeline = davinci_export.create_otio_timeline( - active_sequence, fps) + otio_timeline = davinci_export.create_otio_timeline(project) instance_data = { "name": "{}_{}".format(asset, subset), From b0b785ac80914576d75c458986b1a03275bfc2aa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 10 Dec 2020 21:05:53 +0100 Subject: [PATCH 34/72] feat(global): wip editorial otio --- pype/hosts/resolve/otio/utils.py | 23 +- .../global/publish/collect_otio_review.py | 24 +- .../global/publish/extract_otio_review.py | 232 ++++++++++++++++-- 3 files changed, 234 insertions(+), 45 deletions(-) diff --git a/pype/hosts/resolve/otio/utils.py b/pype/hosts/resolve/otio/utils.py index 54a052bb56..ec514289f5 100644 --- a/pype/hosts/resolve/otio/utils.py +++ b/pype/hosts/resolve/otio/utils.py @@ -1,23 +1,20 @@ import re +import opentimelineio as otio def timecode_to_frames(timecode, framerate): - parts = zip(( - 3600 * framerate, - 60 * framerate, - framerate, 1 - ), timecode.split(":")) - return sum( - f * int(t) for f, t in parts - ) + rt = otio.opentime.from_timecode(timecode, 24) + return int(otio.opentime.to_frames(rt)) def frames_to_timecode(frames, framerate): - return '{0:02d}:{1:02d}:{2:02d}:{3:02d}'.format( - int(frames / (3600 * framerate)), - int(frames / (60 * framerate) % 60), - int(frames / framerate % 60), - int(frames % framerate)) + rt = otio.opentime.from_frames(frames, framerate) + return otio.opentime.to_timecode(rt) + + +def frames_to_secons(frames, framerate): + rt = otio.opentime.from_frames(frames, framerate) + return otio.opentime.to_seconds(rt) def get_reformated_path(path, padded=True): diff --git a/pype/plugins/global/publish/collect_otio_review.py b/pype/plugins/global/publish/collect_otio_review.py index 30240f456e..97f6552c51 100644 --- a/pype/plugins/global/publish/collect_otio_review.py +++ b/pype/plugins/global/publish/collect_otio_review.py @@ -25,27 +25,21 @@ class CollectOcioReview(pyblish.api.InstancePlugin): # get basic variables review_track_name = instance.data["review"] master_layer = instance.data["masterLayer"] - otio_timeline_context = instance.context.data["otioTimeline"] + otio_timeline = instance.context.data["otioTimeline"] otio_clip = instance.data["otioClip"] - otio_clip_range = otio_clip.range_in_parent() + otio_tl_range = otio_clip.range_in_parent() + # skip if master layer is False if not master_layer: return - for _otio_clip in otio_timeline_context.each_clip(): - track_name = _otio_clip.parent().name - parent_range = _otio_clip.range_in_parent() - if track_name not in review_track_name: + for track in otio_timeline.tracks: + if review_track_name not in track.name: continue - if isinstance(_otio_clip, otio.schema.Clip): - test_start, test_end = pype.lib.otio_range_to_frame_range( - parent_range) - main_start, main_end = pype.lib.otio_range_to_frame_range( - otio_clip_range) - if pype.lib.is_overlapping_otio_ranges( - parent_range, otio_clip_range, strict=False): - # add found clips to list - otio_review_clips.append(_otio_clip) + otio_review_clips = otio.algorithms.track_trimmed_to_range( + track, + otio_tl_range + ) instance.data["otioReviewClips"] = otio_review_clips self.log.debug( diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py index f829659dff..bbb6f7097e 100644 --- a/pype/plugins/global/publish/extract_otio_review.py +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -2,14 +2,30 @@ import os import sys import six import errno -from pyblish import api -import pype import clique from avalon.vendor import filelink +import opentimelineio as otio +from pyblish import api +import pype + class ExtractOTIOReview(pype.api.Extractor): - """Extract OTIO timeline into one concuted video file""" + """ Extract OTIO timeline into one concuted video file. + + Expecting (instance.data): + otioClip (otio.schema.clip): clip from otio timeline + otioReviewClips (list): list with instances of otio.schema.clip + or otio.schema.gap + + Process description: + Comparing `otioClip` parent range with `otioReviewClip` parent range + will result in frame range witch is the trimmed cut. In case more otio + clips or otio gaps are found in otioReviewClips then ffmpeg will + generate multiple clips and those are then concuted together to one + video file or image sequence. Resulting files are then added to + instance as representation ready for review family plugins. + """ # order = api.ExtractorOrder order = api.CollectorOrder @@ -17,24 +33,206 @@ class ExtractOTIOReview(pype.api.Extractor): hosts = ["resolve"] families = ["review"] + collections = list() + sequence_workflow = False + + def _trim_available_range(self, avl_range, start, duration, fps): + avl_start = int(avl_range.start_time.value) + avl_durtation = int(avl_range.duration.value) + src_start = int(avl_start + start) + + self.log.debug(f"_ avl_start: {avl_start}") + self.log.debug(f"_ avl_durtation: {avl_durtation}") + self.log.debug(f"_ src_start: {src_start}") + # it only trims to source if + if src_start < avl_start: + if self.sequence_workflow: + gap_range = list(range(src_start, avl_start)) + _collection = self.create_gap_collection( + self.sequence_workflow, -1, _range=gap_range) + self.collections.append(_collection) + start = 0 + # if duration < avl_durtation: + # end = int(start + duration - 1) + # av_end = avl_start + avl_durtation - 1 + # self.collections.append(range(av_end, end)) + # duration = avl_durtation + return self._trim_media_range( + avl_range, self._range_from_frames(start, duration, fps) + ) + def process(self, instance): # self.create_representation( # _otio_clip, otio_clip_range, instance) - # """ - # Expecting (instance.data): - # otioClip (otio.schema.clip): clip from otio timeline - # otioReviewClips (list): list with instances of otio.schema.clip - # or otio.schema.gap - # - # Process description: - # Comparing `otioClip` parent range with `otioReviewClip` parent range will result in frame range witch is the trimmed cut. In case more otio clips or otio gaps are found in otioReviewClips then ffmpeg will generate multiple clips and those are then concuted together to one video file or image sequence. Resulting files are then added to instance as representation ready for review family plugins. - # """" - - otio_clip = instance.data["otioClip"] - media_reference = otio_clip.media_reference - self.log.debug(media_reference.metadata) + # get ranges and other time info from instance clip + staging_dir = self.staging_dir(instance) + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] otio_review_clips = instance.data["otioReviewClips"] - self.log.debug(otio_review_clips) + + # in case of more than one clip check if second clip is sequence + # this will define what ffmpeg workflow will be used + # test first clip if it is not gap + test_clip = otio_review_clips[0] + if not isinstance(test_clip, otio.schema.Clip): + # if first was gap then test second + test_clip = otio_review_clips[1] + + # make sure second clip is not gap + if isinstance(test_clip, otio.schema.Clip): + metadata = test_clip.media_reference.metadata + is_sequence = metadata.get("isSequence") + if is_sequence: + path = test_clip.media_reference.target_url + available_range = self._trim_media_range( + test_clip.available_range(), + test_clip.source_range + ) + collection = self._make_collection( + path, available_range, metadata) + self.sequence_workflow = collection + + # loop all otio review clips + for index, r_otio_cl in enumerate(otio_review_clips): + self.log.debug(f">>> r_otio_cl: {r_otio_cl}") + src_range = r_otio_cl.source_range + start = src_range.start_time.value + duration = src_range.duration.value + available_range = None + fps = src_range.duration.rate + + # add available range only if not gap + if isinstance(r_otio_cl, otio.schema.Clip): + available_range = r_otio_cl.available_range() + fps = available_range.duration.rate + + # reframing handles conditions + if (len(otio_review_clips) > 1) and (index == 0): + # more clips | first clip reframing with handle + start -= handle_start + duration += handle_start + elif len(otio_review_clips) > 1 \ + and (index == len(otio_review_clips) - 1): + # more clips | last clip reframing with handle + duration += handle_end + elif len(otio_review_clips) == 1: + # one clip | add both handles + start -= handle_start + duration += (handle_start + handle_end) + + if available_range: + available_range = self._trim_available_range( + available_range, start, duration, fps) + + first, last = pype.lib.otio_range_to_frame_range( + available_range) + self.log.debug(f"_ first, last: {first}-{last}") + + # media source info + if isinstance(r_otio_cl, otio.schema.Clip): + path = r_otio_cl.media_reference.target_url + metadata = r_otio_cl.media_reference.metadata + + if self.sequence_workflow: + _collection = self._make_collection( + path, available_range, metadata) + self.collections.append(_collection) + self.sequence_workflow = _collection + + # create seconds values + start_sec = self._frames_to_secons( + start, + src_range.start_time.rate) + duration_sec = self._frames_to_secons( + duration, + src_range.duration.rate) + else: + # create seconds values + start_sec = 0 + duration_sec = self._frames_to_secons( + duration, + src_range.duration.rate) + + # if sequence workflow + if self.sequence_workflow: + _collection = self.create_gap_collection( + self.sequence_workflow, index, duration=duration + ) + self.collections.append(_collection) + self.sequence_workflow = _collection + + self.log.debug(f"_ start_sec: {start_sec}") + self.log.debug(f"_ duration_sec: {duration_sec}") + + self.log.debug(f"_ self.sequence_workflow: {self.sequence_workflow}") + self.log.debug(f"_ self.collections: {self.collections}") + + @staticmethod + def _frames_to_secons(frames, framerate): + rt = otio.opentime.from_frames(frames, framerate) + return otio.opentime.to_seconds(rt) + + @staticmethod + def _make_collection(path, otio_range, metadata): + if "%" not in path: + return None + basename = os.path.basename(path) + head = basename.split("%")[0] + tail = os.path.splitext(basename)[-1] + first, last = pype.lib.otio_range_to_frame_range(otio_range) + collection = clique.Collection( + head=head, tail=tail, padding=metadata["padding"]) + collection.indexes.update([i for i in range(first, (last + 1))]) + return collection + + @staticmethod + def _trim_media_range(media_range, source_range): + rw_media_start = otio.opentime.RationalTime( + media_range.start_time.value + source_range.start_time.value, + media_range.start_time.rate + ) + rw_media_duration = otio.opentime.RationalTime( + source_range.duration.value, + media_range.duration.rate + ) + return otio.opentime.TimeRange( + rw_media_start, rw_media_duration) + + @staticmethod + def _range_from_frames(start, duration, fps): + return otio.opentime.TimeRange( + otio.opentime.RationalTime(start, fps), + otio.opentime.RationalTime(duration, fps) + ) + + @staticmethod + def create_gap_collection(collection, index, duration=None, _range=None): + head = "gap" + collection.head[-1] + tail = collection.tail + padding = collection.padding + first_frame = min(collection.indexes) + last_frame = max(collection.indexes) + 1 + + if _range: + new_range = _range + if duration: + if index == 0: + new_range = range( + int(first_frame - duration), first_frame) + else: + new_range = range( + last_frame, int(last_frame + duration)) + + return clique.Collection( + head, tail, padding, indexes=set(new_range)) + + # otio_src_range_handles = pype.lib.otio_range_with_handles( + # otio_src_range, instance) + # self.log.debug(otio_src_range_handles) + # range_convert = pype.lib.otio_range_to_frame_range + # tl_start, tl_end = range_convert(otio_tl_range) + # self.log.debug((tl_start, tl_end)) + # inst_data = instance.data # asset = inst_data['asset'] From eea9a699d807e1451fef131f7154261e9100a130 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 11 Dec 2020 18:45:39 +0100 Subject: [PATCH 35/72] feat(global): otio review sequence extract - known bug of sequence numbering --- .../global/publish/extract_otio_review.py | 391 ++++++++++++------ 1 file changed, 264 insertions(+), 127 deletions(-) diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py index bbb6f7097e..57bc82cf22 100644 --- a/pype/plugins/global/publish/extract_otio_review.py +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -2,9 +2,9 @@ import os import sys import six import errno -import clique -from avalon.vendor import filelink +import clique +import shutil import opentimelineio as otio from pyblish import api import pype @@ -33,78 +33,82 @@ class ExtractOTIOReview(pype.api.Extractor): hosts = ["resolve"] families = ["review"] - collections = list() + # plugin default attributes + temp_file_head = "tempFile." + padding = "%08d" + next_sequence_frame = 0 + to_width = 800 + to_height = 600 + representation_files = list() sequence_workflow = False - def _trim_available_range(self, avl_range, start, duration, fps): - avl_start = int(avl_range.start_time.value) - avl_durtation = int(avl_range.duration.value) - src_start = int(avl_start + start) - - self.log.debug(f"_ avl_start: {avl_start}") - self.log.debug(f"_ avl_durtation: {avl_durtation}") - self.log.debug(f"_ src_start: {src_start}") - # it only trims to source if - if src_start < avl_start: - if self.sequence_workflow: - gap_range = list(range(src_start, avl_start)) - _collection = self.create_gap_collection( - self.sequence_workflow, -1, _range=gap_range) - self.collections.append(_collection) - start = 0 - # if duration < avl_durtation: - # end = int(start + duration - 1) - # av_end = avl_start + avl_durtation - 1 - # self.collections.append(range(av_end, end)) - # duration = avl_durtation - return self._trim_media_range( - avl_range, self._range_from_frames(start, duration, fps) - ) - def process(self, instance): - # self.create_representation( - # _otio_clip, otio_clip_range, instance) - # get ranges and other time info from instance clip - staging_dir = self.staging_dir(instance) + # reset to empty list > for some reason it is inheriting data + # from previouse instances + if self.representation_files: + self.representation_files = list() + + # get otio clip and other time info from instance clip handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] otio_review_clips = instance.data["otioReviewClips"] + # skip instance if no reviewable data available + if (not isinstance(otio_review_clips[0], otio.schema.Clip)) \ + and (len(otio_review_clips) == 1): + self.log.warning( + "Instance `{}` has nothing to process".format(instance)) + return + else: + self.staging_dir = self.staging_dir(instance) + if not instance.data.get("representations"): + instance.data["representations"] = list() + # in case of more than one clip check if second clip is sequence # this will define what ffmpeg workflow will be used # test first clip if it is not gap test_clip = otio_review_clips[0] - if not isinstance(test_clip, otio.schema.Clip): - # if first was gap then test second + if (not isinstance(test_clip, otio.schema.Clip)) \ + and (len(otio_review_clips) > 1): + # if first was gap then test second in case there are more test_clip = otio_review_clips[1] # make sure second clip is not gap if isinstance(test_clip, otio.schema.Clip): metadata = test_clip.media_reference.metadata + + # get resolution data from metadata if they are available + self.to_width = metadata.get("width") or self.to_width + self.to_height = metadata.get("height") or self.to_height + self.actual_fps = test_clip.source_range.start_time.rate + + # define future workflow sequencial or movie is_sequence = metadata.get("isSequence") + if is_sequence: path = test_clip.media_reference.target_url available_range = self._trim_media_range( test_clip.available_range(), test_clip.source_range ) - collection = self._make_collection( + _dir_path, collection = self._make_sequence_collection( path, available_range, metadata) + self.padding = collection.format("{padding}") + self.next_sequence_frame = 1001 self.sequence_workflow = collection # loop all otio review clips for index, r_otio_cl in enumerate(otio_review_clips): - self.log.debug(f">>> r_otio_cl: {r_otio_cl}") src_range = r_otio_cl.source_range start = src_range.start_time.value duration = src_range.duration.value available_range = None - fps = src_range.duration.rate + self.actual_fps = src_range.duration.rate # add available range only if not gap if isinstance(r_otio_cl, otio.schema.Clip): available_range = r_otio_cl.available_range() - fps = available_range.duration.rate + self.actual_fps = available_range.duration.rate # reframing handles conditions if (len(otio_review_clips) > 1) and (index == 0): @@ -122,11 +126,10 @@ class ExtractOTIOReview(pype.api.Extractor): if available_range: available_range = self._trim_available_range( - available_range, start, duration, fps) + available_range, start, duration, self.actual_fps) first, last = pype.lib.otio_range_to_frame_range( available_range) - self.log.debug(f"_ first, last: {first}-{last}") # media source info if isinstance(r_otio_cl, otio.schema.Clip): @@ -134,10 +137,20 @@ class ExtractOTIOReview(pype.api.Extractor): metadata = r_otio_cl.media_reference.metadata if self.sequence_workflow: - _collection = self._make_collection( + dir_path, collection = self._make_sequence_collection( path, available_range, metadata) - self.collections.append(_collection) - self.sequence_workflow = _collection + + # to preserve future sequence numbering + # if index <= 1: + # self.next_sequence_frame = max(collection.indexes) + + # render segment + self._render_sequence_seqment( + collection, + input_dir=dir_path + ) + self.representation_files.extend([f for f in collection]) + self.sequence_workflow = collection # create seconds values start_sec = self._frames_to_secons( @@ -155,17 +168,168 @@ class ExtractOTIOReview(pype.api.Extractor): # if sequence workflow if self.sequence_workflow: - _collection = self.create_gap_collection( - self.sequence_workflow, index, duration=duration + collection = self._create_gap_collection( + self.sequence_workflow, **{ + "eventNumber": index, + "duration": duration + } ) - self.collections.append(_collection) - self.sequence_workflow = _collection + self.representation_files.extend([f for f in collection]) + self.sequence_workflow = collection self.log.debug(f"_ start_sec: {start_sec}") self.log.debug(f"_ duration_sec: {duration_sec}") - self.log.debug(f"_ self.sequence_workflow: {self.sequence_workflow}") - self.log.debug(f"_ self.collections: {self.collections}") + # creating and registering representation + representation = self.create_representation(start, duration) + instance.data["representations"].append(representation) + self.log.info(f"Adding representation: {representation}") + + def create_representation(self, start, duration): + end = start + duration + files = self.representation_files.pop() + ext = os.path.splitext(files)[-1] + + # create default representation data + representation_data = { + "ext": ext[1:], + "name": ext[1:], + "files": files, + "frameStart": start, + "frameEnd": end, + "stagingDir": self.staging_dir, + "tags": ["review", "ftrackreview", "delete"] + } + + # update data if sequence workflow + if self.sequence_workflow: + collections, _rem = clique.assemble(self.representation_files) + collection = collections.pop() + start = min(collection.indexes) + end = max(collection.indexes) + files = self.representation_files + representation_data.update({ + "files": files, + "frameStart": start, + "frameEnd": end, + }) + return representation_data + + def _trim_available_range(self, avl_range, start, duration, fps): + avl_start = int(avl_range.start_time.value) + src_start = int(avl_start + start) + avl_durtation = int(avl_range.duration.value - start) + + # it only trims to source if + if src_start < avl_start: + if self.sequence_workflow: + start_gap_range = list(range(src_start, avl_start)) + collection = self._create_gap_collection( + self.sequence_workflow, **{"range": start_gap_range}) + self.representation_files.extend([f for f in collection]) + start = 0 + duration -= len(start_gap_range) + if duration > avl_durtation: + if self.sequence_workflow: + gap_start = int(src_start + avl_durtation) + gap_end = int(src_start + duration) + end_gap_range = list(range(gap_start, gap_end)) + collection = self._create_gap_collection( + self.sequence_workflow, **{"range": end_gap_range}) + self.representation_files.extend([f for f in collection]) + duration = avl_durtation + return self._trim_media_range( + avl_range, self._range_from_frames(start, duration, fps) + ) + + def _render_sequence_seqment(self, collection, input_dir=None): + # get rendering app path + ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") + + if input_dir: + # copying files to temp folder + for indx, file_item in enumerate(collection): + seq_number = self.padding % ( + self.next_sequence_frame + indx) + # create path to source + output_file = "{}{}{}".format( + self.temp_file_head, + seq_number, + collection.format("{tail}")) + input_path = os.path.join(input_dir, file_item) + output_path = os.path.join(self.staging_dir, output_file) + try: + shutil.copyfile(input_path, output_path) + except OSError as e: + self.log.critical( + "Cannot copy {} to {}".format(input_path, output_path)) + self.log.critical(e) + six.reraise(*sys.exc_info()) + self.next_sequence_frame = int(seq_number) + 1 + else: + # generating gap files + file = "{}{}".format(self.temp_file_head, + collection.format("{padding}{tail}")) + frame_start = min(collection.indexes) + frame_duration = len(collection.indexes) + sec_duration = frame_duration / self.actual_fps + + # create path to destination + output_path = os.path.join(self.staging_dir, file) + # form command for rendering gap files + gap_cmd = " ".join([ + ffmpeg_path, + "-t {secDuration} -r {frameRate}", + "-f lavfi -i color=c=black:s={width}x{height}", + "-tune stillimage", + "-start_number {frameStart}", + output_path + ]).format( + secDuration=sec_duration, + frameRate=self.actual_fps, + frameStart=frame_start, + width=self.to_width, + height=self.to_height + ) + # execute + self.log.debug("Executing: {}".format(gap_cmd)) + output = pype.api.subprocess(gap_cmd, shell=True) + self.log.debug("Output: {}".format(output)) + + def _create_gap_collection(self, collection, **kwargs): + head = collection.head + tail = collection.tail + padding = collection.padding + first_frame = self.next_sequence_frame + last_frame = first_frame + len(collection.indexes) + + new_range = kwargs.get("range") + + if not new_range: + duration = kwargs.get("duration") + event_number = kwargs.get("eventNumber") + + # validate kwards + e_msg = ("Missing required kargs `duration` or `eventNumber`" + "kwargs: `{}`").format(kwargs) + assert duration, e_msg + assert event_number is not None, e_msg + + # create new range + if event_number == 0: + new_range = range(first_frame, (last_frame + 1)) + else: + new_range = range(first_frame, (last_frame - 1)) + + # create collection + collection = clique.Collection( + head, tail, padding, indexes=set(new_range)) + + # render segment + self._render_sequence_seqment(collection) + self.next_sequence_frame = max(collection.indexes) + 1 + + return collection @staticmethod def _frames_to_secons(frames, framerate): @@ -173,17 +337,18 @@ class ExtractOTIOReview(pype.api.Extractor): return otio.opentime.to_seconds(rt) @staticmethod - def _make_collection(path, otio_range, metadata): + def _make_sequence_collection(path, otio_range, metadata): if "%" not in path: return None - basename = os.path.basename(path) - head = basename.split("%")[0] - tail = os.path.splitext(basename)[-1] + file_name = os.path.basename(path) + dir_path = os.path.dirname(path) + head = file_name.split("%")[0] + tail = os.path.splitext(file_name)[-1] first, last = pype.lib.otio_range_to_frame_range(otio_range) collection = clique.Collection( head=head, tail=tail, padding=metadata["padding"]) collection.indexes.update([i for i in range(first, (last + 1))]) - return collection + return dir_path, collection @staticmethod def _trim_media_range(media_range, source_range): @@ -205,34 +370,6 @@ class ExtractOTIOReview(pype.api.Extractor): otio.opentime.RationalTime(duration, fps) ) - @staticmethod - def create_gap_collection(collection, index, duration=None, _range=None): - head = "gap" + collection.head[-1] - tail = collection.tail - padding = collection.padding - first_frame = min(collection.indexes) - last_frame = max(collection.indexes) + 1 - - if _range: - new_range = _range - if duration: - if index == 0: - new_range = range( - int(first_frame - duration), first_frame) - else: - new_range = range( - last_frame, int(last_frame + duration)) - - return clique.Collection( - head, tail, padding, indexes=set(new_range)) - - # otio_src_range_handles = pype.lib.otio_range_with_handles( - # otio_src_range, instance) - # self.log.debug(otio_src_range_handles) - # range_convert = pype.lib.otio_range_to_frame_range - # tl_start, tl_end = range_convert(otio_tl_range) - # self.log.debug((tl_start, tl_end)) - # inst_data = instance.data # asset = inst_data['asset'] @@ -555,48 +692,48 @@ class ExtractOTIOReview(pype.api.Extractor): # self.log.critical("An unexpected error occurred.") # six.reraise(*sys.exc_info()) # - # def create_representation(self, otio_clip, to_otio_range, instance): - # to_tl_start, to_tl_end = pype.lib.otio_range_to_frame_range( - # to_otio_range) - # tl_start, tl_end = pype.lib.otio_range_to_frame_range( - # otio_clip.range_in_parent()) - # source_start, source_end = pype.lib.otio_range_to_frame_range( - # otio_clip.source_range) - # media_reference = otio_clip.media_reference - # metadata = media_reference.metadata - # mr_start, mr_end = pype.lib.otio_range_to_frame_range( - # media_reference.available_range) - # path = media_reference.target_url - # reference_frame_start = (mr_start + source_start) + ( - # to_tl_start - tl_start) - # reference_frame_end = (mr_start + source_end) - ( - # tl_end - to_tl_end) - # - # base_name = os.path.basename(path) - # staging_dir = os.path.dirname(path) - # ext = os.path.splitext(base_name)[1][1:] - # - # if metadata.get("isSequence"): - # files = list() - # padding = metadata["padding"] - # base_name = pype.lib.convert_to_padded_path(base_name, padding) - # for index in range( - # reference_frame_start, (reference_frame_end + 1)): - # file_name = base_name % index - # path_test = os.path.join(staging_dir, file_name) - # if os.path.exists(path_test): - # files.append(file_name) - # - # self.log.debug(files) - # else: - # files = base_name - # - # representation = { - # "ext": ext, - # "name": ext, - # "files": files, - # "frameStart": reference_frame_start, - # "frameEnd": reference_frame_end, - # "stagingDir": staging_dir - # } - # self.log.debug(representation) +# def create_representation(self, otio_clip, to_otio_range, instance): +# to_tl_start, to_tl_end = pype.lib.otio_range_to_frame_range( +# to_otio_range) +# tl_start, tl_end = pype.lib.otio_range_to_frame_range( +# otio_clip.range_in_parent()) +# source_start, source_end = pype.lib.otio_range_to_frame_range( +# otio_clip.source_range) +# media_reference = otio_clip.media_reference +# metadata = media_reference.metadata +# mr_start, mr_end = pype.lib.otio_range_to_frame_range( +# media_reference.available_range) +# path = media_reference.target_url +# reference_frame_start = (mr_start + source_start) + ( +# to_tl_start - tl_start) +# reference_frame_end = (mr_start + source_end) - ( +# tl_end - to_tl_end) +# +# base_name = os.path.basename(path) +# staging_dir = os.path.dirname(path) +# ext = os.path.splitext(base_name)[1][1:] +# +# if metadata.get("isSequence"): +# files = list() +# padding = metadata["padding"] +# base_name = pype.lib.convert_to_padded_path(base_name, padding) +# for index in range( +# reference_frame_start, (reference_frame_end + 1)): +# file_name = base_name % index +# path_test = os.path.join(staging_dir, file_name) +# if os.path.exists(path_test): +# files.append(file_name) +# +# self.log.debug(files) +# else: +# files = base_name +# +# representation = { +# "ext": ext, +# "name": ext, +# "files": files, +# "frameStart": reference_frame_start, +# "frameEnd": reference_frame_end, +# "stagingDir": staging_dir +# } +# self.log.debug(representation) From 8d8c6f4a044c1e8616fd1239e7601f2c8f47053c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Dec 2020 11:14:26 +0100 Subject: [PATCH 36/72] feat(resolve): extract review sequence wip --- .../global/publish/extract_otio_review.py | 173 +++++++++--------- 1 file changed, 82 insertions(+), 91 deletions(-) diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py index 57bc82cf22..aad9b63830 100644 --- a/pype/plugins/global/publish/extract_otio_review.py +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -35,18 +35,19 @@ class ExtractOTIOReview(pype.api.Extractor): # plugin default attributes temp_file_head = "tempFile." - padding = "%08d" - next_sequence_frame = 0 to_width = 800 to_height = 600 - representation_files = list() sequence_workflow = False + sequence_ext = ".jpg" def process(self, instance): - # reset to empty list > for some reason it is inheriting data - # from previouse instances - if self.representation_files: - self.representation_files = list() + self.representation_files = list() + self.used_frames = list() + self.workfile_start = int(instance.data.get( + "workfileFrameStart", 1001)) + self.padding = "%0{}d".format(len(str(self.workfile_start))) + self.used_frames.append(self.workfile_start) + self.log.debug(f"_ self.used_frames-0: {self.used_frames}") # get otio clip and other time info from instance clip handle_start = instance.data["handleStart"] @@ -93,9 +94,8 @@ class ExtractOTIOReview(pype.api.Extractor): ) _dir_path, collection = self._make_sequence_collection( path, available_range, metadata) - self.padding = collection.format("{padding}") - self.next_sequence_frame = 1001 self.sequence_workflow = collection + self.sequence_ext = collection.format("{tail}") # loop all otio review clips for index, r_otio_cl in enumerate(otio_review_clips): @@ -130,6 +130,7 @@ class ExtractOTIOReview(pype.api.Extractor): first, last = pype.lib.otio_range_to_frame_range( available_range) + self.log.debug(f"_ first, last: {first}-{last}") # media source info if isinstance(r_otio_cl, otio.schema.Clip): @@ -140,17 +141,11 @@ class ExtractOTIOReview(pype.api.Extractor): dir_path, collection = self._make_sequence_collection( path, available_range, metadata) - # to preserve future sequence numbering - # if index <= 1: - # self.next_sequence_frame = max(collection.indexes) - # render segment self._render_sequence_seqment( - collection, + collection=collection, input_dir=dir_path ) - self.representation_files.extend([f for f in collection]) - self.sequence_workflow = collection # create seconds values start_sec = self._frames_to_secons( @@ -168,14 +163,7 @@ class ExtractOTIOReview(pype.api.Extractor): # if sequence workflow if self.sequence_workflow: - collection = self._create_gap_collection( - self.sequence_workflow, **{ - "eventNumber": index, - "duration": duration - } - ) - self.representation_files.extend([f for f in collection]) - self.sequence_workflow = collection + self._render_sequence_seqment(gap=duration) self.log.debug(f"_ start_sec: {start_sec}") self.log.debug(f"_ duration_sec: {duration_sec}") @@ -187,14 +175,9 @@ class ExtractOTIOReview(pype.api.Extractor): def create_representation(self, start, duration): end = start + duration - files = self.representation_files.pop() - ext = os.path.splitext(files)[-1] # create default representation data representation_data = { - "ext": ext[1:], - "name": ext[1:], - "files": files, "frameStart": start, "frameEnd": end, "stagingDir": self.staging_dir, @@ -203,12 +186,20 @@ class ExtractOTIOReview(pype.api.Extractor): # update data if sequence workflow if self.sequence_workflow: - collections, _rem = clique.assemble(self.representation_files) - collection = collections.pop() + collection = clique.Collection( + self.temp_file_head, + tail=self.sequence_ext, + padding=len(str(self.workfile_start)), + indexes=set(self.used_frames) + ) start = min(collection.indexes) end = max(collection.indexes) - files = self.representation_files + self.log.debug(collection) + files = [f for f in collection] + ext = collection.format("{tail}") representation_data.update({ + "name": ext[1:], + "ext": ext[1:], "files": files, "frameStart": start, "frameEnd": end, @@ -220,42 +211,59 @@ class ExtractOTIOReview(pype.api.Extractor): src_start = int(avl_start + start) avl_durtation = int(avl_range.duration.value - start) - # it only trims to source if + # if media start is les then clip requires if src_start < avl_start: + # calculate gap + gap_duration = src_start - avl_start + + # create gap data to disk if self.sequence_workflow: - start_gap_range = list(range(src_start, avl_start)) - collection = self._create_gap_collection( - self.sequence_workflow, **{"range": start_gap_range}) - self.representation_files.extend([f for f in collection]) + self._render_sequence_seqment(gap=gap_duration) + self.log.debug(f"_ self.used_frames-1: {self.used_frames}") + # fix start and end to correct values start = 0 - duration -= len(start_gap_range) + duration -= len(gap_duration) + + # if media duration is shorter then clip requirement if duration > avl_durtation: + # calculate gap + gap_start = int(src_start + avl_durtation) + gap_end = int(src_start + duration) + gap_duration = gap_start - gap_end + + # create gap data to disk if self.sequence_workflow: - gap_start = int(src_start + avl_durtation) - gap_end = int(src_start + duration) - end_gap_range = list(range(gap_start, gap_end)) - collection = self._create_gap_collection( - self.sequence_workflow, **{"range": end_gap_range}) - self.representation_files.extend([f for f in collection]) + self._render_sequence_seqment(gap=gap_duration) + self.log.debug(f"_ self.used_frames-2: {self.used_frames}") + + # fix duration lenght duration = avl_durtation + + # return correct trimmed range return self._trim_media_range( avl_range, self._range_from_frames(start, duration, fps) ) - def _render_sequence_seqment(self, collection, input_dir=None): + def _render_sequence_seqment(self, + collection=None, input_dir=None, gap=None): # get rendering app path ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") - - if input_dir: + if input_dir and collection: # copying files to temp folder - for indx, file_item in enumerate(collection): - seq_number = self.padding % ( - self.next_sequence_frame + indx) + for file_item in collection: + if self.used_frames[-1] == self.workfile_start: + seq_number = self.padding % (self.used_frames[-1]) + self.workfile_start -= 1 + else: + seq_number = self.padding % ( + self.used_frames[-1] + 1) + self.used_frames.append(int(seq_number)) # create path to source output_file = "{}{}{}".format( self.temp_file_head, seq_number, - collection.format("{tail}")) + self.sequence_ext + ) input_path = os.path.join(input_dir, file_item) output_path = os.path.join(self.staging_dir, output_file) try: @@ -265,14 +273,21 @@ class ExtractOTIOReview(pype.api.Extractor): "Cannot copy {} to {}".format(input_path, output_path)) self.log.critical(e) six.reraise(*sys.exc_info()) - self.next_sequence_frame = int(seq_number) + 1 + self.log.debug(f"_ self.used_frames-2: {self.used_frames}") else: + self.log.debug(f"_ gap: {gap}") # generating gap files - file = "{}{}".format(self.temp_file_head, - collection.format("{padding}{tail}")) - frame_start = min(collection.indexes) - frame_duration = len(collection.indexes) - sec_duration = frame_duration / self.actual_fps + file = "{}{}{}".format( + self.temp_file_head, + self.padding, + self.sequence_ext + ) + frame_start = self.used_frames[-1] + 1 + + if self.used_frames[-1] == self.workfile_start: + frame_start = self.used_frames[-1] + + sec_duration = self._frames_to_secons(gap, self.actual_fps) # create path to destination output_path = os.path.join(self.staging_dir, file) @@ -296,40 +311,16 @@ class ExtractOTIOReview(pype.api.Extractor): output = pype.api.subprocess(gap_cmd, shell=True) self.log.debug("Output: {}".format(output)) - def _create_gap_collection(self, collection, **kwargs): - head = collection.head - tail = collection.tail - padding = collection.padding - first_frame = self.next_sequence_frame - last_frame = first_frame + len(collection.indexes) - - new_range = kwargs.get("range") - - if not new_range: - duration = kwargs.get("duration") - event_number = kwargs.get("eventNumber") - - # validate kwards - e_msg = ("Missing required kargs `duration` or `eventNumber`" - "kwargs: `{}`").format(kwargs) - assert duration, e_msg - assert event_number is not None, e_msg - - # create new range - if event_number == 0: - new_range = range(first_frame, (last_frame + 1)) - else: - new_range = range(first_frame, (last_frame - 1)) - - # create collection - collection = clique.Collection( - head, tail, padding, indexes=set(new_range)) - - # render segment - self._render_sequence_seqment(collection) - self.next_sequence_frame = max(collection.indexes) + 1 - - return collection + if output: + # generate used frames + for _i in range(1, (int(gap) + 1)): + if self.used_frames[-1] == self.workfile_start: + seq_number = self.padding % (self.used_frames[-1]) + self.workfile_start -= 1 + else: + seq_number = self.padding % ( + self.used_frames[-1] + 1) + self.used_frames.append(int(seq_number)) @staticmethod def _frames_to_secons(frames, framerate): From 6ad0c22553cbbfe998c73c829daa42009e06abad Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Dec 2020 12:08:42 +0100 Subject: [PATCH 37/72] feat(resolve): wip sequence rendering otio extract --- .../global/publish/extract_otio_review.py | 100 +++++++++++------- 1 file changed, 63 insertions(+), 37 deletions(-) diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py index aad9b63830..d9066093b3 100644 --- a/pype/plugins/global/publish/extract_otio_review.py +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -245,12 +245,33 @@ class ExtractOTIOReview(pype.api.Extractor): ) def _render_sequence_seqment(self, - collection=None, input_dir=None, gap=None): + collection=None, input_dir=None, + video_path=None, gap=None): # get rendering app path ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") + if input_dir and collection: - # copying files to temp folder - for file_item in collection: + output_file = "{}{}{}".format( + self.temp_file_head, + self.padding, + self.sequence_ext + ) + # create path to destination + output_path = os.path.join(self.staging_dir, output_file) + + # generate frame start + out_frame_start = self.used_frames[-1] + 1 + if self.used_frames[-1] == self.workfile_start: + out_frame_start = self.used_frames[-1] + + in_frame_start = min(collection.indexes) + + # converting image sequence to image sequence + input_file = collection.format("{head}{padding}{tail}") + input_path = os.path.join(input_dir, input_file) + + # generate used frames + for _i in collection: if self.used_frames[-1] == self.workfile_start: seq_number = self.padding % (self.used_frames[-1]) self.workfile_start -= 1 @@ -258,24 +279,28 @@ class ExtractOTIOReview(pype.api.Extractor): seq_number = self.padding % ( self.used_frames[-1] + 1) self.used_frames.append(int(seq_number)) - # create path to source - output_file = "{}{}{}".format( - self.temp_file_head, - seq_number, - self.sequence_ext - ) - input_path = os.path.join(input_dir, file_item) - output_path = os.path.join(self.staging_dir, output_file) - try: - shutil.copyfile(input_path, output_path) - except OSError as e: - self.log.critical( - "Cannot copy {} to {}".format(input_path, output_path)) - self.log.critical(e) - six.reraise(*sys.exc_info()) - self.log.debug(f"_ self.used_frames-2: {self.used_frames}") - else: - self.log.debug(f"_ gap: {gap}") + + # form command for rendering gap files + command = " ".join([ + ffmpeg_path, + "-start_number {inFrameStart}", + "-i {inputPath}", + "-start_number {outFrameStart}", + output_path + ]).format( + inputPath=input_path, + inFrameStart=in_frame_start, + outFrameStart=out_frame_start, + # TODO: reformating to output resolution + width=self.to_width, + height=self.to_height + ) + elif video_path: + # TODO: when input is video file + # and want to convert to image sequence + pass + elif gap: + # TODO: function to create default output file and out frame start # generating gap files file = "{}{}{}".format( self.temp_file_head, @@ -287,16 +312,28 @@ class ExtractOTIOReview(pype.api.Extractor): if self.used_frames[-1] == self.workfile_start: frame_start = self.used_frames[-1] + # TODO: function for adding used frames with input frame duration + # generate used frames + for _i in range(1, (int(gap) + 1)): + if self.used_frames[-1] == self.workfile_start: + seq_number = self.padding % (self.used_frames[-1]) + self.workfile_start -= 1 + else: + seq_number = self.padding % ( + self.used_frames[-1] + 1) + self.used_frames.append(int(seq_number)) + sec_duration = self._frames_to_secons(gap, self.actual_fps) # create path to destination output_path = os.path.join(self.staging_dir, file) # form command for rendering gap files - gap_cmd = " ".join([ + command = " ".join([ ffmpeg_path, "-t {secDuration} -r {frameRate}", "-f lavfi -i color=c=black:s={width}x{height}", "-tune stillimage", + # TODO: add this with function for output file path framestart "-start_number {frameStart}", output_path ]).format( @@ -306,21 +343,10 @@ class ExtractOTIOReview(pype.api.Extractor): width=self.to_width, height=self.to_height ) - # execute - self.log.debug("Executing: {}".format(gap_cmd)) - output = pype.api.subprocess(gap_cmd, shell=True) - self.log.debug("Output: {}".format(output)) - - if output: - # generate used frames - for _i in range(1, (int(gap) + 1)): - if self.used_frames[-1] == self.workfile_start: - seq_number = self.padding % (self.used_frames[-1]) - self.workfile_start -= 1 - else: - seq_number = self.padding % ( - self.used_frames[-1] + 1) - self.used_frames.append(int(seq_number)) + # execute + self.log.debug("Executing: {}".format(command)) + output = pype.api.subprocess(command, shell=True) + self.log.debug("Output: {}".format(output)) @staticmethod def _frames_to_secons(frames, framerate): From 900ccedc325ac3575102bcb50d326b9b7fa39394 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Dec 2020 19:06:55 +0100 Subject: [PATCH 38/72] feat(resolve): wip extract otio review adding video file to sequence workflow --- .../global/publish/extract_otio_review.py | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py index d9066093b3..41b72b131d 100644 --- a/pype/plugins/global/publish/extract_otio_review.py +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -138,14 +138,20 @@ class ExtractOTIOReview(pype.api.Extractor): metadata = r_otio_cl.media_reference.metadata if self.sequence_workflow: - dir_path, collection = self._make_sequence_collection( - path, available_range, metadata) + if metadata.get("padding"): + # render image sequence to sequence + dir_path, collection = self._make_sequence_collection( + path, available_range, metadata) - # render segment - self._render_sequence_seqment( - collection=collection, - input_dir=dir_path - ) + # render segment + self._render_sequence_seqment( + sequence=[dir_path, collection] + ) + else: + # render video file to sequence + self._render_sequence_seqment( + video=[path, available_range] + ) # create seconds values start_sec = self._frames_to_secons( @@ -226,6 +232,7 @@ class ExtractOTIOReview(pype.api.Extractor): # if media duration is shorter then clip requirement if duration > avl_durtation: + # TODO: this will render missing frame before not at the end of footage. need to fix this so the rendered frames will be numbered after the footage. # calculate gap gap_start = int(src_start + avl_durtation) gap_end = int(src_start + duration) @@ -245,12 +252,14 @@ class ExtractOTIOReview(pype.api.Extractor): ) def _render_sequence_seqment(self, - collection=None, input_dir=None, - video_path=None, gap=None): + sequence=None, + video=None, + gap=None): # get rendering app path ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") - if input_dir and collection: + if sequence: + input_dir, collection = sequence output_file = "{}{}{}".format( self.temp_file_head, self.padding, @@ -295,10 +304,10 @@ class ExtractOTIOReview(pype.api.Extractor): width=self.to_width, height=self.to_height ) - elif video_path: - # TODO: when input is video file - # and want to convert to image sequence - pass + elif video: + video_path, otio_range = video + self.log.debug( + f">> video_path, otio_range: {video_path},{otio_range}") elif gap: # TODO: function to create default output file and out frame start # generating gap files From eb91b46fc6096d958e58854eca0b798fa358f947 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Dec 2020 20:55:57 +0100 Subject: [PATCH 39/72] feat(global): improving code wip --- .../global/publish/extract_otio_review.py | 156 ++++++++---------- 1 file changed, 73 insertions(+), 83 deletions(-) diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py index 41b72b131d..f4108bad49 100644 --- a/pype/plugins/global/publish/extract_otio_review.py +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -258,105 +258,95 @@ class ExtractOTIOReview(pype.api.Extractor): # get rendering app path ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") + # create path and frame start to destination + output_path, out_frame_start = self.add_sequence_output() + + # start command list + command = [ffmpeg_path] + if sequence: input_dir, collection = sequence - output_file = "{}{}{}".format( - self.temp_file_head, - self.padding, - self.sequence_ext - ) - # create path to destination - output_path = os.path.join(self.staging_dir, output_file) - - # generate frame start - out_frame_start = self.used_frames[-1] + 1 - if self.used_frames[-1] == self.workfile_start: - out_frame_start = self.used_frames[-1] - + frame_duration = len(collection.indexes) in_frame_start = min(collection.indexes) # converting image sequence to image sequence input_file = collection.format("{head}{padding}{tail}") input_path = os.path.join(input_dir, input_file) - # generate used frames - for _i in collection: - if self.used_frames[-1] == self.workfile_start: - seq_number = self.padding % (self.used_frames[-1]) - self.workfile_start -= 1 - else: - seq_number = self.padding % ( - self.used_frames[-1] + 1) - self.used_frames.append(int(seq_number)) - # form command for rendering gap files - command = " ".join([ - ffmpeg_path, - "-start_number {inFrameStart}", - "-i {inputPath}", - "-start_number {outFrameStart}", - output_path - ]).format( - inputPath=input_path, - inFrameStart=in_frame_start, - outFrameStart=out_frame_start, - # TODO: reformating to output resolution - width=self.to_width, - height=self.to_height - ) + command.extend([ + "-start_number {}".format(in_frame_start), + "-i {}".format(input_path) + ]) + elif video: video_path, otio_range = video - self.log.debug( - f">> video_path, otio_range: {video_path},{otio_range}") - elif gap: - # TODO: function to create default output file and out frame start - # generating gap files - file = "{}{}{}".format( - self.temp_file_head, - self.padding, - self.sequence_ext - ) - frame_start = self.used_frames[-1] + 1 + frame_start = otio_range.start_time.value + input_fps = otio_range.start_time.rate + frame_duration = otio_range.duration.value + sec_start = self._frames_to_secons(frame_start, input_fps) + sec_duration = self._frames_to_secons(frame_duration, input_fps) - if self.used_frames[-1] == self.workfile_start: - frame_start = self.used_frames[-1] - - # TODO: function for adding used frames with input frame duration - # generate used frames - for _i in range(1, (int(gap) + 1)): - if self.used_frames[-1] == self.workfile_start: - seq_number = self.padding % (self.used_frames[-1]) - self.workfile_start -= 1 - else: - seq_number = self.padding % ( - self.used_frames[-1] + 1) - self.used_frames.append(int(seq_number)) - - sec_duration = self._frames_to_secons(gap, self.actual_fps) - - # create path to destination - output_path = os.path.join(self.staging_dir, file) # form command for rendering gap files - command = " ".join([ - ffmpeg_path, - "-t {secDuration} -r {frameRate}", - "-f lavfi -i color=c=black:s={width}x{height}", - "-tune stillimage", - # TODO: add this with function for output file path framestart - "-start_number {frameStart}", - output_path - ]).format( - secDuration=sec_duration, - frameRate=self.actual_fps, - frameStart=frame_start, - width=self.to_width, - height=self.to_height - ) + command.extend([ + "-ss {}".format(sec_start), + "-t {}".format(sec_duration), + "-i {}".format(video_path) + ]) + + elif gap: + frame_duration = gap + sec_duration = self._frames_to_secons( + frame_duration, self.actual_fps) + + # form command for rendering gap files + command.extend([ + "-t {} -r {}".format(sec_duration, self.actual_fps), + "-f lavfi", + "-i color=c=black:s={}x{}".format(self.to_width, + self.to_height), + "-tune stillimage" + ]) + + # add output attributes + command.extend([ + "-start_number {}".format(out_frame_start), + output_path + ]) # execute - self.log.debug("Executing: {}".format(command)) - output = pype.api.subprocess(command, shell=True) + self.log.debug("Executing: {}".format(" ".join(command))) + output = pype.api.subprocess(" ".join(command), shell=True) self.log.debug("Output: {}".format(output)) + # generate used frames + self.generate_used_frames(frame_duration) + + def generate_used_frames(self, duration): + for _i in range(1, (int(duration) + 1)): + if self.used_frames[-1] == self.workfile_start: + seq_number = self.padding % (self.used_frames[-1]) + self.workfile_start -= 1 + else: + seq_number = self.padding % ( + self.used_frames[-1] + 1) + self.used_frames.append(int(seq_number)) + + def add_sequence_output(self): + output_file = "{}{}{}".format( + self.temp_file_head, + self.padding, + self.sequence_ext + ) + # create path to destination + output_path = os.path.join(self.staging_dir, output_file) + + # generate frame start + out_frame_start = self.used_frames[-1] + 1 + if self.used_frames[-1] == self.workfile_start: + out_frame_start = self.used_frames[-1] + + return output_path, out_frame_start + @staticmethod def _frames_to_secons(frames, framerate): rt = otio.opentime.from_frames(frames, framerate) From 9497fc621e11423afa5629f899ca3dda4f15986f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Dec 2020 21:36:01 +0100 Subject: [PATCH 40/72] feat(global): clean code and finalizing --- .../global/publish/extract_otio_review.py | 536 ++---------------- 1 file changed, 54 insertions(+), 482 deletions(-) diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py index f4108bad49..07f75e3312 100644 --- a/pype/plugins/global/publish/extract_otio_review.py +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -37,17 +37,20 @@ class ExtractOTIOReview(pype.api.Extractor): temp_file_head = "tempFile." to_width = 800 to_height = 600 - sequence_workflow = False - sequence_ext = ".jpg" + output_ext = ".jpg" def process(self, instance): self.representation_files = list() self.used_frames = list() self.workfile_start = int(instance.data.get( "workfileFrameStart", 1001)) - self.padding = "%0{}d".format(len(str(self.workfile_start))) + self.padding = len(str(self.workfile_start)) self.used_frames.append(self.workfile_start) self.log.debug(f"_ self.used_frames-0: {self.used_frames}") + self.to_width = instance.data.get( + "resolutionWidth") or self.to_width + self.to_height = instance.data.get( + "resolutionHeight") or self.to_height # get otio clip and other time info from instance clip handle_start = instance.data["handleStart"] @@ -65,39 +68,6 @@ class ExtractOTIOReview(pype.api.Extractor): if not instance.data.get("representations"): instance.data["representations"] = list() - # in case of more than one clip check if second clip is sequence - # this will define what ffmpeg workflow will be used - # test first clip if it is not gap - test_clip = otio_review_clips[0] - if (not isinstance(test_clip, otio.schema.Clip)) \ - and (len(otio_review_clips) > 1): - # if first was gap then test second in case there are more - test_clip = otio_review_clips[1] - - # make sure second clip is not gap - if isinstance(test_clip, otio.schema.Clip): - metadata = test_clip.media_reference.metadata - - # get resolution data from metadata if they are available - self.to_width = metadata.get("width") or self.to_width - self.to_height = metadata.get("height") or self.to_height - self.actual_fps = test_clip.source_range.start_time.rate - - # define future workflow sequencial or movie - is_sequence = metadata.get("isSequence") - - if is_sequence: - path = test_clip.media_reference.target_url - available_range = self._trim_media_range( - test_clip.available_range(), - test_clip.source_range - ) - _dir_path, collection = self._make_sequence_collection( - path, available_range, metadata) - self.sequence_workflow = collection - self.sequence_ext = collection.format("{tail}") - - # loop all otio review clips for index, r_otio_cl in enumerate(otio_review_clips): src_range = r_otio_cl.source_range start = src_range.start_time.value @@ -128,58 +98,35 @@ class ExtractOTIOReview(pype.api.Extractor): available_range = self._trim_available_range( available_range, start, duration, self.actual_fps) - first, last = pype.lib.otio_range_to_frame_range( - available_range) - self.log.debug(f"_ first, last: {first}-{last}") - # media source info if isinstance(r_otio_cl, otio.schema.Clip): path = r_otio_cl.media_reference.target_url metadata = r_otio_cl.media_reference.metadata - if self.sequence_workflow: - if metadata.get("padding"): - # render image sequence to sequence - dir_path, collection = self._make_sequence_collection( - path, available_range, metadata) + if metadata.get("padding"): + # render image sequence to sequence + dir_path, collection = self._make_sequence_collection( + path, available_range, metadata) - # render segment - self._render_sequence_seqment( - sequence=[dir_path, collection] - ) - else: - # render video file to sequence - self._render_sequence_seqment( - video=[path, available_range] - ) + # render segment + self._render_seqment( + sequence=[dir_path, collection] + ) + else: + # render video file to sequence + self._render_seqment( + video=[path, available_range] + ) - # create seconds values - start_sec = self._frames_to_secons( - start, - src_range.start_time.rate) - duration_sec = self._frames_to_secons( - duration, - src_range.duration.rate) else: - # create seconds values - start_sec = 0 - duration_sec = self._frames_to_secons( - duration, - src_range.duration.rate) - - # if sequence workflow - if self.sequence_workflow: - self._render_sequence_seqment(gap=duration) - - self.log.debug(f"_ start_sec: {start_sec}") - self.log.debug(f"_ duration_sec: {duration_sec}") + self._render_seqment(gap=duration) # creating and registering representation - representation = self.create_representation(start, duration) + representation = self._create_representation(start, duration) instance.data["representations"].append(representation) self.log.info(f"Adding representation: {representation}") - def create_representation(self, start, duration): + def _create_representation(self, start, duration): end = start + duration # create default representation data @@ -190,26 +137,24 @@ class ExtractOTIOReview(pype.api.Extractor): "tags": ["review", "ftrackreview", "delete"] } - # update data if sequence workflow - if self.sequence_workflow: - collection = clique.Collection( - self.temp_file_head, - tail=self.sequence_ext, - padding=len(str(self.workfile_start)), - indexes=set(self.used_frames) - ) - start = min(collection.indexes) - end = max(collection.indexes) - self.log.debug(collection) - files = [f for f in collection] - ext = collection.format("{tail}") - representation_data.update({ - "name": ext[1:], - "ext": ext[1:], - "files": files, - "frameStart": start, - "frameEnd": end, - }) + collection = clique.Collection( + self.temp_file_head, + tail=self.output_ext, + padding=self.padding, + indexes=set(self.used_frames) + ) + start = min(collection.indexes) + end = max(collection.indexes) + + files = [f for f in collection] + ext = collection.format("{tail}") + representation_data.update({ + "name": ext[1:], + "ext": ext[1:], + "files": files, + "frameStart": start, + "frameEnd": end, + }) return representation_data def _trim_available_range(self, avl_range, start, duration, fps): @@ -223,9 +168,8 @@ class ExtractOTIOReview(pype.api.Extractor): gap_duration = src_start - avl_start # create gap data to disk - if self.sequence_workflow: - self._render_sequence_seqment(gap=gap_duration) - self.log.debug(f"_ self.used_frames-1: {self.used_frames}") + self._render_seqment(gap=gap_duration) + self.log.debug(f"_ self.used_frames-1: {self.used_frames}") # fix start and end to correct values start = 0 duration -= len(gap_duration) @@ -239,9 +183,8 @@ class ExtractOTIOReview(pype.api.Extractor): gap_duration = gap_start - gap_end # create gap data to disk - if self.sequence_workflow: - self._render_sequence_seqment(gap=gap_duration) - self.log.debug(f"_ self.used_frames-2: {self.used_frames}") + self._render_seqment(gap=gap_duration) + self.log.debug(f"_ self.used_frames-2: {self.used_frames}") # fix duration lenght duration = avl_durtation @@ -251,15 +194,12 @@ class ExtractOTIOReview(pype.api.Extractor): avl_range, self._range_from_frames(start, duration, fps) ) - def _render_sequence_seqment(self, - sequence=None, - video=None, - gap=None): + def _render_seqment(self, sequence=None, video=None, gap=None): # get rendering app path ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") # create path and frame start to destination - output_path, out_frame_start = self.add_sequence_output() + output_path, out_frame_start = self._add_ffmpeg_output() # start command list command = [ffmpeg_path] @@ -319,23 +259,23 @@ class ExtractOTIOReview(pype.api.Extractor): self.log.debug("Output: {}".format(output)) # generate used frames - self.generate_used_frames(frame_duration) + self._generate_used_frames(frame_duration) - def generate_used_frames(self, duration): + def _generate_used_frames(self, duration): + padding = "{{:0{}d}}".format(self.padding) for _i in range(1, (int(duration) + 1)): if self.used_frames[-1] == self.workfile_start: - seq_number = self.padding % (self.used_frames[-1]) + seq_number = padding.format(self.used_frames[-1]) self.workfile_start -= 1 else: - seq_number = self.padding % ( - self.used_frames[-1] + 1) + seq_number = padding.format(self.used_frames[-1] + 1) self.used_frames.append(int(seq_number)) - def add_sequence_output(self): + def _add_ffmpeg_output(self): output_file = "{}{}{}".format( self.temp_file_head, - self.padding, - self.sequence_ext + "%0{}d".format(self.padding), + self.output_ext ) # create path to destination output_path = os.path.join(self.staging_dir, output_file) @@ -385,371 +325,3 @@ class ExtractOTIOReview(pype.api.Extractor): otio.opentime.RationalTime(start, fps), otio.opentime.RationalTime(duration, fps) ) - - - # inst_data = instance.data - # asset = inst_data['asset'] - # item = inst_data['item'] - # event_number = int(item.eventNumber()) - # - # # 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"] - # - # ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") - # ffprobe_path = pype.lib.get_ffmpeg_tool_path("ffprobe") - # - # # filter out mov and img sequences - # representations_new = representations[:] - # for repre in representations: - # input_args = list() - # output_args = list() - # - # tags = repre.get("tags", []) - # - # # check if supported tags are in representation for activation - # filter_tag = False - # for tag in ["_cut-bigger", "_cut-smaller"]: - # if tag in tags: - # filter_tag = True - # break - # if not filter_tag: - # continue - # - # self.log.debug("__ repre: {}".format(repre)) - # - # files = repre.get("files") - # staging_dir = repre.get("stagingDir") - # fps = repre.get("fps") - # ext = repre.get("ext") - # - # # make paths - # full_output_dir = os.path.join( - # staging_dir, "cuts") - # - # if is_sequence: - # new_files = list() - # - # # frame range delivery included handles - # frame_start = ( - # inst_data["frameStart"] - inst_data["handleStart"]) - # frame_end = ( - # inst_data["frameEnd"] + inst_data["handleEnd"]) - # self.log.debug("_ frame_start: {}".format(frame_start)) - # self.log.debug("_ frame_end: {}".format(frame_end)) - # - # # make collection from input files list - # collections, remainder = clique.assemble(files) - # collection = collections.pop() - # self.log.debug("_ collection: {}".format(collection)) - # - # # name components - # head = collection.format("{head}") - # padding = collection.format("{padding}") - # tail = collection.format("{tail}") - # self.log.debug("_ head: {}".format(head)) - # self.log.debug("_ padding: {}".format(padding)) - # self.log.debug("_ tail: {}".format(tail)) - # - # # make destination file with instance data - # # frame start and end range - # 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 - # ]) - # 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)) - # self.hardlink_file(src, dst) - # new_files.append(dst_file_name) - # index += 1 - # - # self.log.debug("_ new_files: {}".format(new_files)) - # - # else: - # # ffmpeg when single file - # new_files = "{}_{}".format(asset, files) - # - # # frame range - # frame_start = repre.get("frameStart") - # frame_end = repre.get("frameEnd") - # - # full_input_path = os.path.join( - # staging_dir, files) - # - # os.path.isdir(full_output_dir) or os.makedirs(full_output_dir) - # - # full_output_path = os.path.join( - # full_output_dir, new_files) - # - # self.log.debug( - # "__ full_input_path: {}".format(full_input_path)) - # self.log.debug( - # "__ full_output_path: {}".format(full_output_path)) - # - # # check if audio stream is in input video file - # ffprob_cmd = ( - # "\"{ffprobe_path}\" -i \"{full_input_path}\" -show_streams" - # " -select_streams a -loglevel error" - # ).format(**locals()) - # - # self.log.debug("ffprob_cmd: {}".format(ffprob_cmd)) - # audio_check_output = pype.api.subprocess(ffprob_cmd) - # self.log.debug( - # "audio_check_output: {}".format(audio_check_output)) - # - # # Fix one frame difference - # """ TODO: this is just work-around for issue: - # https://github.com/pypeclub/pype/issues/659 - # """ - # frame_duration_extend = 1 - # if audio_check_output: - # frame_duration_extend = 0 - # - # # translate frame to sec - # start_sec = float(frame_start) / fps - # duration_sec = float( - # (frame_end - frame_start) + frame_duration_extend) / fps - # - # empty_add = None - # - # # check if not missing frames at start - # if (start_sec < 0) or (media_duration < frame_end): - # # for later swithing off `-c:v copy` output arg - # empty_add = True - # - # # init empty variables - # video_empty_start = video_layer_start = "" - # audio_empty_start = audio_layer_start = "" - # video_empty_end = video_layer_end = "" - # audio_empty_end = audio_layer_end = "" - # audio_input = audio_output = "" - # v_inp_idx = 0 - # concat_n = 1 - # - # # try to get video native resolution data - # try: - # resolution_output = pype.api.subprocess(( - # "\"{ffprobe_path}\" -i \"{full_input_path}\"" - # " -v error " - # "-select_streams v:0 -show_entries " - # "stream=width,height -of csv=s=x:p=0" - # ).format(**locals())) - # - # x, y = resolution_output.split("x") - # resolution_width = int(x) - # resolution_height = int(y) - # except Exception as _ex: - # self.log.warning( - # "Video native resolution is untracable: {}".format( - # _ex)) - # - # if audio_check_output: - # # adding input for empty audio - # input_args.append("-f lavfi -i anullsrc") - # - # # define audio empty concat variables - # audio_input = "[1:a]" - # audio_output = ":a=1" - # v_inp_idx = 1 - # - # # adding input for video black frame - # input_args.append(( - # "-f lavfi -i \"color=c=black:" - # "s={resolution_width}x{resolution_height}:r={fps}\"" - # ).format(**locals())) - # - # if (start_sec < 0): - # # recalculate input video timing - # empty_start_dur = abs(start_sec) - # start_sec = 0 - # duration_sec = float(frame_end - ( - # frame_start + (empty_start_dur * fps)) + 1) / fps - # - # # define starting empty video concat variables - # video_empty_start = ( - # "[{v_inp_idx}]trim=duration={empty_start_dur}[gv0];" # noqa - # ).format(**locals()) - # video_layer_start = "[gv0]" - # - # if audio_check_output: - # # define starting empty audio concat variables - # audio_empty_start = ( - # "[0]atrim=duration={empty_start_dur}[ga0];" - # ).format(**locals()) - # audio_layer_start = "[ga0]" - # - # # alter concat number of clips - # concat_n += 1 - # - # # check if not missing frames at the end - # if (media_duration < frame_end): - # # recalculate timing - # empty_end_dur = float( - # frame_end - media_duration + 1) / fps - # duration_sec = float( - # media_duration - frame_start) / fps - # - # # define ending empty video concat variables - # video_empty_end = ( - # "[{v_inp_idx}]trim=duration={empty_end_dur}[gv1];" - # ).format(**locals()) - # video_layer_end = "[gv1]" - # - # if audio_check_output: - # # define ending empty audio concat variables - # audio_empty_end = ( - # "[0]atrim=duration={empty_end_dur}[ga1];" - # ).format(**locals()) - # audio_layer_end = "[ga0]" - # - # # alter concat number of clips - # concat_n += 1 - # - # # concatting black frame togather - # output_args.append(( - # "-filter_complex \"" - # "{audio_empty_start}" - # "{video_empty_start}" - # "{audio_empty_end}" - # "{video_empty_end}" - # "{video_layer_start}{audio_layer_start}[1:v]{audio_input}" # noqa - # "{video_layer_end}{audio_layer_end}" - # "concat=n={concat_n}:v=1{audio_output}\"" - # ).format(**locals())) - # - # # append ffmpeg input video clip - # input_args.append("-ss {:0.2f}".format(start_sec)) - # input_args.append("-t {:0.2f}".format(duration_sec)) - # input_args.append("-i \"{}\"".format(full_input_path)) - # - # # add copy audio video codec if only shortening clip - # if ("_cut-bigger" in tags) and (not empty_add): - # output_args.append("-c:v copy") - # - # # make sure it is having no frame to frame comprassion - # output_args.append("-intra") - # - # # output filename - # output_args.append("-y \"{}\"".format(full_output_path)) - # - # mov_args = [ - # "\"{}\"".format(ffmpeg_path), - # " ".join(input_args), - # " ".join(output_args) - # ] - # subprcs_cmd = " ".join(mov_args) - # - # # run subprocess - # self.log.debug("Executing: {}".format(subprcs_cmd)) - # output = pype.api.subprocess(subprcs_cmd) - # self.log.debug("Output: {}".format(output)) - # - # repre_new = { - # "files": new_files, - # "stagingDir": full_output_dir, - # "frameStart": frame_start, - # "frameEnd": frame_end, - # "frameStartFtrack": frame_start, - # "frameEndFtrack": frame_end, - # "step": 1, - # "fps": fps, - # "name": "cut_up_preview", - # "tags": ["review"] + self.tags_addition, - # "ext": ext, - # "anatomy_template": "publish" - # } - # - # representations_new.append(repre_new) - # - # for repre in representations_new: - # if ("delete" in repre.get("tags", [])) and ( - # "cut_up_preview" not in repre["name"]): - # representations_new.remove(repre) - # - # self.log.debug( - # "Representations: {}".format(representations_new)) - # instance.data["representations"] = representations_new - # - # def hardlink_file(self, src, dst): - # dirname = os.path.dirname(dst) - # - # # make sure the destination folder exist - # try: - # os.makedirs(dirname) - # except OSError as e: - # if e.errno == errno.EEXIST: - # pass - # else: - # self.log.critical("An unexpected error occurred.") - # six.reraise(*sys.exc_info()) - # - # # create hardlined file - # try: - # filelink.create(src, dst, filelink.HARDLINK) - # except OSError as e: - # if e.errno == errno.EEXIST: - # pass - # else: - # self.log.critical("An unexpected error occurred.") - # six.reraise(*sys.exc_info()) - # -# def create_representation(self, otio_clip, to_otio_range, instance): -# to_tl_start, to_tl_end = pype.lib.otio_range_to_frame_range( -# to_otio_range) -# tl_start, tl_end = pype.lib.otio_range_to_frame_range( -# otio_clip.range_in_parent()) -# source_start, source_end = pype.lib.otio_range_to_frame_range( -# otio_clip.source_range) -# media_reference = otio_clip.media_reference -# metadata = media_reference.metadata -# mr_start, mr_end = pype.lib.otio_range_to_frame_range( -# media_reference.available_range) -# path = media_reference.target_url -# reference_frame_start = (mr_start + source_start) + ( -# to_tl_start - tl_start) -# reference_frame_end = (mr_start + source_end) - ( -# tl_end - to_tl_end) -# -# base_name = os.path.basename(path) -# staging_dir = os.path.dirname(path) -# ext = os.path.splitext(base_name)[1][1:] -# -# if metadata.get("isSequence"): -# files = list() -# padding = metadata["padding"] -# base_name = pype.lib.convert_to_padded_path(base_name, padding) -# for index in range( -# reference_frame_start, (reference_frame_end + 1)): -# file_name = base_name % index -# path_test = os.path.join(staging_dir, file_name) -# if os.path.exists(path_test): -# files.append(file_name) -# -# self.log.debug(files) -# else: -# files = base_name -# -# representation = { -# "ext": ext, -# "name": ext, -# "files": files, -# "frameStart": reference_frame_start, -# "frameEnd": reference_frame_end, -# "stagingDir": staging_dir -# } -# self.log.debug(representation) From 8c94caf330f14f4da74cd7122840fab824bc2dba Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 16 Dec 2020 17:01:45 +0100 Subject: [PATCH 41/72] feat(global): finalized collect and extract otio review plugin adding docstirngs and comments --- .../global/publish/collect_otio_review.py | 55 +++- .../global/publish/extract_otio_review.py | 238 ++++++++++++++---- 2 files changed, 238 insertions(+), 55 deletions(-) diff --git a/pype/plugins/global/publish/collect_otio_review.py b/pype/plugins/global/publish/collect_otio_review.py index 97f6552c51..c197e0066d 100644 --- a/pype/plugins/global/publish/collect_otio_review.py +++ b/pype/plugins/global/publish/collect_otio_review.py @@ -1,19 +1,21 @@ """ Requires: - otioTimeline -> context data attribute - review -> instance data attribute - masterLayer -> instance data attribute - otioClipRange -> instance data attribute + instance -> review + instance -> masterLayer + instance -> otioClip + context -> otioTimeline + +Provides: + instance -> otioReviewClips """ -# import os + import opentimelineio as otio import pyblish.api -import pype.lib from pprint import pformat class CollectOcioReview(pyblish.api.InstancePlugin): - """Get matching otio from defined review layer""" + """Get matching otio track from defined review layer""" label = "Collect OTIO review" order = pyblish.api.CollectorOrder - 0.57 @@ -27,8 +29,14 @@ class CollectOcioReview(pyblish.api.InstancePlugin): master_layer = instance.data["masterLayer"] otio_timeline = instance.context.data["otioTimeline"] otio_clip = instance.data["otioClip"] + + # generate range in parent otio_tl_range = otio_clip.range_in_parent() + # calculate real timeline end needed for the clip + clip_end_frame = int( + otio_tl_range.start_time.value + otio_tl_range.duration.value) + # skip if master layer is False if not master_layer: return @@ -36,10 +44,43 @@ class CollectOcioReview(pyblish.api.InstancePlugin): for track in otio_timeline.tracks: if review_track_name not in track.name: continue + + # process correct track + otio_gap = None + + # get track parent range + track_rip = track.range_in_parent() + + # calculate real track end frame + track_end_frame = int( + track_rip.start_time.value + track_rip.duration.value) + + # check if the end of track is not lower then clip requirement + if clip_end_frame > track_end_frame: + # calculate diference duration + gap_duration = clip_end_frame - track_end_frame + # create rational time range for gap + otio_gap_range = otio.opentime.TimeRange( + start_time=otio.opentime.RationalTime( + float(0), + track_rip.start_time.rate + ), + duration=otio.opentime.RationalTime( + float(gap_duration), + track_rip.start_time.rate + ) + ) + # crate gap + otio_gap = otio.schema.Gap(source_range=otio_gap_range) + + # trim available clips from devined track as reviewable source otio_review_clips = otio.algorithms.track_trimmed_to_range( track, otio_tl_range ) + # add gap at the end if track end is shorter then needed + if otio_gap: + otio_review_clips.append(otio_gap) instance.data["otioReviewClips"] = otio_review_clips self.log.debug( diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py index 07f75e3312..6c4dfd65ea 100644 --- a/pype/plugins/global/publish/extract_otio_review.py +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -1,30 +1,41 @@ -import os -import sys -import six -import errno +""" +Requires: + instance -> handleStart + instance -> handleEnd + instance -> otioClip + instance -> otioReviewClips +Optional: + instance -> workfileFrameStart + instance -> resolutionWidth + instance -> resolutionHeight + +Provides: + instance -> otioReviewClips +""" + +import os import clique -import shutil import opentimelineio as otio from pyblish import api import pype class ExtractOTIOReview(pype.api.Extractor): - """ Extract OTIO timeline into one concuted video file. + """ + Extract OTIO timeline into one concuted image sequence file. - Expecting (instance.data): - otioClip (otio.schema.clip): clip from otio timeline - otioReviewClips (list): list with instances of otio.schema.clip - or otio.schema.gap + The `otioReviewClip` is holding trimmed range of clips relative to + the `otioClip`. Handles are added during looping by available list + of Gap and clips in the track. Handle start (head) is added before + first Gap or Clip and Handle end (tail) is added at the end of last + Clip or Gap. In case there is missing source material after the + handles addition Gap will be added. At the end all Gaps are converted + to black frames and available material is converted to image sequence + frames. At the end representation is created and added to the instance. + + At the moment only image sequence output is supported - Process description: - Comparing `otioClip` parent range with `otioReviewClip` parent range - will result in frame range witch is the trimmed cut. In case more otio - clips or otio gaps are found in otioReviewClips then ffmpeg will - generate multiple clips and those are then concuted together to one - video file or image sequence. Resulting files are then added to - instance as representation ready for review family plugins. """ # order = api.ExtractorOrder @@ -40,23 +51,26 @@ class ExtractOTIOReview(pype.api.Extractor): output_ext = ".jpg" def process(self, instance): + # TODO: convert resulting image sequence to mp4 + # TODO: add oudio ouput to the mp4 if audio in review is on. + + # get otio clip and other time info from instance clip + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] + otio_review_clips = instance.data["otioReviewClips"] + + # add plugin wide attributes self.representation_files = list() self.used_frames = list() self.workfile_start = int(instance.data.get( "workfileFrameStart", 1001)) self.padding = len(str(self.workfile_start)) self.used_frames.append(self.workfile_start) - self.log.debug(f"_ self.used_frames-0: {self.used_frames}") self.to_width = instance.data.get( "resolutionWidth") or self.to_width self.to_height = instance.data.get( "resolutionHeight") or self.to_height - # get otio clip and other time info from instance clip - handle_start = instance.data["handleStart"] - handle_end = instance.data["handleEnd"] - otio_review_clips = instance.data["otioReviewClips"] - # skip instance if no reviewable data available if (not isinstance(otio_review_clips[0], otio.schema.Clip)) \ and (len(otio_review_clips) == 1): @@ -68,7 +82,9 @@ class ExtractOTIOReview(pype.api.Extractor): if not instance.data.get("representations"): instance.data["representations"] = list() + # loop available clips in otio track for index, r_otio_cl in enumerate(otio_review_clips): + # get frame range values src_range = r_otio_cl.source_range start = src_range.start_time.value duration = src_range.duration.value @@ -110,16 +126,22 @@ class ExtractOTIOReview(pype.api.Extractor): # render segment self._render_seqment( - sequence=[dir_path, collection] - ) + sequence=[dir_path, collection]) + # generate used frames + self._generate_used_frames( + len(collection.indexes)) else: # render video file to sequence self._render_seqment( - video=[path, available_range] - ) + video=[path, available_range]) + # generate used frames + self._generate_used_frames( + available_range.duration.value) else: self._render_seqment(gap=duration) + # generate used frames + self._generate_used_frames(duration) # creating and registering representation representation = self._create_representation(start, duration) @@ -127,6 +149,17 @@ class ExtractOTIOReview(pype.api.Extractor): self.log.info(f"Adding representation: {representation}") def _create_representation(self, start, duration): + """ + Creating representation data. + + Args: + start (int): start frame + duration (int): duration frames + + Returns: + dict: representation data + """ + end = start + duration # create default representation data @@ -158,6 +191,21 @@ class ExtractOTIOReview(pype.api.Extractor): return representation_data def _trim_available_range(self, avl_range, start, duration, fps): + """ + Trim available media range to source range. + + If missing media range is detected it will convert it into + black frames gaps. + + Args: + avl_range (otio.time.TimeRange): media available time range + start (int): start frame + duration (int): duration frames + fps (float): frame rate + + Returns: + otio.time.TimeRange: trimmed available range + """ avl_start = int(avl_range.start_time.value) src_start = int(avl_start + start) avl_durtation = int(avl_range.duration.value - start) @@ -169,22 +217,24 @@ class ExtractOTIOReview(pype.api.Extractor): # create gap data to disk self._render_seqment(gap=gap_duration) - self.log.debug(f"_ self.used_frames-1: {self.used_frames}") + # generate used frames + self._generate_used_frames(gap_duration) + # fix start and end to correct values start = 0 duration -= len(gap_duration) # if media duration is shorter then clip requirement if duration > avl_durtation: - # TODO: this will render missing frame before not at the end of footage. need to fix this so the rendered frames will be numbered after the footage. # calculate gap gap_start = int(src_start + avl_durtation) gap_end = int(src_start + duration) - gap_duration = gap_start - gap_end + gap_duration = gap_end - gap_start # create gap data to disk - self._render_seqment(gap=gap_duration) - self.log.debug(f"_ self.used_frames-2: {self.used_frames}") + self._render_seqment(gap=gap_duration, end_offset=avl_durtation) + # generate used frames + self._generate_used_frames(gap_duration, end_offset=avl_durtation) # fix duration lenght duration = avl_durtation @@ -194,19 +244,37 @@ class ExtractOTIOReview(pype.api.Extractor): avl_range, self._range_from_frames(start, duration, fps) ) - def _render_seqment(self, sequence=None, video=None, gap=None): + def _render_seqment(self, sequence=None, + video=None, gap=None, end_offset=None): + """ + Render seqment into image sequence frames. + + Using ffmpeg to convert compatible video and image source + to defined image sequence format. + + Args: + sequence (list): input dir path string, collection object in list + video (list)[optional]: video_path string, otio_range in list + gap (int)[optional]: gap duration + end_offset (int)[optional]: offset gap frame start in frames + + Returns: + otio.time.TimeRange: trimmed available range + """ # get rendering app path ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") # create path and frame start to destination - output_path, out_frame_start = self._add_ffmpeg_output() + output_path, out_frame_start = self._get_ffmpeg_output() + + if end_offset: + out_frame_start += end_offset # start command list command = [ffmpeg_path] if sequence: input_dir, collection = sequence - frame_duration = len(collection.indexes) in_frame_start = min(collection.indexes) # converting image sequence to image sequence @@ -235,9 +303,8 @@ class ExtractOTIOReview(pype.api.Extractor): ]) elif gap: - frame_duration = gap sec_duration = self._frames_to_secons( - frame_duration, self.actual_fps) + gap, self.actual_fps) # form command for rendering gap files command.extend([ @@ -258,20 +325,49 @@ class ExtractOTIOReview(pype.api.Extractor): output = pype.api.subprocess(" ".join(command), shell=True) self.log.debug("Output: {}".format(output)) - # generate used frames - self._generate_used_frames(frame_duration) + def _generate_used_frames(self, duration, end_offset=None): + """ + Generating used frames into plugin argument `used_frames`. + + The argument `used_frames` is used for checking next available + frame to start with during rendering sequence segments. + + Args: + duration (int): duration of frames needed to be generated + end_offset (int)[optional]: in case frames need to be offseted + + """ - def _generate_used_frames(self, duration): padding = "{{:0{}d}}".format(self.padding) - for _i in range(1, (int(duration) + 1)): - if self.used_frames[-1] == self.workfile_start: - seq_number = padding.format(self.used_frames[-1]) - self.workfile_start -= 1 - else: - seq_number = padding.format(self.used_frames[-1] + 1) - self.used_frames.append(int(seq_number)) + if end_offset: + new_frames = list() + start_frame = self.used_frames[-1] + for index in range((end_offset + 1), + (int(end_offset + duration) + 1)): + seq_number = padding.format(start_frame + index) + self.log.debug( + f"index: `{index}` | seq_number: `{seq_number}`") + new_frames.append(int(seq_number)) + new_frames += self.used_frames + self.used_frames = new_frames + else: + for _i in range(1, (int(duration) + 1)): + if self.used_frames[-1] == self.workfile_start: + seq_number = padding.format(self.used_frames[-1]) + self.workfile_start -= 1 + else: + seq_number = padding.format(self.used_frames[-1] + 1) + self.used_frames.append(int(seq_number)) - def _add_ffmpeg_output(self): + def _get_ffmpeg_output(self): + """ + Returning ffmpeg output command arguments. + + Returns: + str: output_path is path for image sequence output + int: out_frame_start is starting sequence frame + + """ output_file = "{}{}{}".format( self.temp_file_head, "%0{}d".format(self.padding), @@ -289,11 +385,34 @@ class ExtractOTIOReview(pype.api.Extractor): @staticmethod def _frames_to_secons(frames, framerate): + """ + Returning secons. + + Args: + frames (int): frame + framerate (flaot): frame rate + + Returns: + float: second value + + """ rt = otio.opentime.from_frames(frames, framerate) return otio.opentime.to_seconds(rt) @staticmethod def _make_sequence_collection(path, otio_range, metadata): + """ + Make collection from path otio range and otio metadata. + + Args: + path (str): path to image sequence with `%d` + otio_range (otio.opentime.TimeRange): range to be used + metadata (dict): data where padding value can be found + + Returns: + list: dir_path (str): path to sequence, collection object + + """ if "%" not in path: return None file_name = os.path.basename(path) @@ -308,6 +427,17 @@ class ExtractOTIOReview(pype.api.Extractor): @staticmethod def _trim_media_range(media_range, source_range): + """ + Trim input media range with clip source range. + + Args: + media_range (otio.opentime.TimeRange): available range of media + source_range (otio.opentime.TimeRange): clip required range + + Returns: + otio.opentime.TimeRange: trimmed media range + + """ rw_media_start = otio.opentime.RationalTime( media_range.start_time.value + source_range.start_time.value, media_range.start_time.rate @@ -321,6 +451,18 @@ class ExtractOTIOReview(pype.api.Extractor): @staticmethod def _range_from_frames(start, duration, fps): + """ + Returns otio time range. + + Args: + start (int): frame start + duration (int): frame duration + fps (float): frame range + + Returns: + otio.opentime.TimeRange: crated range + + """ return otio.opentime.TimeRange( otio.opentime.RationalTime(start, fps), otio.opentime.RationalTime(duration, fps) From c04093e2b8b08c7c3a92cee66a015a9a1daa38f6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Dec 2020 14:37:36 +0100 Subject: [PATCH 42/72] feat(resolve): moving resolve to pype3 --- pype/hooks/global/pre_with_windows_shell.py | 2 +- pype/hooks/resolve/pre_resolve_setup.py | 13 +++++++------ .../defaults/system_settings/applications.json | 11 +++++++---- pype/settings/defaults/system_settings/modules.json | 2 +- requirements.txt | 2 +- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/pype/hooks/global/pre_with_windows_shell.py b/pype/hooks/global/pre_with_windows_shell.py index 918c0d63fd..d675c9bf5b 100644 --- a/pype/hooks/global/pre_with_windows_shell.py +++ b/pype/hooks/global/pre_with_windows_shell.py @@ -11,7 +11,7 @@ class LaunchWithWindowsShell(PreLaunchHook): """ order = 10 - app_groups = ["nuke", "nukex", "hiero", "nukestudio"] + app_groups = ["resolve", "nuke", "nukex", "hiero", "nukestudio"] platforms = ["windows"] def execute(self): diff --git a/pype/hooks/resolve/pre_resolve_setup.py b/pype/hooks/resolve/pre_resolve_setup.py index 4f6d33c6eb..3799e227ff 100644 --- a/pype/hooks/resolve/pre_resolve_setup.py +++ b/pype/hooks/resolve/pre_resolve_setup.py @@ -15,7 +15,8 @@ class ResolvePrelaunch(PreLaunchHook): def execute(self): # making sure pyton 3.6 is installed at provided path - py36_dir = os.path.normpath(self.env.get("PYTHON36_RESOLVE", "")) + py36_dir = os.path.normpath( + self.launch_context.env.get("PYTHON36_RESOLVE", "")) assert os.path.isdir(py36_dir), ( "Python 3.6 is not installed at the provided folder path. Either " "make sure the `environments\resolve.json` is having correctly " @@ -23,11 +24,10 @@ class ResolvePrelaunch(PreLaunchHook): f"in given path. \nPYTHON36_RESOLVE: `{py36_dir}`" ) self.log.info(f"Path to Resolve Python folder: `{py36_dir}`...") - self.env["PYTHON36_RESOLVE"] = py36_dir # setting utility scripts dir for scripts syncing us_dir = os.path.normpath( - self.env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "") + self.launch_context.env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "") ) assert os.path.isdir(us_dir), ( "Resolve utility script dir does not exists. Either make sure " @@ -38,8 +38,9 @@ class ResolvePrelaunch(PreLaunchHook): self.log.debug(f"-- us_dir: `{us_dir}`") # correctly format path for pre python script - pre_py_sc = os.path.normpath(self.env.get("PRE_PYTHON_SCRIPT", "")) - self.env["PRE_PYTHON_SCRIPT"] = pre_py_sc + pre_py_sc = os.path.normpath( + self.launch_context.env.get("PRE_PYTHON_SCRIPT", "")) + self.launch_context.env["PRE_PYTHON_SCRIPT"] = pre_py_sc self.log.debug(f"-- pre_py_sc: `{pre_py_sc}`...") try: __import__("pype.hosts.resolve") @@ -55,4 +56,4 @@ class ResolvePrelaunch(PreLaunchHook): # Resolve Setup integration importlib.reload(utils) self.log.debug(f"-- utils.__file__: `{utils.__file__}`") - utils.setup(self.env) + utils.setup(self.launch_context.env) diff --git a/pype/settings/defaults/system_settings/applications.json b/pype/settings/defaults/system_settings/applications.json index 79d39c94f9..639b52e423 100644 --- a/pype/settings/defaults/system_settings/applications.json +++ b/pype/settings/defaults/system_settings/applications.json @@ -788,9 +788,7 @@ "RESOLVE_DEV" ] }, - "RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR": [ - "{STUDIO_SOFT}/davinci_resolve/scripts/python" - ], + "RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR": [], "RESOLVE_SCRIPT_API": { "windows": "{PROGRAMDATA}/Blackmagic Design/DaVinci Resolve/Support/Developer/Scripting", "darvin": "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting", @@ -834,7 +832,12 @@ "variant_label": "16", "icon": "", "executables": { - "windows": [], + "windows": [ + [ + "C:/Program Files/Blackmagic Design/DaVinci Resolve/Resolve.exe", + "" + ] + ], "darwin": [], "linux": [] }, diff --git a/pype/settings/defaults/system_settings/modules.json b/pype/settings/defaults/system_settings/modules.json index 0f4b0b37f3..488cb91827 100644 --- a/pype/settings/defaults/system_settings/modules.json +++ b/pype/settings/defaults/system_settings/modules.json @@ -153,4 +153,4 @@ "idle_manager": { "enabled": true } -} +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 658405e2fb..c719b06b9c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ google-api-python-client jsonschema keyring log4mongo -OpenTimelineIO +OpenTimelineIO==0.11.0 pathlib2 Pillow pynput From 5edb912a8d0bddcb160bde961c4823e676b89678 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Dec 2020 15:53:55 +0100 Subject: [PATCH 43/72] fix(global): do `blessed` exception in terminal.py --- pype/lib/terminal.py | 83 +++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/pype/lib/terminal.py b/pype/lib/terminal.py index afaca8241a..461d13f84a 100644 --- a/pype/lib/terminal.py +++ b/pype/lib/terminal.py @@ -13,10 +13,12 @@ import re import os import sys -import blessed +try: + import blessed + term = blessed.Terminal() - -term = blessed.Terminal() +except Exception: + term = None class Terminal: @@ -29,44 +31,45 @@ class Terminal: """ # shortcuts for colorama codes + _sdict = {} + if term: + _SB = term.bold + _RST = "" + _LR = term.tomato2 + _LG = term.aquamarine3 + _LB = term.turquoise2 + _LM = term.slateblue2 + _LY = term.gold + _R = term.red + _G = term.green + _B = term.blue + _C = term.cyan + _Y = term.yellow + _W = term.white - _SB = term.bold - _RST = "" - _LR = term.tomato2 - _LG = term.aquamarine3 - _LB = term.turquoise2 - _LM = term.slateblue2 - _LY = term.gold - _R = term.red - _G = term.green - _B = term.blue - _C = term.cyan - _Y = term.yellow - _W = term.white + # dictionary replacing string sequences with colorized one + _sdict = { - # dictionary replacing string sequences with colorized one - _sdict = { - - r">>> ": _SB + _LG + r">>> " + _RST, - r"!!!(?!\sCRI|\sERR)": _SB + _R + r"!!! " + _RST, - r"\-\-\- ": _SB + _C + r"--- " + _RST, - r"\*\*\*(?!\sWRN)": _SB + _LY + r"***" + _RST, - r"\*\*\* WRN": _SB + _LY + r"*** WRN" + _RST, - r" \- ": _SB + _LY + r" - " + _RST, - r"\[ ": _SB + _LG + r"[ " + _RST, - r"\]": _SB + _LG + r"]" + _RST, - r"{": _LG + r"{", - r"}": r"}" + _RST, - r"\(": _LY + r"(", - r"\)": r")" + _RST, - r"^\.\.\. ": _SB + _LR + r"... " + _RST, - r"!!! ERR: ": - _SB + _LR + r"!!! ERR: " + _RST, - r"!!! CRI: ": - _SB + _R + r"!!! CRI: " + _RST, - r"(?i)failed": _SB + _LR + "FAILED" + _RST, - r"(?i)error": _SB + _LR + "ERROR" + _RST - } + r">>> ": _SB + _LG + r">>> " + _RST, + r"!!!(?!\sCRI|\sERR)": _SB + _R + r"!!! " + _RST, + r"\-\-\- ": _SB + _C + r"--- " + _RST, + r"\*\*\*(?!\sWRN)": _SB + _LY + r"***" + _RST, + r"\*\*\* WRN": _SB + _LY + r"*** WRN" + _RST, + r" \- ": _SB + _LY + r" - " + _RST, + r"\[ ": _SB + _LG + r"[ " + _RST, + r"\]": _SB + _LG + r"]" + _RST, + r"{": _LG + r"{", + r"}": r"}" + _RST, + r"\(": _LY + r"(", + r"\)": r")" + _RST, + r"^\.\.\. ": _SB + _LR + r"... " + _RST, + r"!!! ERR: ": + _SB + _LR + r"!!! ERR: " + _RST, + r"!!! CRI: ": + _SB + _R + r"!!! CRI: " + _RST, + r"(?i)failed": _SB + _LR + "FAILED" + _RST, + r"(?i)error": _SB + _LR + "ERROR" + _RST + } def __init__(self): pass @@ -124,7 +127,7 @@ class Terminal: """ T = Terminal # if we dont want colors, just print raw message - if os.environ.get('PYPE_LOG_NO_COLORS'): + if not T._sdict or os.environ.get('PYPE_LOG_NO_COLORS'): return message else: message = re.sub(r'\[(.*)\]', '[ ' + T._SB + T._W + From abc60f3aae8e7d2ab82be483e2b20a0e0c2f72d4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Dec 2020 17:11:00 +0100 Subject: [PATCH 44/72] hound fixes and other --- pype/hosts/resolve/lib_hiero.py | 838 --------------------------- pype/hosts/resolve/pipeline_hiero.py | 302 ---------- pype/hosts/resolve/rendering.py | 111 ---- pype/hosts/resolve/todo-rendering.py | 135 +++++ 4 files changed, 135 insertions(+), 1251 deletions(-) delete mode 100644 pype/hosts/resolve/lib_hiero.py delete mode 100644 pype/hosts/resolve/pipeline_hiero.py delete mode 100644 pype/hosts/resolve/rendering.py create mode 100644 pype/hosts/resolve/todo-rendering.py diff --git a/pype/hosts/resolve/lib_hiero.py b/pype/hosts/resolve/lib_hiero.py deleted file mode 100644 index 891ca3905c..0000000000 --- a/pype/hosts/resolve/lib_hiero.py +++ /dev/null @@ -1,838 +0,0 @@ -""" -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__, "hiero") - -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() diff --git a/pype/hosts/resolve/pipeline_hiero.py b/pype/hosts/resolve/pipeline_hiero.py deleted file mode 100644 index 73025e790f..0000000000 --- a/pype/hosts/resolve/pipeline_hiero.py +++ /dev/null @@ -1,302 +0,0 @@ -""" -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 -import pype -from pype.api import Logger - -from . import lib, menu, events - -log = Logger().get_logger(__name__, "hiero") - -AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") - -# plugin paths -LOAD_PATH = os.path.join(pype.PLUGINS_DIR, "hiero", "load") -CREATE_PATH = os.path.join(pype.PLUGINS_DIR, "hiero", "create") -INVENTORY_PATH = os.path.join(pype.PLUGINS_DIR, "hiero", "inventory") - -PUBLISH_PATH = os.path.join( - pype.PLUGINS_DIR, "hiero", "publish" -).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 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) diff --git a/pype/hosts/resolve/rendering.py b/pype/hosts/resolve/rendering.py deleted file mode 100644 index e38466e5d4..0000000000 --- a/pype/hosts/resolve/rendering.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python - -""" -Example DaVinci Resolve script: -Load a still from DRX file, apply the still to all clips in all timelines. Set render format and codec, add render jobs for all timelines, render to specified path and wait for rendering completion. -Once render is complete, delete all jobs -""" - -from python_get_resolve import GetResolve -import sys -import time - -def AddTimelineToRender( project, timeline, presetName, targetDirectory, renderFormat, renderCodec ): - project.SetCurrentTimeline(timeline) - project.LoadRenderPreset(presetName) - - if not project.SetCurrentRenderFormatAndCodec(renderFormat, renderCodec): - return False - - project.SetRenderSettings({"SelectAllFrames" : 1, "TargetDir" : targetDirectory}) - return project.AddRenderJob() - -def RenderAllTimelines( resolve, presetName, targetDirectory, renderFormat, renderCodec ): - projectManager = resolve.GetProjectManager() - project = projectManager.GetCurrentProject() - if not project: - return False - - resolve.OpenPage("Deliver") - timelineCount = project.GetTimelineCount() - - for index in range (0, int(timelineCount)): - if not AddTimelineToRender(project, project.GetTimelineByIndex(index + 1), presetName, targetDirectory, renderFormat, renderCodec): - return False - return project.StartRendering() - -def IsRenderingInProgress( resolve ): - projectManager = resolve.GetProjectManager() - project = projectManager.GetCurrentProject() - if not project: - return False - - return project.IsRenderingInProgress() - -def WaitForRenderingCompletion( resolve ): - while IsRenderingInProgress(resolve): - time.sleep(1) - return - -def ApplyDRXToAllTimelineClips( timeline, path, gradeMode = 0 ): - trackCount = timeline.GetTrackCount("video") - - clips = {} - for index in range (1, int(trackCount) + 1): - clips.update( timeline.GetItemsInTrack("video", index) ) - return timeline.ApplyGradeFromDRX(path, int(gradeMode), clips) - -def ApplyDRXToAllTimelines( resolve, path, gradeMode = 0 ): - projectManager = resolve.GetProjectManager() - project = projectManager.GetCurrentProject() - if not project: - return False - timelineCount = project.GetTimelineCount() - - for index in range (0, int(timelineCount)): - timeline = project.GetTimelineByIndex(index + 1) - project.SetCurrentTimeline( timeline ) - if not ApplyDRXToAllTimelineClips(timeline, path, gradeMode): - return False - return True - -def DeleteAllRenderJobs( resolve ): - projectManager = resolve.GetProjectManager() - project = projectManager.GetCurrentProject() - project.DeleteAllRenderJobs() - return - -# Inputs: -# - DRX file to import grade still and apply it for clips -# - grade mode (0, 1 or 2) -# - preset name for rendering -# - render path -# - render format -# - render codec -if len(sys.argv) < 7: - print("input parameters for scripts are [drx file path] [grade mode] [render preset name] [render path] [render format] [render codec]") - sys.exit() - -drxPath = sys.argv[1] -gradeMode = sys.argv[2] -renderPresetName = sys.argv[3] -renderPath = sys.argv[4] -renderFormat = sys.argv[5] -renderCodec = sys.argv[6] - -# Get currently open project -resolve = GetResolve() - -if not ApplyDRXToAllTimelines(resolve, drxPath, gradeMode): - print("Unable to apply a still from drx file to all timelines") - sys.exit() - -if not RenderAllTimelines(resolve, renderPresetName, renderPath, renderFormat, renderCodec): - print("Unable to set all timelines for rendering") - sys.exit() - -WaitForRenderingCompletion(resolve) - -DeleteAllRenderJobs(resolve) - -print("Rendering is completed.") diff --git a/pype/hosts/resolve/todo-rendering.py b/pype/hosts/resolve/todo-rendering.py new file mode 100644 index 0000000000..cff9eebead --- /dev/null +++ b/pype/hosts/resolve/todo-rendering.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +# TODO: convert this script to be usable with PYPE +""" +Example DaVinci Resolve script: +Load a still from DRX file, apply the still to all clips in all timelines. +Set render format and codec, add render jobs for all timelines, render +to specified path and wait for rendering completion. +Once render is complete, delete all jobs + +clonned from: https://github.com/survos/transcribe/blob/fe3cf51eb95b82dabcf21fbe5f89bfb3d8bb6ce2/python/3_grade_and_render_all_timelines.py +""" + +from python_get_resolve import GetResolve +import sys +import time + + +def AddTimelineToRender(project, timeline, presetName, + targetDirectory, renderFormat, renderCodec): + project.SetCurrentTimeline(timeline) + project.LoadRenderPreset(presetName) + + if not project.SetCurrentRenderFormatAndCodec(renderFormat, renderCodec): + return False + + project.SetRenderSettings( + {"SelectAllFrames": 1, "TargetDir": targetDirectory}) + return project.AddRenderJob() + + +def RenderAllTimelines(resolve, presetName, targetDirectory, + renderFormat, renderCodec): + projectManager = resolve.GetProjectManager() + project = projectManager.GetCurrentProject() + if not project: + return False + + resolve.OpenPage("Deliver") + timelineCount = project.GetTimelineCount() + + for index in range(0, int(timelineCount)): + if not AddTimelineToRender( + project, + project.GetTimelineByIndex(index + 1), + presetName, + targetDirectory, + renderFormat, + renderCodec): + return False + return project.StartRendering() + + +def IsRenderingInProgress(resolve): + projectManager = resolve.GetProjectManager() + project = projectManager.GetCurrentProject() + if not project: + return False + + return project.IsRenderingInProgress() + + +def WaitForRenderingCompletion(resolve): + while IsRenderingInProgress(resolve): + time.sleep(1) + return + + +def ApplyDRXToAllTimelineClips(timeline, path, gradeMode=0): + trackCount = timeline.GetTrackCount("video") + + clips = {} + for index in range(1, int(trackCount) + 1): + clips.update(timeline.GetItemsInTrack("video", index)) + return timeline.ApplyGradeFromDRX(path, int(gradeMode), clips) + + +def ApplyDRXToAllTimelines(resolve, path, gradeMode=0): + projectManager = resolve.GetProjectManager() + project = projectManager.GetCurrentProject() + if not project: + return False + timelineCount = project.GetTimelineCount() + + for index in range(0, int(timelineCount)): + timeline = project.GetTimelineByIndex(index + 1) + project.SetCurrentTimeline(timeline) + if not ApplyDRXToAllTimelineClips(timeline, path, gradeMode): + return False + return True + + +def DeleteAllRenderJobs(resolve): + projectManager = resolve.GetProjectManager() + project = projectManager.GetCurrentProject() + project.DeleteAllRenderJobs() + return + + +# Inputs: +# - DRX file to import grade still and apply it for clips +# - grade mode (0, 1 or 2) +# - preset name for rendering +# - render path +# - render format +# - render codec +if len(sys.argv) < 7: + print( + "input parameters for scripts are [drx file path] [grade mode] " + "[render preset name] [render path] [render format] [render codec]") + sys.exit() + +drxPath = sys.argv[1] +gradeMode = sys.argv[2] +renderPresetName = sys.argv[3] +renderPath = sys.argv[4] +renderFormat = sys.argv[5] +renderCodec = sys.argv[6] + +# Get currently open project +resolve = GetResolve() + +if not ApplyDRXToAllTimelines(resolve, drxPath, gradeMode): + print("Unable to apply a still from drx file to all timelines") + sys.exit() + +if not RenderAllTimelines(resolve, renderPresetName, renderPath, + renderFormat, renderCodec): + print("Unable to set all timelines for rendering") + sys.exit() + +WaitForRenderingCompletion(resolve) + +DeleteAllRenderJobs(resolve) + +print("Rendering is completed.") From ae608efdbc8c2c0aa15ec5efd141669ae2ed6afe Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Dec 2020 17:25:23 +0100 Subject: [PATCH 45/72] hound fixes --- pype/hosts/resolve/todo-rendering.py | 3 +-- pype/hosts/resolve/utility_scripts/OTIO_export.py | 5 ++--- pype/hosts/resolve/utility_scripts/OTIO_import.py | 5 ++--- pype/lib/__init__.py | 6 +++--- pype/plugins/resolve/publish/extract_workfile.py | 1 + 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/pype/hosts/resolve/todo-rendering.py b/pype/hosts/resolve/todo-rendering.py index cff9eebead..87b04dd98f 100644 --- a/pype/hosts/resolve/todo-rendering.py +++ b/pype/hosts/resolve/todo-rendering.py @@ -6,9 +6,8 @@ Load a still from DRX file, apply the still to all clips in all timelines. Set render format and codec, add render jobs for all timelines, render to specified path and wait for rendering completion. Once render is complete, delete all jobs - -clonned from: https://github.com/survos/transcribe/blob/fe3cf51eb95b82dabcf21fbe5f89bfb3d8bb6ce2/python/3_grade_and_render_all_timelines.py """ +# clonned from: https://github.com/survos/transcribe/blob/fe3cf51eb95b82dabcf21fbe5f89bfb3d8bb6ce2/python/3_grade_and_render_all_timelines.py # noqa from python_get_resolve import GetResolve import sys diff --git a/pype/hosts/resolve/utility_scripts/OTIO_export.py b/pype/hosts/resolve/utility_scripts/OTIO_export.py index 3e08cb370d..a1142f56dd 100644 --- a/pype/hosts/resolve/utility_scripts/OTIO_export.py +++ b/pype/hosts/resolve/utility_scripts/OTIO_export.py @@ -1,13 +1,12 @@ #!/usr/bin/env python import os -import sys from pype.hosts.resolve.otio import davinci_export as otio_export -resolve = bmd.scriptapp("Resolve") +resolve = bmd.scriptapp("Resolve") # noqa fu = resolve.Fusion() ui = fu.UIManager -disp = bmd.UIDispatcher(fu.UIManager) +disp = bmd.UIDispatcher(fu.UIManager) # noqa title_font = ui.Font({"PixelSize": 18}) diff --git a/pype/hosts/resolve/utility_scripts/OTIO_import.py b/pype/hosts/resolve/utility_scripts/OTIO_import.py index 879f7eb0b5..5719ec3e3c 100644 --- a/pype/hosts/resolve/utility_scripts/OTIO_import.py +++ b/pype/hosts/resolve/utility_scripts/OTIO_import.py @@ -1,12 +1,11 @@ #!/usr/bin/env python import os -import sys from pype.hosts.resolve.otio import davinci_import as otio_import -resolve = bmd.scriptapp("Resolve") +resolve = bmd.scriptapp("Resolve") # noqa fu = resolve.Fusion() ui = fu.UIManager -disp = bmd.UIDispatcher(fu.UIManager) +disp = bmd.UIDispatcher(fu.UIManager) # noqa title_font = ui.Font({"PixelSize": 18}) diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 7992815a75..598dd757b8 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -143,10 +143,10 @@ __all__ = [ "IniSettingRegistry", "JSONSettingRegistry", "PypeSettingsRegistry", - "timeit" + "timeit", "is_overlapping_otio_ranges", "otio_range_with_handles", - "convert_to_padded_path" - "otio_range_to_frame_range", + "convert_to_padded_path", + "otio_range_to_frame_range" ] diff --git a/pype/plugins/resolve/publish/extract_workfile.py b/pype/plugins/resolve/publish/extract_workfile.py index a88794841b..e52e829ee4 100644 --- a/pype/plugins/resolve/publish/extract_workfile.py +++ b/pype/plugins/resolve/publish/extract_workfile.py @@ -3,6 +3,7 @@ import pyblish.api import pype.api from pype.hosts import resolve + class ExtractWorkfile(pype.api.Extractor): """ Extractor export DRP workfile file representation From a9a2d983b95dba615c4ac919a1421845770f6ee2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Dec 2020 17:25:37 +0100 Subject: [PATCH 46/72] davinci otio import wip --- pype/hosts/resolve/otio/davinci_import.py | 64 ++++++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/pype/hosts/resolve/otio/davinci_import.py b/pype/hosts/resolve/otio/davinci_import.py index 19133279bb..3bbb007b25 100644 --- a/pype/hosts/resolve/otio/davinci_import.py +++ b/pype/hosts/resolve/otio/davinci_import.py @@ -1,4 +1,5 @@ import sys +import json import DaVinciResolveScript import opentimelineio as otio @@ -17,30 +18,89 @@ self.project_fps = None def build_timeline(otio_timeline): + # TODO: build timeline in mediapool `otioImport` folder + # TODO: loop otio tracks and build them in the new timeline for clip in otio_timeline.each_clip(): + # TODO: create track item print(clip.name) print(clip.parent().name) print(clip.range_in_parent()) def _build_track(otio_track): + # TODO: _build_track pass def _build_media_pool_item(otio_media_reference): + # TODO: _build_media_pool_item pass def _build_track_item(otio_clip): + # TODO: _build_track_item pass def _build_gap(otio_clip): + # TODO: _build_gap pass -def _build_marker(otio_marker): - pass +def _build_marker(track_item, otio_marker): + frame_start = otio_marker.marked_range.start_time.value + frame_duration = otio_marker.marked_range.duration.value + + # marker attributes + frameId = (frame_start / 10) * 10 + color = otio_marker.color + name = otio_marker.name + note = otio_marker.metadata.get("note") or json.dumps(otio_marker.metadata) + duration = (frame_duration / 10) * 10 + + track_item.AddMarker( + frameId, + color, + name, + note, + duration + ) + + +def _build_media_pool_folder(name): + """ + Returns folder with input name and sets it as current folder. + + It will create new media bin if none is found in root media bin + + Args: + name (str): name of bin + + Returns: + resolve.api.MediaPool.Folder: description + + """ + + root_folder = self.media_pool.GetRootFolder() + sub_folders = root_folder.GetSubFolderList() + testing_names = list() + + for subfolder in sub_folders: + subf_name = subfolder.GetName() + if name in subf_name: + testing_names.append(subfolder) + else: + testing_names.append(False) + + matching = next((f for f in testing_names if f is not False), None) + + if not matching: + new_folder = self.media_pool.AddSubFolder(root_folder, name) + self.media_pool.SetCurrentFolder(new_folder) + else: + self.media_pool.SetCurrentFolder(matching) + + return self.media_pool.GetCurrentFolder() def read_from_file(otio_file): From 566c1fc2a965350b3be006bb740f32e2ac8da98e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Dec 2020 18:06:55 +0100 Subject: [PATCH 47/72] resolve plugin fix --- pype/hosts/resolve/plugin.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index 1b7e6fc051..e5ba39c535 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -2,7 +2,6 @@ import re from avalon import api from pype.hosts import resolve from avalon.vendor import qargparse -from pype.api import config from . import lib from Qt import QtWidgets, QtCore @@ -344,8 +343,12 @@ class Creator(api.Creator): def __init__(self, *args, **kwargs): super(Creator, self).__init__(*args, **kwargs) - self.presets = config.get_presets()['plugins']["resolve"][ - "create"].get(self.__class__.__name__, {}) + from pype.api import get_current_project_settings + resolve_p_settings = get_current_project_settings().get("resolve") + self.presets = dict() + if resolve_p_settings: + self.presets = resolve_p_settings["create"].get( + self.__class__.__name__, {}) # adding basic current context resolve objects self.project = resolve.get_current_project() From e9704dace72b08a261fbf4eaab38d19f7645d819 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 21 Dec 2020 19:38:42 +0100 Subject: [PATCH 48/72] adding settings for resolve creator --- .../schema_project_resolve.json | 89 +++++++++++++++++-- 1 file changed, 80 insertions(+), 9 deletions(-) diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_resolve.json b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_resolve.json index fcd649db83..18f6817407 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_resolve.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_resolve.json @@ -19,19 +19,90 @@ "is_group": true, "children": [ { - "type": "text", - "key": "clipName", - "label": "Clip name template" + "type": "collapsible-wrap", + "label": "Shot Hierarchy And Rename Settings", + "collapsable": false, + "children": [ + { + "type": "text", + "key": "hierarchy", + "label": "Shot parent hierarchy" + }, + { + "type": "boolean", + "key": "clipRename", + "label": "Rename clips" + }, + { + "type": "text", + "key": "clipName", + "label": "Clip name template" + }, + { + "type": "number", + "key": "countFrom", + "label": "Count sequence from" + }, + { + "type": "number", + "key": "countSteps", + "label": "Stepping number" + } + ] }, { - "type": "text", - "key": "folder", - "label": "Folder" + "type": "collapsible-wrap", + "label": "Shot Template Keywords", + "collapsable": false, + "children": [ + { + "type": "text", + "key": "folder", + "label": "{folder}" + }, + { + "type": "text", + "key": "episode", + "label": "{episode}" + }, + { + "type": "text", + "key": "sequence", + "label": "{sequence}" + }, + { + "type": "text", + "key": "track", + "label": "{track}" + }, + { + "type": "text", + "key": "shot", + "label": "{shot}" + } + ] }, { - "type": "number", - "key": "steps", - "label": "Steps" + "type": "collapsible-wrap", + "label": "Shot Attributes", + "collapsable": false, + "children": [ + { + "type": "number", + "key": "workfileFrameStart", + "label": "Workfiles Start Frame" + }, + { + "type": "number", + "key": "handleStart", + "label": "Handle start (head)" + }, + { + "type": "number", + "key": "handleEnd", + "label": "Handle end (tail)" + } + ] } ] } From 51ff88581536110793211ca8b7eebfd920510fb2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 21 Dec 2020 19:39:15 +0100 Subject: [PATCH 49/72] creator plugin rename and delete old --- .../resolve/create/create_shot_clip.py | 256 ++++++++++++++--- .../resolve/create/create_shot_clip_new.py | 264 ------------------ 2 files changed, 221 insertions(+), 299 deletions(-) delete mode 100644 pype/plugins/resolve/create/create_shot_clip_new.py diff --git a/pype/plugins/resolve/create/create_shot_clip.py b/pype/plugins/resolve/create/create_shot_clip.py index bd2e013fac..35cb30636e 100644 --- a/pype/plugins/resolve/create/create_shot_clip.py +++ b/pype/plugins/resolve/create/create_shot_clip.py @@ -6,45 +6,218 @@ from pype.hosts.resolve import lib class CreateShotClip(resolve.Creator): """Publishable clip""" - label = "Shot" + label = "Create Publishable Clip" family = "clip" icon = "film" defaults = ["Main"] - gui_name = "Pype sequencial rename with hirerarchy" - gui_info = "Define sequencial rename and fill hierarchy data." + gui_tracks = resolve.get_video_track_names() + gui_name = "Pype publish attributes creator" + gui_info = "Define sequential rename and fill hierarchy data." gui_inputs = { - "clipName": "{episode}{sequence}{shot}", - "hierarchy": "{folder}/{sequence}/{shot}", - "countFrom": 10, - "steps": 10, + "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": { - "folder": "shots", - "shot": "sh####", - "track": "{track}", - "sequence": "sc010", - "episode": "ep01" + "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": ["", "main", "bg", "fg", "bg", + "animatic"], + "type": "QComboBox", + "label": "Subset Name", + "target": "ui", + "toolTip": "chose subset name patern, if 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}, + } + }, + "shotAttr": { + "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 (head)", + "target": "tag", + "toolTip": "Handle at start of clip", # noqa + "order": 1}, + "handleEnd": { + "value": 0, + "type": "QSpinBox", + "label": "Handle end (tail)", + "target": "tag", + "toolTip": "Handle at end of clip", # noqa + "order": 2}, + } } } + presets = None def process(self): - # solve gui inputs overwrites from presets - # overwrite gui inputs from presets + print("_____ presets: {}".format(pformat(self.presets))) + # get key pares from presets and match it on ui inputs for k, v in self.gui_inputs.items(): - if isinstance(v, dict): - # nested dictionary (only one level allowed) - for _k, _v in v.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][_k] = self.presets[_k] + self.gui_inputs[k][ + "value"][_k]["value"] = self.presets[_k] if self.presets.get(k): - self.gui_inputs[k] = self.presets[k] + self.gui_inputs[k]["value"] = self.presets[k] + print(pformat(self.gui_inputs)) # open widget for plugins inputs widget = self.widget(self.gui_name, self.gui_info, self.gui_inputs) widget.exec_() - print(f"__ selected_clips: {self.selected}") if len(self.selected) < 1: return @@ -52,28 +225,41 @@ class CreateShotClip(resolve.Creator): 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 track_item_data in self.selected: + if track_item_data["track"]["name"] in v_sync_track: + sorted_selected_track_items.append(track_item_data) + else: + unsorted_selected_track_items.append(track_item_data) + + sorted_selected_track_items.extend(unsorted_selected_track_items) + # sequence attrs sq_frame_start = self.sequence.GetStartFrame() sq_markers = self.sequence.GetMarkers() - print(f"__ sq_frame_start: {pformat(sq_frame_start)}") - print(f"__ seq_markers: {pformat(sq_markers)}") # create media bin for compound clips (trackItems) mp_folder = resolve.create_current_sequence_media_bin(self.sequence) - print(f"_ mp_folder: {mp_folder.GetName()}") - lib.rename_add = 0 - for i, t_data in enumerate(self.selected): - lib.rename_index = i + kwargs = { + "ui_inputs": widget.result, + "avalon": self.data, + "mp_folder": mp_folder, + "sq_frame_start": sq_frame_start, + "sq_markers": sq_markers + } - # clear color after it is done - t_data["clip"]["item"].ClearClipColor() + for i, track_item_data in enumerate(sorted_selected_track_items): + self.rename_index = i # convert track item to timeline media pool item - resolve.create_compound_clip( - t_data, - mp_folder, - rename=True, - **dict( - {"presets": widget.result}) - ) + track_item = resolve.PublishClip( + self, track_item_data, **kwargs).convert() + track_item.SetClipColor(lib.publish_clip_color) diff --git a/pype/plugins/resolve/create/create_shot_clip_new.py b/pype/plugins/resolve/create/create_shot_clip_new.py deleted file mode 100644 index 5f6790394b..0000000000 --- a/pype/plugins/resolve/create/create_shot_clip_new.py +++ /dev/null @@ -1,264 +0,0 @@ -from pprint import pformat -from pype.hosts import resolve -from pype.hosts.resolve import lib - - -class CreateShotClipNew(resolve.Creator): - """Publishable clip""" - - label = "Create Publishable Clip [New]" - family = "clip" - icon = "film" - defaults = ["Main"] - - gui_tracks = resolve.get_video_track_names() - 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": ["", "main", "bg", "fg", "bg", - "animatic"], - "type": "QComboBox", - "label": "Subset Name", - "target": "ui", - "toolTip": "chose subset name patern, if 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] - - print(pformat(self.gui_inputs)) - # 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 track_item_data in self.selected: - if track_item_data["track"]["name"] in v_sync_track: - sorted_selected_track_items.append(track_item_data) - else: - unsorted_selected_track_items.append(track_item_data) - - sorted_selected_track_items.extend(unsorted_selected_track_items) - - # sequence attrs - sq_frame_start = self.sequence.GetStartFrame() - sq_markers = self.sequence.GetMarkers() - - # create media bin for compound clips (trackItems) - mp_folder = resolve.create_current_sequence_media_bin(self.sequence) - - kwargs = { - "ui_inputs": widget.result, - "avalon": self.data, - "mp_folder": mp_folder, - "sq_frame_start": sq_frame_start, - "sq_markers": sq_markers - } - - for i, track_item_data in enumerate(sorted_selected_track_items): - self.rename_index = i - - # convert track item to timeline media pool item - track_item = resolve.PublishClip( - self, track_item_data, **kwargs).convert() - track_item.SetClipColor(lib.publish_clip_color) From 821631f669fb712b3436d7a122d624d6d3b4ee08 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 21 Dec 2020 19:39:46 +0100 Subject: [PATCH 50/72] resolve: moving functions to pype.lib --- pype/lib/__init__.py | 10 +- pype/lib/editorial.py | 60 ++++++++++ .../global/publish/extract_otio_review.py | 106 ++++++------------ 3 files changed, 102 insertions(+), 74 deletions(-) diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 598dd757b8..35ae0a901a 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -80,7 +80,10 @@ from .editorial import ( is_overlapping_otio_ranges, otio_range_to_frame_range, otio_range_with_handles, - convert_to_padded_path + convert_to_padded_path, + trim_media_range, + range_from_frames, + frames_to_secons ) terminal = Terminal @@ -148,5 +151,8 @@ __all__ = [ "is_overlapping_otio_ranges", "otio_range_with_handles", "convert_to_padded_path", - "otio_range_to_frame_range" + "otio_range_to_frame_range", + "trim_media_range", + "range_from_frames", + "frames_to_secons" ] diff --git a/pype/lib/editorial.py b/pype/lib/editorial.py index c0ad4ace00..42c345b47d 100644 --- a/pype/lib/editorial.py +++ b/pype/lib/editorial.py @@ -1,4 +1,5 @@ import re +from opentimelineio import opentime from opentimelineio.opentime import ( to_frames, RationalTime, TimeRange) @@ -70,3 +71,62 @@ def convert_to_padded_path(path, padding): if "%d" in path: path = re.sub("%d", "%0{padding}d".format(padding=padding), path) return path + + +def trim_media_range(media_range, source_range): + """ + Trim input media range with clip source range. + + Args: + media_range (otio.opentime.TimeRange): available range of media + source_range (otio.opentime.TimeRange): clip required range + + Returns: + otio.opentime.TimeRange: trimmed media range + + """ + rw_media_start = RationalTime( + media_range.start_time.value + source_range.start_time.value, + media_range.start_time.rate + ) + rw_media_duration = RationalTime( + source_range.duration.value, + media_range.duration.rate + ) + return TimeRange( + rw_media_start, rw_media_duration) + + +def range_from_frames(start, duration, fps): + """ + Returns otio time range. + + Args: + start (int): frame start + duration (int): frame duration + fps (float): frame range + + Returns: + otio.opentime.TimeRange: crated range + + """ + return TimeRange( + RationalTime(start, fps), + RationalTime(duration, fps) + ) + + +def frames_to_secons(frames, framerate): + """ + Returning secons. + + Args: + frames (int): frame + framerate (flaot): frame rate + + Returns: + float: second value + + """ + rt = opentime.from_frames(frames, framerate) + return opentime.to_seconds(rt) diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py index 6c4dfd65ea..63b3331174 100644 --- a/pype/plugins/global/publish/extract_otio_review.py +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -38,16 +38,15 @@ class ExtractOTIOReview(pype.api.Extractor): """ - # order = api.ExtractorOrder - order = api.CollectorOrder + order = api.ExtractorOrder label = "Extract OTIO review" hosts = ["resolve"] families = ["review"] # plugin default attributes temp_file_head = "tempFile." - to_width = 800 - to_height = 600 + to_width = 1280 + to_height = 720 output_ext = ".jpg" def process(self, instance): @@ -116,11 +115,32 @@ class ExtractOTIOReview(pype.api.Extractor): # media source info if isinstance(r_otio_cl, otio.schema.Clip): - path = r_otio_cl.media_reference.target_url - metadata = r_otio_cl.media_reference.metadata + media_ref = r_otio_cl.media_reference + metadata = media_ref.metadata - if metadata.get("padding"): - # render image sequence to sequence + if isinstance(media_ref, otio.schema.ImageSequenceReference): + dirname = media_ref.target_url_base + head = media_ref.name_prefix + tail = media_ref.name_suffix + first, last = pype.lib.otio_range_to_frame_range( + available_range) + collection = clique.Collection( + head=head, + tail=tail, + padding=media_ref.frame_zero_padding + ) + collection.indexes.update( + [i for i in range(first, (last + 1))]) + # render segment + self._render_seqment( + sequence=[dirname, collection]) + # generate used frames + self._generate_used_frames( + len(collection.indexes)) + elif metadata.get("padding"): + # in case it is file sequence but not new OTIO schema + # `ImageSequenceReference` + path = media_ref.target_url dir_path, collection = self._make_sequence_collection( path, available_range, metadata) @@ -131,6 +151,7 @@ class ExtractOTIOReview(pype.api.Extractor): self._generate_used_frames( len(collection.indexes)) else: + path = media_ref.target_url # render video file to sequence self._render_seqment( video=[path, available_range]) @@ -240,8 +261,8 @@ class ExtractOTIOReview(pype.api.Extractor): duration = avl_durtation # return correct trimmed range - return self._trim_media_range( - avl_range, self._range_from_frames(start, duration, fps) + return pype.lib.trim_media_range( + avl_range, pype.lib.range_from_frames(start, duration, fps) ) def _render_seqment(self, sequence=None, @@ -292,8 +313,8 @@ class ExtractOTIOReview(pype.api.Extractor): frame_start = otio_range.start_time.value input_fps = otio_range.start_time.rate frame_duration = otio_range.duration.value - sec_start = self._frames_to_secons(frame_start, input_fps) - sec_duration = self._frames_to_secons(frame_duration, input_fps) + sec_start = pype.lib.frames_to_secons(frame_start, input_fps) + sec_duration = pype.lib.frames_to_secons(frame_duration, input_fps) # form command for rendering gap files command.extend([ @@ -303,7 +324,7 @@ class ExtractOTIOReview(pype.api.Extractor): ]) elif gap: - sec_duration = self._frames_to_secons( + sec_duration = pype.lib.frames_to_secons( gap, self.actual_fps) # form command for rendering gap files @@ -383,22 +404,6 @@ class ExtractOTIOReview(pype.api.Extractor): return output_path, out_frame_start - @staticmethod - def _frames_to_secons(frames, framerate): - """ - Returning secons. - - Args: - frames (int): frame - framerate (flaot): frame rate - - Returns: - float: second value - - """ - rt = otio.opentime.from_frames(frames, framerate) - return otio.opentime.to_seconds(rt) - @staticmethod def _make_sequence_collection(path, otio_range, metadata): """ @@ -424,46 +429,3 @@ class ExtractOTIOReview(pype.api.Extractor): head=head, tail=tail, padding=metadata["padding"]) collection.indexes.update([i for i in range(first, (last + 1))]) return dir_path, collection - - @staticmethod - def _trim_media_range(media_range, source_range): - """ - Trim input media range with clip source range. - - Args: - media_range (otio.opentime.TimeRange): available range of media - source_range (otio.opentime.TimeRange): clip required range - - Returns: - otio.opentime.TimeRange: trimmed media range - - """ - rw_media_start = otio.opentime.RationalTime( - media_range.start_time.value + source_range.start_time.value, - media_range.start_time.rate - ) - rw_media_duration = otio.opentime.RationalTime( - source_range.duration.value, - media_range.duration.rate - ) - return otio.opentime.TimeRange( - rw_media_start, rw_media_duration) - - @staticmethod - def _range_from_frames(start, duration, fps): - """ - Returns otio time range. - - Args: - start (int): frame start - duration (int): frame duration - fps (float): frame range - - Returns: - otio.opentime.TimeRange: crated range - - """ - return otio.opentime.TimeRange( - otio.opentime.RationalTime(start, fps), - otio.opentime.RationalTime(duration, fps) - ) From 0a77c1b32e6ccb296d88f36ee04036d745012407 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 21 Dec 2020 19:41:34 +0100 Subject: [PATCH 51/72] resolve: available functions for OTIO 0.13.0 --- pype/hosts/resolve/otio/davinci_export.py | 51 +++++++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/pype/hosts/resolve/otio/davinci_export.py b/pype/hosts/resolve/otio/davinci_export.py index 7244544183..0909bcb26a 100644 --- a/pype/hosts/resolve/otio/davinci_export.py +++ b/pype/hosts/resolve/otio/davinci_export.py @@ -1,7 +1,13 @@ +""" compatibility OpenTimelineIO 0.12.0 and older +""" + +import os +import re import sys import json import opentimelineio as otio from . import utils +import clique self = sys.modules[__name__] self.track_types = { @@ -29,7 +35,7 @@ def create_otio_reference(media_pool_item): metadata = _get_metadata_media_pool_item(media_pool_item) mp_clip_property = media_pool_item.GetClipProperty() path = mp_clip_property["File Path"] - reformat_path = utils.get_reformated_path(path, padded=False) + reformat_path = utils.get_reformated_path(path, padded=True) padding = utils.get_padding_from_path(path) if padding: @@ -40,7 +46,7 @@ def create_otio_reference(media_pool_item): # get clip property regarding to type mp_clip_property = media_pool_item.GetClipProperty() - fps = mp_clip_property["FPS"] + fps = float(mp_clip_property["FPS"]) if mp_clip_property["Type"] == "Video": frame_start = int(mp_clip_property["Start"]) frame_duration = int(mp_clip_property["Frames"]) @@ -50,14 +56,41 @@ def create_otio_reference(media_pool_item): frame_duration = int(utils.timecode_to_frames( audio_duration, float(fps))) - otio_ex_ref_item = otio.schema.ExternalReference( - target_url=reformat_path, - available_range=create_otio_time_range( - frame_start, - frame_duration, - fps + otio_ex_ref_item = None + + if padding: + # if it is file sequence try to create `ImageSequenceReference` + # the OTIO might not be compatible so return nothing and do it old way + try: + dirname, filename = os.path.split(path) + collection = clique.parse(filename, '{head}[{ranges}]{tail}') + padding_num = len(re.findall("(\\d+)(?=-)", filename).pop()) + otio_ex_ref_item = otio.schema.ImageSequenceReference( + target_url_base=dirname + os.sep, + name_prefix=collection.format("{head}"), + name_suffix=collection.format("{tail}"), + start_frame=frame_start, + frame_zero_padding=padding_num, + rate=fps, + available_range=create_otio_time_range( + frame_start, + frame_duration, + fps + ) + ) + except AttributeError: + pass + + if not otio_ex_ref_item: + # in case old OTIO or video file create `ExternalReference` + otio_ex_ref_item = otio.schema.ExternalReference( + target_url=reformat_path, + available_range=create_otio_time_range( + frame_start, + frame_duration, + fps + ) ) - ) # add metadata to otio item add_otio_metadata(otio_ex_ref_item, media_pool_item, **metadata) From 7a6034c11b7c07525750941cb0369bd4cf2231a3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 21 Dec 2020 19:42:33 +0100 Subject: [PATCH 52/72] OTIO requirements for 0.13 with fixes cpp build --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c719b06b9c..c1f72f9582 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ google-api-python-client jsonschema keyring log4mongo -OpenTimelineIO==0.11.0 +git+https://github.com/PixarAnimationStudios/OpenTimelineIO.git@5aa24fbe89d615448876948fe4b4900455c9a3e8 pathlib2 Pillow pynput From c2069caa48577d1086761bc5b47fa8c338f92b23 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 21 Dec 2020 19:43:07 +0100 Subject: [PATCH 53/72] resolve: wip on otio subset resources plugin --- .../publish/collect_otio_frame_ranges.py | 4 +- .../publish/collect_otio_subset_resources.py | 100 ++++++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 pype/plugins/global/publish/collect_otio_subset_resources.py diff --git a/pype/plugins/global/publish/collect_otio_frame_ranges.py b/pype/plugins/global/publish/collect_otio_frame_ranges.py index 5d1370850f..4224abe5a4 100644 --- a/pype/plugins/global/publish/collect_otio_frame_ranges.py +++ b/pype/plugins/global/publish/collect_otio_frame_ranges.py @@ -29,13 +29,10 @@ class CollectOcioFrameRanges(pyblish.api.InstancePlugin): # get ranges otio_tl_range = otio_clip.range_in_parent() - self.log.debug(otio_tl_range) otio_src_range = otio_clip.source_range otio_avalable_range = otio_clip.available_range() - self.log.debug(otio_avalable_range) otio_tl_range_handles = pype.lib.otio_range_with_handles( otio_tl_range, instance) - self.log.debug(otio_tl_range_handles) otio_src_range_handles = pype.lib.otio_range_with_handles( otio_src_range, instance) @@ -43,6 +40,7 @@ class CollectOcioFrameRanges(pyblish.api.InstancePlugin): src_starting_from = otio.opentime.to_frames( otio_avalable_range.start_time, otio_avalable_range.start_time.rate) + # convert to frames range_convert = pype.lib.otio_range_to_frame_range tl_start, tl_end = range_convert(otio_tl_range) diff --git a/pype/plugins/global/publish/collect_otio_subset_resources.py b/pype/plugins/global/publish/collect_otio_subset_resources.py new file mode 100644 index 0000000000..1642c3371d --- /dev/null +++ b/pype/plugins/global/publish/collect_otio_subset_resources.py @@ -0,0 +1,100 @@ +""" +Requires: + instance -> review + instance -> masterLayer + instance -> otioClip + context -> otioTimeline + +Provides: + instance -> otioReviewClips +""" + +import clique +import opentimelineio as otio +import pyblish.api +import pype + + +class CollectOcioSubsetResources(pyblish.api.InstancePlugin): + """Get Resources for a subset version""" + + label = "Collect OTIO subset resources" + order = pyblish.api.CollectorOrder - 0.57 + families = ["clip"] + hosts = ["resolve"] + + def process(self, instance): + # get basic variables + otio_clip = instance.data["otioClip"] + + # generate range in parent + otio_src_range = otio_clip.source_range + otio_avalable_range = otio_clip.available_range() + otio_visible_range = otio_clip.visible_range() + otio_src_range_handles = pype.lib.otio_range_with_handles( + otio_src_range, instance) + trimmed_media_range = pype.lib.trim_media_range( + otio_avalable_range, otio_src_range_handles) + + self.log.debug( + "_ otio_avalable_range: {}".format(otio_avalable_range)) + self.log.debug( + "_ otio_visible_range: {}".format(otio_visible_range)) + self.log.debug( + "_ otio_src_range: {}".format(otio_src_range)) + self.log.debug( + "_ otio_src_range_handles: {}".format(otio_src_range_handles)) + self.log.debug( + "_ trimmed_media_range: {}".format(trimmed_media_range)) + + # + # media_ref = otio_clip.media_reference + # metadata = media_ref.metadata + # + # if isinstance(media_ref, otio.schema.ImageSequenceReference): + # dirname = media_ref.target_url_base + # head = media_ref.name_prefix + # tail = media_ref.name_suffix + # first, last = pype.lib.otio_range_to_frame_range( + # available_range) + # collection = clique.Collection( + # head=head, + # tail=tail, + # padding=media_ref.frame_zero_padding + # ) + # collection.indexes.update( + # [i for i in range(first, (last + 1))]) + # # render segment + # self._render_seqment( + # sequence=[dirname, collection]) + # # generate used frames + # self._generate_used_frames( + # len(collection.indexes)) + # elif metadata.get("padding"): + # # in case it is file sequence but not new OTIO schema + # # `ImageSequenceReference` + # path = media_ref.target_url + # dir_path, collection = self._make_sequence_collection( + # path, available_range, metadata) + # + # # render segment + # self._render_seqment( + # sequence=[dir_path, collection]) + # # generate used frames + # self._generate_used_frames( + # len(collection.indexes)) + # else: + # path = media_ref.target_url + # # render video file to sequence + # self._render_seqment( + # video=[path, available_range]) + # # generate used frames + # self._generate_used_frames( + # available_range.duration.value) + # + # instance.data["otioReviewClips"] = otio_review_clips + # self.log.debug( + # "_ otio_review_clips: {}".format(otio_review_clips)) + # + # self.log.debug( + # "_ instance.data: {}".format(pformat(instance.data))) From c03660c6ecd4b71a7aa484fcd639b02dbd29e5a7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Dec 2020 15:44:01 +0100 Subject: [PATCH 54/72] fix older version OTIO --- .../global/publish/extract_otio_review.py | 87 +++++++++++-------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py index 63b3331174..e960358749 100644 --- a/pype/plugins/global/publish/extract_otio_review.py +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -83,6 +83,8 @@ class ExtractOTIOReview(pype.api.Extractor): # loop available clips in otio track for index, r_otio_cl in enumerate(otio_review_clips): + # QUESTION: what if transition on clip? + # get frame range values src_range = r_otio_cl.source_range start = src_range.start_time.value @@ -113,44 +115,60 @@ class ExtractOTIOReview(pype.api.Extractor): available_range = self._trim_available_range( available_range, start, duration, self.actual_fps) - # media source info + # process all track items of the track if isinstance(r_otio_cl, otio.schema.Clip): + # process Clip media_ref = r_otio_cl.media_reference metadata = media_ref.metadata + is_sequence = None - if isinstance(media_ref, otio.schema.ImageSequenceReference): - dirname = media_ref.target_url_base - head = media_ref.name_prefix - tail = media_ref.name_suffix - first, last = pype.lib.otio_range_to_frame_range( - available_range) - collection = clique.Collection( - head=head, - tail=tail, - padding=media_ref.frame_zero_padding - ) - collection.indexes.update( - [i for i in range(first, (last + 1))]) - # render segment - self._render_seqment( - sequence=[dirname, collection]) - # generate used frames - self._generate_used_frames( - len(collection.indexes)) - elif metadata.get("padding"): - # in case it is file sequence but not new OTIO schema - # `ImageSequenceReference` - path = media_ref.target_url - dir_path, collection = self._make_sequence_collection( - path, available_range, metadata) - - # render segment - self._render_seqment( - sequence=[dir_path, collection]) - # generate used frames - self._generate_used_frames( - len(collection.indexes)) + # check in two way if it is sequence + if hasattr(otio.schema, "ImageSequenceReference"): + # for OpenTimelineIO 0.13 and newer + if isinstance(media_ref, + otio.schema.ImageSequenceReference): + is_sequence = True else: + # for OpenTimelineIO 0.12 and older + if metadata.get("padding"): + is_sequence = True + + if is_sequence: + # file sequence way + if hasattr(media_ref, "target_url_base"): + dirname = media_ref.target_url_base + head = media_ref.name_prefix + tail = media_ref.name_suffix + first, last = pype.lib.otio_range_to_frame_range( + available_range) + collection = clique.Collection( + head=head, + tail=tail, + padding=media_ref.frame_zero_padding + ) + collection.indexes.update( + [i for i in range(first, (last + 1))]) + # render segment + self._render_seqment( + sequence=[dirname, collection]) + # generate used frames + self._generate_used_frames( + len(collection.indexes)) + else: + # in case it is file sequence but not new OTIO schema + # `ImageSequenceReference` + path = media_ref.target_url + dir_path, collection = self._make_sequence_collection( + path, available_range, metadata) + + # render segment + self._render_seqment( + sequence=[dir_path, collection]) + # generate used frames + self._generate_used_frames( + len(collection.indexes)) + else: + # single video file way path = media_ref.target_url # render video file to sequence self._render_seqment( @@ -158,8 +176,9 @@ class ExtractOTIOReview(pype.api.Extractor): # generate used frames self._generate_used_frames( available_range.duration.value) - + # QUESTION: what if nested track composition is in place? else: + # at last process a Gap self._render_seqment(gap=duration) # generate used frames self._generate_used_frames(duration) From 0f1367f4d5b029a2a946101ce5ed39ee080d9365 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Dec 2020 21:09:55 +0100 Subject: [PATCH 55/72] global: review layer refactory --- pype/hosts/resolve/plugin.py | 6 ++---- .../plugins/global/publish/collect_otio_review.py | 15 ++++++++------- .../publish/collect_otio_subset_resources.py | 2 +- pype/plugins/resolve/publish/collect_instances.py | 5 ----- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index e5ba39c535..111fd8f4e2 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -255,7 +255,7 @@ class CreatorWidget(QtWidgets.QDialog): elif v["type"] == "QSpinBox": data[k]["value"] = self.create_row( content_layout, "QSpinBox", v["label"], - setRange=(1, 99999), + setRange=(0, 99999), setValue=v["value"], setToolTip=tool_tip) return data @@ -621,9 +621,7 @@ class PublishClip: self.tag_data.update(tag_hierarchy_data) if master_layer and self.review_layer: - self.tag_data.update({"review": self.review_layer}) - else: - self.tag_data.update({"review": False}) + 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. """ diff --git a/pype/plugins/global/publish/collect_otio_review.py b/pype/plugins/global/publish/collect_otio_review.py index c197e0066d..77698d7223 100644 --- a/pype/plugins/global/publish/collect_otio_review.py +++ b/pype/plugins/global/publish/collect_otio_review.py @@ -17,7 +17,7 @@ from pprint import pformat class CollectOcioReview(pyblish.api.InstancePlugin): """Get matching otio track from defined review layer""" - label = "Collect OTIO review" + label = "Collect OTIO Review" order = pyblish.api.CollectorOrder - 0.57 families = ["clip"] hosts = ["resolve"] @@ -25,8 +25,7 @@ class CollectOcioReview(pyblish.api.InstancePlugin): def process(self, instance): otio_review_clips = list() # get basic variables - review_track_name = instance.data["review"] - master_layer = instance.data["masterLayer"] + review_track_name = instance.data.get("reviewTrack") otio_timeline = instance.context.data["otioTimeline"] otio_clip = instance.data["otioClip"] @@ -37,8 +36,7 @@ class CollectOcioReview(pyblish.api.InstancePlugin): clip_end_frame = int( otio_tl_range.start_time.value + otio_tl_range.duration.value) - # skip if master layer is False - if not master_layer: + if not review_track_name: return for track in otio_timeline.tracks: @@ -82,9 +80,12 @@ class CollectOcioReview(pyblish.api.InstancePlugin): if otio_gap: otio_review_clips.append(otio_gap) - instance.data["otioReviewClips"] = otio_review_clips + if otio_review_clips: + instance.data["families"] += ["review", "ftrack"] + instance.data["otioReviewClips"] = otio_review_clips self.log.debug( "_ otio_review_clips: {}".format(otio_review_clips)) - self.log.debug( "_ instance.data: {}".format(pformat(instance.data))) + self.log.debug( + "_ families: {}".format(instance.data["families"])) diff --git a/pype/plugins/global/publish/collect_otio_subset_resources.py b/pype/plugins/global/publish/collect_otio_subset_resources.py index 1642c3371d..f6bfe11941 100644 --- a/pype/plugins/global/publish/collect_otio_subset_resources.py +++ b/pype/plugins/global/publish/collect_otio_subset_resources.py @@ -18,7 +18,7 @@ import pype class CollectOcioSubsetResources(pyblish.api.InstancePlugin): """Get Resources for a subset version""" - label = "Collect OTIO subset resources" + label = "Collect OTIO Subset Resources" order = pyblish.api.CollectorOrder - 0.57 families = ["clip"] hosts = ["resolve"] diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py index 4693b94e4b..c64d069909 100644 --- a/pype/plugins/resolve/publish/collect_instances.py +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -48,17 +48,12 @@ class CollectInstances(pyblish.api.ContextPlugin): asset = tag_data["asset"] subset = tag_data["subset"] - review = tag_data["review"] # insert family into families family = tag_data["family"] families = [str(f) for f in tag_data["families"]] families.insert(0, str(family)) - # apply only for feview and master track instance - if review: - families += ["review", "ftrack"] - data.update({ "name": "{} {} {}".format(asset, subset, families), "asset": asset, From 600f60188cf80a40a51e8b43c3e2f5e2971ab88b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Dec 2020 11:22:17 +0100 Subject: [PATCH 56/72] fix one track review on main track --- pype/hosts/resolve/lib.py | 5 +++++ .../global/publish/collect_otio_review.py | 20 +++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index 5324f868d6..6589603545 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -695,6 +695,11 @@ def get_otio_clip_instance_data(otio_timeline, track_item_data): continue if pype.lib.is_overlapping_otio_ranges( parent_range, timeline_range, strict=True): + + # add pypedata marker to otio_clip metadata + for marker in otio_clip.markers: + if self.pype_marker_name in marker.name: + otio_clip.metadata.update(marker.metadata) return {"otioClip": otio_clip} return None diff --git a/pype/plugins/global/publish/collect_otio_review.py b/pype/plugins/global/publish/collect_otio_review.py index 77698d7223..0c7eeaea44 100644 --- a/pype/plugins/global/publish/collect_otio_review.py +++ b/pype/plugins/global/publish/collect_otio_review.py @@ -1,12 +1,14 @@ """ Requires: - instance -> review - instance -> masterLayer instance -> otioClip context -> otioTimeline +Optional: + otioClip.metadata -> masterLayer + Provides: instance -> otioReviewClips + instance -> families (adding ["review", "ftrack"]) """ import opentimelineio as otio @@ -23,12 +25,14 @@ class CollectOcioReview(pyblish.api.InstancePlugin): hosts = ["resolve"] def process(self, instance): - otio_review_clips = list() # get basic variables - review_track_name = instance.data.get("reviewTrack") + otio_review_clips = list() otio_timeline = instance.context.data["otioTimeline"] otio_clip = instance.data["otioClip"] + # optionally get `reviewTrack` + review_track_name = otio_clip.metadata.get("reviewTrack") + # generate range in parent otio_tl_range = otio_clip.range_in_parent() @@ -36,14 +40,17 @@ class CollectOcioReview(pyblish.api.InstancePlugin): clip_end_frame = int( otio_tl_range.start_time.value + otio_tl_range.duration.value) + # skip if no review track available if not review_track_name: return + # loop all tracks and match with name in `reviewTrack` for track in otio_timeline.tracks: if review_track_name not in track.name: continue # process correct track + # establish gap otio_gap = None # get track parent range @@ -83,8 +90,9 @@ class CollectOcioReview(pyblish.api.InstancePlugin): if otio_review_clips: instance.data["families"] += ["review", "ftrack"] instance.data["otioReviewClips"] = otio_review_clips - self.log.debug( - "_ otio_review_clips: {}".format(otio_review_clips)) + self.log.info( + "Creating review track: {}".format(otio_review_clips)) + self.log.debug( "_ instance.data: {}".format(pformat(instance.data))) self.log.debug( From a04aab3371c991ff77ac6b408aa46d32e3fb859d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Dec 2020 15:13:32 +0100 Subject: [PATCH 57/72] resolve: wip publishing and creation --- pype/hosts/resolve/plugin.py | 3 +- pype/lib/__init__.py | 6 ++-- pype/lib/editorial.py | 28 ++++++++++++++++++ .../global/publish/extract_otio_review.py | 29 ++----------------- .../resolve/create/create_shot_clip.py | 6 ++-- .../defaults/project_settings/resolve.json | 13 ++++++++- .../schema_project_resolve.json | 12 ++++++++ 7 files changed, 62 insertions(+), 35 deletions(-) diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index 111fd8f4e2..2616491652 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -595,8 +595,9 @@ class PublishClip: hierarchy_formating_data ) + tag_hierarchy_data.update({"masterLayer": True}) if master_layer and self.vertical_sync: - tag_hierarchy_data.update({"masterLayer": True}) + # tag_hierarchy_data.update({"masterLayer": True}) self.vertical_clip_match.update({ (self.clip_in, self.clip_out): tag_hierarchy_data }) diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 35ae0a901a..ae022e3073 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -83,7 +83,8 @@ from .editorial import ( convert_to_padded_path, trim_media_range, range_from_frames, - frames_to_secons + frames_to_secons, + make_sequence_collection ) terminal = Terminal @@ -154,5 +155,6 @@ __all__ = [ "otio_range_to_frame_range", "trim_media_range", "range_from_frames", - "frames_to_secons" + "frames_to_secons", + "make_sequence_collection" ] diff --git a/pype/lib/editorial.py b/pype/lib/editorial.py index 42c345b47d..7f29bf00bb 100644 --- a/pype/lib/editorial.py +++ b/pype/lib/editorial.py @@ -1,4 +1,6 @@ +import os import re +import clique from opentimelineio import opentime from opentimelineio.opentime import ( to_frames, RationalTime, TimeRange) @@ -130,3 +132,29 @@ def frames_to_secons(frames, framerate): """ rt = opentime.from_frames(frames, framerate) return opentime.to_seconds(rt) + + +def make_sequence_collection(path, otio_range, metadata): + """ + Make collection from path otio range and otio metadata. + + Args: + path (str): path to image sequence with `%d` + otio_range (otio.opentime.TimeRange): range to be used + metadata (dict): data where padding value can be found + + Returns: + list: dir_path (str): path to sequence, collection object + + """ + if "%" not in path: + return None + file_name = os.path.basename(path) + dir_path = os.path.dirname(path) + head = file_name.split("%")[0] + tail = os.path.splitext(file_name)[-1] + first, last = otio_range_to_frame_range(otio_range) + collection = clique.Collection( + head=head, tail=tail, padding=metadata["padding"]) + collection.indexes.update([i for i in range(first, (last + 1))]) + return dir_path, collection diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py index e960358749..c45e2a5d9f 100644 --- a/pype/plugins/global/publish/extract_otio_review.py +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -158,8 +158,9 @@ class ExtractOTIOReview(pype.api.Extractor): # in case it is file sequence but not new OTIO schema # `ImageSequenceReference` path = media_ref.target_url - dir_path, collection = self._make_sequence_collection( + collection_data = pype.lib.make_sequence_collection( path, available_range, metadata) + dir_path, collection = collection_data # render segment self._render_seqment( @@ -422,29 +423,3 @@ class ExtractOTIOReview(pype.api.Extractor): out_frame_start = self.used_frames[-1] return output_path, out_frame_start - - @staticmethod - def _make_sequence_collection(path, otio_range, metadata): - """ - Make collection from path otio range and otio metadata. - - Args: - path (str): path to image sequence with `%d` - otio_range (otio.opentime.TimeRange): range to be used - metadata (dict): data where padding value can be found - - Returns: - list: dir_path (str): path to sequence, collection object - - """ - if "%" not in path: - return None - file_name = os.path.basename(path) - dir_path = os.path.dirname(path) - head = file_name.split("%")[0] - tail = os.path.splitext(file_name)[-1] - first, last = pype.lib.otio_range_to_frame_range(otio_range) - collection = clique.Collection( - head=head, tail=tail, padding=metadata["padding"]) - collection.indexes.update([i for i in range(first, (last + 1))]) - return dir_path, collection diff --git a/pype/plugins/resolve/create/create_shot_clip.py b/pype/plugins/resolve/create/create_shot_clip.py index 35cb30636e..19e613ee7a 100644 --- a/pype/plugins/resolve/create/create_shot_clip.py +++ b/pype/plugins/resolve/create/create_shot_clip.py @@ -1,4 +1,4 @@ -from pprint import pformat +# from pprint import pformat from pype.hosts import resolve from pype.hosts.resolve import lib @@ -200,20 +200,18 @@ class CreateShotClip(resolve.Creator): presets = None def process(self): - print("_____ presets: {}".format(pformat(self.presets))) # 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): + if self.presets.get(_k) is not None: self.gui_inputs[k][ "value"][_k]["value"] = self.presets[_k] if self.presets.get(k): self.gui_inputs[k]["value"] = self.presets[k] - print(pformat(self.gui_inputs)) # open widget for plugins inputs widget = self.widget(self.gui_name, self.gui_info, self.gui_inputs) widget.exec_() diff --git a/pype/settings/defaults/project_settings/resolve.json b/pype/settings/defaults/project_settings/resolve.json index cb7064ee76..b210759417 100644 --- a/pype/settings/defaults/project_settings/resolve.json +++ b/pype/settings/defaults/project_settings/resolve.json @@ -1,9 +1,20 @@ { "create": { "CreateShotClip": { + "hierarchy": "{folder}/{sequence}", + "clipRename": true, "clipName": "{track}{sequence}{shot}", + "countFrom": 10, + "countSteps": 10, "folder": "takes", - "steps": 20 + "episode": "ep01", + "sequence": "sq01", + "track": "{_track_}", + "shot": "sh###", + "vSyncOn": false, + "workfileFrameStart": 1001, + "handleStart": 10, + "handleEnd": 10 } } } \ No newline at end of file diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_resolve.json b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_resolve.json index 18f6817407..fb9b9b7a0a 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_resolve.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_resolve.json @@ -82,6 +82,18 @@ } ] }, + { + "type": "collapsible-wrap", + "label": "Vertical Synchronization Of Attributes", + "collapsable": false, + "children": [ + { + "type": "boolean", + "key": "vSyncOn", + "label": "Enable Vertical Sync" + } + ] + }, { "type": "collapsible-wrap", "label": "Shot Attributes", From ee7b506d31d7f1ea9d68f9125ecaaa0678086fbe Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Dec 2020 15:13:48 +0100 Subject: [PATCH 58/72] resolve: collecting shot instances --- .../resolve/publish/collect_instances.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py index c64d069909..2ef910aa8e 100644 --- a/pype/plugins/resolve/publish/collect_instances.py +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -73,6 +73,9 @@ class CollectInstances(pyblish.api.ContextPlugin): # create instance instance = context.create_instance(**data) + # create shot instance for shot attributes create/update + self.create_shot_instance(context, track_item, **data) + self.log.info("Creating instance: {}".format(instance)) self.log.debug( "_ instance.data: {}".format(pformat(instance.data))) @@ -94,3 +97,30 @@ class CollectInstances(pyblish.api.ContextPlugin): "resolutionWidth": otio_tl_metadata["width"], "resolutionHeight": otio_tl_metadata["height"] }) + + def create_shot_instance(self, context, track_item, **data): + master_layer = data.get("masterLayer") + hierarchy_data = data.get("hierarchyData") + + if not master_layer: + return + + if not hierarchy_data: + return + + asset = data["asset"] + subset = "shotMain" + + # insert family into families + family = "shot" + + data.update({ + "name": "{} {} {}".format(asset, subset, family), + "subset": subset, + "asset": asset, + "family": family, + "families": [], + "publish": resolve.get_publish_attribute(track_item) + }) + + context.create_instance(**data) From 75fa36c8e9f38df65bb749dbfb2ac3f3cc2d5739 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Dec 2020 15:14:09 +0100 Subject: [PATCH 59/72] resolve: wip subset resources collection --- .../publish/collect_otio_subset_resources.py | 115 ++++++++---------- 1 file changed, 52 insertions(+), 63 deletions(-) diff --git a/pype/plugins/global/publish/collect_otio_subset_resources.py b/pype/plugins/global/publish/collect_otio_subset_resources.py index f6bfe11941..019b9f902f 100644 --- a/pype/plugins/global/publish/collect_otio_subset_resources.py +++ b/pype/plugins/global/publish/collect_otio_subset_resources.py @@ -1,9 +1,10 @@ +# TODO: this head doc string """ Requires: + instance -> otio_clip + +Optional: instance -> review - instance -> masterLayer - instance -> otioClip - context -> otioTimeline Provides: instance -> otioReviewClips @@ -30,71 +31,59 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): # generate range in parent otio_src_range = otio_clip.source_range otio_avalable_range = otio_clip.available_range() - otio_visible_range = otio_clip.visible_range() otio_src_range_handles = pype.lib.otio_range_with_handles( otio_src_range, instance) trimmed_media_range = pype.lib.trim_media_range( otio_avalable_range, otio_src_range_handles) - self.log.debug( - "_ otio_avalable_range: {}".format(otio_avalable_range)) - self.log.debug( - "_ otio_visible_range: {}".format(otio_visible_range)) - self.log.debug( - "_ otio_src_range: {}".format(otio_src_range)) - self.log.debug( - "_ otio_src_range_handles: {}".format(otio_src_range_handles)) self.log.debug( "_ trimmed_media_range: {}".format(trimmed_media_range)) - # - # media_ref = otio_clip.media_reference - # metadata = media_ref.metadata - # - # if isinstance(media_ref, otio.schema.ImageSequenceReference): - # dirname = media_ref.target_url_base - # head = media_ref.name_prefix - # tail = media_ref.name_suffix - # first, last = pype.lib.otio_range_to_frame_range( - # available_range) - # collection = clique.Collection( - # head=head, - # tail=tail, - # padding=media_ref.frame_zero_padding - # ) - # collection.indexes.update( - # [i for i in range(first, (last + 1))]) - # # render segment - # self._render_seqment( - # sequence=[dirname, collection]) - # # generate used frames - # self._generate_used_frames( - # len(collection.indexes)) - # elif metadata.get("padding"): - # # in case it is file sequence but not new OTIO schema - # # `ImageSequenceReference` - # path = media_ref.target_url - # dir_path, collection = self._make_sequence_collection( - # path, available_range, metadata) - # - # # render segment - # self._render_seqment( - # sequence=[dir_path, collection]) - # # generate used frames - # self._generate_used_frames( - # len(collection.indexes)) - # else: - # path = media_ref.target_url - # # render video file to sequence - # self._render_seqment( - # video=[path, available_range]) - # # generate used frames - # self._generate_used_frames( - # available_range.duration.value) - # - # instance.data["otioReviewClips"] = otio_review_clips - # self.log.debug( - # "_ otio_review_clips: {}".format(otio_review_clips)) - # - # self.log.debug( - # "_ instance.data: {}".format(pformat(instance.data))) + media_ref = otio_clip.media_reference + metadata = media_ref.metadata + + # check in two way if it is sequence + if hasattr(otio.schema, "ImageSequenceReference"): + # for OpenTimelineIO 0.13 and newer + if isinstance(media_ref, + otio.schema.ImageSequenceReference): + is_sequence = True + else: + # for OpenTimelineIO 0.12 and older + if metadata.get("padding"): + is_sequence = True + + first, last = pype.lib.otio_range_to_frame_range( + trimmed_media_range) + + self.log.info( + "first-last: {}-{}".format(first, last)) + + if is_sequence: + # file sequence way + if hasattr(media_ref, "target_url_base"): + dirname = media_ref.target_url_base + head = media_ref.name_prefix + tail = media_ref.name_suffix + collection = clique.Collection( + head=head, + tail=tail, + padding=media_ref.frame_zero_padding + ) + collection.indexes.update( + [i for i in range(first, (last + 1))]) + # TODO: add representation + self.log.debug((dirname, collection)) + else: + # in case it is file sequence but not new OTIO schema + # `ImageSequenceReference` + path = media_ref.target_url + dir_path, collection = pype.lib.make_sequence_collection( + path, trimmed_media_range, metadata) + + # TODO: add representation + self.log.debug((dir_path, collection)) + else: + path = media_ref.target_url + # TODO: add representation + self.log.debug(path) From 3683005518a86ff1aa24e5d58094cc470230152e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Dec 2020 16:21:33 +0100 Subject: [PATCH 60/72] hiero otio adding pixel aspect attribute to metadata --- pype/hosts/resolve/otio/davinci_export.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pype/hosts/resolve/otio/davinci_export.py b/pype/hosts/resolve/otio/davinci_export.py index 0909bcb26a..7912b1abd8 100644 --- a/pype/hosts/resolve/otio/davinci_export.py +++ b/pype/hosts/resolve/otio/davinci_export.py @@ -210,6 +210,15 @@ def _get_metadata_media_pool_item(media_pool_item): "width": int(width), "height": int(height) }) + if "PAR" in name and "" != value: + try: + data.update({"pixelAspect": float(value)}) + except ValueError: + if "Square" in value: + data.update({"pixelAspect": float(1)}) + else: + data.update({"pixelAspect": float(1)}) + return data From 862cfd328a95b6a4b58a217b38de633d4c5e357b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Dec 2020 16:22:02 +0100 Subject: [PATCH 61/72] ftrack: hierarchy to accept `resolve` host --- pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py index a1377cc771..c4f7726071 100644 --- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py +++ b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py @@ -36,7 +36,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder - 0.04 label = 'Integrate Hierarchy To Ftrack' families = ["shot"] - hosts = ["hiero"] + hosts = ["hiero", "resolve"] optional = False def process(self, context): From 46b80651933c7a58482297000359e0c6b34603b6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Dec 2020 16:22:30 +0100 Subject: [PATCH 62/72] global: otio frame range `shot` family accepting --- .../global/publish/collect_otio_frame_ranges.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pype/plugins/global/publish/collect_otio_frame_ranges.py b/pype/plugins/global/publish/collect_otio_frame_ranges.py index 4224abe5a4..248158a1f4 100644 --- a/pype/plugins/global/publish/collect_otio_frame_ranges.py +++ b/pype/plugins/global/publish/collect_otio_frame_ranges.py @@ -19,7 +19,7 @@ class CollectOcioFrameRanges(pyblish.api.InstancePlugin): label = "Collect OTIO Frame Ranges" order = pyblish.api.CollectorOrder - 0.58 - families = ["clip"] + families = ["clip", "shot"] hosts = ["resolve"] def process(self, instance): @@ -54,10 +54,10 @@ class CollectOcioFrameRanges(pyblish.api.InstancePlugin): data = { "frameStart": frame_start, "frameEnd": frame_end, - "clipStart": tl_start, - "clipEnd": tl_end, - "clipStartH": tl_start_h, - "clipEndH": tl_end_h, + "clipIn": tl_start, + "clipOut": tl_end, + "clipInH": tl_start_h, + "clipOutH": tl_end_h, "sourceStart": src_starting_from + src_start, "sourceEnd": src_starting_from + src_end, "sourceStartH": src_starting_from + src_start_h, From 88b276aa0983fe1c3f14613066c8f52915245b5a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Dec 2020 16:23:44 +0100 Subject: [PATCH 63/72] resolve and global: adding hierarchy collect for shots --- .../global/publish/collect_hierarchy.py | 112 ++++++++++++++++++ .../resolve/publish/collect_instances.py | 6 +- 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 pype/plugins/global/publish/collect_hierarchy.py diff --git a/pype/plugins/global/publish/collect_hierarchy.py b/pype/plugins/global/publish/collect_hierarchy.py new file mode 100644 index 0000000000..176420e937 --- /dev/null +++ b/pype/plugins/global/publish/collect_hierarchy.py @@ -0,0 +1,112 @@ +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 - 0.57 + families = ["shot"] + + 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 = {} + family = instance.data.get("family") + + # filter out all unepropriate instances + if not instance.data["publish"]: + continue + + # exclude other families then self.families with intersection + if not set(self.families).intersection([family]): + continue + + # exclude if not masterLayer True + if not instance.data.get("masterLayer"): + continue + + # 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 diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py index 2ef910aa8e..56f1009805 100644 --- a/pype/plugins/resolve/publish/collect_instances.py +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -89,13 +89,15 @@ class CollectInstances(pyblish.api.ContextPlugin): "otioClip"].media_reference.metadata data.update({ "resolutionWidth": otio_clip_metadata["width"], - "resolutionHeight": otio_clip_metadata["height"] + "resolutionHeight": otio_clip_metadata["height"], + "pixelAspect": otio_clip_metadata["pixelAspect"] }) else: otio_tl_metadata = context.data["otioTimeline"].metadata data.update({ "resolutionWidth": otio_tl_metadata["width"], - "resolutionHeight": otio_tl_metadata["height"] + "resolutionHeight": otio_tl_metadata["height"], + "pixelAspect": otio_tl_metadata["pixelAspect"] }) def create_shot_instance(self, context, track_item, **data): From 127eb01138764e9e2d73e7ddda3044adcb0e1055 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Dec 2020 17:57:35 +0100 Subject: [PATCH 64/72] resolve: swap family with families --- pype/hosts/resolve/plugin.py | 3 +- .../publish/collect_otio_frame_ranges.py | 2 +- .../publish/collect_otio_subset_resources.py | 109 +++++++++++++++--- 3 files changed, 95 insertions(+), 19 deletions(-) diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index 2616491652..fa4559efac 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -636,7 +636,8 @@ class PublishClip: "parents": self.parents, "hierarchyData": hierarchy_formating_data, "subset": self.subset, - "families": [self.subset_family] + "family": self.subset_family, + "families": ["clip"] } def _convert_to_entity(self, key): diff --git a/pype/plugins/global/publish/collect_otio_frame_ranges.py b/pype/plugins/global/publish/collect_otio_frame_ranges.py index 248158a1f4..d82c00412d 100644 --- a/pype/plugins/global/publish/collect_otio_frame_ranges.py +++ b/pype/plugins/global/publish/collect_otio_frame_ranges.py @@ -19,7 +19,7 @@ class CollectOcioFrameRanges(pyblish.api.InstancePlugin): label = "Collect OTIO Frame Ranges" order = pyblish.api.CollectorOrder - 0.58 - families = ["clip", "shot"] + families = ["shot"] hosts = ["resolve"] def process(self, instance): diff --git a/pype/plugins/global/publish/collect_otio_subset_resources.py b/pype/plugins/global/publish/collect_otio_subset_resources.py index 019b9f902f..e207885bb8 100644 --- a/pype/plugins/global/publish/collect_otio_subset_resources.py +++ b/pype/plugins/global/publish/collect_otio_subset_resources.py @@ -3,13 +3,10 @@ Requires: instance -> otio_clip -Optional: - instance -> review - Provides: instance -> otioReviewClips """ - +import os import clique import opentimelineio as otio import pyblish.api @@ -25,6 +22,9 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): hosts = ["resolve"] def process(self, instance): + if not instance.data.get("representations"): + instance.data["representations"] = list() + # get basic variables otio_clip = instance.data["otioClip"] @@ -36,6 +36,26 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): trimmed_media_range = pype.lib.trim_media_range( otio_avalable_range, otio_src_range_handles) + a_frame_start, a_frame_end = pype.lib.otio_range_to_frame_range( + otio_avalable_range) + + frame_start, frame_end = pype.lib.otio_range_to_frame_range( + trimmed_media_range) + + # fix frame_start and frame_end frame to be in range of + if frame_start < a_frame_start: + frame_start = a_frame_start + + if frame_end > a_frame_end: + frame_end = a_frame_end + + instance.data.update({ + "frameStart": frame_start, + "frameEnd": frame_end + }) + + self.log.debug( + "_ otio_avalable_range: {}".format(otio_avalable_range)) self.log.debug( "_ trimmed_media_range: {}".format(trimmed_media_range)) @@ -53,16 +73,13 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): if metadata.get("padding"): is_sequence = True - first, last = pype.lib.otio_range_to_frame_range( - trimmed_media_range) - self.log.info( - "first-last: {}-{}".format(first, last)) + "frame_start-frame_end: {}-{}".format(frame_start, frame_end)) if is_sequence: # file sequence way if hasattr(media_ref, "target_url_base"): - dirname = media_ref.target_url_base + self.staging_dir = media_ref.target_url_base head = media_ref.name_prefix tail = media_ref.name_suffix collection = clique.Collection( @@ -71,19 +88,77 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): padding=media_ref.frame_zero_padding ) collection.indexes.update( - [i for i in range(first, (last + 1))]) - # TODO: add representation - self.log.debug((dirname, collection)) + [i for i in range(frame_start, (frame_end + 1))]) + + self.log.debug(collection) + repre = self._create_representation( + frame_start, frame_end, collection=collection) + self.log.debug(repre) else: # in case it is file sequence but not new OTIO schema # `ImageSequenceReference` path = media_ref.target_url - dir_path, collection = pype.lib.make_sequence_collection( + collection_data = pype.lib.make_sequence_collection( path, trimmed_media_range, metadata) + self.staging_dir, collection = collection_data - # TODO: add representation - self.log.debug((dir_path, collection)) + self.log.debug(collection) + repre = self._create_representation( + frame_start, frame_end, collection=collection) + self.log.debug(repre) else: - path = media_ref.target_url - # TODO: add representation + dirname, filename = os.path.split(media_ref.target_url) + self.staging_dir = dirname + self.log.debug(path) + repre = self._create_representation( + frame_start, frame_end, file=filename) + self.log.debug(repre) + + if repre: + instance.data + instance.data["representations"].append(repre) + + def _create_representation(self, start, end, **kwargs): + """ + Creating representation data. + + Args: + start (int): start frame + end (int): end frame + kwargs (dict): optional data + + Returns: + dict: representation data + """ + + # create default representation data + representation_data = { + "frameStart": start, + "frameEnd": end, + "stagingDir": self.staging_dir + } + + if kwargs.get("collection"): + collection = kwargs.get("collection") + files = [f for f in collection] + ext = collection.format("{tail}") + representation_data.update({ + "name": ext[1:], + "ext": ext[1:], + "files": files, + "frameStart": start, + "frameEnd": end, + }) + return representation_data + if kwargs.get("file"): + file = kwargs.get("file") + ext = os.path.splitext(file)[-1] + representation_data.update({ + "name": ext[1:], + "ext": ext[1:], + "files": file, + "frameStart": start, + "frameEnd": end, + }) + return representation_data From b3798cf9508657cf6a6041c942613dec2f57d59c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Dec 2020 14:12:47 +0100 Subject: [PATCH 65/72] reverse pype.lib.terminal --- pype/lib/terminal.py | 69 ++++++++++++++------------------------------ 1 file changed, 21 insertions(+), 48 deletions(-) diff --git a/pype/lib/terminal.py b/pype/lib/terminal.py index d54d52e9be..a47d58ec3b 100644 --- a/pype/lib/terminal.py +++ b/pype/lib/terminal.py @@ -11,14 +11,8 @@ # ..---===[[ PyP3 Setup ]]===---... # import re -import os -import sys -try: - import blessed - term = blessed.Terminal() - -except Exception: - term = None +import time +import threading class Terminal: @@ -30,49 +24,28 @@ class Terminal: Using **PYPE_LOG_NO_COLORS** environment variable. """ - # shortcuts for colorama codes + # Is Terminal initialized + _initialized = False + # Thread lock for initialization to avoid race conditions + _init_lock = threading.Lock() + # Use colorized output + use_colors = True + # Output message replacements mapping - set on initialization _sdict = {} - if term: - _SB = term.bold - _RST = "" - _LR = term.tomato2 - _LG = term.aquamarine3 - _LB = term.turquoise2 - _LM = term.slateblue2 - _LY = term.gold - _R = term.red - _G = term.green - _B = term.blue - _C = term.cyan - _Y = term.yellow - _W = term.white - # dictionary replacing string sequences with colorized one - _sdict = { + @staticmethod + def _initialize(): + """Initialize Terminal class as object. - r">>> ": _SB + _LG + r">>> " + _RST, - r"!!!(?!\sCRI|\sERR)": _SB + _R + r"!!! " + _RST, - r"\-\-\- ": _SB + _C + r"--- " + _RST, - r"\*\*\*(?!\sWRN)": _SB + _LY + r"***" + _RST, - r"\*\*\* WRN": _SB + _LY + r"*** WRN" + _RST, - r" \- ": _SB + _LY + r" - " + _RST, - r"\[ ": _SB + _LG + r"[ " + _RST, - r"\]": _SB + _LG + r"]" + _RST, - r"{": _LG + r"{", - r"}": r"}" + _RST, - r"\(": _LY + r"(", - r"\)": r")" + _RST, - r"^\.\.\. ": _SB + _LR + r"... " + _RST, - r"!!! ERR: ": - _SB + _LR + r"!!! ERR: " + _RST, - r"!!! CRI: ": - _SB + _R + r"!!! CRI: " + _RST, - r"(?i)failed": _SB + _LR + "FAILED" + _RST, - r"(?i)error": _SB + _LR + "ERROR" + _RST - } + First check if colorized output is disabled by environment variable + `PYPE_LOG_NO_COLORS` value. By default is colorized output turned on. - def __init__(self): - pass + Then tries to import python module that do the colors magic and create + it's terminal object. Colorized output is not used if import of python + module or terminal object creation fails. + + Set `_initialized` attribute to `True` when is done. + """ from pype.lib import env_value_to_bool use_colors = env_value_to_bool( @@ -218,7 +191,7 @@ class Terminal: time.sleep(0.1) # if we dont want colors, just print raw message - if not T._sdict or os.environ.get('PYPE_LOG_NO_COLORS'): + if not T.use_colors: return message message = re.sub(r'\[(.*)\]', '[ ' + T._SB + T._W + From eb34003418bcd84c0a7c3da57ad28f45481078da Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 29 Dec 2020 11:36:56 +0100 Subject: [PATCH 66/72] fix folder from `takes` to `shots` --- pype/settings/defaults/project_settings/resolve.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/settings/defaults/project_settings/resolve.json b/pype/settings/defaults/project_settings/resolve.json index b210759417..b6fbdecc95 100644 --- a/pype/settings/defaults/project_settings/resolve.json +++ b/pype/settings/defaults/project_settings/resolve.json @@ -6,7 +6,7 @@ "clipName": "{track}{sequence}{shot}", "countFrom": 10, "countSteps": 10, - "folder": "takes", + "folder": "shots", "episode": "ep01", "sequence": "sq01", "track": "{_track_}", From 197589611446e11b4e600a347bc8b7a553a0d4d5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 29 Dec 2020 11:37:27 +0100 Subject: [PATCH 67/72] fix families and hosts for plugins --- pype/plugins/global/publish/collect_hierarchy.py | 1 + pype/plugins/global/publish/collect_otio_frame_ranges.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/collect_hierarchy.py b/pype/plugins/global/publish/collect_hierarchy.py index 176420e937..5c5dbf018c 100644 --- a/pype/plugins/global/publish/collect_hierarchy.py +++ b/pype/plugins/global/publish/collect_hierarchy.py @@ -15,6 +15,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): label = "Collect Hierarchy" order = pyblish.api.CollectorOrder - 0.57 families = ["shot"] + hosts = ["resolve"] def process(self, context): temp_context = {} diff --git a/pype/plugins/global/publish/collect_otio_frame_ranges.py b/pype/plugins/global/publish/collect_otio_frame_ranges.py index d82c00412d..849a2c2475 100644 --- a/pype/plugins/global/publish/collect_otio_frame_ranges.py +++ b/pype/plugins/global/publish/collect_otio_frame_ranges.py @@ -19,7 +19,7 @@ class CollectOcioFrameRanges(pyblish.api.InstancePlugin): label = "Collect OTIO Frame Ranges" order = pyblish.api.CollectorOrder - 0.58 - families = ["shot"] + families = ["shot", "clip"] hosts = ["resolve"] def process(self, instance): From aba3b63cad802c0af3f3fab8dd52aa22315960f2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 29 Dec 2020 13:46:26 +0100 Subject: [PATCH 68/72] global: editorial subset resources collect --- .../publish/collect_otio_subset_resources.py | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/pype/plugins/global/publish/collect_otio_subset_resources.py b/pype/plugins/global/publish/collect_otio_subset_resources.py index e207885bb8..d1fd47debd 100644 --- a/pype/plugins/global/publish/collect_otio_subset_resources.py +++ b/pype/plugins/global/publish/collect_otio_subset_resources.py @@ -24,40 +24,58 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): def process(self, instance): if not instance.data.get("representations"): instance.data["representations"] = list() + version_data = dict() # get basic variables otio_clip = instance.data["otioClip"] + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] # generate range in parent otio_src_range = otio_clip.source_range otio_avalable_range = otio_clip.available_range() + trimmed_media_range = pype.lib.trim_media_range( + otio_avalable_range, otio_src_range) + + # calculate wth handles otio_src_range_handles = pype.lib.otio_range_with_handles( otio_src_range, instance) - trimmed_media_range = pype.lib.trim_media_range( + trimmed_media_range_h = pype.lib.trim_media_range( otio_avalable_range, otio_src_range_handles) + # frame start and end from media + s_frame_start, s_frame_end = pype.lib.otio_range_to_frame_range( + trimmed_media_range) a_frame_start, a_frame_end = pype.lib.otio_range_to_frame_range( otio_avalable_range) + a_frame_start_h, a_frame_end_h = pype.lib.otio_range_to_frame_range( + trimmed_media_range_h) - frame_start, frame_end = pype.lib.otio_range_to_frame_range( - trimmed_media_range) + # fix frame_start and frame_end frame to be in range of media + if a_frame_start_h < a_frame_start: + a_frame_start_h = a_frame_start - # fix frame_start and frame_end frame to be in range of - if frame_start < a_frame_start: - frame_start = a_frame_start + if a_frame_end_h > a_frame_end: + a_frame_end_h = a_frame_end - if frame_end > a_frame_end: - frame_end = a_frame_end + # count the difference for frame_start and frame_end + diff_start = s_frame_start - a_frame_start_h + diff_end = a_frame_end_h - s_frame_end - instance.data.update({ + # add to version data start and end range data + # for loader plugins to be correctly displayed and loaded + version_data.update({ "frameStart": frame_start, - "frameEnd": frame_end + "frameEnd": frame_end, + "handleStart": diff_start, + "handleEnd": diff_end, + "fps": otio_avalable_range.start_time.rate }) - self.log.debug( - "_ otio_avalable_range: {}".format(otio_avalable_range)) - self.log.debug( - "_ trimmed_media_range: {}".format(trimmed_media_range)) + # change frame_start and frame_end values + # for representation to be correctly renumbered in integrate_new + frame_start -= diff_start + frame_end += diff_end media_ref = otio_clip.media_reference metadata = media_ref.metadata @@ -88,12 +106,11 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): padding=media_ref.frame_zero_padding ) collection.indexes.update( - [i for i in range(frame_start, (frame_end + 1))]) + [i for i in range(a_frame_start_h, (a_frame_end_h + 1))]) self.log.debug(collection) repre = self._create_representation( frame_start, frame_end, collection=collection) - self.log.debug(repre) else: # in case it is file sequence but not new OTIO schema # `ImageSequenceReference` @@ -105,7 +122,6 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): self.log.debug(collection) repre = self._create_representation( frame_start, frame_end, collection=collection) - self.log.debug(repre) else: dirname, filename = os.path.split(media_ref.target_url) self.staging_dir = dirname @@ -113,11 +129,13 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): self.log.debug(path) repre = self._create_representation( frame_start, frame_end, file=filename) - self.log.debug(repre) if repre: - instance.data + instance.data["versionData"] = version_data + self.log.debug(">>>>>>>> version data {}".format(version_data)) + # add representation to instance data instance.data["representations"].append(repre) + self.log.debug(">>>>>>>> {}".format(repre)) def _create_representation(self, start, end, **kwargs): """ From 47ce7b0703f6d2a3126540e1216db6edf167bbe9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 29 Dec 2020 16:18:56 +0100 Subject: [PATCH 69/72] global: using middle frame for thumbnail enabling for resolve and plate and take family --- pype/plugins/global/publish/extract_jpeg.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index af90d4366d..d5e9a896dc 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -12,9 +12,12 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): """Create jpg thumbnail from sequence using ffmpeg""" label = "Extract Jpeg EXR" - hosts = ["shell", "fusion"] order = pyblish.api.ExtractorOrder - families = ["imagesequence", "render", "render2d", "source"] + families = [ + "imagesequence", "render", "render2d", + "source", "plate", "take" + ] + hosts = ["shell", "fusion", "resolve"] enabled = False # presetable attribute @@ -50,7 +53,8 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): if not isinstance(repre['files'], (list, tuple)): input_file = repre['files'] else: - input_file = repre['files'][0] + file_index = int(float(len(repre['files'])) * 0.5) + input_file = repre['files'][file_index] stagingdir = os.path.normpath(repre.get("stagingDir")) From 3f7178ec5e7b2daa6efb1713c6cdc6cd3b142288 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 29 Dec 2020 16:19:28 +0100 Subject: [PATCH 70/72] global: enabling resolve host for review --- pype/plugins/global/publish/extract_review.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 37fe83bf10..9b8c097dca 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -33,7 +33,8 @@ class ExtractReview(pyblish.api.InstancePlugin): "harmony", "standalonepublisher", "fusion", - "tvpaint" + "tvpaint", + "resolve" ] # Supported extensions From 2b832fd46bb5d25a4d2850ed8daf01afdda26764 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 29 Dec 2020 16:21:15 +0100 Subject: [PATCH 71/72] resolve, global: publish plates with review --- pype/plugins/global/publish/extract_otio_file.py | 2 +- pype/plugins/global/publish/extract_otio_review.py | 11 ++++++----- pype/plugins/resolve/publish/collect_instances.py | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pype/plugins/global/publish/extract_otio_file.py b/pype/plugins/global/publish/extract_otio_file.py index c93cf34c79..84932f07a8 100644 --- a/pype/plugins/global/publish/extract_otio_file.py +++ b/pype/plugins/global/publish/extract_otio_file.py @@ -10,7 +10,7 @@ class ExtractOTIOFile(pype.api.Extractor): """ label = "Extract OTIO file" - order = pyblish.api.ExtractorOrder + order = pyblish.api.ExtractorOrder - 0.45 families = ["workfile"] hosts = ["resolve"] diff --git a/pype/plugins/global/publish/extract_otio_review.py b/pype/plugins/global/publish/extract_otio_review.py index c45e2a5d9f..396dcb08a8 100644 --- a/pype/plugins/global/publish/extract_otio_review.py +++ b/pype/plugins/global/publish/extract_otio_review.py @@ -38,7 +38,7 @@ class ExtractOTIOReview(pype.api.Extractor): """ - order = api.ExtractorOrder + order = api.ExtractorOrder - 0.45 label = "Extract OTIO review" hosts = ["resolve"] families = ["review"] @@ -54,6 +54,7 @@ class ExtractOTIOReview(pype.api.Extractor): # TODO: add oudio ouput to the mp4 if audio in review is on. # get otio clip and other time info from instance clip + # TODO: what if handles are different in `versionData`? handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] otio_review_clips = instance.data["otioReviewClips"] @@ -62,7 +63,7 @@ class ExtractOTIOReview(pype.api.Extractor): self.representation_files = list() self.used_frames = list() self.workfile_start = int(instance.data.get( - "workfileFrameStart", 1001)) + "workfileFrameStart", 1001)) - handle_start self.padding = len(str(self.workfile_start)) self.used_frames.append(self.workfile_start) self.to_width = instance.data.get( @@ -249,12 +250,12 @@ class ExtractOTIOReview(pype.api.Extractor): """ avl_start = int(avl_range.start_time.value) src_start = int(avl_start + start) - avl_durtation = int(avl_range.duration.value - start) + avl_durtation = int(avl_range.duration.value) # if media start is les then clip requires if src_start < avl_start: # calculate gap - gap_duration = src_start - avl_start + gap_duration = avl_start - src_start # create gap data to disk self._render_seqment(gap=gap_duration) @@ -263,7 +264,7 @@ class ExtractOTIOReview(pype.api.Extractor): # fix start and end to correct values start = 0 - duration -= len(gap_duration) + duration -= gap_duration # if media duration is shorter then clip requirement if duration > avl_durtation: diff --git a/pype/plugins/resolve/publish/collect_instances.py b/pype/plugins/resolve/publish/collect_instances.py index 56f1009805..76332b03c2 100644 --- a/pype/plugins/resolve/publish/collect_instances.py +++ b/pype/plugins/resolve/publish/collect_instances.py @@ -59,7 +59,8 @@ class CollectInstances(pyblish.api.ContextPlugin): "asset": asset, "item": track_item, "families": families, - "publish": resolve.get_publish_attribute(track_item) + "publish": resolve.get_publish_attribute(track_item), + "fps": context.data["fps"] }) # otio clip data From a8d3eb003c23a56cb1e0aa8971172a46a90ae052 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Jan 2021 15:24:30 +0100 Subject: [PATCH 72/72] otio publish wip --- pype/hooks/resolve/pre_resolve_setup.py | 1 + pype/plugins/global/publish/extract_burnin.py | 3 ++- requirements.txt | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pype/hooks/resolve/pre_resolve_setup.py b/pype/hooks/resolve/pre_resolve_setup.py index 3799e227ff..19a0817a0d 100644 --- a/pype/hooks/resolve/pre_resolve_setup.py +++ b/pype/hooks/resolve/pre_resolve_setup.py @@ -14,6 +14,7 @@ class ResolvePrelaunch(PreLaunchHook): app_groups = ["resolve"] def execute(self): + # TODO: add OTIO installation from `pype/requirements.py` # making sure pyton 3.6 is installed at provided path py36_dir = os.path.normpath( self.launch_context.env.get("PYTHON36_RESOLVE", "")) diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index d29af63483..28dd6730cb 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -32,7 +32,8 @@ class ExtractBurnin(pype.api.Extractor): "standalonepublisher", "harmony", "fusion", - "aftereffects" + "aftereffects", + # "resolve" ] optional = True diff --git a/requirements.txt b/requirements.txt index c1f72f9582..a690498bab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ google-api-python-client jsonschema keyring log4mongo -git+https://github.com/PixarAnimationStudios/OpenTimelineIO.git@5aa24fbe89d615448876948fe4b4900455c9a3e8 +git+https://github.com/pypeclub/OpenTimelineIO.git@develop pathlib2 Pillow pynput