mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into feature/AY-7125_advanced-editorial-publish-to-ayon-38
This commit is contained in:
commit
1c943d78e9
31 changed files with 590 additions and 201 deletions
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,20 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to AYON Tray
|
||||
options:
|
||||
- 1.0.14
|
||||
- 1.0.13
|
||||
- 1.0.12
|
||||
- 1.0.11
|
||||
- 1.0.10
|
||||
- 1.0.9
|
||||
- 1.0.8
|
||||
- 1.0.7
|
||||
- 1.0.6
|
||||
- 1.0.5
|
||||
- 1.0.4
|
||||
- 1.0.3
|
||||
- 1.0.2
|
||||
- 1.0.1
|
||||
- 1.0.0
|
||||
- 0.4.4
|
||||
- 0.4.3
|
||||
|
|
|
|||
|
|
@ -26,10 +26,12 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
|||
"photoshop",
|
||||
"tvpaint",
|
||||
"substancepainter",
|
||||
"substancedesigner",
|
||||
"aftereffects",
|
||||
"wrap",
|
||||
"openrv",
|
||||
"cinema4d"
|
||||
"cinema4d",
|
||||
"silhouette",
|
||||
}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class OCIOEnvHook(PreLaunchHook):
|
|||
order = 0
|
||||
hosts = {
|
||||
"substancepainter",
|
||||
"substancedesigner",
|
||||
"fusion",
|
||||
"blender",
|
||||
"aftereffects",
|
||||
|
|
@ -20,7 +21,8 @@ class OCIOEnvHook(PreLaunchHook):
|
|||
"hiero",
|
||||
"resolve",
|
||||
"openrv",
|
||||
"cinema4d"
|
||||
"cinema4d",
|
||||
"silhouette",
|
||||
}
|
||||
launch_types = set()
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from ayon_core.pipeline.plugin_discover import (
|
|||
deregister_plugin,
|
||||
deregister_plugin_path
|
||||
)
|
||||
from ayon_core.pipeline import get_staging_dir_info
|
||||
from ayon_core.pipeline.staging_dir import get_staging_dir_info, StagingDir
|
||||
|
||||
from .constants import DEFAULT_VARIANT_VALUE
|
||||
from .product_name import get_product_name
|
||||
|
|
@ -833,7 +833,7 @@ class Creator(BaseCreator):
|
|||
"""
|
||||
return self.pre_create_attr_defs
|
||||
|
||||
def get_staging_dir(self, instance):
|
||||
def get_staging_dir(self, instance) -> Optional[StagingDir]:
|
||||
"""Return the staging dir and persistence from instance.
|
||||
|
||||
Args:
|
||||
|
|
@ -915,7 +915,8 @@ class Creator(BaseCreator):
|
|||
|
||||
instance.transient_data.update({
|
||||
"stagingDir": staging_dir_path,
|
||||
"stagingDir_persistent": staging_dir_info.persistent,
|
||||
"stagingDir_persistent": staging_dir_info.is_persistent,
|
||||
"stagingDir_is_custom": staging_dir_info.is_custom,
|
||||
})
|
||||
|
||||
self.log.info(f"Applied staging dir to instance: {staging_dir_path}")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import os
|
|||
import logging
|
||||
import collections
|
||||
|
||||
from ayon_core.pipeline.constants import AVALON_INSTANCE_ID
|
||||
from ayon_core.pipeline.constants import AYON_INSTANCE_ID
|
||||
|
||||
from .product_name import get_product_name
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ class LegacyCreator:
|
|||
# Default data
|
||||
self.data = collections.OrderedDict()
|
||||
# TODO use 'AYON_INSTANCE_ID' when all hosts support it
|
||||
self.data["id"] = AVALON_INSTANCE_ID
|
||||
self.data["id"] = AYON_INSTANCE_ID
|
||||
self.data["productType"] = self.product_type
|
||||
self.data["folderPath"] = folder_path
|
||||
self.data["productName"] = name
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import copy
|
||||
import collections
|
||||
from uuid import uuid4
|
||||
import typing
|
||||
from typing import Optional, Dict, List, Any
|
||||
|
||||
from ayon_core.lib.attribute_definitions import (
|
||||
|
|
@ -17,6 +18,9 @@ from ayon_core.pipeline import (
|
|||
from .exceptions import ImmutableKeyError
|
||||
from .changes import TrackChangesItem
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .creator_plugins import BaseCreator
|
||||
|
||||
|
||||
class ConvertorItem:
|
||||
"""Item representing convertor plugin.
|
||||
|
|
@ -444,10 +448,11 @@ class CreatedInstance:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
product_type,
|
||||
product_name,
|
||||
data,
|
||||
creator,
|
||||
product_type: str,
|
||||
product_name: str,
|
||||
data: Dict[str, Any],
|
||||
creator: "BaseCreator",
|
||||
transient_data: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
self._creator = creator
|
||||
creator_identifier = creator.identifier
|
||||
|
|
@ -462,7 +467,9 @@ class CreatedInstance:
|
|||
self._members = []
|
||||
|
||||
# Data that can be used for lifetime of object
|
||||
self._transient_data = {}
|
||||
if transient_data is None:
|
||||
transient_data = {}
|
||||
self._transient_data = transient_data
|
||||
|
||||
# Create a copy of passed data to avoid changing them on the fly
|
||||
data = copy.deepcopy(data or {})
|
||||
|
|
@ -492,7 +499,7 @@ class CreatedInstance:
|
|||
item_id = data.get("id")
|
||||
# TODO use only 'AYON_INSTANCE_ID' when all hosts support it
|
||||
if item_id not in {AYON_INSTANCE_ID, AVALON_INSTANCE_ID}:
|
||||
item_id = AVALON_INSTANCE_ID
|
||||
item_id = AYON_INSTANCE_ID
|
||||
self._data["id"] = item_id
|
||||
self._data["productType"] = product_type
|
||||
self._data["productName"] = product_name
|
||||
|
|
@ -787,16 +794,26 @@ class CreatedInstance:
|
|||
self._create_context.instance_create_attr_defs_changed(self.id)
|
||||
|
||||
@classmethod
|
||||
def from_existing(cls, instance_data, creator):
|
||||
def from_existing(
|
||||
cls,
|
||||
instance_data: Dict[str, Any],
|
||||
creator: "BaseCreator",
|
||||
transient_data: Optional[Dict[str, Any]] = None,
|
||||
) -> "CreatedInstance":
|
||||
"""Convert instance data from workfile to CreatedInstance.
|
||||
|
||||
Args:
|
||||
instance_data (Dict[str, Any]): Data in a structure ready for
|
||||
'CreatedInstance' object.
|
||||
creator (BaseCreator): Creator plugin which is creating the
|
||||
instance of for which the instance belong.
|
||||
"""
|
||||
instance of for which the instance belongs.
|
||||
transient_data (Optional[dict[str, Any]]): Instance transient
|
||||
data.
|
||||
|
||||
Returns:
|
||||
CreatedInstance: Instance object.
|
||||
|
||||
"""
|
||||
instance_data = copy.deepcopy(instance_data)
|
||||
|
||||
product_type = instance_data.get("productType")
|
||||
|
|
@ -809,7 +826,11 @@ class CreatedInstance:
|
|||
product_name = instance_data.get("subset")
|
||||
|
||||
return cls(
|
||||
product_type, product_name, instance_data, creator
|
||||
product_type,
|
||||
product_name,
|
||||
instance_data,
|
||||
creator,
|
||||
transient_data=transient_data,
|
||||
)
|
||||
|
||||
def attribute_value_changed(self, key, changes):
|
||||
|
|
|
|||
|
|
@ -196,11 +196,11 @@ def is_clip_from_media_sequence(otio_clip):
|
|||
return is_input_sequence or is_input_sequence_legacy
|
||||
|
||||
|
||||
def remap_range_on_file_sequence(otio_clip, in_out_range):
|
||||
def remap_range_on_file_sequence(otio_clip, otio_range):
|
||||
"""
|
||||
Args:
|
||||
otio_clip (otio.schema.Clip): The OTIO clip to check.
|
||||
in_out_range (tuple[float, float]): The in-out range to remap.
|
||||
otio_range (otio.schema.TimeRange): The trim range to apply.
|
||||
|
||||
Returns:
|
||||
tuple(int, int): The remapped range as discrete frame number.
|
||||
|
|
@ -211,17 +211,24 @@ def remap_range_on_file_sequence(otio_clip, in_out_range):
|
|||
if not is_clip_from_media_sequence(otio_clip):
|
||||
raise ValueError(f"Cannot map on non-file sequence clip {otio_clip}.")
|
||||
|
||||
try:
|
||||
media_in_trimmed, media_out_trimmed = in_out_range
|
||||
|
||||
except ValueError as error:
|
||||
raise ValueError("Invalid in_out_range provided.") from error
|
||||
|
||||
media_ref = otio_clip.media_reference
|
||||
available_range = otio_clip.available_range()
|
||||
source_range = otio_clip.source_range
|
||||
available_range_rate = available_range.start_time.rate
|
||||
media_in = available_range.start_time.value
|
||||
|
||||
# Backward-compatibility for Hiero OTIO exporter.
|
||||
# NTSC compatibility might introduce floating rates, when these are
|
||||
# not exactly the same (23.976 vs 23.976024627685547)
|
||||
# this will cause precision issue in computation.
|
||||
# Currently round to 2 decimals for comparison,
|
||||
# but this should always rescale after that.
|
||||
rounded_av_rate = round(available_range_rate, 2)
|
||||
rounded_range_rate = round(otio_range.start_time.rate, 2)
|
||||
|
||||
if rounded_av_rate != rounded_range_rate:
|
||||
raise ValueError("Inconsistent range between clip and provided clip")
|
||||
|
||||
source_range = otio_clip.source_range
|
||||
media_in = available_range.start_time
|
||||
available_range_start_frame = (
|
||||
available_range.start_time.to_frames()
|
||||
)
|
||||
|
|
@ -231,19 +238,26 @@ def remap_range_on_file_sequence(otio_clip, in_out_range):
|
|||
# source range for image sequence. Following code maintain
|
||||
# backward-compatibility by adjusting media_in
|
||||
# while we are updating those.
|
||||
conformed_src_in = source_range.start_time.rescaled_to(
|
||||
available_range_rate
|
||||
)
|
||||
if (
|
||||
is_clip_from_media_sequence(otio_clip)
|
||||
and available_range_start_frame == media_ref.start_frame
|
||||
and source_range.start_time.to_frames() < media_ref.start_frame
|
||||
and conformed_src_in.to_frames() < media_ref.start_frame
|
||||
):
|
||||
media_in = 0
|
||||
media_in = otio.opentime.RationalTime(
|
||||
0, rate=available_range_rate
|
||||
)
|
||||
|
||||
src_offset_in = otio_range.start_time - media_in
|
||||
frame_in = otio.opentime.RationalTime.from_frames(
|
||||
media_in_trimmed - media_in + media_ref.start_frame,
|
||||
media_ref.start_frame + src_offset_in.to_frames(),
|
||||
rate=available_range_rate,
|
||||
).to_frames()
|
||||
|
||||
frame_out = otio.opentime.RationalTime.from_frames(
|
||||
media_out_trimmed - media_in + media_ref.start_frame,
|
||||
frame_in + otio_range.duration.to_frames() - 1,
|
||||
rate=available_range_rate,
|
||||
).to_frames()
|
||||
|
||||
|
|
@ -261,21 +275,6 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
|
|||
media_ref = otio_clip.media_reference
|
||||
is_input_sequence = is_clip_from_media_sequence(otio_clip)
|
||||
|
||||
# Temporary.
|
||||
# Some AYON custom OTIO exporter were implemented with relative
|
||||
# source range for image sequence. Following code maintain
|
||||
# backward-compatibility by adjusting available range
|
||||
# while we are updating those.
|
||||
if (
|
||||
is_input_sequence
|
||||
and available_range.start_time.to_frames() == media_ref.start_frame
|
||||
and source_range.start_time.to_frames() < media_ref.start_frame
|
||||
):
|
||||
available_range = _ot.TimeRange(
|
||||
_ot.RationalTime(0, rate=available_range_rate),
|
||||
available_range.duration,
|
||||
)
|
||||
|
||||
# Conform source range bounds to available range rate
|
||||
# .e.g. embedded TC of (3600 sec/ 1h), duration 100 frames
|
||||
#
|
||||
|
|
@ -320,6 +319,22 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
|
|||
else:
|
||||
conformed_source_range = source_range
|
||||
|
||||
# Temporary.
|
||||
# Some AYON custom OTIO exporter were implemented with relative
|
||||
# source range for image sequence. Following code maintain
|
||||
# backward-compatibility by adjusting available range
|
||||
# while we are updating those.
|
||||
if (
|
||||
is_input_sequence
|
||||
and available_range.start_time.to_frames() == media_ref.start_frame
|
||||
and conformed_source_range.start_time.to_frames() <
|
||||
media_ref.start_frame
|
||||
):
|
||||
available_range = _ot.TimeRange(
|
||||
_ot.RationalTime(0, rate=available_range_rate),
|
||||
available_range.duration,
|
||||
)
|
||||
|
||||
# modifiers
|
||||
time_scalar = 1.
|
||||
offset_in = 0
|
||||
|
|
@ -374,31 +389,47 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
|
|||
offset_in, offset_out = offset_out, offset_in
|
||||
handle_start, handle_end = handle_end, handle_start
|
||||
|
||||
# compute retimed range
|
||||
media_in_trimmed = conformed_source_range.start_time.value + offset_in
|
||||
media_out_trimmed = media_in_trimmed + (
|
||||
(
|
||||
conformed_source_range.duration.value
|
||||
* abs(time_scalar)
|
||||
+ offset_out
|
||||
) - 1
|
||||
)
|
||||
|
||||
media_in = available_range.start_time.value
|
||||
media_out = available_range.end_time_inclusive().value
|
||||
|
||||
# If media source is an image sequence, returned
|
||||
# mediaIn/mediaOut have to correspond
|
||||
# to frame numbers from source sequence.
|
||||
if is_input_sequence:
|
||||
|
||||
src_in = conformed_source_range.start_time
|
||||
src_duration = conformed_source_range.duration
|
||||
|
||||
offset_in = otio.opentime.RationalTime(offset_in, rate=src_in.rate)
|
||||
offset_duration = otio.opentime.RationalTime(
|
||||
offset_out,
|
||||
rate=src_duration.rate
|
||||
)
|
||||
|
||||
trim_range = otio.opentime.TimeRange(
|
||||
start_time=src_in + offset_in,
|
||||
duration=src_duration + offset_duration
|
||||
)
|
||||
|
||||
# preserve discrete frame numbers
|
||||
media_in_trimmed, media_out_trimmed = remap_range_on_file_sequence(
|
||||
otio_clip,
|
||||
(media_in_trimmed, media_out_trimmed)
|
||||
trim_range,
|
||||
)
|
||||
media_in = media_ref.start_frame
|
||||
media_out = media_in + available_range.duration.to_frames() - 1
|
||||
|
||||
else:
|
||||
# compute retimed range
|
||||
media_in_trimmed = conformed_source_range.start_time.value + offset_in
|
||||
media_out_trimmed = media_in_trimmed + (
|
||||
(
|
||||
conformed_source_range.duration.value
|
||||
* abs(time_scalar)
|
||||
+ offset_out
|
||||
) - 1
|
||||
)
|
||||
|
||||
media_in = available_range.start_time.value
|
||||
media_out = available_range.end_time_inclusive().value
|
||||
|
||||
# adjust available handles if needed
|
||||
if (media_in_trimmed - media_in) < handle_start:
|
||||
handle_start = max(0, media_in_trimmed - media_in)
|
||||
|
|
|
|||
|
|
@ -708,6 +708,7 @@ def get_instance_staging_dir(instance):
|
|||
project_settings=context.data["project_settings"],
|
||||
template_data=template_data,
|
||||
always_return_path=True,
|
||||
username=context.data["user"],
|
||||
)
|
||||
|
||||
staging_dir_path = staging_dir_info.directory
|
||||
|
|
@ -716,8 +717,8 @@ def get_instance_staging_dir(instance):
|
|||
os.makedirs(staging_dir_path, exist_ok=True)
|
||||
instance.data.update({
|
||||
"stagingDir": staging_dir_path,
|
||||
"stagingDir_persistent": staging_dir_info.persistent,
|
||||
"stagingDir_custom": staging_dir_info.custom
|
||||
"stagingDir_persistent": staging_dir_info.is_persistent,
|
||||
"stagingDir_is_custom": staging_dir_info.is_custom
|
||||
})
|
||||
|
||||
return staging_dir_path
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import logging
|
||||
import warnings
|
||||
from typing import Optional, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ayon_core.lib import Logger, filter_profiles
|
||||
|
|
@ -11,21 +14,41 @@ from .tempdir import get_temp_dir
|
|||
@dataclass
|
||||
class StagingDir:
|
||||
directory: str
|
||||
persistent: bool
|
||||
custom: bool # Whether the staging dir is a custom staging dir
|
||||
is_persistent: bool
|
||||
# Whether the staging dir is a custom staging dir
|
||||
is_custom: bool
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if key == "persistent":
|
||||
warnings.warn(
|
||||
"'StagingDir.persistent' is deprecated."
|
||||
" Use 'StagingDir.is_persistent' instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
key = "is_persistent"
|
||||
super().__setattr__(key, value)
|
||||
|
||||
@property
|
||||
def persistent(self):
|
||||
warnings.warn(
|
||||
"'StagingDir.persistent' is deprecated."
|
||||
" Use 'StagingDir.is_persistent' instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
return self.is_persistent
|
||||
|
||||
|
||||
def get_staging_dir_config(
|
||||
project_name,
|
||||
task_type,
|
||||
task_name,
|
||||
product_type,
|
||||
product_name,
|
||||
host_name,
|
||||
project_settings=None,
|
||||
anatomy=None,
|
||||
log=None,
|
||||
):
|
||||
project_name: str,
|
||||
task_type: Optional[str],
|
||||
task_name: Optional[str],
|
||||
product_type: str,
|
||||
product_name: str,
|
||||
host_name: str,
|
||||
project_settings: Optional[Dict[str, Any]] = None,
|
||||
anatomy: Optional[Anatomy] = None,
|
||||
log: Optional[logging.Logger] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Get matching staging dir profile.
|
||||
|
||||
Args:
|
||||
|
|
@ -76,7 +99,6 @@ def get_staging_dir_config(
|
|||
|
||||
# get template from template name
|
||||
template_name = profile["template_name"]
|
||||
_validate_template_name(project_name, template_name, anatomy)
|
||||
|
||||
template = anatomy.get_template_item("staging", template_name)
|
||||
|
||||
|
|
@ -93,35 +115,23 @@ def get_staging_dir_config(
|
|||
return {"template": template, "persistence": data_persistence}
|
||||
|
||||
|
||||
def _validate_template_name(project_name, template_name, anatomy):
|
||||
"""Check that staging dir section with appropriate template exist.
|
||||
|
||||
Raises:
|
||||
ValueError - if misconfigured template
|
||||
"""
|
||||
if template_name not in anatomy.templates["staging"]:
|
||||
raise ValueError(
|
||||
f'Anatomy of project "{project_name}" does not have set'
|
||||
f' "{template_name}" template key at Staging Dir category!'
|
||||
)
|
||||
|
||||
|
||||
def get_staging_dir_info(
|
||||
project_entity,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
product_type,
|
||||
product_name,
|
||||
host_name,
|
||||
anatomy=None,
|
||||
project_settings=None,
|
||||
template_data=None,
|
||||
always_return_path=True,
|
||||
force_tmp_dir=False,
|
||||
logger=None,
|
||||
prefix=None,
|
||||
suffix=None,
|
||||
):
|
||||
project_entity: Dict[str, Any],
|
||||
folder_entity: Optional[Dict[str, Any]],
|
||||
task_entity: Optional[Dict[str, Any]],
|
||||
product_type: str,
|
||||
product_name: str,
|
||||
host_name: str,
|
||||
anatomy: Optional[Anatomy] = None,
|
||||
project_settings: Optional[Dict[str, Any]] = None,
|
||||
template_data: Optional[Dict[str, Any]] = None,
|
||||
always_return_path: bool = True,
|
||||
force_tmp_dir: bool = False,
|
||||
logger: Optional[logging.Logger] = None,
|
||||
prefix: Optional[str] = None,
|
||||
suffix: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
) -> Optional[StagingDir]:
|
||||
"""Get staging dir info data.
|
||||
|
||||
If `force_temp` is set, staging dir will be created as tempdir.
|
||||
|
|
@ -148,6 +158,7 @@ def get_staging_dir_info(
|
|||
logger (Optional[logging.Logger]): Logger instance.
|
||||
prefix (Optional[str]) Optional prefix for staging dir name.
|
||||
suffix (Optional[str]): Optional suffix for staging dir name.
|
||||
username (Optional[str]): AYON Username.
|
||||
|
||||
Returns:
|
||||
Optional[StagingDir]: Staging dir info data
|
||||
|
|
@ -161,16 +172,22 @@ def get_staging_dir_info(
|
|||
)
|
||||
|
||||
if force_tmp_dir:
|
||||
return get_temp_dir(
|
||||
project_name=project_entity["name"],
|
||||
anatomy=anatomy,
|
||||
prefix=prefix,
|
||||
suffix=suffix,
|
||||
return StagingDir(
|
||||
get_temp_dir(
|
||||
project_name=project_entity["name"],
|
||||
anatomy=anatomy,
|
||||
prefix=prefix,
|
||||
suffix=suffix,
|
||||
),
|
||||
is_persistent=False,
|
||||
is_custom=False
|
||||
)
|
||||
|
||||
# making few queries to database
|
||||
ctx_data = get_template_data(
|
||||
project_entity, folder_entity, task_entity, host_name
|
||||
project_entity, folder_entity, task_entity, host_name,
|
||||
settings=project_settings,
|
||||
username=username
|
||||
)
|
||||
|
||||
# add additional data
|
||||
|
|
@ -205,8 +222,8 @@ def get_staging_dir_info(
|
|||
dir_template = staging_dir_config["template"]["directory"]
|
||||
return StagingDir(
|
||||
dir_template.format_strict(ctx_data),
|
||||
persistent=staging_dir_config["persistence"],
|
||||
custom=True
|
||||
is_persistent=staging_dir_config["persistence"],
|
||||
is_custom=True
|
||||
)
|
||||
|
||||
# no config found but force an output
|
||||
|
|
@ -218,8 +235,8 @@ def get_staging_dir_info(
|
|||
prefix=prefix,
|
||||
suffix=suffix,
|
||||
),
|
||||
persistent=False,
|
||||
custom=False
|
||||
is_persistent=False,
|
||||
is_custom=False
|
||||
)
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from ayon_core.settings import get_studio_settings
|
|||
from ayon_core.lib.local_settings import get_ayon_username
|
||||
|
||||
|
||||
def get_general_template_data(settings=None):
|
||||
def get_general_template_data(settings=None, username=None):
|
||||
"""General template data based on system settings or machine.
|
||||
|
||||
Output contains formatting keys:
|
||||
|
|
@ -14,17 +14,22 @@ def get_general_template_data(settings=None):
|
|||
|
||||
Args:
|
||||
settings (Dict[str, Any]): Studio or project settings.
|
||||
username (Optional[str]): AYON Username.
|
||||
"""
|
||||
|
||||
if not settings:
|
||||
settings = get_studio_settings()
|
||||
|
||||
if username is None:
|
||||
username = get_ayon_username()
|
||||
|
||||
core_settings = settings["core"]
|
||||
return {
|
||||
"studio": {
|
||||
"name": core_settings["studio_name"],
|
||||
"code": core_settings["studio_code"]
|
||||
},
|
||||
"user": get_ayon_username()
|
||||
"user": username
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -145,6 +150,7 @@ def get_template_data(
|
|||
task_entity=None,
|
||||
host_name=None,
|
||||
settings=None,
|
||||
username=None
|
||||
):
|
||||
"""Prepare data for templates filling from entered documents and info.
|
||||
|
||||
|
|
@ -167,12 +173,13 @@ def get_template_data(
|
|||
host_name (Optional[str]): Used to fill '{app}' key.
|
||||
settings (Union[Dict, None]): Prepared studio or project settings.
|
||||
They're queried if not passed (may be slower).
|
||||
username (Optional[str]): AYON Username.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Data prepared for filling workdir template.
|
||||
"""
|
||||
|
||||
template_data = get_general_template_data(settings)
|
||||
template_data = get_general_template_data(settings, username=username)
|
||||
template_data.update(get_project_template_data(project_entity))
|
||||
if folder_entity:
|
||||
template_data.update(get_folder_template_data(
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin):
|
|||
# NOTE we should use 'context.data["user"]' but that has higher
|
||||
# order.
|
||||
("AYON_USERNAME", get_ayon_username()),
|
||||
("AYON_HOST_NAME", context.data["hostName"]),
|
||||
):
|
||||
if value:
|
||||
self.log.debug(f"Setting job env: {key}: {value}")
|
||||
|
|
|
|||
|
|
@ -93,8 +93,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
|
|||
|
||||
# now we can just add instances from json file and we are done
|
||||
any_staging_dir_persistent = False
|
||||
for instance_data in data.get("instances"):
|
||||
|
||||
for instance_data in data["instances"]:
|
||||
self.log.debug(" - processing instance for {}".format(
|
||||
instance_data.get("productName")))
|
||||
instance = self._context.create_instance(
|
||||
|
|
@ -105,7 +104,11 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
|
|||
instance.data.update(instance_data)
|
||||
|
||||
# stash render job id for later validation
|
||||
instance.data["render_job_id"] = data.get("job").get("_id")
|
||||
instance.data["publishJobMetadata"] = data
|
||||
# TODO remove 'render_job_id' here and rather use
|
||||
# 'publishJobMetadata' where is needed.
|
||||
# - this is deadline specific
|
||||
instance.data["render_job_id"] = data.get("job", {}).get("_id")
|
||||
staging_dir_persistent = instance.data.get(
|
||||
"stagingDir_persistent", False
|
||||
)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
|
|||
"yeticacheUE",
|
||||
"tycache",
|
||||
"usd",
|
||||
"oxrig"
|
||||
"oxrig",
|
||||
"sbsar",
|
||||
]
|
||||
|
||||
def process(self, instance):
|
||||
|
|
|
|||
|
|
@ -209,13 +209,9 @@ class ExtractOTIOReview(
|
|||
# File sequence way
|
||||
if is_sequence:
|
||||
# Remap processing range to input file sequence.
|
||||
processing_range_as_frames = (
|
||||
processing_range.start_time.to_frames(),
|
||||
processing_range.end_time_inclusive().to_frames()
|
||||
)
|
||||
first, last = remap_range_on_file_sequence(
|
||||
r_otio_cl,
|
||||
processing_range_as_frames,
|
||||
processing_range,
|
||||
)
|
||||
input_fps = processing_range.start_time.rate
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin):
|
|||
label = "Validate File Saved"
|
||||
order = pyblish.api.ValidatorOrder - 0.1
|
||||
hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter",
|
||||
"cinema4d"]
|
||||
"cinema4d", "silhouette"]
|
||||
actions = [SaveByVersionUpAction, ShowWorkfilesAction]
|
||||
|
||||
def process(self, context):
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ Enabled vs Disabled logic in most of stylesheets
|
|||
font-family: "Noto Sans";
|
||||
font-weight: 450;
|
||||
outline: none;
|
||||
|
||||
/* Define icon size to fix size issues for most of DCCs */
|
||||
icon-size: 16px;
|
||||
}
|
||||
|
||||
QWidget {
|
||||
|
|
@ -1168,6 +1171,8 @@ ValidationArtistMessage QLabel {
|
|||
|
||||
#PublishLogMessage {
|
||||
font-family: "Noto Sans Mono";
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#PublishInstanceLogsLabel {
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ class ConvertorItemsGroupWidget(BaseGroupWidget):
|
|||
else:
|
||||
widget = ConvertorItemCardWidget(item, self)
|
||||
widget.selected.connect(self._on_widget_selection)
|
||||
widget.double_clicked(self.double_clicked)
|
||||
widget.double_clicked.connect(self.double_clicked)
|
||||
self._widgets_by_id[item.id] = widget
|
||||
self._content_layout.insertWidget(widget_idx, widget)
|
||||
widget_idx += 1
|
||||
|
|
|
|||
|
|
@ -1117,6 +1117,57 @@ class LogIconFrame(QtWidgets.QFrame):
|
|||
painter.end()
|
||||
|
||||
|
||||
class LogItemMessage(QtWidgets.QTextEdit):
|
||||
def __init__(self, msg, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
# Set as plain text to propagate new line characters
|
||||
self.setPlainText(msg)
|
||||
|
||||
self.setObjectName("PublishLogMessage")
|
||||
self.setReadOnly(True)
|
||||
self.setFrameStyle(QtWidgets.QFrame.NoFrame)
|
||||
self.setLineWidth(0)
|
||||
self.setMidLineWidth(0)
|
||||
pal = self.palette()
|
||||
pal.setColor(QtGui.QPalette.Base, QtCore.Qt.transparent)
|
||||
self.setPalette(pal)
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
viewport = self.viewport()
|
||||
viewport.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.setTextInteractionFlags(
|
||||
QtCore.Qt.TextBrowserInteraction)
|
||||
self.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
|
||||
self.setLineWrapMode(QtWidgets.QTextEdit.WidgetWidth)
|
||||
self.setWordWrapMode(
|
||||
QtGui.QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere
|
||||
)
|
||||
self.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Preferred,
|
||||
QtWidgets.QSizePolicy.Maximum
|
||||
)
|
||||
document = self.document()
|
||||
document.documentLayout().documentSizeChanged.connect(
|
||||
self._adjust_minimum_size
|
||||
)
|
||||
document.setDocumentMargin(0.0)
|
||||
self._height = None
|
||||
|
||||
def _adjust_minimum_size(self, size):
|
||||
self._height = size.height() + (2 * self.frameWidth())
|
||||
self.updateGeometry()
|
||||
|
||||
def sizeHint(self):
|
||||
size = super().sizeHint()
|
||||
if self._height is not None:
|
||||
size.setHeight(self._height)
|
||||
return size
|
||||
|
||||
def minimumSizeHint(self):
|
||||
return self.sizeHint()
|
||||
|
||||
|
||||
class LogItemWidget(QtWidgets.QWidget):
|
||||
log_level_to_flag = {
|
||||
10: LOG_DEBUG_VISIBLE,
|
||||
|
|
@ -1132,12 +1183,7 @@ class LogItemWidget(QtWidgets.QWidget):
|
|||
type_flag, level_n = self._get_log_info(log)
|
||||
icon_label = LogIconFrame(
|
||||
self, log["type"], level_n, log.get("is_validation_error"))
|
||||
message_label = QtWidgets.QLabel(log["msg"].rstrip(), self)
|
||||
message_label.setObjectName("PublishLogMessage")
|
||||
message_label.setTextInteractionFlags(
|
||||
QtCore.Qt.TextBrowserInteraction)
|
||||
message_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
|
||||
message_label.setWordWrap(True)
|
||||
message_label = LogItemMessage(log["msg"].rstrip(), self)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -1290,6 +1336,7 @@ class InstanceLogsWidget(QtWidgets.QWidget):
|
|||
|
||||
label_widget = QtWidgets.QLabel(instance.label, self)
|
||||
label_widget.setObjectName("PublishInstanceLogsLabel")
|
||||
label_widget.setWordWrap(True)
|
||||
logs_grid = LogsWithIconsView(instance.logs, self)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
|
@ -1329,9 +1376,11 @@ class InstancesLogsView(QtWidgets.QFrame):
|
|||
|
||||
content_wrap_widget = QtWidgets.QWidget(scroll_area)
|
||||
content_wrap_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
content_wrap_widget.setMinimumWidth(80)
|
||||
|
||||
content_widget = QtWidgets.QWidget(content_wrap_widget)
|
||||
content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
content_layout = QtWidgets.QVBoxLayout(content_widget)
|
||||
content_layout.setContentsMargins(8, 8, 8, 8)
|
||||
content_layout.setSpacing(15)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ def find_free_port(
|
|||
exclude_ports (list, tuple, set): List of ports that won't be
|
||||
checked form entered range.
|
||||
host (str): Host where will check for free ports. Set to
|
||||
"localhost" by default.
|
||||
"127.0.0.1" by default.
|
||||
"""
|
||||
if port_from is None:
|
||||
port_from = 8079
|
||||
|
|
@ -42,7 +42,7 @@ def find_free_port(
|
|||
|
||||
# Default host is localhost but it is possible to look for other hosts
|
||||
if host is None:
|
||||
host = "localhost"
|
||||
host = "127.0.0.1"
|
||||
|
||||
found_port = None
|
||||
while True:
|
||||
|
|
@ -78,7 +78,7 @@ class WebServerManager:
|
|||
self._log = None
|
||||
|
||||
self.port = port or 8079
|
||||
self.host = host or "localhost"
|
||||
self.host = host or "127.0.0.1"
|
||||
|
||||
self.on_stop_callbacks = []
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from ayon_core.resources import get_image_path
|
||||
from ayon_core.tools.flickcharm import FlickCharm
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui, QtSvg
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
|
||||
class DeselectableTreeView(QtWidgets.QTreeView):
|
||||
|
|
@ -19,48 +18,6 @@ class DeselectableTreeView(QtWidgets.QTreeView):
|
|||
QtWidgets.QTreeView.mousePressEvent(self, event)
|
||||
|
||||
|
||||
class TreeViewSpinner(QtWidgets.QTreeView):
|
||||
size = 160
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(TreeViewSpinner, self).__init__(parent=parent)
|
||||
|
||||
loading_image_path = get_image_path("spinner-200.svg")
|
||||
|
||||
self.spinner = QtSvg.QSvgRenderer(loading_image_path)
|
||||
|
||||
self.is_loading = False
|
||||
self.is_empty = True
|
||||
|
||||
def paint_loading(self, event):
|
||||
rect = event.rect()
|
||||
rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight())
|
||||
rect.moveTo(
|
||||
rect.x() + rect.width() / 2 - self.size / 2,
|
||||
rect.y() + rect.height() / 2 - self.size / 2
|
||||
)
|
||||
rect.setSize(QtCore.QSizeF(self.size, self.size))
|
||||
painter = QtGui.QPainter(self.viewport())
|
||||
self.spinner.render(painter, rect)
|
||||
|
||||
def paint_empty(self, event):
|
||||
painter = QtGui.QPainter(self.viewport())
|
||||
rect = event.rect()
|
||||
rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight())
|
||||
qtext_opt = QtGui.QTextOption(
|
||||
QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter
|
||||
)
|
||||
painter.drawText(rect, "No Data", qtext_opt)
|
||||
|
||||
def paintEvent(self, event):
|
||||
if self.is_loading:
|
||||
self.paint_loading(event)
|
||||
elif self.is_empty:
|
||||
self.paint_empty(event)
|
||||
else:
|
||||
super(TreeViewSpinner, self).paintEvent(event)
|
||||
|
||||
|
||||
class TreeView(QtWidgets.QTreeView):
|
||||
"""Ultimate TreeView with flick charm and double click signals.
|
||||
|
||||
|
|
|
|||
|
|
@ -184,9 +184,10 @@ class WorkareaModel:
|
|||
return items
|
||||
|
||||
for filename in os.listdir(workdir):
|
||||
# We want to support both files and folders. e.g. Silhoutte uses
|
||||
# folders as its project files. So we do not check whether it is
|
||||
# a file or not.
|
||||
filepath = os.path.join(workdir, filename)
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext not in self._extensions:
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
|
||||
# Initial setup
|
||||
workarea_btn_open.setEnabled(False)
|
||||
workarea_btn_browse.setEnabled(False)
|
||||
workarea_btn_save.setEnabled(False)
|
||||
published_btn_copy_n_open.setEnabled(False)
|
||||
published_btn_change_context.setEnabled(False)
|
||||
published_btn_cancel.setVisible(False)
|
||||
|
|
@ -278,8 +280,9 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
self._published_btn_change_context.setEnabled(enabled)
|
||||
|
||||
def _update_workarea_btns_state(self):
|
||||
enabled = self._is_save_enabled
|
||||
enabled = self._is_save_enabled and self._valid_selected_context
|
||||
self._workarea_btn_save.setEnabled(enabled)
|
||||
self._workarea_btn_browse.setEnabled(self._valid_selected_context)
|
||||
|
||||
def _on_published_repre_changed(self, event):
|
||||
self._valid_representation_id = event["representation_id"] is not None
|
||||
|
|
@ -294,6 +297,7 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
and self._selected_task_id is not None
|
||||
)
|
||||
self._update_published_btns_state()
|
||||
self._update_workarea_btns_state()
|
||||
|
||||
def _on_published_save_clicked(self):
|
||||
result = self._exec_save_as_dialog()
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.addWidget(pages_widget, 1)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
overlay_messages_widget = MessageOverlayObject(self)
|
||||
overlay_invalid_host = InvalidHostOverlay(self)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'core' version."""
|
||||
__version__ = "1.0.12+dev"
|
||||
__version__ = "1.0.15-dev"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name = "core"
|
||||
title = "Core"
|
||||
version = "1.0.12+dev"
|
||||
version = "1.0.15-dev"
|
||||
|
||||
client_dir = "ayon_core"
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
[tool.poetry]
|
||||
name = "ayon-core"
|
||||
version = "1.0.12+dev"
|
||||
version = "1.0.15-dev"
|
||||
description = ""
|
||||
authors = ["Ynput Team <team@ynput.io>"]
|
||||
readme = "README.md"
|
||||
|
|
|
|||
|
|
@ -1008,8 +1008,8 @@ DEFAULT_PUBLISH_VALUES = {
|
|||
{"name": "model", "order": 100},
|
||||
{"name": "assembly", "order": 150},
|
||||
{"name": "groom", "order": 175},
|
||||
{"name": "look", "order": 300},
|
||||
{"name": "rig", "order": 100},
|
||||
{"name": "look", "order": 200},
|
||||
{"name": "rig", "order": 300},
|
||||
# Shot layers
|
||||
{"name": "layout", "order": 200},
|
||||
{"name": "animation", "order": 300},
|
||||
|
|
@ -1033,7 +1033,8 @@ DEFAULT_PUBLISH_VALUES = {
|
|||
"maya",
|
||||
"nuke",
|
||||
"photoshop",
|
||||
"substancepainter"
|
||||
"substancepainter",
|
||||
"silhouette",
|
||||
],
|
||||
"enabled": True,
|
||||
"optional": False,
|
||||
|
|
@ -1053,7 +1054,8 @@ DEFAULT_PUBLISH_VALUES = {
|
|||
"harmony",
|
||||
"photoshop",
|
||||
"aftereffects",
|
||||
"fusion"
|
||||
"fusion",
|
||||
"silhouette",
|
||||
],
|
||||
"enabled": True,
|
||||
"optional": True,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"OTIO_SCHEMA": "Clip.2",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 23.976,
|
||||
"value": 108.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 23.976,
|
||||
"value": 883159.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"media_references": {
|
||||
"DEFAULT_MEDIA": {
|
||||
"OTIO_SCHEMA": "ImageSequenceReference.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"available_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 755.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 883750.0
|
||||
}
|
||||
},
|
||||
"available_image_bounds": null,
|
||||
"target_url_base": "/mnt/jobs/yahoo_theDog_1132/IN/FOOTAGE/SCANS_LINEAR/Panasonic Rec 709 to ACESCG/Panasonic P2 /A001_S001_S001_T004/",
|
||||
"name_prefix": "A001_S001_S001_T004.",
|
||||
"name_suffix": ".exr",
|
||||
"start_frame": 883750,
|
||||
"frame_step": 1,
|
||||
"rate": 1.0,
|
||||
"frame_zero_padding": 0,
|
||||
"missing_frame_policy": "error"
|
||||
}
|
||||
},
|
||||
"active_media_reference_key": "DEFAULT_MEDIA"
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
{
|
||||
"OTIO_SCHEMA": "Clip.2",
|
||||
"metadata": {},
|
||||
"name": "Main088sh110",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 23.976024627685547,
|
||||
"value": 82.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 23.976024627685547,
|
||||
"value": 1937905.9905694576
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Marker.2",
|
||||
"metadata": {
|
||||
"applieswhole": "1",
|
||||
"hiero_source_type": "TrackItem",
|
||||
"json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.shot\", \"variant\": \"main\", \"folderPath\": \"/shots/088/Main088sh110\", \"task\": null, \"clip_index\": \"70C9FA86-76A5-A045-A004-3158FB3F27C5\", \"hierarchy\": \"shots/088\", \"folder\": \"shots\", \"episode\": \"404\", \"sequence\": \"088\", \"track\": \"Main\", \"shot\": \"sh110\", \"reviewableSource\": \"Reference\", \"sourceResolution\": false, \"workfileFrameStart\": 1009, \"handleStart\": 8, \"handleEnd\": 8, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"088\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"404\", \"sequence\": \"088\", \"track\": \"Main\"}, \"heroTrack\": true, \"uuid\": \"8b0d1db8-7094-48ba-b2cd-df0d43cfffda\", \"reviewTrack\": \"Reference\", \"review\": true, \"folderName\": \"Main088sh110\", \"label\": \"/shots/088/Main088sh110 shotMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"f6b7f12c-f3a8-44fd-b4e4-acc63ed80bb1\", \"creator_attributes\": {\"workfileFrameStart\": 1009, \"handleStart\": 8, \"handleEnd\": 8, \"frameStart\": 1009, \"frameEnd\": 1091, \"clipIn\": 80, \"clipOut\": 161, \"clipDuration\": 82, \"sourceIn\": 8.0, \"sourceOut\": 89.0, \"fps\": \"from_selection\"}, \"publish_attributes\": {}}, \"io.ayon.creators.hiero.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"Main\", \"folderPath\": \"/shots/088/Main088sh110\", \"task\": null, \"clip_index\": \"70C9FA86-76A5-A045-A004-3158FB3F27C5\", \"hierarchy\": \"shots/088\", \"folder\": \"shots\", \"episode\": \"404\", \"sequence\": \"088\", \"track\": \"Main\", \"shot\": \"sh110\", \"reviewableSource\": \"Reference\", \"sourceResolution\": false, \"workfileFrameStart\": 1009, \"handleStart\": 8, \"handleEnd\": 8, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"088\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"404\", \"sequence\": \"088\", \"track\": \"Main\"}, \"heroTrack\": true, \"uuid\": \"8b0d1db8-7094-48ba-b2cd-df0d43cfffda\", \"reviewTrack\": \"Reference\", \"review\": true, \"folderName\": \"Main088sh110\", \"parent_instance_id\": \"f6b7f12c-f3a8-44fd-b4e4-acc63ed80bb1\", \"label\": \"/shots/088/Main088sh110 plateMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"64b54c11-7ab1-45ef-b156-9ed5d5552b9b\", \"creator_attributes\": {\"parentInstance\": \"/shots/088/Main088sh110 shotMain\", \"review\": true, \"reviewableSource\": \"Reference\"}, \"publish_attributes\": {}}}, \"clip_index\": \"70C9FA86-76A5-A045-A004-3158FB3F27C5\"}",
|
||||
"label": "AYONdata_6b797112",
|
||||
"note": "AYON data container"
|
||||
},
|
||||
"name": "AYONdata_6b797112",
|
||||
"color": "RED",
|
||||
"marked_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 23.976024627685547,
|
||||
"value": 0.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 23.976024627685547,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"comment": ""
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"media_references": {
|
||||
"DEFAULT_MEDIA": {
|
||||
"OTIO_SCHEMA": "ImageSequenceReference.1",
|
||||
"metadata": {
|
||||
"ayon.source.colorspace": "Input - Sony - Linear - Venice S-Gamut3.Cine",
|
||||
"ayon.source.height": 2160,
|
||||
"ayon.source.pixelAspect": 1.0,
|
||||
"ayon.source.width": 4096,
|
||||
"clip.properties.blendfunc": "0",
|
||||
"clip.properties.colourspacename": "default",
|
||||
"clip.properties.domainroot": "",
|
||||
"clip.properties.enabled": "1",
|
||||
"clip.properties.expanded": "1",
|
||||
"clip.properties.opacity": "1",
|
||||
"clip.properties.valuesource": "",
|
||||
"foundry.source.audio": "",
|
||||
"foundry.source.bitmapsize": "0",
|
||||
"foundry.source.bitsperchannel": "0",
|
||||
"foundry.source.channelformat": "integer",
|
||||
"foundry.source.colourtransform": "Input - Sony - Linear - Venice S-Gamut3.Cine",
|
||||
"foundry.source.duration": "98",
|
||||
"foundry.source.filename": "409_083_0015.%04d.exr 1001-1098",
|
||||
"foundry.source.filesize": "",
|
||||
"foundry.source.fragments": "98",
|
||||
"foundry.source.framerate": "23.98",
|
||||
"foundry.source.fullpath": "",
|
||||
"foundry.source.height": "2160",
|
||||
"foundry.source.layers": "colour",
|
||||
"foundry.source.path": "X:/prj/AYON_CIRCUIT_TEST/data/OBX_20240729_P159_DOG_409/EXR/409_083_0015/409_083_0015.%04d.exr 1001-1098",
|
||||
"foundry.source.pixelAspect": "1",
|
||||
"foundry.source.pixelAspectRatio": "",
|
||||
"foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 368",
|
||||
"foundry.source.reelID": "",
|
||||
"foundry.source.resolution": "",
|
||||
"foundry.source.samplerate": "Invalid",
|
||||
"foundry.source.shortfilename": "409_083_0015.%04d.exr 1001-1098",
|
||||
"foundry.source.shot": "",
|
||||
"foundry.source.shotDate": "",
|
||||
"foundry.source.startTC": "",
|
||||
"foundry.source.starttime": "1001",
|
||||
"foundry.source.timecode": "1937896",
|
||||
"foundry.source.umid": "4b3e13b3-e465-4df4-cb1f-257091b63815",
|
||||
"foundry.source.umidOriginator": "foundry.source.umid",
|
||||
"foundry.source.width": "4096",
|
||||
"foundry.timeline.colorSpace": "Input - Sony - Linear - Venice S-Gamut3.Cine",
|
||||
"foundry.timeline.duration": "98",
|
||||
"foundry.timeline.framerate": "23.98",
|
||||
"foundry.timeline.outputformat": "",
|
||||
"foundry.timeline.poster": "0",
|
||||
"foundry.timeline.posterLayer": "colour",
|
||||
"foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAABqAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=",
|
||||
"foundry.timeline.samplerate": "Invalid",
|
||||
"isSequence": true,
|
||||
"media.exr.camera_camera_type": "AXS-R7",
|
||||
"media.exr.camera_fps": "23.976",
|
||||
"media.exr.camera_id": "MPC-3610 0010762 Version6.30",
|
||||
"media.exr.camera_iso": "2500",
|
||||
"media.exr.camera_lens_type": "Unknown",
|
||||
"media.exr.camera_monitor_space": "OBX4_LUT_1_Night.cube",
|
||||
"media.exr.camera_nd_filter": "1",
|
||||
"media.exr.camera_roll_angle": "0.3",
|
||||
"media.exr.camera_shutter_angle": "180.0",
|
||||
"media.exr.camera_shutter_speed": "0.0208333",
|
||||
"media.exr.camera_shutter_type": "Speed and Angle",
|
||||
"media.exr.camera_sl_num": "00011434",
|
||||
"media.exr.camera_tilt_angle": "-7.4",
|
||||
"media.exr.camera_type": "Sony",
|
||||
"media.exr.camera_white_kelvin": "3200",
|
||||
"media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}",
|
||||
"media.exr.clip_details_codec": "F55_X-OCN_ST_4096_2160",
|
||||
"media.exr.clip_details_pixel_aspect_ratio": "1",
|
||||
"media.exr.clip_details_shot_frame_rate": "23.98p",
|
||||
"media.exr.compression": "0",
|
||||
"media.exr.compressionName": "none",
|
||||
"media.exr.dataWindow": "0,0,4095,2159",
|
||||
"media.exr.displayWindow": "0,0,4095,2159",
|
||||
"media.exr.lineOrder": "0",
|
||||
"media.exr.owner": "C272C010_240530HO",
|
||||
"media.exr.pixelAspectRatio": "1",
|
||||
"media.exr.screenWindowCenter": "0,0",
|
||||
"media.exr.screenWindowWidth": "1",
|
||||
"media.exr.tech_details_aspect_ratio": "1.8963",
|
||||
"media.exr.tech_details_cdl_sat": "1",
|
||||
"media.exr.tech_details_cdl_sop": "(1 1 1)(0 0 0)(1 1 1)",
|
||||
"media.exr.tech_details_gamma_space": "R709 Video",
|
||||
"media.exr.tech_details_par": "1",
|
||||
"media.exr.type": "scanlineimage",
|
||||
"media.input.bitsperchannel": "16-bit half float",
|
||||
"media.input.ctime": "2024-07-30 18:51:38",
|
||||
"media.input.filename": "X:/prj/AYON_CIRCUIT_TEST/data/OBX_20240729_P159_DOG_409/EXR/409_083_0015/409_083_0015.1001.exr",
|
||||
"media.input.filereader": "exr",
|
||||
"media.input.filesize": "53120020",
|
||||
"media.input.frame": "1",
|
||||
"media.input.frame_rate": "23.976",
|
||||
"media.input.height": "2160",
|
||||
"media.input.mtime": "2024-07-30 18:51:38",
|
||||
"media.input.timecode": "22:25:45:16",
|
||||
"media.input.width": "4096",
|
||||
"padding": 4
|
||||
},
|
||||
"name": "",
|
||||
"available_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 23.976,
|
||||
"value": 98.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 23.976,
|
||||
"value": 1937896.0
|
||||
}
|
||||
},
|
||||
"available_image_bounds": null,
|
||||
"target_url_base": "X:/prj/AYON_CIRCUIT_TEST/data/OBX_20240729_P159_DOG_409/EXR/409_083_0015\\",
|
||||
"name_prefix": "409_083_0015.",
|
||||
"name_suffix": ".exr",
|
||||
"start_frame": 1001,
|
||||
"frame_step": 1,
|
||||
"rate": 23.976,
|
||||
"frame_zero_padding": 4,
|
||||
"missing_frame_policy": "error"
|
||||
}
|
||||
},
|
||||
"active_media_reference_key": "DEFAULT_MEDIA"
|
||||
}
|
||||
|
|
@ -252,48 +252,48 @@ def test_multiple_review_clips_no_gap():
|
|||
|
||||
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
|
||||
f'C:\\no_tc{os.sep}output.%04d.tif '
|
||||
'-start_number 1199 C:/result/output.%04d.jpg',
|
||||
'-start_number 1198 C:/result/output.%04d.jpg',
|
||||
|
||||
'/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i '
|
||||
f'C:\\with_tc{os.sep}output.%04d.exr '
|
||||
'-start_number 1300 C:/result/output.%04d.jpg',
|
||||
'-start_number 1299 C:/result/output.%04d.jpg',
|
||||
|
||||
# Repeated 25fps tiff sequence multiple times till the end
|
||||
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
|
||||
f'C:\\no_tc{os.sep}output.%04d.tif '
|
||||
'-start_number 1397 C:/result/output.%04d.jpg',
|
||||
'-start_number 1395 C:/result/output.%04d.jpg',
|
||||
|
||||
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
|
||||
f'C:\\no_tc{os.sep}output.%04d.tif '
|
||||
'-start_number 1498 C:/result/output.%04d.jpg',
|
||||
'-start_number 1496 C:/result/output.%04d.jpg',
|
||||
|
||||
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
|
||||
f'C:\\no_tc{os.sep}output.%04d.tif '
|
||||
'-start_number 1599 C:/result/output.%04d.jpg',
|
||||
'-start_number 1597 C:/result/output.%04d.jpg',
|
||||
|
||||
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
|
||||
f'C:\\no_tc{os.sep}output.%04d.tif '
|
||||
'-start_number 1700 C:/result/output.%04d.jpg',
|
||||
'-start_number 1698 C:/result/output.%04d.jpg',
|
||||
|
||||
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
|
||||
f'C:\\no_tc{os.sep}output.%04d.tif '
|
||||
'-start_number 1801 C:/result/output.%04d.jpg',
|
||||
'-start_number 1799 C:/result/output.%04d.jpg',
|
||||
|
||||
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
|
||||
f'C:\\no_tc{os.sep}output.%04d.tif '
|
||||
'-start_number 1902 C:/result/output.%04d.jpg',
|
||||
'-start_number 1900 C:/result/output.%04d.jpg',
|
||||
|
||||
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
|
||||
f'C:\\no_tc{os.sep}output.%04d.tif '
|
||||
'-start_number 2003 C:/result/output.%04d.jpg',
|
||||
'-start_number 2001 C:/result/output.%04d.jpg',
|
||||
|
||||
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
|
||||
f'C:\\no_tc{os.sep}output.%04d.tif '
|
||||
'-start_number 2104 C:/result/output.%04d.jpg',
|
||||
'-start_number 2102 C:/result/output.%04d.jpg',
|
||||
|
||||
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
|
||||
f'C:\\no_tc{os.sep}output.%04d.tif '
|
||||
'-start_number 2205 C:/result/output.%04d.jpg'
|
||||
'-start_number 2203 C:/result/output.%04d.jpg'
|
||||
]
|
||||
|
||||
assert calls == expected
|
||||
|
|
|
|||
|
|
@ -64,6 +64,28 @@ def test_movie_embedded_tc_handle():
|
|||
)
|
||||
|
||||
|
||||
def test_movie_23fps_qt_embedded_tc():
|
||||
"""
|
||||
Movie clip (embedded timecode 1h)
|
||||
available_range = 1937896-1937994 23.976fps
|
||||
source_range = 1937905-1937987 23.97602462768554fps
|
||||
"""
|
||||
expected_data = {
|
||||
'mediaIn': 1009,
|
||||
'mediaOut': 1090,
|
||||
'handleStart': 8,
|
||||
'handleEnd': 8,
|
||||
'speed': 1.0
|
||||
}
|
||||
|
||||
_check_expected_retimed_values(
|
||||
"qt_23.976_embedded_long_tc.json",
|
||||
expected_data,
|
||||
handle_start=8,
|
||||
handle_end=8,
|
||||
)
|
||||
|
||||
|
||||
def test_movie_retime_effect():
|
||||
"""
|
||||
Movie clip (embedded timecode 1h)
|
||||
|
|
@ -187,3 +209,29 @@ def test_img_sequence_conform_to_23_976fps():
|
|||
handle_start=0,
|
||||
handle_end=8,
|
||||
)
|
||||
|
||||
|
||||
def test_img_sequence_conform_from_24_to_23_976fps():
|
||||
"""
|
||||
Img sequence clip
|
||||
available files = 883750-884504 24fps
|
||||
source_range = 883159-883267 23.976fps
|
||||
|
||||
This test ensures such entries do not trigger
|
||||
the legacy Hiero export compatibility.
|
||||
"""
|
||||
expected_data = {
|
||||
'mediaIn': 884043,
|
||||
'mediaOut': 884150,
|
||||
'handleStart': 0,
|
||||
'handleEnd': 0,
|
||||
'speed': 1.0
|
||||
}
|
||||
|
||||
_check_expected_retimed_values(
|
||||
"img_seq_24_to_23.976_no_legacy.json",
|
||||
expected_data,
|
||||
handle_start=0,
|
||||
handle_end=0,
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue