[Automated] Merged develop into main

This commit is contained in:
pypebot 2021-08-07 05:36:36 +02:00 committed by GitHub
commit 2fe75c8d90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 473 additions and 243 deletions

View file

@ -98,6 +98,11 @@ def install():
.get(platform_name) .get(platform_name)
) or [] ) or []
for path in project_plugins: for path in project_plugins:
try:
path = str(path.format(**os.environ))
except KeyError:
pass
if not path or not os.path.exists(path): if not path or not os.path.exists(path):
continue continue

View file

@ -0,0 +1,17 @@
from openpype.hosts.aftereffects.plugins.create import create_render
import logging
log = logging.getLogger(__name__)
class CreateLocalRender(create_render.CreateRender):
""" Creator to render locally.
Created only after default render on farm. So family 'render.local' is
used for backward compatibility.
"""
name = "renderDefault"
label = "Render Locally"
family = "renderLocal"

View file

@ -1,10 +1,14 @@
from openpype.lib import abstract_collect_render
from openpype.lib.abstract_collect_render import RenderInstance
import pyblish.api
import attr
import os import os
import re
import attr
import tempfile
from avalon import aftereffects from avalon import aftereffects
import pyblish.api
from openpype.settings import get_project_settings
from openpype.lib import abstract_collect_render
from openpype.lib.abstract_collect_render import RenderInstance
@attr.s @attr.s
@ -13,6 +17,8 @@ class AERenderInstance(RenderInstance):
comp_name = attr.ib(default=None) comp_name = attr.ib(default=None)
comp_id = attr.ib(default=None) comp_id = attr.ib(default=None)
fps = attr.ib(default=None) fps = attr.ib(default=None)
projectEntity = attr.ib(default=None)
stagingDir = attr.ib(default=None)
class CollectAERender(abstract_collect_render.AbstractCollectRender): class CollectAERender(abstract_collect_render.AbstractCollectRender):
@ -21,6 +27,11 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
label = "Collect After Effects Render Layers" label = "Collect After Effects Render Layers"
hosts = ["aftereffects"] hosts = ["aftereffects"]
# internal
family_remapping = {
"render": ("render.farm", "farm"), # (family, label)
"renderLocal": ("render", "local")
}
padding_width = 6 padding_width = 6
rendered_extension = 'png' rendered_extension = 'png'
@ -62,14 +73,16 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
fps = work_area_info.frameRate fps = work_area_info.frameRate
# TODO add resolution when supported by extension # TODO add resolution when supported by extension
if inst["family"] == "render" and inst["active"]: if inst["family"] in self.family_remapping.keys() \
and inst["active"]:
remapped_family = self.family_remapping[inst["family"]]
instance = AERenderInstance( instance = AERenderInstance(
family="render.farm", # other way integrate would catch it family=remapped_family[0],
families=["render.farm"], families=[remapped_family[0]],
version=version, version=version,
time="", time="",
source=current_file, source=current_file,
label="{} - farm".format(inst["subset"]), label="{} - {}".format(inst["subset"], remapped_family[1]),
subset=inst["subset"], subset=inst["subset"],
asset=context.data["assetEntity"]["name"], asset=context.data["assetEntity"]["name"],
attachTo=False, attachTo=False,
@ -105,6 +118,30 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
instance.outputDir = self._get_output_dir(instance) instance.outputDir = self._get_output_dir(instance)
settings = get_project_settings(os.getenv("AVALON_PROJECT"))
reviewable_subset_filter = \
(settings["deadline"]
["publish"]
["ProcessSubmittedJobOnFarm"]
["aov_filter"])
if inst["family"] == "renderLocal":
# for local renders
instance.anatomyData["version"] = instance.version
instance.anatomyData["subset"] = instance.subset
instance.stagingDir = tempfile.mkdtemp()
instance.projectEntity = project_entity
if self.hosts[0] in reviewable_subset_filter.keys():
for aov_pattern in \
reviewable_subset_filter[self.hosts[0]]:
if re.match(aov_pattern, instance.subset):
instance.families.append("review")
instance.review = True
break
self.log.info("New instance:: {}".format(instance))
instances.append(instance) instances.append(instance)
return instances return instances

View file

@ -0,0 +1,82 @@
import os
import six
import sys
import openpype.api
from avalon import aftereffects
class ExtractLocalRender(openpype.api.Extractor):
"""Render RenderQueue locally."""
order = openpype.api.Extractor.order - 0.47
label = "Extract Local Render"
hosts = ["aftereffects"]
families = ["render"]
def process(self, instance):
stub = aftereffects.stub()
staging_dir = instance.data["stagingDir"]
self.log.info("staging_dir::{}".format(staging_dir))
stub.render(staging_dir)
# pull file name from Render Queue Output module
render_q = stub.get_render_info()
if not render_q:
raise ValueError("No file extension set in Render Queue")
_, ext = os.path.splitext(os.path.basename(render_q.file_name))
ext = ext[1:]
first_file_path = None
files = []
self.log.info("files::{}".format(os.listdir(staging_dir)))
for file_name in os.listdir(staging_dir):
files.append(file_name)
if first_file_path is None:
first_file_path = os.path.join(staging_dir,
file_name)
resulting_files = files
if len(files) == 1:
resulting_files = files[0]
repre_data = {
"frameStart": instance.data["frameStart"],
"frameEnd": instance.data["frameEnd"],
"name": ext,
"ext": ext,
"files": resulting_files,
"stagingDir": staging_dir
}
if instance.data["review"]:
repre_data["tags"] = ["review"]
instance.data["representations"] = [repre_data]
ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg")
# Generate thumbnail.
thumbnail_path = os.path.join(staging_dir,
"thumbnail.jpg")
args = [
ffmpeg_path, "-y",
"-i", first_file_path,
"-vf", "scale=300:-1",
"-vframes", "1",
thumbnail_path
]
self.log.debug("Thumbnail args:: {}".format(args))
try:
output = openpype.lib.run_subprocess(args)
except TypeError:
self.log.warning("Error in creating thumbnail")
six.reraise(*sys.exc_info())
instance.data["representations"].append({
"name": "thumbnail",
"ext": "jpg",
"files": os.path.basename(thumbnail_path),
"stagingDir": staging_dir,
"tags": ["thumbnail"]
})

View file

@ -53,7 +53,7 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
order = pyblish.api.ValidatorOrder order = pyblish.api.ValidatorOrder
label = "Validate Scene Settings" label = "Validate Scene Settings"
families = ["render.farm"] families = ["render.farm", "render"]
hosts = ["aftereffects"] hosts = ["aftereffects"]
optional = True optional = True

View file

@ -0,0 +1,61 @@
from openpype.modules.ftrack.lib import ServerAction
class PrivateProjectDetectionAction(ServerAction):
"""Action helps to identify if does not have access to project."""
identifier = "server.missing.perm.private.project"
label = "Missing permissions"
description = (
"Main ftrack event server does not have access to this project."
)
def _discover(self, event):
"""Show action only if there is a selection in event data."""
entities = self._translate_event(event)
if entities:
return None
selection = event["data"].get("selection")
if not selection:
return None
return {
"items": [{
"label": self.label,
"variant": self.variant,
"description": self.description,
"actionIdentifier": self.discover_identifier,
"icon": self.icon,
}]
}
def _launch(self, event):
# Ignore if there are values in event data
# - somebody clicked on submit button
values = event["data"].get("values")
if values:
return None
title = "# Private project (missing permissions) #"
msg = (
"User ({}) or API Key used on Ftrack event server"
" does not have permissions to access this private project."
).format(self.session.api_user)
return {
"type": "form",
"title": "Missing permissions",
"items": [
{"type": "label", "value": title},
{"type": "label", "value": msg},
# Add hidden to be able detect if was clicked on submit
{"type": "hidden", "value": "1", "name": "hidden"}
],
"submit_button_label": "Got it"
}
def register(session):
'''Register plugin. Called when used as an plugin.'''
PrivateProjectDetectionAction(session).register()

View file

@ -1,33 +1,98 @@
import platform
import socket
import getpass
from openpype.modules.ftrack.lib import BaseAction, statics_icon from openpype.modules.ftrack.lib import BaseAction, statics_icon
class ActionAskWhereIRun(BaseAction): class ActionWhereIRun(BaseAction):
""" Sometimes user forget where pipeline with his credentials is running. """Show where same user has running OpenPype instances."""
- this action triggers `ActionShowWhereIRun`
"""
ignore_me = True
identifier = 'ask.where.i.run'
label = 'Ask where I run'
description = 'Triggers PC info where user have running OpenPype'
icon = statics_icon("ftrack", "action_icons", "ActionAskWhereIRun.svg")
def discover(self, session, entities, event): identifier = "ask.where.i.run"
""" Hide by default - Should be enabled only if you want to run. show_identifier = "show.where.i.run"
- best practise is to create another action that triggers this one label = "OpenPype Admin"
""" variant = "- Where I run"
description = "Show PC info where user have running OpenPype"
return True def _discover(self, _event):
return {
"items": [{
"label": self.label,
"variant": self.variant,
"description": self.description,
"actionIdentifier": self.discover_identifier,
"icon": self.icon,
}]
}
def launch(self, session, entities, event): def _launch(self, event):
more_data = {"event_hub_id": session.event_hub.id} self.trigger_action(self.show_identifier, event)
self.trigger_action(
"show.where.i.run", event, additional_event_data=more_data def register(self):
# Register default action callbacks
super(ActionWhereIRun, self).register()
# Add show identifier
show_subscription = (
"topic=ftrack.action.launch"
" and data.actionIdentifier={}"
" and source.user.username={}"
).format(
self.show_identifier,
self.session.api_user
)
self.session.event_hub.subscribe(
show_subscription,
self._show_info
) )
return True def _show_info(self, event):
title = "Where Do I Run?"
msgs = {}
all_keys = ["Hostname", "IP", "Username", "System name", "PC name"]
try:
host_name = socket.gethostname()
msgs["Hostname"] = host_name
host_ip = socket.gethostbyname(host_name)
msgs["IP"] = host_ip
except Exception:
pass
try:
system_name, pc_name, *_ = platform.uname()
msgs["System name"] = system_name
msgs["PC name"] = pc_name
except Exception:
pass
try:
msgs["Username"] = getpass.getuser()
except Exception:
pass
for key in all_keys:
if not msgs.get(key):
msgs[key] = "-Undefined-"
items = []
first = True
separator = {"type": "label", "value": "---"}
for key, value in msgs.items():
if first:
first = False
else:
items.append(separator)
self.log.debug("{}: {}".format(key, value))
subtitle = {"type": "label", "value": "<h3>{}</h3>".format(key)}
items.append(subtitle)
message = {"type": "label", "value": "<p>{}</p>".format(value)}
items.append(message)
self.show_interface(items, title, event=event)
def register(session): def register(session):
'''Register plugin. Called when used as an plugin.''' '''Register plugin. Called when used as an plugin.'''
ActionAskWhereIRun(session).register() ActionWhereIRun(session).register()

View file

@ -1,86 +0,0 @@
import platform
import socket
import getpass
from openpype.modules.ftrack.lib import BaseAction
class ActionShowWhereIRun(BaseAction):
""" Sometimes user forget where pipeline with his credentials is running.
- this action shows on which PC, Username and IP is running
- requirement action MUST be registered where we want to locate the PC:
- - can't be used retrospectively...
"""
#: Action identifier.
identifier = 'show.where.i.run'
#: Action label.
label = 'Show where I run'
#: Action description.
description = 'Shows PC info where user have running OpenPype'
def discover(self, session, entities, event):
""" Hide by default - Should be enabled only if you want to run.
- best practise is to create another action that triggers this one
"""
return False
@property
def launch_identifier(self):
return self.identifier
def launch(self, session, entities, event):
# Don't show info when was launch from this session
if session.event_hub.id == event.get("data", {}).get("event_hub_id"):
return True
title = "Where Do I Run?"
msgs = {}
all_keys = ["Hostname", "IP", "Username", "System name", "PC name"]
try:
host_name = socket.gethostname()
msgs["Hostname"] = host_name
host_ip = socket.gethostbyname(host_name)
msgs["IP"] = host_ip
except Exception:
pass
try:
system_name, pc_name, *_ = platform.uname()
msgs["System name"] = system_name
msgs["PC name"] = pc_name
except Exception:
pass
try:
msgs["Username"] = getpass.getuser()
except Exception:
pass
for key in all_keys:
if not msgs.get(key):
msgs[key] = "-Undefined-"
items = []
first = True
splitter = {'type': 'label', 'value': '---'}
for key, value in msgs.items():
if first:
first = False
else:
items.append(splitter)
self.log.debug("{}: {}".format(key, value))
subtitle = {'type': 'label', 'value': '<h3>{}</h3>'.format(key)}
items.append(subtitle)
message = {'type': 'label', 'value': '<p>{}</p>'.format(value)}
items.append(message)
self.show_interface(items, title, event=event)
return True
def register(session):
'''Register plugin. Called when used as an plugin.'''
ActionShowWhereIRun(session).register()

View file

@ -191,15 +191,22 @@ class BaseHandler(object):
if session is None: if session is None:
session = self.session session = self.session
_entities = event['data'].get('entities_object', None) _entities = event["data"].get("entities_object", None)
if _entities is not None and not _entities:
return _entities
if ( if (
_entities is None or _entities is None
_entities[0].get( or _entities[0].get(
'link', None "link", None
) == ftrack_api.symbol.NOT_SET ) == ftrack_api.symbol.NOT_SET
): ):
_entities = self._get_entities(event) _entities = [
event['data']['entities_object'] = _entities item
for item in self._get_entities(event)
if item is not None
]
event["data"]["entities_object"] = _entities
return _entities return _entities

View file

@ -44,7 +44,8 @@ class ExtractBurnin(openpype.api.Extractor):
"harmony", "harmony",
"fusion", "fusion",
"aftereffects", "aftereffects",
"tvpaint" "tvpaint",
"aftereffects"
# "resolve" # "resolve"
] ]
optional = True optional = True

View file

@ -44,7 +44,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
"standalonepublisher", "standalonepublisher",
"fusion", "fusion",
"tvpaint", "tvpaint",
"resolve" "resolve",
"aftereffects"
] ]
# Supported extensions # Supported extensions

View file

@ -113,6 +113,10 @@ def _h264_codec_args(ffprobe_data):
output.extend(["-codec:v", "h264"]) output.extend(["-codec:v", "h264"])
bit_rate = ffprobe_data.get("bit_rate")
if bit_rate:
output.extend(["-b:v", bit_rate])
pix_fmt = ffprobe_data.get("pix_fmt") pix_fmt = ffprobe_data.get("pix_fmt")
if pix_fmt: if pix_fmt:
output.extend(["-pix_fmt", pix_fmt]) output.extend(["-pix_fmt", pix_fmt])

View file

@ -11,6 +11,30 @@
"deadline" "deadline"
] ]
}, },
"ProcessSubmittedJobOnFarm": {
"enabled": true,
"deadline_department": "",
"deadline_pool": "",
"deadline_group": "",
"deadline_chunk_size": 1,
"deadline_priority": 50,
"publishing_script": "",
"skip_integration_repre_list": [],
"aov_filter": {
"maya": [
".+(?:\\.|_)([Bb]eauty)(?:\\.|_).*"
],
"nuke": [
".*"
],
"aftereffects": [
".*"
],
"celaction": [
".*"
]
}
},
"MayaSubmitDeadline": { "MayaSubmitDeadline": {
"enabled": true, "enabled": true,
"optional": false, "optional": false,

View file

@ -298,6 +298,17 @@
"add_ftrack_family": true "add_ftrack_family": true
} }
] ]
},
{
"hosts": [
"aftereffects"
],
"families": [
"render"
],
"tasks": [],
"add_ftrack_family": true,
"advanced_filtering": []
} }
] ]
}, },

View file

@ -173,28 +173,6 @@
} }
] ]
}, },
"ProcessSubmittedJobOnFarm": {
"enabled": true,
"deadline_department": "",
"deadline_pool": "",
"deadline_group": "",
"deadline_chunk_size": 1,
"deadline_priority": 50,
"publishing_script": "",
"skip_integration_repre_list": [],
"aov_filter": {
"maya": [
".+(?:\\.|_)([Bb]eauty)(?:\\.|_).*"
],
"nuke": [],
"aftereffects": [
".*"
],
"celaction": [
".*"
]
}
},
"CleanUp": { "CleanUp": {
"paterns": [], "paterns": [],
"remove_temp_renders": false "remove_temp_renders": false
@ -257,6 +235,16 @@
], ],
"tasks": [], "tasks": [],
"template": "{family}{Task}" "template": "{family}{Task}"
},
{
"families": [
"renderLocal"
],
"hosts": [
"aftereffects"
],
"tasks": [],
"template": "render{Task}{Variant}"
} }
] ]
}, },

View file

@ -52,6 +52,101 @@
} }
] ]
}, },
{
"type": "dict",
"collapsible": true,
"key": "ProcessSubmittedJobOnFarm",
"label": "ProcessSubmittedJobOnFarm",
"checkbox_key": "enabled",
"is_group": true,
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "text",
"key": "deadline_department",
"label": "Deadline department"
},
{
"type": "text",
"key": "deadline_pool",
"label": "Deadline Pool"
},
{
"type": "text",
"key": "deadline_group",
"label": "Deadline Group"
},
{
"type": "number",
"key": "deadline_chunk_size",
"label": "Deadline Chunk Size"
},
{
"type": "number",
"key": "deadline_priority",
"label": "Deadline Priotity"
},
{
"type": "splitter"
},
{
"type": "text",
"key": "publishing_script",
"label": "Publishing script path"
},
{
"type": "list",
"key": "skip_integration_repre_list",
"label": "Skip integration of representation with ext",
"object_type": {
"type": "text"
}
},
{
"type": "dict",
"key": "aov_filter",
"label": "Reviewable subsets filter",
"children": [
{
"type": "list",
"key": "maya",
"label": "Maya",
"object_type": {
"type": "text"
}
},
{
"type": "list",
"key": "nuke",
"label": "Nuke",
"object_type": {
"type": "text"
}
},
{
"type": "list",
"key": "aftereffects",
"label": "After Effects",
"object_type": {
"type": "text"
}
},
{
"type": "list",
"key": "celaction",
"label": "Celaction",
"object_type": {
"type": "text"
}
}
]
}
]
},
{ {
"type": "dict", "type": "dict",
"collapsible": true, "collapsible": true,

View file

@ -556,101 +556,6 @@
} }
] ]
}, },
{
"type": "dict",
"collapsible": true,
"key": "ProcessSubmittedJobOnFarm",
"label": "ProcessSubmittedJobOnFarm",
"checkbox_key": "enabled",
"is_group": true,
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "text",
"key": "deadline_department",
"label": "Deadline department"
},
{
"type": "text",
"key": "deadline_pool",
"label": "Deadline Pool"
},
{
"type": "text",
"key": "deadline_group",
"label": "Deadline Group"
},
{
"type": "number",
"key": "deadline_chunk_size",
"label": "Deadline Chunk Size"
},
{
"type": "number",
"key": "deadline_priority",
"label": "Deadline Priotity"
},
{
"type": "splitter"
},
{
"type": "text",
"key": "publishing_script",
"label": "Publishing script path"
},
{
"type": "list",
"key": "skip_integration_repre_list",
"label": "Skip integration of representation with ext",
"object_type": {
"type": "text"
}
},
{
"type": "dict",
"key": "aov_filter",
"label": "Reviewable subsets filter",
"children": [
{
"type": "list",
"key": "maya",
"label": "Maya",
"object_type": {
"type": "text"
}
},
{
"type": "list",
"key": "nuke",
"label": "Nuke",
"object_type": {
"type": "text"
}
},
{
"type": "list",
"key": "aftereffects",
"label": "After Effects",
"object_type": {
"type": "text"
}
},
{
"type": "list",
"key": "celaction",
"label": "Celaction",
"object_type": {
"type": "text"
}
}
]
}
]
},
{ {
"type": "dict", "type": "dict",
"collapsible": true, "collapsible": true,

View file

@ -316,6 +316,7 @@ class Controller(QtCore.QObject):
self.was_skipped.emit(plugin) self.was_skipped.emit(plugin)
continue continue
in_collect_stage = self.collect_state == 0
if plugin.__instanceEnabled__: if plugin.__instanceEnabled__:
instances = pyblish.logic.instances_by_plugin( instances = pyblish.logic.instances_by_plugin(
self.context, plugin self.context, plugin
@ -325,7 +326,10 @@ class Controller(QtCore.QObject):
continue continue
for instance in instances: for instance in instances:
if instance.data.get("publish") is False: if (
not in_collect_stage
and instance.data.get("publish") is False
):
pyblish.logic.log.debug( pyblish.logic.log.debug(
"%s was inactive, skipping.." % instance "%s was inactive, skipping.." % instance
) )
@ -338,7 +342,7 @@ class Controller(QtCore.QObject):
yield (plugin, instance) yield (plugin, instance)
else: else:
families = util.collect_families_from_instances( families = util.collect_families_from_instances(
self.context, only_active=True self.context, only_active=not in_collect_stage
) )
plugins = pyblish.logic.plugins_by_families( plugins = pyblish.logic.plugins_by_families(
[plugin], families [plugin], families

View file

@ -498,6 +498,9 @@ class PluginModel(QtGui.QStandardItemModel):
): ):
new_flag_states[PluginStates.HasError] = True new_flag_states[PluginStates.HasError] = True
if not publish_states & PluginStates.IsCompatible:
new_flag_states[PluginStates.IsCompatible] = True
item.setData(new_flag_states, Roles.PublishFlagsRole) item.setData(new_flag_states, Roles.PublishFlagsRole)
records = item.data(Roles.LogRecordsRole) or [] records = item.data(Roles.LogRecordsRole) or []

View file

@ -22,7 +22,7 @@ Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension w
## Implemented functionality ## Implemented functionality
AfterEffects implementation currently allows you to import and add various media to composition (image plates, renders, audio files, video files etc.) AfterEffects implementation currently allows you to import and add various media to composition (image plates, renders, audio files, video files etc.)
and send prepared composition for rendering to Deadline. and send prepared composition for rendering to Deadline or render locally.
## Usage ## Usage
@ -53,6 +53,12 @@ will be changed.
### Publish ### Publish
#### RenderQueue
AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. Currently its expected to have only single render item and single output module in the Render Queue.
AE might throw some warning windows during publishing locally, so please pay attention to them in a case publishing seems to be stuck in a `Extract Local Render`.
When you are ready to share your work, you will need to publish it. This is done by opening the `Publish` by clicking the corresponding button in the OpenPype Panel. When you are ready to share your work, you will need to publish it. This is done by opening the `Publish` by clicking the corresponding button in the OpenPype Panel.
![Publish](assets/aftereffects_publish.png) ![Publish](assets/aftereffects_publish.png)