Merge branch 'develop' into enhancement/1524-yn-0156-usd-contribution-workflow-layer-strength-configured-hierarchically

This commit is contained in:
Mustafa Zaky Jafar 2025-12-16 11:47:26 +02:00 committed by GitHub
commit 448d32fa42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 160 additions and 62 deletions

View file

@ -169,7 +169,9 @@ class ExtractReview(pyblish.api.InstancePlugin):
settings_category = "core" settings_category = "core"
# Supported extensions # 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"} video_exts = {"mov", "mp4"}
supported_exts = image_exts | video_exts supported_exts = image_exts | video_exts

View file

@ -105,7 +105,6 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
"unreal", "unreal",
"houdini", "houdini",
"batchdelivery", "batchdelivery",
"webpublisher",
] ]
settings_category = "core" settings_category = "core"
enabled = False enabled = False

View file

@ -14,6 +14,7 @@ Todos:
import os import os
import tempfile import tempfile
from typing import List, Optional
import pyblish.api import pyblish.api
from ayon_core.lib import ( from ayon_core.lib import (
@ -22,6 +23,7 @@ from ayon_core.lib import (
is_oiio_supported, is_oiio_supported,
run_subprocess, 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. 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 # Before 'ExtractThumbnail' in global plugins
order = pyblish.api.ExtractorOrder - 0.00001 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) 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") thumbnail_source = instance.data.get("thumbnailSource")
if not thumbnail_source: if not thumbnail_source:
self.log.debug("Thumbnail source not filled. Skipping.") self.log.debug("Thumbnail source not filled. Skipping.")
@ -69,6 +74,8 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
"outputName": "thumbnail", "outputName": "thumbnail",
} }
new_repre["tags"].append("delete")
# adding representation # adding representation
self.log.debug( self.log.debug(
"Adding thumbnail representation: {}".format(new_repre) "Adding thumbnail representation: {}".format(new_repre)
@ -76,7 +83,11 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
instance.data["representations"].append(new_repre) instance.data["representations"].append(new_repre)
instance.data["thumbnailPath"] = dst_filepath 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: if not thumbnail_source:
self.log.debug("Thumbnail source not filled. Skipping.") self.log.debug("Thumbnail source not filled. Skipping.")
return return
@ -131,7 +142,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
self.log.warning("Thumbnail has not been created.") 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: if "representations" not in instance.data:
self.log.warning( self.log.warning(
"Instance does not have 'representations' key filled" "Instance does not have 'representations' key filled"
@ -143,14 +154,24 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
return True return True
return False 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)) self.log.debug("Outputting thumbnail with OIIO: {}".format(dst_path))
oiio_cmd = get_oiio_tool_args( resolution_args = self._get_resolution_args(
"oiiotool", "oiiotool", src_path
"-a", src_path,
"--ch", "R,G,B",
"-o", dst_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))) self.log.debug("Running: {}".format(" ".join(oiio_cmd)))
try: try:
run_subprocess(oiio_cmd, logger=self.log) run_subprocess(oiio_cmd, logger=self.log)
@ -162,7 +183,15 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
) )
return False 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) max_int = str(2147483647)
ffmpeg_cmd = get_ffmpeg_tool_args( ffmpeg_cmd = get_ffmpeg_tool_args(
"ffmpeg", "ffmpeg",
@ -171,9 +200,13 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
"-probesize", max_int, "-probesize", max_int,
"-i", src_path, "-i", src_path,
"-frames:v", "1", "-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))) self.log.debug("Running: {}".format(" ".join(ffmpeg_cmd)))
try: try:
run_subprocess(ffmpeg_cmd, logger=self.log) run_subprocess(ffmpeg_cmd, logger=self.log)
@ -185,10 +218,37 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
) )
return False return False
def _create_context_thumbnail(self, context): def _create_context_thumbnail(
self,
context: pyblish.api.Context,
):
if "thumbnailPath" in context.data: if "thumbnailPath" in context.data:
return return
thumbnail_source = context.data.get("thumbnailSource") thumbnail_source = context.data.get("thumbnailSource")
thumbnail_path = self._create_thumbnail(context, thumbnail_source) context.data["thumbnailPath"] = self._create_thumbnail(
context.data["thumbnailPath"] = thumbnail_path 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,
)

