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"
# 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

View file

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

View file

@ -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,
)

View file

@ -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

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.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)

View file

@ -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)