mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into enhancement/1470-yn-0067-publisher-crashed-plugins
This commit is contained in:
commit
f2e014b3f8
17 changed files with 526 additions and 231 deletions
|
|
@ -6,7 +6,6 @@ import logging
|
|||
import code
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
import warnings
|
||||
|
||||
import click
|
||||
|
||||
|
|
@ -90,54 +89,6 @@ def addon(ctx):
|
|||
pass
|
||||
|
||||
|
||||
@main_cli.command()
|
||||
@click.pass_context
|
||||
@click.argument("output_json_path")
|
||||
@click.option("--project", help="Project name", default=None)
|
||||
@click.option("--asset", help="Folder path", default=None)
|
||||
@click.option("--task", help="Task name", default=None)
|
||||
@click.option("--app", help="Application name", default=None)
|
||||
@click.option(
|
||||
"--envgroup", help="Environment group (e.g. \"farm\")", default=None
|
||||
)
|
||||
def extractenvironments(
|
||||
ctx, output_json_path, project, asset, task, app, envgroup
|
||||
):
|
||||
"""Extract environment variables for entered context to a json file.
|
||||
|
||||
Entered output filepath will be created if does not exists.
|
||||
|
||||
All context options must be passed otherwise only AYON's global
|
||||
environments will be extracted.
|
||||
|
||||
Context options are "project", "asset", "task", "app"
|
||||
|
||||
Deprecated:
|
||||
This function is deprecated and will be removed in future. Please use
|
||||
'addon applications extractenvironments ...' instead.
|
||||
"""
|
||||
warnings.warn(
|
||||
(
|
||||
"Command 'extractenvironments' is deprecated and will be"
|
||||
" removed in future. Please use"
|
||||
" 'addon applications extractenvironments ...' instead."
|
||||
),
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
addons_manager = ctx.obj["addons_manager"]
|
||||
applications_addon = addons_manager.get_enabled_addon("applications")
|
||||
if applications_addon is None:
|
||||
raise RuntimeError(
|
||||
"Applications addon is not available or enabled."
|
||||
)
|
||||
|
||||
# Please ignore the fact this is using private method
|
||||
applications_addon._cli_extract_environments(
|
||||
output_json_path, project, asset, task, app, envgroup
|
||||
)
|
||||
|
||||
|
||||
@main_cli.command()
|
||||
@click.pass_context
|
||||
@click.argument("path", required=True)
|
||||
|
|
|
|||
|
|
@ -134,16 +134,29 @@ def get_transcode_temp_directory():
|
|||
)
|
||||
|
||||
|
||||
def get_oiio_info_for_input(filepath, logger=None, subimages=False):
|
||||
def get_oiio_info_for_input(
|
||||
filepath: str,
|
||||
*,
|
||||
subimages: bool = False,
|
||||
verbose: bool = True,
|
||||
logger: logging.Logger = None,
|
||||
):
|
||||
"""Call oiiotool to get information about input and return stdout.
|
||||
|
||||
Args:
|
||||
filepath (str): Path to file.
|
||||
subimages (bool): include info about subimages in the output.
|
||||
verbose (bool): get the full metadata about each input image.
|
||||
logger (logging.Logger): Logger used for logging.
|
||||
|
||||
Stdout should contain xml format string.
|
||||
"""
|
||||
args = get_oiio_tool_args(
|
||||
"oiiotool",
|
||||
"--info",
|
||||
"-v"
|
||||
)
|
||||
if verbose:
|
||||
args.append("-v")
|
||||
if subimages:
|
||||
args.append("-a")
|
||||
|
||||
|
|
@ -573,7 +586,10 @@ def get_review_layer_name(src_filepath):
|
|||
return None
|
||||
|
||||
# Load info about file from oiio tool
|
||||
input_info = get_oiio_info_for_input(src_filepath)
|
||||
input_info = get_oiio_info_for_input(
|
||||
src_filepath,
|
||||
verbose=False,
|
||||
)
|
||||
if not input_info:
|
||||
return None
|
||||
|
||||
|
|
@ -1234,7 +1250,11 @@ def oiio_color_convert(
|
|||
for token in ["#", "%d"]:
|
||||
first_input_path = first_input_path.replace(token, first_frame)
|
||||
|
||||
input_info = get_oiio_info_for_input(first_input_path, logger=logger)
|
||||
input_info = get_oiio_info_for_input(
|
||||
first_input_path,
|
||||
verbose=False,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
# Collect channels to export
|
||||
input_arg, channels_arg = get_oiio_input_and_channel_args(input_info)
|
||||
|
|
@ -1448,7 +1468,11 @@ def get_rescaled_command_arguments(
|
|||
command_args.extend(["-vf", "{0},{1}".format(scale, pad)])
|
||||
|
||||
elif application == "oiiotool":
|
||||
input_info = get_oiio_info_for_input(input_path, logger=log)
|
||||
input_info = get_oiio_info_for_input(
|
||||
input_path,
|
||||
verbose=False,
|
||||
logger=log,
|
||||
)
|
||||
# Collect channels to export
|
||||
_, channels_arg = get_oiio_input_and_channel_args(
|
||||
input_info, alpha_default=1.0)
|
||||
|
|
@ -1539,7 +1563,11 @@ def _get_image_dimensions(application, input_path, log):
|
|||
# fallback for weird files with width=0, height=0
|
||||
if (input_width == 0 or input_height == 0) and application == "oiiotool":
|
||||
# Load info about file from oiio tool
|
||||
input_info = get_oiio_info_for_input(input_path, logger=log)
|
||||
input_info = get_oiio_info_for_input(
|
||||
input_path,
|
||||
verbose=False,
|
||||
logger=log,
|
||||
)
|
||||
if input_info:
|
||||
input_width = int(input_info["width"])
|
||||
input_height = int(input_info["height"])
|
||||
|
|
@ -1588,10 +1616,13 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None):
|
|||
"""Get input and channel arguments for oiiotool.
|
||||
Args:
|
||||
oiio_input_info (dict): Information about input from oiio tool.
|
||||
Should be output of function `get_oiio_info_for_input`.
|
||||
Should be output of function 'get_oiio_info_for_input' (can be
|
||||
called with 'verbose=False').
|
||||
alpha_default (float, optional): Default value for alpha channel.
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: Tuple of input and channel arguments.
|
||||
|
||||
"""
|
||||
channel_names = oiio_input_info["channelnames"]
|
||||
review_channels = get_convert_rgb_channels(channel_names)
|
||||
|
|
|
|||
|
|
@ -146,7 +146,15 @@ class BaseCreator(ABC):
|
|||
project_settings (dict[str, Any]): Project settings.
|
||||
create_context (CreateContext): Context which initialized creator.
|
||||
headless (bool): Running in headless mode.
|
||||
|
||||
"""
|
||||
# Attribute 'skip_discovery' is used during discovery phase to skip
|
||||
# plugins, which can be used to mark base plugins that should not be
|
||||
# considered as plugins "to use". The discovery logic does NOT use
|
||||
# the attribute value from parent classes. Each base class has to define
|
||||
# the attribute again.
|
||||
skip_discovery = True
|
||||
|
||||
# Label shown in UI
|
||||
label = None
|
||||
group_label = None
|
||||
|
|
@ -642,7 +650,7 @@ class Creator(BaseCreator):
|
|||
|
||||
Creation requires prepared product name and instance data.
|
||||
"""
|
||||
|
||||
skip_discovery = True
|
||||
# GUI Purposes
|
||||
# - default_variants may not be used if `get_default_variants`
|
||||
# is overridden
|
||||
|
|
@ -931,6 +939,8 @@ class Creator(BaseCreator):
|
|||
|
||||
|
||||
class HiddenCreator(BaseCreator):
|
||||
skip_discovery = True
|
||||
|
||||
@abstractmethod
|
||||
def create(self, instance_data, source_data):
|
||||
pass
|
||||
|
|
@ -941,6 +951,7 @@ class AutoCreator(BaseCreator):
|
|||
|
||||
Can be used e.g. for `workfile`.
|
||||
"""
|
||||
skip_discovery = True
|
||||
|
||||
def remove_instances(self, instances):
|
||||
"""Skip removal."""
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@ from .utils import get_representation_path_from_context
|
|||
class LoaderPlugin(list):
|
||||
"""Load representation into host application"""
|
||||
|
||||
# Attribute 'skip_discovery' is used during discovery phase to skip
|
||||
# plugins, which can be used to mark base plugins that should not be
|
||||
# considered as plugins "to use". The discovery logic does NOT use
|
||||
# the attribute value from parent classes. Each base class has to define
|
||||
# the attribute again.
|
||||
skip_discovery = True
|
||||
|
||||
product_types: set[str] = set()
|
||||
product_base_types: Optional[set[str]] = None
|
||||
representations = set()
|
||||
|
|
|
|||
|
|
@ -138,7 +138,14 @@ def discover_plugins(
|
|||
for item in modules:
|
||||
filepath, module = item
|
||||
result.add_module(module)
|
||||
all_plugins.extend(classes_from_module(base_class, module))
|
||||
for cls in classes_from_module(base_class, module):
|
||||
if cls is base_class:
|
||||
continue
|
||||
# Class has defined 'skip_discovery = True'
|
||||
skip_discovery = cls.__dict__.get("skip_discovery")
|
||||
if skip_discovery is True:
|
||||
continue
|
||||
all_plugins.append(cls)
|
||||
|
||||
if base_class not in ignored_classes:
|
||||
ignored_classes.append(base_class)
|
||||
|
|
|
|||
|
|
@ -299,7 +299,6 @@ def add_ordered_sublayer(layer, contribution_path, layer_id, order=None,
|
|||
sdf format args metadata if enabled)
|
||||
|
||||
"""
|
||||
|
||||
# Add the order with the contribution path so that for future
|
||||
# contributions we can again use it to magically fit into the
|
||||
# ordering. We put this in the path because sublayer paths do
|
||||
|
|
@ -317,20 +316,25 @@ def add_ordered_sublayer(layer, contribution_path, layer_id, order=None,
|
|||
# If the layer was already in the layers, then replace it
|
||||
for index, existing_path in enumerate(layer.subLayerPaths):
|
||||
args = get_sdf_format_args(existing_path)
|
||||
existing_layer = args.get("layer_id")
|
||||
if existing_layer == layer_id:
|
||||
existing_layer_id = args.get("layer_id")
|
||||
if existing_layer_id == layer_id:
|
||||
existing_layer = layer.subLayerPaths[index]
|
||||
existing_order = args.get("order")
|
||||
existing_order = int(existing_order) if existing_order else None
|
||||
if order is not None and order != existing_order:
|
||||
# We need to move the layer, so we will remove this index
|
||||
# and then re-insert it below at the right order
|
||||
log.debug(f"Removing existing layer: {existing_layer}")
|
||||
del layer.subLayerPaths[index]
|
||||
break
|
||||
|
||||
# Put it in the same position where it was before when swapping
|
||||
# it with the original, also take over its order metadata
|
||||
order = args.get("order")
|
||||
if order is not None:
|
||||
order = int(order)
|
||||
else:
|
||||
order = None
|
||||
contribution_path = _format_path(contribution_path,
|
||||
order=order,
|
||||
order=existing_order,
|
||||
layer_id=layer_id)
|
||||
log.debug(
|
||||
f"Replacing existing layer: {layer.subLayerPaths[index]} "
|
||||
f"Replacing existing layer: {existing_layer} "
|
||||
f"-> {contribution_path}"
|
||||
)
|
||||
layer.subLayerPaths[index] = contribution_path
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import annotations
|
||||
from typing import Any
|
||||
import ayon_api
|
||||
import ayon_api.utils
|
||||
|
||||
|
|
@ -32,6 +34,8 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin):
|
|||
self.log.debug("No loaded containers found in scene.")
|
||||
return
|
||||
|
||||
containers = self._filter_invalid_containers(containers)
|
||||
|
||||
repre_ids = {
|
||||
container["representation"]
|
||||
for container in containers
|
||||
|
|
@ -78,3 +82,28 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin):
|
|||
|
||||
self.log.debug(f"Collected {len(loaded_versions)} loaded versions.")
|
||||
context.data["loadedVersions"] = loaded_versions
|
||||
|
||||
def _filter_invalid_containers(
|
||||
self,
|
||||
containers: list[dict[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Filter out invalid containers lacking required keys.
|
||||
|
||||
Skip any invalid containers that lack 'representation' or 'name'
|
||||
keys to avoid KeyError.
|
||||
"""
|
||||
# Only filter by what's required for this plug-in instead of validating
|
||||
# a full container schema.
|
||||
required_keys = {"name", "representation"}
|
||||
valid = []
|
||||
for container in containers:
|
||||
missing = [key for key in required_keys if key not in container]
|
||||
if missing:
|
||||
self.log.warning(
|
||||
"Skipping invalid container, missing required keys:"
|
||||
" {}. {}".format(", ".join(missing), container)
|
||||
)
|
||||
continue
|
||||
valid.append(container)
|
||||
|
||||
return valid
|
||||
|
|
|
|||
|
|
@ -169,7 +169,9 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
|
||||
settings_category = "core"
|
||||
# Supported extensions
|
||||
image_exts = {"exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif"}
|
||||
image_exts = {
|
||||
"exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif", "psd"
|
||||
}
|
||||
video_exts = {"mov", "mp4"}
|
||||
supported_exts = image_exts | video_exts
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import copy
|
||||
from dataclasses import dataclass, field, fields
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import re
|
||||
from typing import Dict, Any, List, Tuple, Optional
|
||||
|
||||
import pyblish.api
|
||||
from ayon_core.lib import (
|
||||
|
|
@ -15,6 +16,7 @@ from ayon_core.lib import (
|
|||
|
||||
path_to_subprocess_arg,
|
||||
run_subprocess,
|
||||
filter_profiles,
|
||||
)
|
||||
from ayon_core.lib.transcoding import (
|
||||
MissingRGBAChannelsError,
|
||||
|
|
@ -26,6 +28,61 @@ from ayon_core.lib.transcoding import (
|
|||
from ayon_core.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThumbnailDef:
|
||||
"""
|
||||
Data class representing the full configuration for selected profile
|
||||
|
||||
Any change of controllable fields in Settings must propagate here!
|
||||
"""
|
||||
integrate_thumbnail: bool = False
|
||||
|
||||
target_size: Dict[str, Any] = field(
|
||||
default_factory=lambda: {
|
||||
"type": "source",
|
||||
"resize": {"width": 1920, "height": 1080},
|
||||
}
|
||||
)
|
||||
|
||||
duration_split: float = 0.5
|
||||
|
||||
oiiotool_defaults: Dict[str, str] = field(
|
||||
default_factory=lambda: {
|
||||
"type": "colorspace",
|
||||
"colorspace": "color_picking"
|
||||
}
|
||||
)
|
||||
|
||||
ffmpeg_args: Dict[str, List[Any]] = field(
|
||||
default_factory=lambda: {"input": [], "output": []}
|
||||
)
|
||||
|
||||
# Background color defined as (R, G, B, A) tuple.
|
||||
# Note: Use float for alpha channel (0.0 to 1.0).
|
||||
background_color: Tuple[int, int, int, float] = (0, 0, 0, 0.0)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ThumbnailDef":
|
||||
"""
|
||||
Creates a ThumbnailDef instance from a dictionary, safely ignoring
|
||||
any keys in the dictionary that are not fields in the dataclass.
|
||||
|
||||
Args:
|
||||
data (Dict[str, Any]): The dictionary containing configuration data
|
||||
|
||||
Returns:
|
||||
MediaConfig: A new instance of the dataclass.
|
||||
"""
|
||||
# Get all field names defined in the dataclass
|
||||
field_names = {f.name for f in fields(cls)}
|
||||
|
||||
# Filter the input dictionary to include only keys matching field names
|
||||
filtered_data = {k: v for k, v in data.items() if k in field_names}
|
||||
|
||||
# Unpack the filtered dictionary into the constructor
|
||||
return cls(**filtered_data)
|
||||
|
||||
|
||||
class ExtractThumbnail(pyblish.api.InstancePlugin):
|
||||
"""Create jpg thumbnail from sequence using ffmpeg"""
|
||||
|
||||
|
|
@ -48,35 +105,11 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
"unreal",
|
||||
"houdini",
|
||||
"batchdelivery",
|
||||
"webpublisher",
|
||||
]
|
||||
settings_category = "core"
|
||||
enabled = False
|
||||
|
||||
integrate_thumbnail = False
|
||||
target_size = {
|
||||
"type": "source",
|
||||
"resize": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
}
|
||||
}
|
||||
background_color = (0, 0, 0, 0.0)
|
||||
duration_split = 0.5
|
||||
# attribute presets from settings
|
||||
oiiotool_defaults = {
|
||||
"type": "colorspace",
|
||||
"colorspace": "color_picking",
|
||||
"display_and_view": {
|
||||
"display": "default",
|
||||
"view": "sRGB"
|
||||
}
|
||||
}
|
||||
ffmpeg_args = {
|
||||
"input": [],
|
||||
"output": []
|
||||
}
|
||||
product_names = []
|
||||
profiles = []
|
||||
|
||||
def process(self, instance):
|
||||
# run main process
|
||||
|
|
@ -99,6 +132,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
instance.data["representations"].remove(repre)
|
||||
|
||||
def _main_process(self, instance):
|
||||
if not self.profiles:
|
||||
self.log.debug("No profiles present for extract review thumbnail.")
|
||||
return
|
||||
thumbnail_def = self._get_config_from_profile(instance)
|
||||
if not thumbnail_def:
|
||||
return
|
||||
|
||||
product_name = instance.data["productName"]
|
||||
instance_repres = instance.data.get("representations")
|
||||
if not instance_repres:
|
||||
|
|
@ -131,24 +171,6 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
self.log.debug("Skipping crypto passes.")
|
||||
return
|
||||
|
||||
# We only want to process the produces needed from settings.
|
||||
def validate_string_against_patterns(input_str, patterns):
|
||||
for pattern in patterns:
|
||||
if re.match(pattern, input_str):
|
||||
return True
|
||||
return False
|
||||
|
||||
product_names = self.product_names
|
||||
if product_names:
|
||||
result = validate_string_against_patterns(
|
||||
product_name, product_names
|
||||
)
|
||||
if not result:
|
||||
self.log.debug((
|
||||
"Product name \"{}\" did not match settings filters: {}"
|
||||
).format(product_name, product_names))
|
||||
return
|
||||
|
||||
# first check for any explicitly marked representations for thumbnail
|
||||
explicit_repres = self._get_explicit_repres_for_thumbnail(instance)
|
||||
if explicit_repres:
|
||||
|
|
@ -193,7 +215,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
)
|
||||
file_path = self._create_frame_from_video(
|
||||
video_file_path,
|
||||
dst_staging
|
||||
dst_staging,
|
||||
thumbnail_def
|
||||
)
|
||||
if file_path:
|
||||
src_staging, input_file = os.path.split(file_path)
|
||||
|
|
@ -206,7 +229,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
if "slate-frame" in repre.get("tags", []):
|
||||
repre_files_thumb = repre_files_thumb[1:]
|
||||
file_index = int(
|
||||
float(len(repre_files_thumb)) * self.duration_split)
|
||||
float(len(repre_files_thumb)) * thumbnail_def.duration_split # noqa: E501
|
||||
)
|
||||
input_file = repre_files[file_index]
|
||||
|
||||
full_input_path = os.path.join(src_staging, input_file)
|
||||
|
|
@ -243,13 +267,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
# colorspace data
|
||||
if not repre_thumb_created:
|
||||
repre_thumb_created = self._create_thumbnail_ffmpeg(
|
||||
full_input_path, full_output_path
|
||||
full_input_path, full_output_path, thumbnail_def
|
||||
)
|
||||
|
||||
# Skip representation and try next one if wasn't created
|
||||
if not repre_thumb_created and oiio_supported:
|
||||
repre_thumb_created = self._create_thumbnail_oiio(
|
||||
full_input_path, full_output_path
|
||||
full_input_path, full_output_path, thumbnail_def
|
||||
)
|
||||
|
||||
if not repre_thumb_created:
|
||||
|
|
@ -277,7 +301,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
new_repre_tags = ["thumbnail"]
|
||||
# for workflows which needs to have thumbnails published as
|
||||
# separate representations `delete` tag should not be added
|
||||
if not self.integrate_thumbnail:
|
||||
if not thumbnail_def.integrate_thumbnail:
|
||||
new_repre_tags.append("delete")
|
||||
|
||||
new_repre = {
|
||||
|
|
@ -399,6 +423,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
src_path,
|
||||
dst_path,
|
||||
colorspace_data,
|
||||
thumbnail_def
|
||||
):
|
||||
"""Create thumbnail using OIIO tool oiiotool
|
||||
|
||||
|
|
@ -416,7 +441,9 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
str: path to created thumbnail
|
||||
"""
|
||||
self.log.info("Extracting thumbnail {}".format(dst_path))
|
||||
resolution_arg = self._get_resolution_arg("oiiotool", src_path)
|
||||
resolution_arg = self._get_resolution_args(
|
||||
"oiiotool", src_path, thumbnail_def
|
||||
)
|
||||
|
||||
repre_display = colorspace_data.get("display")
|
||||
repre_view = colorspace_data.get("view")
|
||||
|
|
@ -435,12 +462,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
)
|
||||
# if representation doesn't have display and view then use
|
||||
# oiiotool_defaults
|
||||
elif self.oiiotool_defaults:
|
||||
oiio_default_type = self.oiiotool_defaults["type"]
|
||||
elif thumbnail_def.oiiotool_defaults:
|
||||
oiiotool_defaults = thumbnail_def.oiiotool_defaults
|
||||
oiio_default_type = oiiotool_defaults["type"]
|
||||
if "colorspace" == oiio_default_type:
|
||||
oiio_default_colorspace = self.oiiotool_defaults["colorspace"]
|
||||
oiio_default_colorspace = oiiotool_defaults["colorspace"]
|
||||
else:
|
||||
display_and_view = self.oiiotool_defaults["display_and_view"]
|
||||
display_and_view = oiiotool_defaults["display_and_view"]
|
||||
oiio_default_display = display_and_view["display"]
|
||||
oiio_default_view = display_and_view["view"]
|
||||
|
||||
|
|
@ -467,18 +495,24 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
|
||||
return True
|
||||
|
||||
def _create_thumbnail_oiio(self, src_path, dst_path):
|
||||
def _create_thumbnail_oiio(self, src_path, dst_path, thumbnail_def):
|
||||
self.log.debug(f"Extracting thumbnail with OIIO: {dst_path}")
|
||||
|
||||
try:
|
||||
resolution_arg = self._get_resolution_arg("oiiotool", src_path)
|
||||
resolution_arg = self._get_resolution_args(
|
||||
"oiiotool", src_path, thumbnail_def
|
||||
)
|
||||
except RuntimeError:
|
||||
self.log.warning(
|
||||
"Failed to create thumbnail using oiio", exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
input_info = get_oiio_info_for_input(src_path, logger=self.log)
|
||||
input_info = get_oiio_info_for_input(
|
||||
src_path,
|
||||
logger=self.log,
|
||||
verbose=False,
|
||||
)
|
||||
try:
|
||||
input_arg, channels_arg = get_oiio_input_and_channel_args(
|
||||
input_info
|
||||
|
|
@ -511,9 +545,11 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
)
|
||||
return False
|
||||
|
||||
def _create_thumbnail_ffmpeg(self, src_path, dst_path):
|
||||
def _create_thumbnail_ffmpeg(self, src_path, dst_path, thumbnail_def):
|
||||
try:
|
||||
resolution_arg = self._get_resolution_arg("ffmpeg", src_path)
|
||||
resolution_arg = self._get_resolution_args(
|
||||
"ffmpeg", src_path, thumbnail_def
|
||||
)
|
||||
except RuntimeError:
|
||||
self.log.warning(
|
||||
"Failed to create thumbnail using ffmpeg", exc_info=True
|
||||
|
|
@ -521,7 +557,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
return False
|
||||
|
||||
ffmpeg_path_args = get_ffmpeg_tool_args("ffmpeg")
|
||||
ffmpeg_args = self.ffmpeg_args or {}
|
||||
ffmpeg_args = thumbnail_def.ffmpeg_args or {}
|
||||
|
||||
jpeg_items = [
|
||||
subprocess.list2cmdline(ffmpeg_path_args)
|
||||
|
|
@ -561,7 +597,12 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
)
|
||||
return False
|
||||
|
||||
def _create_frame_from_video(self, video_file_path, output_dir):
|
||||
def _create_frame_from_video(
|
||||
self,
|
||||
video_file_path,
|
||||
output_dir,
|
||||
thumbnail_def
|
||||
):
|
||||
"""Convert video file to one frame image via ffmpeg"""
|
||||
# create output file path
|
||||
base_name = os.path.basename(video_file_path)
|
||||
|
|
@ -586,7 +627,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
seek_position = 0.0
|
||||
# Only use timestamp calculation for videos longer than 0.1 seconds
|
||||
if duration > 0.1:
|
||||
seek_position = duration * self.duration_split
|
||||
seek_position = duration * thumbnail_def.duration_split
|
||||
|
||||
# Build command args
|
||||
cmd_args = []
|
||||
|
|
@ -660,16 +701,17 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
):
|
||||
os.remove(output_thumb_file_path)
|
||||
|
||||
def _get_resolution_arg(
|
||||
def _get_resolution_args(
|
||||
self,
|
||||
application,
|
||||
input_path,
|
||||
thumbnail_def
|
||||
):
|
||||
# get settings
|
||||
if self.target_size["type"] == "source":
|
||||
if thumbnail_def.target_size["type"] == "source":
|
||||
return []
|
||||
|
||||
resize = self.target_size["resize"]
|
||||
resize = thumbnail_def.target_size["resize"]
|
||||
target_width = resize["width"]
|
||||
target_height = resize["height"]
|
||||
|
||||
|
|
@ -679,6 +721,43 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
input_path,
|
||||
target_width,
|
||||
target_height,
|
||||
bg_color=self.background_color,
|
||||
bg_color=thumbnail_def.background_color,
|
||||
log=self.log
|
||||
)
|
||||
|
||||
def _get_config_from_profile(
|
||||
self,
|
||||
instance: pyblish.api.Instance
|
||||
) -> Optional[ThumbnailDef]:
|
||||
"""Returns profile if and how repre should be color transcoded."""
|
||||
host_name = instance.context.data["hostName"]
|
||||
product_type = instance.data["productType"]
|
||||
product_name = instance.data["productName"]
|
||||
task_data = instance.data["anatomyData"].get("task", {})
|
||||
task_name = task_data.get("name")
|
||||
task_type = task_data.get("type")
|
||||
filtering_criteria = {
|
||||
"host_names": host_name,
|
||||
"product_types": product_type,
|
||||
"product_names": product_name,
|
||||
"task_names": task_name,
|
||||
"task_types": task_type,
|
||||
}
|
||||
profile = filter_profiles(
|
||||
self.profiles,
|
||||
filtering_criteria,
|
||||
logger=self.log
|
||||
)
|
||||
|
||||
if not profile:
|
||||
self.log.debug(
|
||||
"Skipped instance. None of profiles in presets are for"
|
||||
f' Host: "{host_name}"'
|
||||
f' | Product types: "{product_type}"'
|
||||
f' | Product names: "{product_name}"'
|
||||
f' | Task name "{task_name}"'
|
||||
f' | Task type "{task_type}"'
|
||||
)
|
||||
return None
|
||||
|
||||
return ThumbnailDef.from_dict(profile)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ Todos:
|
|||
|
||||
import os
|
||||
import tempfile
|
||||
from typing import List, Optional
|
||||
|
||||
import pyblish.api
|
||||
from ayon_core.lib import (
|
||||
|
|
@ -22,6 +23,7 @@ from ayon_core.lib import (
|
|||
is_oiio_supported,
|
||||
|
||||
run_subprocess,
|
||||
get_rescaled_command_arguments,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -31,17 +33,20 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
Thumbnail source must be a single image or video filepath.
|
||||
"""
|
||||
|
||||
label = "Extract Thumbnail (from source)"
|
||||
label = "Extract Thumbnail from source"
|
||||
# Before 'ExtractThumbnail' in global plugins
|
||||
order = pyblish.api.ExtractorOrder - 0.00001
|
||||
|
||||
def process(self, instance):
|
||||
# Settings
|
||||
target_size = {
|
||||
"type": "resize",
|
||||
"resize": {"width": 1920, "height": 1080}
|
||||
}
|
||||
background_color = (0, 0, 0, 0.0)
|
||||
|
||||
def process(self, instance: pyblish.api.Instance):
|
||||
self._create_context_thumbnail(instance.context)
|
||||
|
||||
product_name = instance.data["productName"]
|
||||
self.log.debug(
|
||||
"Processing instance with product name {}".format(product_name)
|
||||
)
|
||||
thumbnail_source = instance.data.get("thumbnailSource")
|
||||
if not thumbnail_source:
|
||||
self.log.debug("Thumbnail source not filled. Skipping.")
|
||||
|
|
@ -69,6 +74,8 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
"outputName": "thumbnail",
|
||||
}
|
||||
|
||||
new_repre["tags"].append("delete")
|
||||
|
||||
# adding representation
|
||||
self.log.debug(
|
||||
"Adding thumbnail representation: {}".format(new_repre)
|
||||
|
|
@ -76,7 +83,11 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
instance.data["representations"].append(new_repre)
|
||||
instance.data["thumbnailPath"] = dst_filepath
|
||||
|
||||
def _create_thumbnail(self, context, thumbnail_source):
|
||||
def _create_thumbnail(
|
||||
self,
|
||||
context: pyblish.api.Context,
|
||||
thumbnail_source: str,
|
||||
) -> Optional[str]:
|
||||
if not thumbnail_source:
|
||||
self.log.debug("Thumbnail source not filled. Skipping.")
|
||||
return
|
||||
|
|
@ -131,7 +142,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
|
||||
self.log.warning("Thumbnail has not been created.")
|
||||
|
||||
def _instance_has_thumbnail(self, instance):
|
||||
def _instance_has_thumbnail(self, instance: pyblish.api.Instance) -> bool:
|
||||
if "representations" not in instance.data:
|
||||
self.log.warning(
|
||||
"Instance does not have 'representations' key filled"
|
||||
|
|
@ -143,14 +154,24 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
return True
|
||||
return False
|
||||
|
||||
def create_thumbnail_oiio(self, src_path, dst_path):
|
||||
def create_thumbnail_oiio(
|
||||
self,
|
||||
src_path: str,
|
||||
dst_path: str,
|
||||
) -> bool:
|
||||
self.log.debug("Outputting thumbnail with OIIO: {}".format(dst_path))
|
||||
oiio_cmd = get_oiio_tool_args(
|
||||
"oiiotool",
|
||||
"-a", src_path,
|
||||
"--ch", "R,G,B",
|
||||
"-o", dst_path
|
||||
resolution_args = self._get_resolution_args(
|
||||
"oiiotool", src_path
|
||||
)
|
||||
oiio_cmd = get_oiio_tool_args("oiiotool", "-a", src_path)
|
||||
if resolution_args:
|
||||
# resize must be before -o
|
||||
oiio_cmd.extend(resolution_args)
|
||||
else:
|
||||
# resize provides own -ch, must be only one
|
||||
oiio_cmd.extend(["--ch", "R,G,B"])
|
||||
|
||||
oiio_cmd.extend(["-o", dst_path])
|
||||
self.log.debug("Running: {}".format(" ".join(oiio_cmd)))
|
||||
try:
|
||||
run_subprocess(oiio_cmd, logger=self.log)
|
||||
|
|
@ -162,7 +183,15 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
)
|
||||
return False
|
||||
|
||||
def create_thumbnail_ffmpeg(self, src_path, dst_path):
|
||||
def create_thumbnail_ffmpeg(
|
||||
self,
|
||||
src_path: str,
|
||||
dst_path: str,
|
||||
) -> bool:
|
||||
resolution_args = self._get_resolution_args(
|
||||
"ffmpeg", src_path
|
||||
)
|
||||
|
||||
max_int = str(2147483647)
|
||||
ffmpeg_cmd = get_ffmpeg_tool_args(
|
||||
"ffmpeg",
|
||||
|
|
@ -171,9 +200,13 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
"-probesize", max_int,
|
||||
"-i", src_path,
|
||||
"-frames:v", "1",
|
||||
dst_path
|
||||
)
|
||||
|
||||
ffmpeg_cmd.extend(resolution_args)
|
||||
|
||||
# possible resize must be before output args
|
||||
ffmpeg_cmd.append(dst_path)
|
||||
|
||||
self.log.debug("Running: {}".format(" ".join(ffmpeg_cmd)))
|
||||
try:
|
||||
run_subprocess(ffmpeg_cmd, logger=self.log)
|
||||
|
|
@ -185,10 +218,37 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
)
|
||||
return False
|
||||
|
||||
def _create_context_thumbnail(self, context):
|
||||
def _create_context_thumbnail(
|
||||
self,
|
||||
context: pyblish.api.Context,
|
||||
):
|
||||
if "thumbnailPath" in context.data:
|
||||
return
|
||||
|
||||
thumbnail_source = context.data.get("thumbnailSource")
|
||||
thumbnail_path = self._create_thumbnail(context, thumbnail_source)
|
||||
context.data["thumbnailPath"] = thumbnail_path
|
||||
context.data["thumbnailPath"] = self._create_thumbnail(
|
||||
context, thumbnail_source
|
||||
)
|
||||
|
||||
def _get_resolution_args(
|
||||
self,
|
||||
application: str,
|
||||
input_path: str,
|
||||
) -> List[str]:
|
||||
# get settings
|
||||
if self.target_size["type"] == "source":
|
||||
return []
|
||||
|
||||
resize = self.target_size["resize"]
|
||||
target_width = resize["width"]
|
||||
target_height = resize["height"]
|
||||
|
||||
# form arg string per application
|
||||
return get_rescaled_command_arguments(
|
||||
application,
|
||||
input_path,
|
||||
target_width,
|
||||
target_height,
|
||||
bg_color=self.background_color,
|
||||
log=self.log,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from ayon_core.lib import (
|
|||
UISeparatorDef,
|
||||
UILabelDef,
|
||||
EnumDef,
|
||||
filter_profiles
|
||||
filter_profiles, NumberDef
|
||||
)
|
||||
try:
|
||||
from ayon_core.pipeline.usdlib import (
|
||||
|
|
@ -275,7 +275,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
# the contributions so that we can design a system where custom
|
||||
# contributions outside the predefined orders are possible to be
|
||||
# managed. So that if a particular asset requires an extra contribution
|
||||
# level, you can add itdirectly from the publisher at that particular
|
||||
# level, you can add it directly from the publisher at that particular
|
||||
# order. Future publishes will then see the existing contribution and will
|
||||
# persist adding it to future bootstraps at that order
|
||||
contribution_layers: Dict[str, int] = {
|
||||
|
|
@ -334,10 +334,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
attr_values[key] = attr_values[key].format(**data)
|
||||
|
||||
# Define contribution
|
||||
order = self.contribution_layers.get(
|
||||
attr_values["contribution_layer"], 0
|
||||
)
|
||||
|
||||
in_layer_order: int = attr_values.get("contribution_in_layer_order", 0)
|
||||
if attr_values["contribution_apply_as_variant"]:
|
||||
contribution = VariantContribution(
|
||||
instance=instance,
|
||||
|
|
@ -346,18 +343,21 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
variant_set_name=attr_values["contribution_variant_set_name"],
|
||||
variant_name=attr_values["contribution_variant"],
|
||||
variant_is_default=attr_values["contribution_variant_is_default"], # noqa: E501
|
||||
order=order
|
||||
order=in_layer_order
|
||||
)
|
||||
else:
|
||||
contribution = SublayerContribution(
|
||||
instance=instance,
|
||||
layer_id=attr_values["contribution_layer"],
|
||||
target_product=attr_values["contribution_target_product"],
|
||||
order=order
|
||||
order=in_layer_order
|
||||
)
|
||||
|
||||
asset_product = contribution.target_product
|
||||
layer_product = "{}_{}".format(asset_product, contribution.layer_id)
|
||||
layer_order: int = self.contribution_layers.get(
|
||||
attr_values["contribution_layer"], 0
|
||||
)
|
||||
|
||||
# Layer contribution instance
|
||||
layer_instance = self.get_or_create_instance(
|
||||
|
|
@ -370,7 +370,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
contribution
|
||||
)
|
||||
layer_instance.data["usd_layer_id"] = contribution.layer_id
|
||||
layer_instance.data["usd_layer_order"] = contribution.order
|
||||
layer_instance.data["usd_layer_order"] = layer_order
|
||||
|
||||
layer_instance.data["productGroup"] = (
|
||||
instance.data.get("productGroup") or "USD Layer"
|
||||
|
|
@ -561,6 +561,19 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
items=list(cls.contribution_layers.keys()),
|
||||
default=default_contribution_layer,
|
||||
visible=visible),
|
||||
# TODO: We may want to make the visibility of this optional
|
||||
# based on studio preference, to avoid complexity when not needed
|
||||
NumberDef("contribution_in_layer_order",
|
||||
label="Strength order",
|
||||
tooltip=(
|
||||
"The contribution inside the department layer will be "
|
||||
"made with this offset applied. A higher number means "
|
||||
"a stronger opinion."
|
||||
),
|
||||
default=0,
|
||||
minimum=-99999,
|
||||
maximum=99999,
|
||||
visible=visible),
|
||||
BoolDef("contribution_apply_as_variant",
|
||||
label="Add as variant",
|
||||
tooltip=(
|
||||
|
|
@ -729,7 +742,7 @@ class ExtractUSDLayerContribution(publish.Extractor):
|
|||
layer=sdf_layer,
|
||||
contribution_path=path,
|
||||
layer_id=product_name,
|
||||
order=None, # unordered
|
||||
order=contribution.order,
|
||||
add_sdf_arguments_metadata=True
|
||||
)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Optional
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -13,8 +15,8 @@ class TabItem:
|
|||
class InterpreterConfig:
|
||||
width: Optional[int]
|
||||
height: Optional[int]
|
||||
splitter_sizes: List[int] = field(default_factory=list)
|
||||
tabs: List[TabItem] = field(default_factory=list)
|
||||
splitter_sizes: list[int] = field(default_factory=list)
|
||||
tabs: list[TabItem] = field(default_factory=list)
|
||||
|
||||
|
||||
class AbstractInterpreterController(ABC):
|
||||
|
|
@ -27,7 +29,7 @@ class AbstractInterpreterController(ABC):
|
|||
self,
|
||||
width: int,
|
||||
height: int,
|
||||
splitter_sizes: List[int],
|
||||
tabs: List[Dict[str, str]],
|
||||
):
|
||||
splitter_sizes: list[int],
|
||||
tabs: list[dict[str, str]],
|
||||
) -> None:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from typing import List, Dict
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
from ayon_core.lib import JSONSettingRegistry
|
||||
from ayon_core.lib.local_settings import get_launcher_local_dir
|
||||
|
|
@ -11,13 +12,15 @@ from .abstract import (
|
|||
|
||||
|
||||
class InterpreterController(AbstractInterpreterController):
|
||||
def __init__(self):
|
||||
def __init__(self, name: Optional[str] = None) -> None:
|
||||
if name is None:
|
||||
name = "python_interpreter_tool"
|
||||
self._registry = JSONSettingRegistry(
|
||||
"python_interpreter_tool",
|
||||
name,
|
||||
get_launcher_local_dir(),
|
||||
)
|
||||
|
||||
def get_config(self):
|
||||
def get_config(self) -> InterpreterConfig:
|
||||
width = None
|
||||
height = None
|
||||
splitter_sizes = []
|
||||
|
|
@ -54,9 +57,9 @@ class InterpreterController(AbstractInterpreterController):
|
|||
self,
|
||||
width: int,
|
||||
height: int,
|
||||
splitter_sizes: List[int],
|
||||
tabs: List[Dict[str, str]],
|
||||
):
|
||||
splitter_sizes: list[int],
|
||||
tabs: list[dict[str, str]],
|
||||
) -> None:
|
||||
self._registry.set_item("width", width)
|
||||
self._registry.set_item("height", height)
|
||||
self._registry.set_item("splitter_sizes", splitter_sizes)
|
||||
|
|
|
|||
|
|
@ -1,42 +1,42 @@
|
|||
import os
|
||||
import sys
|
||||
import collections
|
||||
|
||||
|
||||
class _CustomSTD:
|
||||
def __init__(self, orig_std, write_callback):
|
||||
self.orig_std = orig_std
|
||||
self._valid_orig = bool(orig_std)
|
||||
self._write_callback = write_callback
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.orig_std, attr)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if key in ("orig_std", "_valid_orig", "_write_callback"):
|
||||
super().__setattr__(key, value)
|
||||
else:
|
||||
setattr(self.orig_std, key, value)
|
||||
|
||||
def write(self, text):
|
||||
if self._valid_orig:
|
||||
self.orig_std.write(text)
|
||||
self._write_callback(text)
|
||||
|
||||
|
||||
class StdOEWrap:
|
||||
def __init__(self):
|
||||
self._origin_stdout_write = None
|
||||
self._origin_stderr_write = None
|
||||
self._listening = False
|
||||
self.lines = collections.deque()
|
||||
|
||||
if not sys.stdout:
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
|
||||
if not sys.stderr:
|
||||
sys.stderr = open(os.devnull, "w")
|
||||
|
||||
if self._origin_stdout_write is None:
|
||||
self._origin_stdout_write = sys.stdout.write
|
||||
|
||||
if self._origin_stderr_write is None:
|
||||
self._origin_stderr_write = sys.stderr.write
|
||||
|
||||
self._listening = True
|
||||
sys.stdout.write = self._stdout_listener
|
||||
sys.stderr.write = self._stderr_listener
|
||||
|
||||
self._stdout_wrap = _CustomSTD(sys.stdout, self._listener)
|
||||
self._stderr_wrap = _CustomSTD(sys.stderr, self._listener)
|
||||
|
||||
sys.stdout = self._stdout_wrap
|
||||
sys.stderr = self._stderr_wrap
|
||||
|
||||
def stop_listen(self):
|
||||
self._listening = False
|
||||
|
||||
def _stdout_listener(self, text):
|
||||
def _listener(self, text):
|
||||
if self._listening:
|
||||
self.lines.append(text)
|
||||
if self._origin_stdout_write is not None:
|
||||
self._origin_stdout_write(text)
|
||||
|
||||
def _stderr_listener(self, text):
|
||||
if self._listening:
|
||||
self.lines.append(text)
|
||||
if self._origin_stderr_write is not None:
|
||||
self._origin_stderr_write(text)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ opentimelineio = "^0.17.0"
|
|||
speedcopy = "^2.1"
|
||||
qtpy="^2.4.3"
|
||||
pyside6 = "^6.5.2"
|
||||
pytest-ayon = { git = "https://github.com/ynput/pytest-ayon.git", branch = "chore/align-dependencies" }
|
||||
pytest-ayon = { git = "https://github.com/ynput/pytest-ayon.git", branch = "develop" }
|
||||
|
||||
[tool.codespell]
|
||||
# Ignore words that are not in the dictionary.
|
||||
|
|
|
|||
|
|
@ -158,6 +158,46 @@ def _convert_publish_plugins(overrides):
|
|||
_convert_oiio_transcode_0_4_5(overrides["publish"])
|
||||
|
||||
|
||||
def _convert_extract_thumbnail(overrides):
|
||||
"""ExtractThumbnail config settings did change to profiles."""
|
||||
extract_thumbnail_overrides = (
|
||||
overrides.get("publish", {}).get("ExtractThumbnail")
|
||||
)
|
||||
if extract_thumbnail_overrides is None:
|
||||
return
|
||||
|
||||
base_value = {
|
||||
"product_types": [],
|
||||
"host_names": [],
|
||||
"task_types": [],
|
||||
"task_names": [],
|
||||
"product_names": [],
|
||||
"integrate_thumbnail": True,
|
||||
"target_size": {"type": "source"},
|
||||
"duration_split": 0.5,
|
||||
"oiiotool_defaults": {
|
||||
"type": "colorspace",
|
||||
"colorspace": "color_picking",
|
||||
},
|
||||
"ffmpeg_args": {"input": ["-apply_trc gamma22"], "output": []},
|
||||
}
|
||||
for key in (
|
||||
"product_names",
|
||||
"integrate_thumbnail",
|
||||
"target_size",
|
||||
"duration_split",
|
||||
"oiiotool_defaults",
|
||||
"ffmpeg_args",
|
||||
):
|
||||
if key in extract_thumbnail_overrides:
|
||||
base_value[key] = extract_thumbnail_overrides.pop(key)
|
||||
|
||||
extract_thumbnail_profiles = extract_thumbnail_overrides.setdefault(
|
||||
"profiles", []
|
||||
)
|
||||
extract_thumbnail_profiles.append(base_value)
|
||||
|
||||
|
||||
def convert_settings_overrides(
|
||||
source_version: str,
|
||||
overrides: dict[str, Any],
|
||||
|
|
@ -166,4 +206,5 @@ def convert_settings_overrides(
|
|||
_convert_imageio_configs_0_4_5(overrides)
|
||||
_convert_imageio_configs_1_6_5(overrides)
|
||||
_convert_publish_plugins(overrides)
|
||||
_convert_extract_thumbnail(overrides)
|
||||
return overrides
|
||||
|
|
|
|||
|
|
@ -422,24 +422,30 @@ class ExtractThumbnailOIIODefaultsModel(BaseSettingsModel):
|
|||
)
|
||||
|
||||
|
||||
class ExtractThumbnailModel(BaseSettingsModel):
|
||||
_isGroup = True
|
||||
enabled: bool = SettingsField(True)
|
||||
class ExtractThumbnailProfileModel(BaseSettingsModel):
|
||||
product_types: list[str] = SettingsField(
|
||||
default_factory=list, title="Product types"
|
||||
)
|
||||
host_names: list[str] = SettingsField(
|
||||
default_factory=list, title="Host names"
|
||||
)
|
||||
task_types: list[str] = SettingsField(
|
||||
default_factory=list, title="Task types", enum_resolver=task_types_enum
|
||||
)
|
||||
task_names: list[str] = SettingsField(
|
||||
default_factory=list, title="Task names"
|
||||
)
|
||||
product_names: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Product names"
|
||||
default_factory=list, title="Product names"
|
||||
)
|
||||
integrate_thumbnail: bool = SettingsField(
|
||||
True,
|
||||
title="Integrate Thumbnail Representation"
|
||||
True, title="Integrate Thumbnail Representation"
|
||||
)
|
||||
target_size: ResizeModel = SettingsField(
|
||||
default_factory=ResizeModel,
|
||||
title="Target size"
|
||||
default_factory=ResizeModel, title="Target size"
|
||||
)
|
||||
background_color: ColorRGBA_uint8 = SettingsField(
|
||||
(0, 0, 0, 0.0),
|
||||
title="Background color"
|
||||
(0, 0, 0, 0.0), title="Background color"
|
||||
)
|
||||
duration_split: float = SettingsField(
|
||||
0.5,
|
||||
|
|
@ -456,6 +462,15 @@ class ExtractThumbnailModel(BaseSettingsModel):
|
|||
)
|
||||
|
||||
|
||||
class ExtractThumbnailModel(BaseSettingsModel):
|
||||
_isGroup = True
|
||||
enabled: bool = SettingsField(True)
|
||||
|
||||
profiles: list[ExtractThumbnailProfileModel] = SettingsField(
|
||||
default_factory=list, title="Profiles"
|
||||
)
|
||||
|
||||
|
||||
def _extract_oiio_transcoding_type():
|
||||
return [
|
||||
{"value": "colorspace", "label": "Use Colorspace"},
|
||||
|
|
@ -491,6 +506,18 @@ class UseDisplayViewModel(BaseSettingsModel):
|
|||
)
|
||||
|
||||
|
||||
class ExtractThumbnailFromSourceModel(BaseSettingsModel):
|
||||
"""Thumbnail extraction from source files using ffmpeg and oiiotool."""
|
||||
enabled: bool = SettingsField(True)
|
||||
|
||||
target_size: ResizeModel = SettingsField(
|
||||
default_factory=ResizeModel, title="Target size"
|
||||
)
|
||||
background_color: ColorRGBA_uint8 = SettingsField(
|
||||
(0, 0, 0, 0.0), title="Background color"
|
||||
)
|
||||
|
||||
|
||||
class ExtractOIIOTranscodeOutputModel(BaseSettingsModel):
|
||||
_layout = "expanded"
|
||||
name: str = SettingsField(
|
||||
|
|
@ -1271,6 +1298,16 @@ class PublishPuginsModel(BaseSettingsModel):
|
|||
default_factory=ExtractThumbnailModel,
|
||||
title="Extract Thumbnail"
|
||||
)
|
||||
ExtractThumbnailFromSource: ExtractThumbnailFromSourceModel = SettingsField( # noqa: E501
|
||||
default_factory=ExtractThumbnailFromSourceModel,
|
||||
title="Extract Thumbnail from source",
|
||||
description=(
|
||||
"Extract thumbnails from explicit file set in "
|
||||
"instance.data['thumbnailSource'] using oiiotool"
|
||||
" or ffmpeg."
|
||||
"Used when artist provided thumbnail source."
|
||||
)
|
||||
)
|
||||
ExtractOIIOTranscode: ExtractOIIOTranscodeModel = SettingsField(
|
||||
default_factory=ExtractOIIOTranscodeModel,
|
||||
title="Extract OIIO Transcode"
|
||||
|
|
@ -1489,6 +1526,12 @@ DEFAULT_PUBLISH_VALUES = {
|
|||
},
|
||||
"ExtractThumbnail": {
|
||||
"enabled": True,
|
||||
"profiles": [
|
||||
{
|
||||
"product_types": [],
|
||||
"host_names": [],
|
||||
"task_types": [],
|
||||
"task_names": [],
|
||||
"product_names": [],
|
||||
"integrate_thumbnail": True,
|
||||
"target_size": {
|
||||
|
|
@ -1505,6 +1548,18 @@ DEFAULT_PUBLISH_VALUES = {
|
|||
],
|
||||
"output": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ExtractThumbnailFromSource": {
|
||||
"enabled": True,
|
||||
"target_size": {
|
||||
"type": "resize",
|
||||
"resize": {
|
||||
"width": 300,
|
||||
"height": 170
|
||||
}
|
||||
},
|
||||
},
|
||||
"ExtractOIIOTranscode": {
|
||||
"enabled": True,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue