mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into enhancement/1416-loader-actions
This commit is contained in:
commit
e0f3a6f5d9
17 changed files with 305 additions and 203 deletions
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,10 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to AYON Tray
|
||||
options:
|
||||
- 1.6.5
|
||||
- 1.6.4
|
||||
- 1.6.3
|
||||
- 1.6.2
|
||||
- 1.6.1
|
||||
- 1.6.0
|
||||
- 1.5.3
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
"""Base class for AYON addons."""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
|
@ -13,6 +12,7 @@ import collections
|
|||
import warnings
|
||||
from uuid import uuid4
|
||||
from abc import ABC, abstractmethod
|
||||
from urllib.parse import urlencode
|
||||
from types import ModuleType
|
||||
import typing
|
||||
from typing import Optional, Any, Union
|
||||
|
|
@ -136,39 +136,47 @@ def load_addons(force: bool = False) -> None:
|
|||
time.sleep(0.1)
|
||||
|
||||
|
||||
def _get_ayon_bundle_data() -> Optional[dict[str, Any]]:
|
||||
def _get_ayon_bundle_data() -> tuple[
|
||||
dict[str, Any], Optional[dict[str, Any]]
|
||||
]:
|
||||
studio_bundle_name = os.environ.get("AYON_STUDIO_BUNDLE_NAME")
|
||||
project_bundle_name = os.getenv("AYON_BUNDLE_NAME")
|
||||
bundles = ayon_api.get_bundles()["bundles"]
|
||||
project_bundle = next(
|
||||
studio_bundle = next(
|
||||
(
|
||||
bundle
|
||||
for bundle in bundles
|
||||
if bundle["name"] == project_bundle_name
|
||||
if bundle["name"] == studio_bundle_name
|
||||
),
|
||||
None
|
||||
)
|
||||
studio_bundle = None
|
||||
if studio_bundle_name and project_bundle_name != studio_bundle_name:
|
||||
studio_bundle = next(
|
||||
|
||||
if studio_bundle is None:
|
||||
raise RuntimeError(f"Failed to find bundle '{studio_bundle_name}'.")
|
||||
|
||||
project_bundle = None
|
||||
if project_bundle_name and project_bundle_name != studio_bundle_name:
|
||||
project_bundle = next(
|
||||
(
|
||||
bundle
|
||||
for bundle in bundles
|
||||
if bundle["name"] == studio_bundle_name
|
||||
if bundle["name"] == project_bundle_name
|
||||
),
|
||||
None
|
||||
)
|
||||
|
||||
if project_bundle and studio_bundle:
|
||||
addons = copy.deepcopy(studio_bundle["addons"])
|
||||
addons.update(project_bundle["addons"])
|
||||
project_bundle["addons"] = addons
|
||||
return project_bundle
|
||||
if project_bundle is None:
|
||||
raise RuntimeError(
|
||||
f"Failed to find project bundle '{project_bundle_name}'."
|
||||
)
|
||||
|
||||
return studio_bundle, project_bundle
|
||||
|
||||
|
||||
def _get_ayon_addons_information(
|
||||
bundle_info: dict[str, Any]
|
||||
) -> list[dict[str, Any]]:
|
||||
studio_bundle: dict[str, Any],
|
||||
project_bundle: Optional[dict[str, Any]],
|
||||
) -> dict[str, str]:
|
||||
"""Receive information about addons to use from server.
|
||||
|
||||
Todos:
|
||||
|
|
@ -181,22 +189,20 @@ def _get_ayon_addons_information(
|
|||
list[dict[str, Any]]: List of addon information to use.
|
||||
|
||||
"""
|
||||
output = []
|
||||
bundle_addons = bundle_info["addons"]
|
||||
addons = ayon_api.get_addons_info()["addons"]
|
||||
for addon in addons:
|
||||
name = addon["name"]
|
||||
versions = addon.get("versions")
|
||||
addon_version = bundle_addons.get(name)
|
||||
if addon_version is None or not versions:
|
||||
continue
|
||||
version = versions.get(addon_version)
|
||||
if version:
|
||||
version = copy.deepcopy(version)
|
||||
version["name"] = name
|
||||
version["version"] = addon_version
|
||||
output.append(version)
|
||||
return output
|
||||
key_values = {
|
||||
"summary": "true",
|
||||
"bundle_name": studio_bundle["name"],
|
||||
}
|
||||
if project_bundle:
|
||||
key_values["project_bundle_name"] = project_bundle["name"]
|
||||
|
||||
query = urlencode(key_values)
|
||||
|
||||
response = ayon_api.get(f"settings?{query}")
|
||||
return {
|
||||
addon["name"]: addon["version"]
|
||||
for addon in response.data["addons"]
|
||||
}
|
||||
|
||||
|
||||
def _load_ayon_addons(log: logging.Logger) -> list[ModuleType]:
|
||||
|
|
@ -214,8 +220,8 @@ def _load_ayon_addons(log: logging.Logger) -> list[ModuleType]:
|
|||
|
||||
"""
|
||||
all_addon_modules = []
|
||||
bundle_info = _get_ayon_bundle_data()
|
||||
addons_info = _get_ayon_addons_information(bundle_info)
|
||||
studio_bundle, project_bundle = _get_ayon_bundle_data()
|
||||
addons_info = _get_ayon_addons_information(studio_bundle, project_bundle)
|
||||
if not addons_info:
|
||||
return all_addon_modules
|
||||
|
||||
|
|
@ -227,17 +233,16 @@ def _load_ayon_addons(log: logging.Logger) -> list[ModuleType]:
|
|||
dev_addons_info = {}
|
||||
if dev_mode_enabled:
|
||||
# Get dev addons info only when dev mode is enabled
|
||||
dev_addons_info = bundle_info.get("addonDevelopment", dev_addons_info)
|
||||
dev_addons_info = studio_bundle.get(
|
||||
"addonDevelopment", dev_addons_info
|
||||
)
|
||||
|
||||
addons_dir_exists = os.path.exists(addons_dir)
|
||||
if not addons_dir_exists:
|
||||
log.warning(
|
||||
f"Addons directory does not exists. Path \"{addons_dir}\"")
|
||||
|
||||
for addon_info in addons_info:
|
||||
addon_name = addon_info["name"]
|
||||
addon_version = addon_info["version"]
|
||||
|
||||
for addon_name, addon_version in addons_info.items():
|
||||
# core addon does not have any addon object
|
||||
if addon_name == "core":
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""AYON plugin tools."""
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
import collections
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
CAPITALIZE_REGEX = re.compile(r"[a-zA-Z0-9]")
|
||||
|
||||
|
|
|
|||
|
|
@ -420,11 +420,14 @@ def get_review_info_by_layer_name(channel_names):
|
|||
channel = last_part[0].upper()
|
||||
rgba_by_layer_name[layer_name][channel] = channel_name
|
||||
|
||||
# Put empty layer to the beginning of the list
|
||||
# Put empty layer or 'rgba' to the beginning of the list
|
||||
# - if input has R, G, B, A channels they should be used for review
|
||||
if "" in layer_names_order:
|
||||
layer_names_order.remove("")
|
||||
layer_names_order.insert(0, "")
|
||||
# NOTE They are iterated in reversed order because they're inserted to
|
||||
# the beginning of 'layer_names_order' -> last added will be first.
|
||||
for name in reversed(["", "rgba"]):
|
||||
if name in layer_names_order:
|
||||
layer_names_order.remove(name)
|
||||
layer_names_order.insert(0, name)
|
||||
|
||||
output = []
|
||||
for layer_name in layer_names_order:
|
||||
|
|
|
|||
|
|
@ -249,7 +249,8 @@ def create_skeleton_instance(
|
|||
# map inputVersions `ObjectId` -> `str` so json supports it
|
||||
"inputVersions": list(map(str, data.get("inputVersions", []))),
|
||||
"colorspace": data.get("colorspace"),
|
||||
"hasExplicitFrames": data.get("hasExplicitFrames")
|
||||
"hasExplicitFrames": data.get("hasExplicitFrames", False),
|
||||
"reuseLastVersion": data.get("reuseLastVersion", False),
|
||||
}
|
||||
|
||||
if data.get("renderlayer"):
|
||||
|
|
|
|||
|
|
@ -7,13 +7,20 @@ import copy
|
|||
import warnings
|
||||
import hashlib
|
||||
import xml.etree.ElementTree
|
||||
from typing import TYPE_CHECKING, Optional, Union, List
|
||||
from typing import TYPE_CHECKING, Optional, Union, List, Any
|
||||
import clique
|
||||
import speedcopy
|
||||
import logging
|
||||
|
||||
import ayon_api
|
||||
import pyblish.util
|
||||
import pyblish.plugin
|
||||
import pyblish.api
|
||||
|
||||
from ayon_api import (
|
||||
get_server_api_connection,
|
||||
get_representations,
|
||||
get_last_version_by_product_name
|
||||
)
|
||||
from ayon_core.lib import (
|
||||
import_filepath,
|
||||
Logger,
|
||||
|
|
@ -34,6 +41,8 @@ if TYPE_CHECKING:
|
|||
|
||||
TRAIT_INSTANCE_KEY: str = "representations_with_traits"
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_template_name_profiles(
|
||||
project_name, project_settings=None, logger=None
|
||||
|
|
@ -1030,7 +1039,7 @@ def main_cli_publish(
|
|||
# NOTE: ayon-python-api does not have public api function to find
|
||||
# out if is used service user. So we need to have try > except
|
||||
# block.
|
||||
con = ayon_api.get_server_api_connection()
|
||||
con = get_server_api_connection()
|
||||
try:
|
||||
con.set_default_service_username(username)
|
||||
except ValueError:
|
||||
|
|
@ -1143,3 +1152,90 @@ def get_trait_representations(
|
|||
|
||||
"""
|
||||
return instance.data.get(TRAIT_INSTANCE_KEY, [])
|
||||
|
||||
|
||||
def fill_sequence_gaps_with_previous_version(
|
||||
collection: str,
|
||||
staging_dir: str,
|
||||
instance: pyblish.plugin.Instance,
|
||||
current_repre_name: str,
|
||||
start_frame: int,
|
||||
end_frame: int
|
||||
) -> tuple[Optional[dict[str, Any]], Optional[dict[int, str]]]:
|
||||
"""Tries to replace missing frames from ones from last version"""
|
||||
used_version_entity, repre_file_paths = _get_last_version_files(
|
||||
instance, current_repre_name
|
||||
)
|
||||
if repre_file_paths is None:
|
||||
# issues in getting last version files
|
||||
return (None, None)
|
||||
|
||||
prev_collection = clique.assemble(
|
||||
repre_file_paths,
|
||||
patterns=[clique.PATTERNS["frames"]],
|
||||
minimum_items=1
|
||||
)[0][0]
|
||||
prev_col_format = prev_collection.format("{head}{padding}{tail}")
|
||||
|
||||
added_files = {}
|
||||
anatomy = instance.context.data["anatomy"]
|
||||
col_format = collection.format("{head}{padding}{tail}")
|
||||
for frame in range(start_frame, end_frame + 1):
|
||||
if frame in collection.indexes:
|
||||
continue
|
||||
hole_fpath = os.path.join(staging_dir, col_format % frame)
|
||||
|
||||
previous_version_path = prev_col_format % frame
|
||||
previous_version_path = anatomy.fill_root(previous_version_path)
|
||||
if not os.path.exists(previous_version_path):
|
||||
log.warning(
|
||||
"Missing frame should be replaced from "
|
||||
f"'{previous_version_path}' but that doesn't exist. "
|
||||
)
|
||||
return (None, None)
|
||||
|
||||
log.warning(
|
||||
f"Replacing missing '{hole_fpath}' with "
|
||||
f"'{previous_version_path}'"
|
||||
)
|
||||
speedcopy.copyfile(previous_version_path, hole_fpath)
|
||||
added_files[frame] = hole_fpath
|
||||
|
||||
return (used_version_entity, added_files)
|
||||
|
||||
|
||||
def _get_last_version_files(
|
||||
instance: pyblish.plugin.Instance,
|
||||
current_repre_name: str,
|
||||
) -> tuple[Optional[dict[str, Any]], Optional[list[str]]]:
|
||||
product_name = instance.data["productName"]
|
||||
project_name = instance.data["projectEntity"]["name"]
|
||||
folder_entity = instance.data["folderEntity"]
|
||||
|
||||
version_entity = get_last_version_by_product_name(
|
||||
project_name,
|
||||
product_name,
|
||||
folder_entity["id"],
|
||||
fields={"id", "attrib"}
|
||||
)
|
||||
|
||||
if not version_entity:
|
||||
return None, None
|
||||
|
||||
matching_repres = get_representations(
|
||||
project_name,
|
||||
version_ids=[version_entity["id"]],
|
||||
representation_names=[current_repre_name],
|
||||
fields={"files"}
|
||||
)
|
||||
|
||||
matching_repre = next(matching_repres, None)
|
||||
if not matching_repre:
|
||||
return None, None
|
||||
|
||||
repre_file_paths = [
|
||||
file_info["path"]
|
||||
for file_info in matching_repre["files"]
|
||||
]
|
||||
|
||||
return (version_entity, repre_file_paths)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin):
|
|||
|
||||
for key in [
|
||||
"AYON_BUNDLE_NAME",
|
||||
"AYON_STUDIO_BUNDLE_NAME",
|
||||
"AYON_USE_STAGING",
|
||||
"AYON_IN_TESTS",
|
||||
# NOTE Not sure why workdir is needed?
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import copy
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline.publish import get_publish_template_name
|
||||
|
||||
|
||||
class CollectResourcesPath(pyblish.api.InstancePlugin):
|
||||
"""Generate directory path where the files and resources will be stored.
|
||||
|
|
@ -77,16 +79,29 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
|
|||
|
||||
# This is for cases of Deprecated anatomy without `folder`
|
||||
# TODO remove when all clients have solved this issue
|
||||
template_data.update({
|
||||
"frame": "FRAME_TEMP",
|
||||
"representation": "TEMP"
|
||||
})
|
||||
template_data.update({"frame": "FRAME_TEMP", "representation": "TEMP"})
|
||||
|
||||
publish_templates = anatomy.get_template_item(
|
||||
"publish", "default", "directory"
|
||||
task_name = task_type = None
|
||||
task_entity = instance.data.get("taskEntity")
|
||||
if task_entity:
|
||||
task_name = task_entity["name"]
|
||||
task_type = task_entity["taskType"]
|
||||
|
||||
template_name = get_publish_template_name(
|
||||
project_name=instance.context.data["projectName"],
|
||||
host_name=instance.context.data["hostName"],
|
||||
product_type=instance.data["productType"],
|
||||
task_name=task_name,
|
||||
task_type=task_type,
|
||||
project_settings=instance.context.data["project_settings"],
|
||||
logger=self.log,
|
||||
)
|
||||
|
||||
publish_template = anatomy.get_template_item(
|
||||
"publish", template_name, "directory")
|
||||
|
||||
publish_folder = os.path.normpath(
|
||||
publish_templates.format_strict(template_data)
|
||||
publish_template.format_strict(template_data)
|
||||
)
|
||||
resources_folder = os.path.join(publish_folder, "resources")
|
||||
|
||||
|
|
|
|||
|
|
@ -13,14 +13,15 @@ import clique
|
|||
import speedcopy
|
||||
import pyblish.api
|
||||
|
||||
from ayon_api import get_last_version_by_product_name, get_representations
|
||||
|
||||
from ayon_core.lib import (
|
||||
get_ffmpeg_tool_args,
|
||||
filter_profiles,
|
||||
path_to_subprocess_arg,
|
||||
run_subprocess,
|
||||
)
|
||||
from ayon_core.pipeline.publish.lib import (
|
||||
fill_sequence_gaps_with_previous_version
|
||||
)
|
||||
from ayon_core.lib.transcoding import (
|
||||
IMAGE_EXTENSIONS,
|
||||
get_ffprobe_streams,
|
||||
|
|
@ -130,7 +131,7 @@ def frame_to_timecode(frame: int, fps: float) -> str:
|
|||
|
||||
|
||||
class ExtractReview(pyblish.api.InstancePlugin):
|
||||
"""Extracting Review mov file for Ftrack
|
||||
"""Extracting Reviewable medias
|
||||
|
||||
Compulsory attribute of representation is tags list with "review",
|
||||
otherwise the representation is ignored.
|
||||
|
|
@ -508,10 +509,10 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
resolution_width=temp_data.resolution_width,
|
||||
resolution_height=temp_data.resolution_height,
|
||||
extension=temp_data.input_ext,
|
||||
temp_data=temp_data
|
||||
temp_data=temp_data,
|
||||
)
|
||||
elif fill_missing_frames == "previous_version":
|
||||
new_frame_files = self.fill_sequence_gaps_with_previous(
|
||||
fill_output = fill_sequence_gaps_with_previous_version(
|
||||
collection=collection,
|
||||
staging_dir=new_repre["stagingDir"],
|
||||
instance=instance,
|
||||
|
|
@ -519,8 +520,13 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
start_frame=temp_data.frame_start,
|
||||
end_frame=temp_data.frame_end,
|
||||
)
|
||||
_, new_frame_files = fill_output
|
||||
# fallback to original workflow
|
||||
if new_frame_files is None:
|
||||
self.log.warning(
|
||||
"Falling back to filling from currently "
|
||||
"last rendered."
|
||||
)
|
||||
new_frame_files = (
|
||||
self.fill_sequence_gaps_from_existing(
|
||||
collection=collection,
|
||||
|
|
@ -612,8 +618,6 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
"name": "{}_{}".format(output_name, output_ext),
|
||||
"outputName": output_name,
|
||||
"outputDef": output_def,
|
||||
"frameStartFtrack": temp_data.output_frame_start,
|
||||
"frameEndFtrack": temp_data.output_frame_end,
|
||||
"ffmpeg_cmd": subprcs_cmd
|
||||
})
|
||||
|
||||
|
|
@ -1050,92 +1054,6 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
|
||||
return all_args
|
||||
|
||||
def fill_sequence_gaps_with_previous(
|
||||
self,
|
||||
collection: str,
|
||||
staging_dir: str,
|
||||
instance: pyblish.plugin.Instance,
|
||||
current_repre_name: str,
|
||||
start_frame: int,
|
||||
end_frame: int
|
||||
) -> Optional[dict[int, str]]:
|
||||
"""Tries to replace missing frames from ones from last version"""
|
||||
repre_file_paths = self._get_last_version_files(
|
||||
instance, current_repre_name)
|
||||
if repre_file_paths is None:
|
||||
# issues in getting last version files, falling back
|
||||
return None
|
||||
|
||||
prev_collection = clique.assemble(
|
||||
repre_file_paths,
|
||||
patterns=[clique.PATTERNS["frames"]],
|
||||
minimum_items=1
|
||||
)[0][0]
|
||||
prev_col_format = prev_collection.format("{head}{padding}{tail}")
|
||||
|
||||
added_files = {}
|
||||
anatomy = instance.context.data["anatomy"]
|
||||
col_format = collection.format("{head}{padding}{tail}")
|
||||
for frame in range(start_frame, end_frame + 1):
|
||||
if frame in collection.indexes:
|
||||
continue
|
||||
hole_fpath = os.path.join(staging_dir, col_format % frame)
|
||||
|
||||
previous_version_path = prev_col_format % frame
|
||||
previous_version_path = anatomy.fill_root(previous_version_path)
|
||||
if not os.path.exists(previous_version_path):
|
||||
self.log.warning(
|
||||
"Missing frame should be replaced from "
|
||||
f"'{previous_version_path}' but that doesn't exist. "
|
||||
"Falling back to filling from currently last rendered."
|
||||
)
|
||||
return None
|
||||
|
||||
self.log.warning(
|
||||
f"Replacing missing '{hole_fpath}' with "
|
||||
f"'{previous_version_path}'"
|
||||
)
|
||||
speedcopy.copyfile(previous_version_path, hole_fpath)
|
||||
added_files[frame] = hole_fpath
|
||||
|
||||
return added_files
|
||||
|
||||
def _get_last_version_files(
|
||||
self,
|
||||
instance: pyblish.plugin.Instance,
|
||||
current_repre_name: str,
|
||||
):
|
||||
product_name = instance.data["productName"]
|
||||
project_name = instance.data["projectEntity"]["name"]
|
||||
folder_entity = instance.data["folderEntity"]
|
||||
|
||||
version_entity = get_last_version_by_product_name(
|
||||
project_name,
|
||||
product_name,
|
||||
folder_entity["id"],
|
||||
fields={"id"}
|
||||
)
|
||||
if not version_entity:
|
||||
return None
|
||||
|
||||
matching_repres = get_representations(
|
||||
project_name,
|
||||
version_ids=[version_entity["id"]],
|
||||
representation_names=[current_repre_name],
|
||||
fields={"files"}
|
||||
)
|
||||
|
||||
if not matching_repres:
|
||||
return None
|
||||
matching_repre = list(matching_repres)[0]
|
||||
|
||||
repre_file_paths = [
|
||||
file_info["path"]
|
||||
for file_info in matching_repre["files"]
|
||||
]
|
||||
|
||||
return repre_file_paths
|
||||
|
||||
def fill_sequence_gaps_with_blanks(
|
||||
self,
|
||||
collection: str,
|
||||
|
|
@ -1384,15 +1302,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
return audio_in_args, audio_filters, audio_out_args
|
||||
|
||||
for audio in audio_inputs:
|
||||
# NOTE modified, always was expected "frameStartFtrack" which is
|
||||
# STRANGE?!!! There should be different key, right?
|
||||
# TODO use different frame start!
|
||||
offset_seconds = 0
|
||||
frame_start_ftrack = instance.data.get("frameStartFtrack")
|
||||
if frame_start_ftrack is not None:
|
||||
offset_frames = frame_start_ftrack - audio["offset"]
|
||||
offset_seconds = offset_frames / temp_data.fps
|
||||
|
||||
if offset_seconds > 0:
|
||||
audio_in_args.append(
|
||||
"-ss {}".format(offset_seconds)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import re
|
|||
|
||||
import pyblish.api
|
||||
from ayon_core.lib import (
|
||||
get_oiio_tool_args,
|
||||
get_ffmpeg_tool_args,
|
||||
get_ffprobe_data,
|
||||
|
||||
|
|
@ -15,7 +16,11 @@ from ayon_core.lib import (
|
|||
path_to_subprocess_arg,
|
||||
run_subprocess,
|
||||
)
|
||||
from ayon_core.lib.transcoding import oiio_color_convert
|
||||
from ayon_core.lib.transcoding import (
|
||||
oiio_color_convert,
|
||||
get_oiio_input_and_channel_args,
|
||||
get_oiio_info_for_input,
|
||||
)
|
||||
|
||||
from ayon_core.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS
|
||||
|
||||
|
|
@ -210,6 +215,12 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
full_output_path = os.path.join(dst_staging, jpeg_file)
|
||||
colorspace_data = repre.get("colorspaceData")
|
||||
|
||||
# NOTE We should find out what is happening here. Why don't we
|
||||
# use oiiotool all the time if it is available? Only possible
|
||||
# reason might be that video files should be converted using
|
||||
# ffmpeg, but other then that, we should use oiio all the time.
|
||||
# - We should also probably get rid of the ffmpeg settings...
|
||||
|
||||
# only use OIIO if it is supported and representation has
|
||||
# colorspace data
|
||||
if oiio_supported and colorspace_data:
|
||||
|
|
@ -219,7 +230,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
)
|
||||
# If the input can read by OIIO then use OIIO method for
|
||||
# conversion otherwise use ffmpeg
|
||||
repre_thumb_created = self._create_thumbnail_oiio(
|
||||
repre_thumb_created = self._create_colorspace_thumbnail(
|
||||
full_input_path,
|
||||
full_output_path,
|
||||
colorspace_data
|
||||
|
|
@ -229,17 +240,16 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
# oiiotool isn't available or representation is not having
|
||||
# colorspace data
|
||||
if not repre_thumb_created:
|
||||
if oiio_supported:
|
||||
self.log.debug(
|
||||
"Converting with FFMPEG because input"
|
||||
" can't be read by OIIO."
|
||||
)
|
||||
|
||||
repre_thumb_created = self._create_thumbnail_ffmpeg(
|
||||
full_input_path, full_output_path
|
||||
)
|
||||
|
||||
# Skip representation and try next one if wasn't created
|
||||
# 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
|
||||
)
|
||||
|
||||
if not repre_thumb_created:
|
||||
continue
|
||||
|
||||
|
|
@ -382,7 +392,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
|
||||
return ext in IMAGE_EXTENSIONS or ext in VIDEO_EXTENSIONS
|
||||
|
||||
def _create_thumbnail_oiio(
|
||||
def _create_colorspace_thumbnail(
|
||||
self,
|
||||
src_path,
|
||||
dst_path,
|
||||
|
|
@ -455,9 +465,50 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
|
||||
return True
|
||||
|
||||
def _create_thumbnail_oiio(self, src_path, dst_path):
|
||||
self.log.debug(f"Extracting thumbnail with OIIO: {dst_path}")
|
||||
|
||||
try:
|
||||
resolution_arg = self._get_resolution_arg("oiiotool", src_path)
|
||||
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_arg, channels_arg = get_oiio_input_and_channel_args(input_info)
|
||||
oiio_cmd = get_oiio_tool_args(
|
||||
"oiiotool",
|
||||
input_arg, src_path,
|
||||
# Tell oiiotool which channels should be put to top stack
|
||||
# (and output)
|
||||
"--ch", channels_arg,
|
||||
# Use first subimage
|
||||
"--subimage", "0"
|
||||
)
|
||||
oiio_cmd.extend(resolution_arg)
|
||||
oiio_cmd.extend(("-o", dst_path))
|
||||
self.log.debug("Running: {}".format(" ".join(oiio_cmd)))
|
||||
try:
|
||||
run_subprocess(oiio_cmd, logger=self.log)
|
||||
return True
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to create thumbnail using oiiotool",
|
||||
exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
def _create_thumbnail_ffmpeg(self, src_path, dst_path):
|
||||
self.log.debug("Extracting thumbnail with FFMPEG: {}".format(dst_path))
|
||||
resolution_arg = self._get_resolution_arg("ffmpeg", src_path)
|
||||
try:
|
||||
resolution_arg = self._get_resolution_arg("ffmpeg", src_path)
|
||||
except RuntimeError:
|
||||
self.log.warning(
|
||||
"Failed to create thumbnail using ffmpeg", exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
ffmpeg_path_args = get_ffmpeg_tool_args("ffmpeg")
|
||||
ffmpeg_args = self.ffmpeg_args or {}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,12 @@ import json
|
|||
import tempfile
|
||||
from string import Formatter
|
||||
|
||||
import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins
|
||||
try:
|
||||
from otio_burnins_adapter import ffmpeg_burnins
|
||||
except ImportError:
|
||||
import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins
|
||||
from PIL import ImageFont
|
||||
|
||||
from ayon_core.lib import (
|
||||
get_ffmpeg_tool_args,
|
||||
get_ffmpeg_codec_args,
|
||||
|
|
@ -36,6 +41,39 @@ TIMECODE_KEY = "{timecode}"
|
|||
SOURCE_TIMECODE_KEY = "{source_timecode}"
|
||||
|
||||
|
||||
def _drawtext(align, resolution, text, options):
|
||||
"""
|
||||
:rtype: {'x': int, 'y': int}
|
||||
"""
|
||||
x_pos = "0"
|
||||
if align in (ffmpeg_burnins.TOP_CENTERED, ffmpeg_burnins.BOTTOM_CENTERED):
|
||||
x_pos = "w/2-tw/2"
|
||||
|
||||
elif align in (ffmpeg_burnins.TOP_RIGHT, ffmpeg_burnins.BOTTOM_RIGHT):
|
||||
ifont = ImageFont.truetype(options["font"], options["font_size"])
|
||||
if hasattr(ifont, "getbbox"):
|
||||
left, top, right, bottom = ifont.getbbox(text)
|
||||
box_size = right - left, bottom - top
|
||||
else:
|
||||
box_size = ifont.getsize(text)
|
||||
x_pos = resolution[0] - (box_size[0] + options["x_offset"])
|
||||
elif align in (ffmpeg_burnins.TOP_LEFT, ffmpeg_burnins.BOTTOM_LEFT):
|
||||
x_pos = options["x_offset"]
|
||||
|
||||
if align in (
|
||||
ffmpeg_burnins.TOP_CENTERED,
|
||||
ffmpeg_burnins.TOP_RIGHT,
|
||||
ffmpeg_burnins.TOP_LEFT
|
||||
):
|
||||
y_pos = "%d" % options["y_offset"]
|
||||
else:
|
||||
y_pos = "h-text_h-%d" % (options["y_offset"])
|
||||
return {"x": x_pos, "y": y_pos}
|
||||
|
||||
|
||||
ffmpeg_burnins._drawtext = _drawtext
|
||||
|
||||
|
||||
def _get_ffprobe_data(source):
|
||||
"""Reimplemented from otio burnins to be able use full path to ffprobe
|
||||
:param str source: source media file
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class ScrollMessageBox(QtWidgets.QDialog):
|
|||
|
||||
"""
|
||||
def __init__(self, icon, title, messages, cancelable=False):
|
||||
super(ScrollMessageBox, self).__init__()
|
||||
super().__init__()
|
||||
self.setWindowTitle(title)
|
||||
self.icon = icon
|
||||
|
||||
|
|
@ -49,8 +49,6 @@ class ScrollMessageBox(QtWidgets.QDialog):
|
|||
|
||||
self.setWindowFlags(QtCore.Qt.WindowTitleHint)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
scroll_widget = QtWidgets.QScrollArea(self)
|
||||
scroll_widget.setWidgetResizable(True)
|
||||
content_widget = QtWidgets.QWidget(self)
|
||||
|
|
@ -63,14 +61,8 @@ class ScrollMessageBox(QtWidgets.QDialog):
|
|||
content_layout.addWidget(label_widget)
|
||||
message_len = max(message_len, len(message))
|
||||
|
||||
# guess size of scrollable area
|
||||
# WARNING: 'desktop' method probably won't work in PySide6
|
||||
desktop = QtWidgets.QApplication.desktop()
|
||||
max_width = desktop.availableGeometry().width()
|
||||
scroll_widget.setMinimumWidth(
|
||||
min(max_width, message_len * 6)
|
||||
)
|
||||
layout.addWidget(scroll_widget)
|
||||
# Set minimum width
|
||||
scroll_widget.setMinimumWidth(360)
|
||||
|
||||
buttons = QtWidgets.QDialogButtonBox.Ok
|
||||
if cancelable:
|
||||
|
|
@ -86,7 +78,9 @@ class ScrollMessageBox(QtWidgets.QDialog):
|
|||
btn.clicked.connect(self._on_copy_click)
|
||||
btn_box.addButton(btn, QtWidgets.QDialogButtonBox.NoRole)
|
||||
|
||||
layout.addWidget(btn_box)
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.addWidget(scroll_widget, 1)
|
||||
main_layout.addWidget(btn_box, 0)
|
||||
|
||||
def _on_copy_click(self):
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
|
|
@ -104,7 +98,7 @@ class SimplePopup(QtWidgets.QDialog):
|
|||
on_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None, *args, **kwargs):
|
||||
super(SimplePopup, self).__init__(parent=parent, *args, **kwargs)
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
|
||||
# Set default title
|
||||
self.setWindowTitle("Popup")
|
||||
|
|
@ -161,7 +155,7 @@ class SimplePopup(QtWidgets.QDialog):
|
|||
geo = self._calculate_window_geometry()
|
||||
self.setGeometry(geo)
|
||||
|
||||
return super(SimplePopup, self).showEvent(event)
|
||||
return super().showEvent(event)
|
||||
|
||||
def _on_clicked(self):
|
||||
"""Callback for when the 'show' button is clicked.
|
||||
|
|
@ -228,9 +222,7 @@ class PopupUpdateKeys(SimplePopup):
|
|||
on_clicked_state = QtCore.Signal(bool)
|
||||
|
||||
def __init__(self, parent=None, *args, **kwargs):
|
||||
super(PopupUpdateKeys, self).__init__(
|
||||
parent=parent, *args, **kwargs
|
||||
)
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
|
||||
layout = self.layout()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'core' version."""
|
||||
__version__ = "1.6.1+dev"
|
||||
__version__ = "1.6.5+dev"
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ qtawesome = "0.7.3"
|
|||
[ayon.runtimeDependencies]
|
||||
aiohttp-middlewares = "^2.0.0"
|
||||
Click = "^8"
|
||||
OpenTimelineIO = "0.17.0"
|
||||
otio-burnins-adapter = "1.0.0"
|
||||
OpenTimelineIO = "0.16.0"
|
||||
opencolorio = "^2.3.2,<2.4.0"
|
||||
Pillow = "9.5.0"
|
||||
websocket-client = ">=0.40.0,<2"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name = "core"
|
||||
title = "Core"
|
||||
version = "1.6.1+dev"
|
||||
version = "1.6.5+dev"
|
||||
|
||||
client_dir = "ayon_core"
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
[tool.poetry]
|
||||
name = "ayon-core"
|
||||
version = "1.6.1+dev"
|
||||
version = "1.6.5+dev"
|
||||
description = ""
|
||||
authors = ["Ynput Team <team@ynput.io>"]
|
||||
readme = "README.md"
|
||||
|
|
@ -27,17 +27,6 @@ codespell = "^2.2.6"
|
|||
semver = "^3.0.2"
|
||||
mypy = "^1.14.0"
|
||||
mock = "^5.0.0"
|
||||
tomlkit = "^0.13.2"
|
||||
requests = "^2.32.3"
|
||||
mkdocs-material = "^9.6.7"
|
||||
mkdocs-autoapi = "^0.4.0"
|
||||
mkdocstrings-python = "^1.16.2"
|
||||
mkdocs-minify-plugin = "^0.8.0"
|
||||
markdown-checklist = "^0.4.4"
|
||||
mdx-gh-links = "^0.4"
|
||||
pymdown-extensions = "^10.14.3"
|
||||
mike = "^2.1.3"
|
||||
mkdocstrings-shell = "^1.0.2"
|
||||
nxtools = "^1.6"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
|
|
|
|||
|
|
@ -454,7 +454,7 @@ DEFAULT_TOOLS_VALUES = {
|
|||
"hosts": [],
|
||||
"task_types": [],
|
||||
"tasks": [],
|
||||
"template": "{product[type]}{Task[name]}{Variant}"
|
||||
"template": "{product[type]}{Task[name]}{Variant}<_{Aov}>"
|
||||
},
|
||||
{
|
||||
"product_types": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue