diff --git a/.github/pr-branch-labeler.yml b/.github/pr-branch-labeler.yml
new file mode 100644
index 0000000000..ca82051006
--- /dev/null
+++ b/.github/pr-branch-labeler.yml
@@ -0,0 +1,15 @@
+# Apply label "feature" if head matches "feature/*"
+'type: feature':
+ head: "feature/*"
+
+# Apply label "feature" if head matches "feature/*"
+'type: enhancement':
+ head: "enhancement/*"
+
+# Apply label "bugfix" if head matches one of "bugfix/*" or "hotfix/*"
+'type: bug':
+ head: ["bugfix/*", "hotfix/*"]
+
+# Apply label "release" if base matches "release/*"
+'Bump Minor':
+ base: "release/next-minor"
\ No newline at end of file
diff --git a/.github/pr-glob-labeler.yml b/.github/pr-glob-labeler.yml
new file mode 100644
index 0000000000..286e7768b5
--- /dev/null
+++ b/.github/pr-glob-labeler.yml
@@ -0,0 +1,102 @@
+# Add type: unittest label if any changes in tests folders
+'type: unittest':
+- '*/*tests*/**/*'
+
+# any changes in documentation structure
+'type: documentation':
+- '*/**/*website*/**/*'
+- '*/**/*docs*/**/*'
+
+# hosts triage
+'host: Nuke':
+- '*/**/*nuke*'
+- '*/**/*nuke*/**/*'
+
+'host: Photoshop':
+- '*/**/*photoshop*'
+- '*/**/*photoshop*/**/*'
+
+'host: Harmony':
+- '*/**/*harmony*'
+- '*/**/*harmony*/**/*'
+
+'host: UE':
+- '*/**/*unreal*'
+- '*/**/*unreal*/**/*'
+
+'host: Houdini':
+- '*/**/*houdini*'
+- '*/**/*houdini*/**/*'
+
+'host: Maya':
+- '*/**/*maya*'
+- '*/**/*maya*/**/*'
+
+'host: Resolve':
+- '*/**/*resolve*'
+- '*/**/*resolve*/**/*'
+
+'host: Blender':
+- '*/**/*blender*'
+- '*/**/*blender*/**/*'
+
+'host: Hiero':
+- '*/**/*hiero*'
+- '*/**/*hiero*/**/*'
+
+'host: Fusion':
+- '*/**/*fusion*'
+- '*/**/*fusion*/**/*'
+
+'host: Flame':
+- '*/**/*flame*'
+- '*/**/*flame*/**/*'
+
+'host: TrayPublisher':
+- '*/**/*traypublisher*'
+- '*/**/*traypublisher*/**/*'
+
+'host: 3dsmax':
+- '*/**/*max*'
+- '*/**/*max*/**/*'
+
+'host: TV Paint':
+- '*/**/*tvpaint*'
+- '*/**/*tvpaint*/**/*'
+
+'host: CelAction':
+- '*/**/*celaction*'
+- '*/**/*celaction*/**/*'
+
+'host: After Effects':
+- '*/**/*aftereffects*'
+- '*/**/*aftereffects*/**/*'
+
+'host: Substance Painter':
+- '*/**/*substancepainter*'
+- '*/**/*substancepainter*/**/*'
+
+# modules triage
+'module: Deadline':
+- '*/**/*deadline*'
+- '*/**/*deadline*/**/*'
+
+'module: RoyalRender':
+- '*/**/*royalrender*'
+- '*/**/*royalrender*/**/*'
+
+'module: Sitesync':
+- '*/**/*sync_server*'
+- '*/**/*sync_server*/**/*'
+
+'module: Ftrack':
+- '*/**/*ftrack*'
+- '*/**/*ftrack*/**/*'
+
+'module: Shotgrid':
+- '*/**/*shotgrid*'
+- '*/**/*shotgrid*/**/*'
+
+'module: Kitsu':
+- '*/**/*kitsu*'
+- '*/**/*kitsu*/**/*'
diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml
index 26bc2b8a1f..1e1a1441f7 100644
--- a/.github/workflows/project_actions.yml
+++ b/.github/workflows/project_actions.yml
@@ -1,8 +1,8 @@
name: project-actions
on:
- pull_request:
- types: [review_requested]
+ pull_request_target:
+ types: [opened, synchronize, assigned, review_requested]
pull_request_review:
types: [submitted]
@@ -20,3 +20,53 @@ jobs:
project_id: 11
resource_node_id: ${{ github.event.pull_request.node_id }}
status_value: Change Requested
+
+ size-label:
+ name: pr_size_label
+ runs-on: ubuntu-latest
+ if: |
+ ${{(github.event_name == 'pull_request' && github.event.action == 'assigned')
+ || (github.event_name == 'pull_request' && github.event.action == 'opened')}}
+
+ steps:
+ - name: Add size label
+ uses: "pascalgn/size-label-action@v0.4.3"
+ env:
+ GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
+ IGNORED: ".gitignore\n*.md\n*.json"
+ with:
+ sizes: >
+ {
+ "0": "XS",
+ "100": "S",
+ "500": "M",
+ "1000": "L",
+ "1500": "XL",
+ "2500": "XXL"
+ }
+
+ label_prs_branch:
+ name: pr_branch_label
+ runs-on: ubuntu-latest
+ if: |
+ ${{(github.event_name == 'pull_request' && github.event.action == 'assigned')
+ || (github.event_name == 'pull_request' && github.event.action == 'opened')}}
+ steps:
+ - name: Label PRs - Branch name detection
+ uses: ffittschen/pr-branch-labeler@v1
+ with:
+ repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
+
+ label_prs_globe:
+ name: pr_globe_label
+ runs-on: ubuntu-latest
+ if: |
+ ${{(github.event_name == 'pull_request' && github.event.action == 'assigned')
+ || (github.event_name == 'pull_request' && github.event.action == 'opened')}}
+ steps:
+ - name: Label PRs - Globe detection
+ uses: actions/labeler@v4.0.3
+ with:
+ repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
+ configuration-path: ".github/pr-glob-labeler.yml"
+ sync-labels: false
diff --git a/openpype/hosts/houdini/plugins/publish/collect_current_file.py b/openpype/hosts/houdini/plugins/publish/collect_current_file.py
index 9cca07fdc7..caf679f98b 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_current_file.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_current_file.py
@@ -1,7 +1,6 @@
import os
import hou
-from openpype.pipeline import legacy_io
import pyblish.api
@@ -11,7 +10,7 @@ class CollectHoudiniCurrentFile(pyblish.api.InstancePlugin):
order = pyblish.api.CollectorOrder - 0.01
label = "Houdini Current File"
hosts = ["houdini"]
- family = ["workfile"]
+ families = ["workfile"]
def process(self, instance):
"""Inject the current working file"""
@@ -21,7 +20,7 @@ class CollectHoudiniCurrentFile(pyblish.api.InstancePlugin):
# By default, Houdini will even point a new scene to a path.
# However if the file is not saved at all and does not exist,
# we assume the user never set it.
- filepath = ""
+ current_file = ""
elif os.path.basename(current_file) == "untitled.hip":
# Due to even a new file being called 'untitled.hip' we are unable
diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py
index 36affe852b..00565c5819 100644
--- a/openpype/hosts/maya/plugins/publish/collect_review.py
+++ b/openpype/hosts/maya/plugins/publish/collect_review.py
@@ -5,6 +5,7 @@ import pyblish.api
from openpype.client import get_subset_by_name
from openpype.pipeline import legacy_io
+from openpype.hosts.maya.api.lib import get_attribute_input
class CollectReview(pyblish.api.InstancePlugin):
@@ -146,3 +147,21 @@ class CollectReview(pyblish.api.InstancePlugin):
"filename": node.filename.get()
}
)
+
+ # Collect focal length.
+ attr = camera + ".focalLength"
+ focal_length = None
+ if get_attribute_input(attr):
+ start = instance.data["frameStart"]
+ end = instance.data["frameEnd"] + 1
+ focal_length = [
+ cmds.getAttr(attr, time=t) for t in range(int(start), int(end))
+ ]
+ else:
+ focal_length = cmds.getAttr(attr)
+
+ key = "focalLength"
+ try:
+ instance.data["burninDataMembers"][key] = focal_length
+ except KeyError:
+ instance.data["burninDataMembers"] = {key: focal_length}
diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py
index 7a929a0ade..6f9a095285 100644
--- a/openpype/lib/execute.py
+++ b/openpype/lib/execute.py
@@ -8,6 +8,8 @@ import tempfile
from .log import Logger
from .vendor_bin_utils import find_executable
+from .openpype_version import is_running_from_build
+
# MSDN process creation flag (Windows only)
CREATE_NO_WINDOW = 0x08000000
@@ -200,6 +202,11 @@ def run_openpype_process(*args, **kwargs):
# Skip envs that can affect OpenPype process
# - fill more if you find more
env = clean_envs_for_openpype_process(os.environ)
+
+ # Only keep OpenPype version if we are running from build.
+ if not is_running_from_build():
+ env.pop("OPENPYPE_VERSION", None)
+
return run_subprocess(args, env=env, **kwargs)
diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py
index 44ba4a5025..95575444b2 100644
--- a/openpype/plugins/publish/extract_burnin.py
+++ b/openpype/plugins/publish/extract_burnin.py
@@ -252,6 +252,9 @@ class ExtractBurnin(publish.Extractor):
# Add context data burnin_data.
burnin_data["custom"] = custom_data
+ # Add data members.
+ burnin_data.update(instance.data.get("burninDataMembers", {}))
+
# Add source camera name to burnin data
camera_name = repre.get("camera_name")
if camera_name:
diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py
index ef449f4f74..d0a4266941 100644
--- a/openpype/scripts/otio_burnin.py
+++ b/openpype/scripts/otio_burnin.py
@@ -4,8 +4,10 @@ import re
import subprocess
import platform
import json
-import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins
+import tempfile
+from string import Formatter
+import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins
from openpype.lib import (
get_ffmpeg_tool_path,
get_ffmpeg_codec_args,
@@ -23,7 +25,7 @@ FFMPEG = (
).format(ffmpeg_path)
DRAWTEXT = (
- "drawtext=fontfile='%(font)s':text=\\'%(text)s\\':"
+ "drawtext@'%(label)s'=fontfile='%(font)s':text=\\'%(text)s\\':"
"x=%(x)s:y=%(y)s:fontcolor=%(color)s@%(opacity).1f:fontsize=%(size)d"
)
TIMECODE = (
@@ -39,6 +41,45 @@ TIMECODE_KEY = "{timecode}"
SOURCE_TIMECODE_KEY = "{source_timecode}"
+def convert_list_to_command(list_to_convert, fps, label=""):
+ """Convert a list of values to a drawtext command file for ffmpeg `sendcmd`
+
+ The list of values is expected to have a value per frame. If the video
+ file ends up being longer than the amount of samples per frame than the
+ last value will be held.
+
+ Args:
+ list_to_convert (list): List of values per frame.
+ fps (float or int): The expected frame per seconds of the output file.
+ label (str): Label for the drawtext, if specific drawtext filter is
+ required
+
+ Returns:
+ str: Filepath to the temporary drawtext command file.
+
+ """
+
+ with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
+ for i, value in enumerate(list_to_convert):
+ seconds = i / fps
+
+ # Escape special character
+ value = str(value).replace(":", "\\:")
+
+ filter = "drawtext"
+ if label:
+ filter += "@" + label
+
+ line = (
+ "{start} {filter} reinit text='{value}';"
+ "\n".format(start=seconds, filter=filter, value=value)
+ )
+
+ f.write(line)
+ f.flush()
+ return f.name
+
+
def _get_ffprobe_data(source):
"""Reimplemented from otio burnins to be able use full path to ffprobe
:param str source: source media file
@@ -144,7 +185,13 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
self.options_init.update(options_init)
def add_text(
- self, text, align, frame_start=None, frame_end=None, options=None
+ self,
+ text,
+ align,
+ frame_start=None,
+ frame_end=None,
+ options=None,
+ cmd=""
):
"""
Adding static text to a filter.
@@ -165,7 +212,13 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
if frame_end is not None:
options["frame_end"] = frame_end
- self._add_burnin(text, align, options, DRAWTEXT)
+ draw_text = DRAWTEXT
+ if cmd:
+ draw_text = "{}, {}".format(cmd, DRAWTEXT)
+
+ options["label"] = align
+
+ self._add_burnin(text, align, options, draw_text)
def add_timecode(
self, align, frame_start=None, frame_end=None, frame_start_tc=None,
@@ -408,11 +461,13 @@ def burnins_from_data(
True by default.
Presets must be set separately. Should be dict with 2 keys:
- - "options" - sets look of burnins - colors, opacity,...(more info: ModifiedBurnins doc)
+ - "options" - sets look of burnins - colors, opacity,...
+ (more info: ModifiedBurnins doc)
- *OPTIONAL* default values are used when not included
- "burnins" - contains dictionary with burnins settings
- *OPTIONAL* burnins won't be added (easier is not to use this)
- - each key of "burnins" represents Alignment, there are 6 possibilities:
+ - each key of "burnins" represents Alignment,
+ there are 6 possibilities:
TOP_LEFT TOP_CENTERED TOP_RIGHT
BOTTOM_LEFT BOTTOM_CENTERED BOTTOM_RIGHT
- value must be string with text you want to burn-in
@@ -491,13 +546,14 @@ def burnins_from_data(
if source_timecode is not None:
data[SOURCE_TIMECODE_KEY[1:-1]] = SOURCE_TIMECODE_KEY
+ clean_up_paths = []
for align_text, value in burnin_values.items():
if not value:
continue
- if isinstance(value, (dict, list, tuple)):
+ if isinstance(value, dict):
raise TypeError((
- "Expected string or number type."
+ "Expected string, number or list type."
" Got: {} - \"{}\""
" (Make sure you have new burnin presets)."
).format(str(type(value)), str(value)))
@@ -533,8 +589,48 @@ def burnins_from_data(
print("Source does not have set timecode value.")
value = value.replace(SOURCE_TIMECODE_KEY, MISSING_KEY_VALUE)
- key_pattern = re.compile(r"(\{.*?[^{0]*\})")
+ # Convert lists.
+ cmd = ""
+ text = None
+ keys = [i[1] for i in Formatter().parse(value) if i[1] is not None]
+ list_to_convert = []
+ # Warn about nested dictionary support for lists. Ei. we dont support
+ # it.
+ if "[" in "".join(keys):
+ print(
+ "We dont support converting nested dictionaries to lists,"
+ " so skipping {}".format(value)
+ )
+ else:
+ for key in keys:
+ data_value = data[key]
+
+ # Multiple lists are not supported.
+ if isinstance(data_value, list) and list_to_convert:
+ raise ValueError(
+ "Found multiple lists to convert, which is not "
+ "supported: {}".format(value)
+ )
+
+ if isinstance(data_value, list):
+ print("Found list to convert: {}".format(data_value))
+ for v in data_value:
+ data[key] = v
+ list_to_convert.append(value.format(**data))
+
+ if list_to_convert:
+ value = list_to_convert[0]
+ path = convert_list_to_command(
+ list_to_convert, data["fps"], label=align
+ )
+ cmd = "sendcmd=f='{}'".format(path)
+ cmd = cmd.replace("\\", "/")
+ cmd = cmd.replace(":", "\\:")
+ clean_up_paths.append(path)
+
+ # Failsafe for missing keys.
+ key_pattern = re.compile(r"(\{.*?[^{0]*\})")
missing_keys = []
for group in key_pattern.findall(value):
try:
@@ -568,7 +664,8 @@ def burnins_from_data(
continue
text = value.format(**data)
- burnin.add_text(text, align, frame_start, frame_end)
+
+ burnin.add_text(text, align, frame_start, frame_end, cmd=cmd)
ffmpeg_args = []
if codec_data:
@@ -599,6 +696,8 @@ def burnins_from_data(
burnin.render(
output_path, args=ffmpeg_args_str, overwrite=overwrite, **data
)
+ for path in clean_up_paths:
+ os.remove(path)
if __name__ == "__main__":
diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json
index aba840da78..30e56300d1 100644
--- a/openpype/settings/defaults/project_settings/global.json
+++ b/openpype/settings/defaults/project_settings/global.json
@@ -249,6 +249,29 @@
}
}
}
+ },
+ {
+ "families": [],
+ "hosts": [
+ "maya"
+ ],
+ "task_types": [],
+ "task_names": [],
+ "subsets": [],
+ "burnins": {
+ "maya_burnin": {
+ "TOP_LEFT": "{yy}-{mm}-{dd}",
+ "TOP_CENTERED": "{focalLength:.2f} mm",
+ "TOP_RIGHT": "{anatomy[version]}",
+ "BOTTOM_LEFT": "{username}",
+ "BOTTOM_CENTERED": "{asset}",
+ "BOTTOM_RIGHT": "{frame_start}-{current_frame}-{frame_end}",
+ "filter": {
+ "families": [],
+ "tags": []
+ }
+ }
+ }
}
]
},
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json
index dec5a5cdc2..416e530db2 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json
@@ -19,12 +19,12 @@
{
"type": "text",
"key": "compression",
- "label": "Compression type"
+ "label": "Encoding"
},
{
"type": "text",
"key": "format",
- "label": "Data format"
+ "label": "Format"
},
{
"type": "number",
diff --git a/website/docs/pype2/admin_presets_plugins.md b/website/docs/pype2/admin_presets_plugins.md
index e589f7d14b..44c2a28dec 100644
--- a/website/docs/pype2/admin_presets_plugins.md
+++ b/website/docs/pype2/admin_presets_plugins.md
@@ -293,6 +293,7 @@ If source representation has suffix **"h264"** and burnin suffix is **"client"**
- It is allowed to use [Anatomy templates](admin_config#anatomy) themselves in burnins if they can be filled with available data.
- Additional keys in burnins:
+
| Burnin key | Description |
| --- | --- |
| frame_start | First frame number. |
@@ -303,6 +304,7 @@ If source representation has suffix **"h264"** and burnin suffix is **"client"**
| resolution_height | Resolution height. |
| fps | Fps of an output. |
| timecode | Timecode by frame start and fps. |
+ | focalLength | **Only available in Maya**
Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. |
:::warning
`timecode` is specific key that can be **only at the end of content**. (`"BOTTOM_RIGHT": "TC: {timecode}"`)