View file

@ -1,6 +1,8 @@
from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Dict, Optional from typing import Optional
@dataclass @dataclass
@ -13,8 +15,8 @@ class TabItem:
class InterpreterConfig: class InterpreterConfig:
width: Optional[int] width: Optional[int]
height: Optional[int] height: Optional[int]
splitter_sizes: List[int] = field(default_factory=list) splitter_sizes: list[int] = field(default_factory=list)
tabs: List[TabItem] = field(default_factory=list) tabs: list[TabItem] = field(default_factory=list)
class AbstractInterpreterController(ABC): class AbstractInterpreterController(ABC):
@ -27,7 +29,7 @@ class AbstractInterpreterController(ABC):
self, self,
width: int, width: int,
height: int, height: int,
splitter_sizes: List[int], splitter_sizes: list[int],
tabs: List[Dict[str, str]], tabs: list[dict[str, str]],
): ) -> None:
pass pass

View file

@ -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 import JSONSettingRegistry
from ayon_core.lib.local_settings import get_launcher_local_dir from ayon_core.lib.local_settings import get_launcher_local_dir
@ -11,13 +12,15 @@ from .abstract import (
class InterpreterController(AbstractInterpreterController): 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( self._registry = JSONSettingRegistry(
"python_interpreter_tool", name,
get_launcher_local_dir(), get_launcher_local_dir(),
) )
def get_config(self): def get_config(self) -> InterpreterConfig:
width = None width = None
height = None height = None
splitter_sizes = [] splitter_sizes = []
@ -54,9 +57,9 @@ class InterpreterController(AbstractInterpreterController):
self, self,
width: int, width: int,
height: int, height: int,
splitter_sizes: List[int], splitter_sizes: list[int],
tabs: List[Dict[str, str]], tabs: list[dict[str, str]],
): ) -> None:
self._registry.set_item("width", width) self._registry.set_item("width", width)
self._registry.set_item("height", height) self._registry.set_item("height", height)
self._registry.set_item("splitter_sizes", splitter_sizes) self._registry.set_item("splitter_sizes", splitter_sizes)

View file

@ -1,42 +1,42 @@
import os
import sys import sys
import collections 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: class StdOEWrap:
def __init__(self): def __init__(self):
self._origin_stdout_write = None
self._origin_stderr_write = None
self._listening = False
self.lines = collections.deque() 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 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): def stop_listen(self):
self._listening = False self._listening = False
def _stdout_listener(self, text): def _listener(self, text):
if self._listening: if self._listening:
self.lines.append(text) 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)

View file

@ -501,6 +501,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): class ExtractOIIOTranscodeOutputModel(BaseSettingsModel):
_layout = "expanded" _layout = "expanded"
name: str = SettingsField( name: str = SettingsField(
@ -1276,6 +1288,16 @@ class PublishPuginsModel(BaseSettingsModel):
default_factory=ExtractThumbnailModel, default_factory=ExtractThumbnailModel,
title="Extract Thumbnail" 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( ExtractOIIOTranscode: ExtractOIIOTranscodeModel = SettingsField(
default_factory=ExtractOIIOTranscodeModel, default_factory=ExtractOIIOTranscodeModel,
title="Extract OIIO Transcode" title="Extract OIIO Transcode"
@ -1515,6 +1537,16 @@ DEFAULT_PUBLISH_VALUES = {
} }
] ]
}, },
"ExtractThumbnailFromSource": {
"enabled": True,
"target_size": {
"type": "resize",
"resize": {
"width": 300,
"height": 170
}
},
},
"ExtractOIIOTranscode": { "ExtractOIIOTranscode": {
"enabled": True, "enabled": True,
"profiles": [] "profiles": []