diff --git a/.github/workflows/milestone_assign.yml b/.github/workflows/milestone_assign.yml
index c5a231e59e..4b52dfc30d 100644
--- a/.github/workflows/milestone_assign.yml
+++ b/.github/workflows/milestone_assign.yml
@@ -2,7 +2,7 @@ name: Milestone - assign to PRs
on:
pull_request_target:
- types: [opened, reopened, edited, synchronize]
+ types: [closed]
jobs:
run_if_release:
diff --git a/openpype/hosts/flame/api/menu.py b/openpype/hosts/flame/api/menu.py
index f72a352bba..319ed7afb6 100644
--- a/openpype/hosts/flame/api/menu.py
+++ b/openpype/hosts/flame/api/menu.py
@@ -225,7 +225,8 @@ class FlameMenuUniversal(_FlameMenuApp):
menu['actions'].append({
"name": "Load...",
- "execute": lambda x: self.tools_helper.show_loader()
+ "execute": lambda x: callback_selection(
+ x, self.tools_helper.show_loader)
})
menu['actions'].append({
"name": "Manage...",
diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py
index 3b049b861b..17ad8075e4 100644
--- a/openpype/hosts/flame/plugins/load/load_clip_batch.py
+++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py
@@ -1,3 +1,4 @@
+from copy import deepcopy
import os
import flame
from pprint import pformat
@@ -22,7 +23,7 @@ class LoadClipBatch(opfapi.ClipLoader):
# settings
reel_name = "OP_LoadedReel"
- clip_name_template = "{asset}_{subset}<_{output}>"
+ clip_name_template = "{batch}_{asset}_{subset}<_{output}>"
def load(self, context, name, namespace, options):
@@ -40,8 +41,11 @@ class LoadClipBatch(opfapi.ClipLoader):
if not context["representation"]["context"].get("output"):
self.clip_name_template.replace("output", "representation")
+ formating_data = deepcopy(context["representation"]["context"])
+ formating_data["batch"] = self.batch.name.get_value()
+
clip_name = StringTemplate(self.clip_name_template).format(
- context["representation"]["context"])
+ formating_data)
# TODO: settings in imageio
# convert colorspace with ocio to flame mapping
@@ -56,6 +60,7 @@ class LoadClipBatch(opfapi.ClipLoader):
openclip_path = os.path.join(
openclip_dir, clip_name + ".clip"
)
+
if not os.path.exists(openclip_dir):
os.makedirs(openclip_dir)
diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py
index ea8a9e836a..5ec1c78aaa 100644
--- a/openpype/hosts/hiero/api/plugin.py
+++ b/openpype/hosts/hiero/api/plugin.py
@@ -170,7 +170,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)
@@ -273,8 +276,8 @@ class CreatorWidget(QtWidgets.QDialog):
elif v["type"] == "QSpinBox":
data[k]["value"] = self.create_row(
content_layout, "QSpinBox", v["label"],
- setValue=v["value"], setMinimum=0,
- setMaximum=100000, setToolTip=tool_tip)
+ setRange=(1, 9999999), setValue=v["value"],
+ setToolTip=tool_tip)
return data
diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py
index 666312167f..b17356c5c7 100644
--- a/openpype/hosts/nuke/plugins/load/load_clip.py
+++ b/openpype/hosts/nuke/plugins/load/load_clip.py
@@ -1,7 +1,8 @@
-import os
import nuke
import qargparse
-
+from pprint import pformat
+from copy import deepcopy
+from openpype.lib import Logger
from openpype.client import (
get_version_by_id,
get_last_version_by_subset_id,
@@ -28,6 +29,7 @@ class LoadClip(plugin.NukeLoader):
Either it is image sequence or video file.
"""
+ log = Logger.get_logger(__name__)
families = [
"source",
@@ -85,24 +87,19 @@ class LoadClip(plugin.NukeLoader):
+ plugin.get_review_presets_config()
)
- def _fix_path_for_knob(self, filepath, repre_cont):
- basename = os.path.basename(filepath)
- dirname = os.path.dirname(filepath)
- frame = repre_cont.get("frame")
- assert frame, "Representation is not sequence"
-
- padding = len(str(frame))
- basename = basename.replace(frame, "#" * padding)
- return os.path.join(dirname, basename).replace("\\", "/")
-
def load(self, context, name, namespace, options):
- repre = context["representation"]
+ representation = context["representation"]
# reste container id so it is always unique for each instance
self.reset_container_id()
- is_sequence = len(repre["files"]) > 1
+ is_sequence = len(representation["files"]) > 1
- filepath = self.fname.replace("\\", "/")
+ if is_sequence:
+ representation = self._representation_with_hash_in_frame(
+ representation
+ )
+ filepath = get_representation_path(representation).replace("\\", "/")
+ self.log.debug("_ filepath: {}".format(filepath))
start_at_workfile = options.get(
"start_at_workfile", self.options_defaults["start_at_workfile"])
@@ -112,11 +109,10 @@ class LoadClip(plugin.NukeLoader):
version = context['version']
version_data = version.get("data", {})
- repre_id = repre["_id"]
+ repre_id = representation["_id"]
- repre_cont = repre["context"]
-
- self.log.info("version_data: {}\n".format(version_data))
+ self.log.debug("_ version_data: {}\n".format(
+ pformat(version_data)))
self.log.debug(
"Representation id `{}` ".format(repre_id))
@@ -132,8 +128,6 @@ class LoadClip(plugin.NukeLoader):
duration = last - first
first = 1
last = first + duration
- elif "#" not in filepath:
- filepath = self._fix_path_for_knob(filepath, repre_cont)
# Fallback to asset name when namespace is None
if namespace is None:
@@ -144,7 +138,7 @@ class LoadClip(plugin.NukeLoader):
"Representation id `{}` is failing to load".format(repre_id))
return
- read_name = self._get_node_name(repre)
+ read_name = self._get_node_name(representation)
# Create the Loader with the filename path set
read_node = nuke.createNode(
@@ -157,7 +151,7 @@ class LoadClip(plugin.NukeLoader):
read_node["file"].setValue(filepath)
used_colorspace = self._set_colorspace(
- read_node, version_data, repre["data"])
+ read_node, version_data, representation["data"])
self._set_range_to_node(read_node, first, last, start_at_workfile)
@@ -179,7 +173,7 @@ class LoadClip(plugin.NukeLoader):
data_imprint[k] = version
elif k == 'colorspace':
- colorspace = repre["data"].get(k)
+ colorspace = representation["data"].get(k)
colorspace = colorspace or version_data.get(k)
data_imprint["db_colorspace"] = colorspace
if used_colorspace:
@@ -213,6 +207,20 @@ class LoadClip(plugin.NukeLoader):
def switch(self, container, representation):
self.update(container, representation)
+ def _representation_with_hash_in_frame(self, representation):
+ """Convert frame key value to padded hash
+
+ Args:
+ representation (dict): representation data
+
+ Returns:
+ dict: altered representation data
+ """
+ representation = deepcopy(representation)
+ frame = representation["context"]["frame"]
+ representation["context"]["frame"] = "#" * len(str(frame))
+ return representation
+
def update(self, container, representation):
"""Update the Loader's path
@@ -225,7 +233,13 @@ class LoadClip(plugin.NukeLoader):
is_sequence = len(representation["files"]) > 1
read_node = nuke.toNode(container['objectName'])
+
+ if is_sequence:
+ representation = self._representation_with_hash_in_frame(
+ representation
+ )
filepath = get_representation_path(representation).replace("\\", "/")
+ self.log.debug("_ filepath: {}".format(filepath))
start_at_workfile = "start at" in read_node['frame_mode'].value()
@@ -240,8 +254,6 @@ class LoadClip(plugin.NukeLoader):
version_data = version_doc.get("data", {})
repre_id = representation["_id"]
- repre_cont = representation["context"]
-
# colorspace profile
colorspace = representation["data"].get("colorspace")
colorspace = colorspace or version_data.get("colorspace")
@@ -258,8 +270,6 @@ class LoadClip(plugin.NukeLoader):
duration = last - first
first = 1
last = first + duration
- elif "#" not in filepath:
- filepath = self._fix_path_for_knob(filepath, repre_cont)
if not filepath:
self.log.warning(
@@ -348,8 +358,10 @@ class LoadClip(plugin.NukeLoader):
time_warp_nodes = version_data.get('timewarps', [])
last_node = None
source_id = self.get_container_id(parent_node)
- self.log.info("__ source_id: {}".format(source_id))
- self.log.info("__ members: {}".format(self.get_members(parent_node)))
+ self.log.debug("__ source_id: {}".format(source_id))
+ self.log.debug("__ members: {}".format(
+ self.get_members(parent_node)))
+
dependent_nodes = self.clear_members(parent_node)
with maintained_selection():
diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
index 3d72ddb1f9..60f6093d0c 100644
--- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py
+++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
@@ -470,9 +470,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
cam = [c for c in cameras if c in col.head]
if cam:
- subset_name = '{}_{}_{}'.format(group_name, cam, aov)
+ if aov:
+ subset_name = '{}_{}_{}'.format(group_name, cam, aov)
+ else:
+ subset_name = '{}_{}'.format(group_name, cam)
else:
- subset_name = '{}_{}'.format(group_name, aov)
+ if aov:
+ subset_name = '{}_{}'.format(group_name, aov)
+ else:
+ subset_name = '{}'.format(group_name)
if isinstance(col, (list, tuple)):
staging = os.path.dirname(col[0])
diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py
new file mode 100644
index 0000000000..896050f7e2
--- /dev/null
+++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+import os
+import re
+
+import pyblish.api
+
+
+class CollectKitsuUsername(pyblish.api.ContextPlugin):
+ """Collect Kitsu username from the kitsu login"""
+
+ order = pyblish.api.CollectorOrder + 0.499
+ label = "Kitsu username"
+
+ def process(self, context):
+ kitsu_login = os.environ.get('KITSU_LOGIN')
+
+ if not kitsu_login:
+ return
+
+ kitsu_username = kitsu_login.split("@")[0].replace('.', ' ')
+ new_username = re.sub('[^a-zA-Z]', ' ', kitsu_username).title()
+
+ for instance in context:
+ # Don't override customData if it already exists
+ if 'customData' not in instance.data:
+ instance.data['customData'] = {}
+
+ instance.data['customData']["kitsuUsername"] = new_username
diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py
index d457bdc988..1f9b30fba3 100644
--- a/openpype/plugins/publish/extract_review.py
+++ b/openpype/plugins/publish/extract_review.py
@@ -3,26 +3,26 @@ import re
import copy
import json
import shutil
-
from abc import ABCMeta, abstractmethod
+
import six
-
import clique
-
+import speedcopy
import pyblish.api
from openpype.lib import (
get_ffmpeg_tool_path,
- get_ffprobe_streams,
path_to_subprocess_arg,
run_subprocess,
-
+)
+from openpype.lib.transcoding import (
+ IMAGE_EXTENSIONS,
+ get_ffprobe_streams,
should_convert_for_ffmpeg,
convert_input_paths_for_ffmpeg,
- get_transcode_temp_directory
+ get_transcode_temp_directory,
)
-import speedcopy
class ExtractReview(pyblish.api.InstancePlugin):
@@ -175,6 +175,26 @@ class ExtractReview(pyblish.api.InstancePlugin):
outputs_per_representations.append((repre, outputs))
return outputs_per_representations
+ def _single_frame_filter(self, input_filepaths, output_defs):
+ single_frame_image = False
+ if len(input_filepaths) == 1:
+ ext = os.path.splitext(input_filepaths[0])[-1]
+ single_frame_image = ext in IMAGE_EXTENSIONS
+
+ filtered_defs = []
+ for output_def in output_defs:
+ output_filters = output_def.get("filter") or {}
+ frame_filter = output_filters.get("single_frame_filter")
+ if (
+ (not single_frame_image and frame_filter == "single_frame")
+ or (single_frame_image and frame_filter == "multi_frame")
+ ):
+ continue
+
+ filtered_defs.append(output_def)
+
+ return filtered_defs
+
@staticmethod
def get_instance_label(instance):
return (
@@ -195,7 +215,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
outputs_per_repres = self._get_outputs_per_representations(
instance, profile_outputs
)
- for repre, outpu_defs in outputs_per_repres:
+ for repre, output_defs in outputs_per_repres:
# Check if input should be preconverted before processing
# Store original staging dir (it's value may change)
src_repre_staging_dir = repre["stagingDir"]
@@ -216,6 +236,16 @@ class ExtractReview(pyblish.api.InstancePlugin):
if first_input_path is None:
first_input_path = filepath
+ filtered_output_defs = self._single_frame_filter(
+ input_filepaths, output_defs
+ )
+ if not filtered_output_defs:
+ self.log.debug((
+ "Repre: {} - All output definitions were filtered"
+ " out by single frame filter. Skipping"
+ ).format(repre["name"]))
+ continue
+
# Skip if file is not set
if first_input_path is None:
self.log.warning((
@@ -249,7 +279,10 @@ class ExtractReview(pyblish.api.InstancePlugin):
try:
self._render_output_definitions(
- instance, repre, src_repre_staging_dir, outpu_defs
+ instance,
+ repre,
+ src_repre_staging_dir,
+ filtered_output_defs
)
finally:
@@ -263,10 +296,10 @@ class ExtractReview(pyblish.api.InstancePlugin):
shutil.rmtree(new_staging_dir)
def _render_output_definitions(
- self, instance, repre, src_repre_staging_dir, outpu_defs
+ self, instance, repre, src_repre_staging_dir, output_defs
):
fill_data = copy.deepcopy(instance.data["anatomyData"])
- for _output_def in outpu_defs:
+ for _output_def in output_defs:
output_def = copy.deepcopy(_output_def)
# Make sure output definition has "tags" key
if "tags" not in output_def:
@@ -1659,9 +1692,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
return True
return False
- def filter_output_defs(
- self, profile, subset_name, families
- ):
+ def filter_output_defs(self, profile, subset_name, families):
"""Return outputs matching input instance families.
Output definitions without families filter are marked as valid.
diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json
index 0f3080ad64..34baf9ba06 100644
--- a/openpype/settings/defaults/project_settings/flame.json
+++ b/openpype/settings/defaults/project_settings/flame.json
@@ -142,7 +142,7 @@
"exr16fpdwaa"
],
"reel_name": "OP_LoadedReel",
- "clip_name_template": "{asset}_{subset}<_{output}>"
+ "clip_name_template": "{batch}_{asset}_{subset}<_{output}>"
}
}
}
\ No newline at end of file
diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json
index b128564bc2..9c3f2f1e1b 100644
--- a/openpype/settings/defaults/project_settings/global.json
+++ b/openpype/settings/defaults/project_settings/global.json
@@ -53,6 +53,62 @@
"families": [],
"hosts": [],
"outputs": {
+ "png": {
+ "ext": "png",
+ "tags": [
+ "ftrackreview"
+ ],
+ "burnins": [],
+ "ffmpeg_args": {
+ "video_filters": [],
+ "audio_filters": [],
+ "input": [],
+ "output": []
+ },
+ "filter": {
+ "families": [
+ "render",
+ "review",
+ "ftrack"
+ ],
+ "subsets": [],
+ "custom_tags": [],
+ "single_frame_filter": "single_frame"
+ },
+ "overscan_crop": "",
+ "overscan_color": [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ "width": 1920,
+ "height": 1080,
+ "scale_pixel_aspect": true,
+ "bg_color": [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "letter_box": {
+ "enabled": false,
+ "ratio": 0.0,
+ "fill_color": [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ "line_thickness": 0,
+ "line_color": [
+ 255,
+ 0,
+ 0,
+ 255
+ ]
+ }
+ },
"h264": {
"ext": "mp4",
"tags": [
@@ -79,7 +135,8 @@
"ftrack"
],
"subsets": [],
- "custom_tags": []
+ "custom_tags": [],
+ "single_frame_filter": "multi_frame"
},
"overscan_crop": "",
"overscan_color": [
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json
index 51fc8dedf3..742437fbde 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json
@@ -304,6 +304,20 @@
"label": "Custom Tags",
"type": "list",
"object_type": "text"
+ },
+ {
+ "type": "label",
+ "label": "Use output always / only if input is 1 frame image / only if has 2+ frames or is video"
+ },
+ {
+ "type": "enum",
+ "key": "single_frame_filter",
+ "default": "everytime",
+ "enum_items": [
+ {"everytime": "Always"},
+ {"single_frame": "Only if input has 1 image frame"},
+ {"multi_frame": "Only if input is video or sequence of frames"}
+ ]
}
]
},
diff --git a/openpype/tools/publisher/publish_report_viewer/widgets.py b/openpype/tools/publisher/publish_report_viewer/widgets.py
index ff388fb277..0d35ac3512 100644
--- a/openpype/tools/publisher/publish_report_viewer/widgets.py
+++ b/openpype/tools/publisher/publish_report_viewer/widgets.py
@@ -27,6 +27,9 @@ class PluginLoadReportModel(QtGui.QStandardItemModel):
parent = self.invisibleRootItem()
parent.removeRows(0, parent.rowCount())
+ if report is None:
+ return
+
new_items = []
new_items_by_filepath = {}
for filepath in report.crashed_plugin_paths.keys():
diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py
index 2c249d058c..646ae69e7f 100644
--- a/openpype/tools/publisher/publish_report_viewer/window.py
+++ b/openpype/tools/publisher/publish_report_viewer/window.py
@@ -367,6 +367,7 @@ class LoadedFilesView(QtWidgets.QTreeView):
def _on_rows_inserted(self):
header = self.header()
header.resizeSections(header.ResizeToContents)
+ self._update_remove_btn()
def resizeEvent(self, event):
super(LoadedFilesView, self).resizeEvent(event)
diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py
index f07995acc6..5875f7aa68 100644
--- a/openpype/tools/publisher/window.py
+++ b/openpype/tools/publisher/window.py
@@ -361,6 +361,13 @@ class PublisherWindow(QtWidgets.QDialog):
super(PublisherWindow, self).resizeEvent(event)
self._update_publish_frame_rect()
+ def keyPressEvent(self, event):
+ # Ignore escape button to close window
+ if event.key() == QtCore.Qt.Key_Escape:
+ event.accept()
+ return
+ super(PublisherWindow, self).keyPressEvent(event)
+
def _on_overlay_message(self, event):
self._overlay_object.add_message(
event["message"],
diff --git a/openpype/version.py b/openpype/version.py
index 46bb4b1cd0..81b2925fb5 100644
--- a/openpype/version.py
+++ b/openpype/version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
-__version__ = "3.14.7-nightly.2"
+__version__ = "3.14.7-nightly.3"
diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md
index 24ea09b6fb..9666c6568a 100644
--- a/website/docs/project_settings/settings_project_global.md
+++ b/website/docs/project_settings/settings_project_global.md
@@ -135,6 +135,12 @@ Profile may generate multiple outputs from a single input. Each output must defi
- set alpha to `0` to not use this option at all (in most of cases background stays black)
- other than `0` alpha will draw color as background
+- **`Additional filtering`**
+ - Profile filtering defines which group of output definitions is used but output definitions may require more specific filters on their own.
+ - They may filter by subset name (regex can be used) or publish families. Publish families are more complex as are based on knowing code base.
+ - Filtering by custom tags -> this is used for targeting to output definitions from other extractors using settings (at this moment only Nuke bake extractor can target using custom tags).
+ - Nuke extractor settings path: `project_settings/nuke/publish/ExtractReviewDataMov/outputs/baking/add_custom_tags`
+ - Filtering by input length. Input may be video, sequence or single image. It is possible that `.mp4` should be created only when input is video or sequence and to create review `.png` when input is single frame. In some cases the output should be created even if it's single frame or multi frame input.
### IntegrateAssetNew