Pull latest changes.

This commit is contained in:
robin@ynput.io 2025-02-11 10:44:28 +01:00
commit 396424c225
32 changed files with 601 additions and 202 deletions

View file

@ -35,6 +35,20 @@ body:
label: Version label: Version
description: What version are you running? Look to AYON Tray description: What version are you running? Look to AYON Tray
options: 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 - 1.0.0
- 0.4.4 - 0.4.4
- 0.4.3 - 0.4.3

View file

@ -26,10 +26,12 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
"photoshop", "photoshop",
"tvpaint", "tvpaint",
"substancepainter", "substancepainter",
"substancedesigner",
"aftereffects", "aftereffects",
"wrap", "wrap",
"openrv", "openrv",
"cinema4d" "cinema4d",
"silhouette",
} }
launch_types = {LaunchTypes.local} launch_types = {LaunchTypes.local}

View file

@ -10,6 +10,7 @@ class OCIOEnvHook(PreLaunchHook):
order = 0 order = 0
hosts = { hosts = {
"substancepainter", "substancepainter",
"substancedesigner",
"fusion", "fusion",
"blender", "blender",
"aftereffects", "aftereffects",
@ -20,7 +21,8 @@ class OCIOEnvHook(PreLaunchHook):
"hiero", "hiero",
"resolve", "resolve",
"openrv", "openrv",
"cinema4d" "cinema4d",
"silhouette",
} }
launch_types = set() launch_types = set()

View file

@ -15,7 +15,7 @@ from ayon_core.pipeline.plugin_discover import (
deregister_plugin, deregister_plugin,
deregister_plugin_path 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 .constants import DEFAULT_VARIANT_VALUE
from .product_name import get_product_name from .product_name import get_product_name
@ -833,7 +833,7 @@ class Creator(BaseCreator):
""" """
return self.pre_create_attr_defs 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. """Return the staging dir and persistence from instance.
Args: Args:
@ -915,7 +915,8 @@ class Creator(BaseCreator):
instance.transient_data.update({ instance.transient_data.update({
"stagingDir": staging_dir_path, "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}") self.log.info(f"Applied staging dir to instance: {staging_dir_path}")

View file

@ -9,7 +9,7 @@ import os
import logging import logging
import collections 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 from .product_name import get_product_name
@ -34,7 +34,7 @@ class LegacyCreator:
# Default data # Default data
self.data = collections.OrderedDict() self.data = collections.OrderedDict()
# TODO use 'AYON_INSTANCE_ID' when all hosts support it # 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["productType"] = self.product_type
self.data["folderPath"] = folder_path self.data["folderPath"] = folder_path
self.data["productName"] = name self.data["productName"] = name

View file

@ -1,6 +1,7 @@
import copy import copy
import collections import collections
from uuid import uuid4 from uuid import uuid4
import typing
from typing import Optional, Dict, List, Any from typing import Optional, Dict, List, Any
from ayon_core.lib.attribute_definitions import ( from ayon_core.lib.attribute_definitions import (
@ -17,6 +18,9 @@ from ayon_core.pipeline import (
from .exceptions import ImmutableKeyError from .exceptions import ImmutableKeyError
from .changes import TrackChangesItem from .changes import TrackChangesItem
if typing.TYPE_CHECKING:
from .creator_plugins import BaseCreator
class ConvertorItem: class ConvertorItem:
"""Item representing convertor plugin. """Item representing convertor plugin.
@ -444,10 +448,11 @@ class CreatedInstance:
def __init__( def __init__(
self, self,
product_type, product_type: str,
product_name, product_name: str,
data, data: Dict[str, Any],
creator, creator: "BaseCreator",
transient_data: Optional[Dict[str, Any]] = None,
): ):
self._creator = creator self._creator = creator
creator_identifier = creator.identifier creator_identifier = creator.identifier
@ -462,7 +467,9 @@ class CreatedInstance:
self._members = [] self._members = []
# Data that can be used for lifetime of object # 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 # Create a copy of passed data to avoid changing them on the fly
data = copy.deepcopy(data or {}) data = copy.deepcopy(data or {})
@ -492,7 +499,7 @@ class CreatedInstance:
item_id = data.get("id") item_id = data.get("id")
# TODO use only 'AYON_INSTANCE_ID' when all hosts support it # TODO use only 'AYON_INSTANCE_ID' when all hosts support it
if item_id not in {AYON_INSTANCE_ID, AVALON_INSTANCE_ID}: 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["id"] = item_id
self._data["productType"] = product_type self._data["productType"] = product_type
self._data["productName"] = product_name self._data["productName"] = product_name
@ -787,16 +794,26 @@ class CreatedInstance:
self._create_context.instance_create_attr_defs_changed(self.id) self._create_context.instance_create_attr_defs_changed(self.id)
@classmethod @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. """Convert instance data from workfile to CreatedInstance.
Args: Args:
instance_data (Dict[str, Any]): Data in a structure ready for instance_data (Dict[str, Any]): Data in a structure ready for
'CreatedInstance' object. 'CreatedInstance' object.
creator (BaseCreator): Creator plugin which is creating the 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) instance_data = copy.deepcopy(instance_data)
product_type = instance_data.get("productType") product_type = instance_data.get("productType")
@ -809,7 +826,11 @@ class CreatedInstance:
product_name = instance_data.get("subset") product_name = instance_data.get("subset")
return cls( 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): def attribute_value_changed(self, key, changes):

View file

@ -196,11 +196,11 @@ def is_clip_from_media_sequence(otio_clip):
return is_input_sequence or is_input_sequence_legacy 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: Args:
otio_clip (otio.schema.Clip): The OTIO clip to check. 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: Returns:
tuple(int, int): The remapped range as discrete frame number. 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): if not is_clip_from_media_sequence(otio_clip):
raise ValueError(f"Cannot map on non-file sequence clip {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 media_ref = otio_clip.media_reference
available_range = otio_clip.available_range() available_range = otio_clip.available_range()
source_range = otio_clip.source_range
available_range_rate = available_range.start_time.rate 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_frame = (
available_range.start_time.to_frames() 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 # source range for image sequence. Following code maintain
# backward-compatibility by adjusting media_in # backward-compatibility by adjusting media_in
# while we are updating those. # while we are updating those.
conformed_src_in = source_range.start_time.rescaled_to(
available_range_rate
)
if ( if (
is_clip_from_media_sequence(otio_clip) is_clip_from_media_sequence(otio_clip)
and available_range_start_frame == media_ref.start_frame 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( 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, rate=available_range_rate,
).to_frames() ).to_frames()
frame_out = otio.opentime.RationalTime.from_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, rate=available_range_rate,
).to_frames() ).to_frames()
@ -261,21 +275,6 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
media_ref = otio_clip.media_reference media_ref = otio_clip.media_reference
is_input_sequence = is_clip_from_media_sequence(otio_clip) 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 # Conform source range bounds to available range rate
# .e.g. embedded TC of (3600 sec/ 1h), duration 100 frames # .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: else:
conformed_source_range = source_range 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 # modifiers
time_scalar = 1. time_scalar = 1.
offset_in = 0 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 offset_in, offset_out = offset_out, offset_in
handle_start, handle_end = handle_end, handle_start 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 # If media source is an image sequence, returned
# mediaIn/mediaOut have to correspond # mediaIn/mediaOut have to correspond
# to frame numbers from source sequence. # to frame numbers from source sequence.
if is_input_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 # preserve discrete frame numbers
media_in_trimmed, media_out_trimmed = remap_range_on_file_sequence( media_in_trimmed, media_out_trimmed = remap_range_on_file_sequence(
otio_clip, otio_clip,
(media_in_trimmed, media_out_trimmed) trim_range,
) )
media_in = media_ref.start_frame media_in = media_ref.start_frame
media_out = media_in + available_range.duration.to_frames() - 1 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 # adjust available handles if needed
if (media_in_trimmed - media_in) < handle_start: if (media_in_trimmed - media_in) < handle_start:
handle_start = max(0, media_in_trimmed - media_in) handle_start = max(0, media_in_trimmed - media_in)

View file

@ -708,6 +708,7 @@ def get_instance_staging_dir(instance):
project_settings=context.data["project_settings"], project_settings=context.data["project_settings"],
template_data=template_data, template_data=template_data,
always_return_path=True, always_return_path=True,
username=context.data["user"],
) )
staging_dir_path = staging_dir_info.directory 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) os.makedirs(staging_dir_path, exist_ok=True)
instance.data.update({ instance.data.update({
"stagingDir": staging_dir_path, "stagingDir": staging_dir_path,
"stagingDir_persistent": staging_dir_info.persistent, "stagingDir_persistent": staging_dir_info.is_persistent,
"stagingDir_custom": staging_dir_info.custom "stagingDir_is_custom": staging_dir_info.is_custom
}) })
return staging_dir_path return staging_dir_path

View file

@ -1,3 +1,6 @@
import logging
import warnings
from typing import Optional, Dict, Any
from dataclasses import dataclass from dataclasses import dataclass
from ayon_core.lib import Logger, filter_profiles from ayon_core.lib import Logger, filter_profiles
@ -11,21 +14,41 @@ from .tempdir import get_temp_dir
@dataclass @dataclass
class StagingDir: class StagingDir:
directory: str directory: str
persistent: bool is_persistent: bool
custom: bool # Whether the staging dir is a custom staging dir # 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( def get_staging_dir_config(
project_name, project_name: str,
task_type, task_type: Optional[str],
task_name, task_name: Optional[str],
product_type, product_type: str,
product_name, product_name: str,
host_name, host_name: str,
project_settings=None, project_settings: Optional[Dict[str, Any]] = None,
anatomy=None, anatomy: Optional[Anatomy] = None,
log=None, log: Optional[logging.Logger] = None,
): ) -> Optional[Dict[str, Any]]:
"""Get matching staging dir profile. """Get matching staging dir profile.
Args: Args:
@ -76,7 +99,6 @@ def get_staging_dir_config(
# get template from template name # get template from template name
template_name = profile["template_name"] template_name = profile["template_name"]
_validate_template_name(project_name, template_name, anatomy)
template = anatomy.get_template_item("staging", template_name) template = anatomy.get_template_item("staging", template_name)
@ -93,35 +115,23 @@ def get_staging_dir_config(
return {"template": template, "persistence": data_persistence} 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( def get_staging_dir_info(
project_entity, project_entity: Dict[str, Any],
folder_entity, folder_entity: Optional[Dict[str, Any]],
task_entity, task_entity: Optional[Dict[str, Any]],
product_type, product_type: str,
product_name, product_name: str,
host_name, host_name: str,
anatomy=None, anatomy: Optional[Anatomy] = None,
project_settings=None, project_settings: Optional[Dict[str, Any]] = None,
template_data=None, template_data: Optional[Dict[str, Any]] = None,
always_return_path=True, always_return_path: bool = True,
force_tmp_dir=False, force_tmp_dir: bool = False,
logger=None, logger: Optional[logging.Logger] = None,
prefix=None, prefix: Optional[str] = None,
suffix=None, suffix: Optional[str] = None,
): username: Optional[str] = None,
) -> Optional[StagingDir]:
"""Get staging dir info data. """Get staging dir info data.
If `force_temp` is set, staging dir will be created as tempdir. 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. logger (Optional[logging.Logger]): Logger instance.
prefix (Optional[str]) Optional prefix for staging dir name. prefix (Optional[str]) Optional prefix for staging dir name.
suffix (Optional[str]): Optional suffix for staging dir name. suffix (Optional[str]): Optional suffix for staging dir name.
username (Optional[str]): AYON Username.
Returns: Returns:
Optional[StagingDir]: Staging dir info data Optional[StagingDir]: Staging dir info data
@ -161,16 +172,22 @@ def get_staging_dir_info(
) )
if force_tmp_dir: if force_tmp_dir:
return get_temp_dir( return StagingDir(
project_name=project_entity["name"], get_temp_dir(
anatomy=anatomy, project_name=project_entity["name"],
prefix=prefix, anatomy=anatomy,
suffix=suffix, prefix=prefix,
suffix=suffix,
),
is_persistent=False,
is_custom=False
) )
# making few queries to database # making few queries to database
ctx_data = get_template_data( 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 # add additional data
@ -205,8 +222,8 @@ def get_staging_dir_info(
dir_template = staging_dir_config["template"]["directory"] dir_template = staging_dir_config["template"]["directory"]
return StagingDir( return StagingDir(
dir_template.format_strict(ctx_data), dir_template.format_strict(ctx_data),
persistent=staging_dir_config["persistence"], is_persistent=staging_dir_config["persistence"],
custom=True is_custom=True
) )
# no config found but force an output # no config found but force an output
@ -218,8 +235,8 @@ def get_staging_dir_info(
prefix=prefix, prefix=prefix,
suffix=suffix, suffix=suffix,
), ),
persistent=False, is_persistent=False,
custom=False is_custom=False
) )
return None return None

View file

@ -4,7 +4,7 @@ from ayon_core.settings import get_studio_settings
from ayon_core.lib.local_settings import get_ayon_username 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. """General template data based on system settings or machine.
Output contains formatting keys: Output contains formatting keys:
@ -14,17 +14,22 @@ def get_general_template_data(settings=None):
Args: Args:
settings (Dict[str, Any]): Studio or project settings. settings (Dict[str, Any]): Studio or project settings.
username (Optional[str]): AYON Username.
""" """
if not settings: if not settings:
settings = get_studio_settings() settings = get_studio_settings()
if username is None:
username = get_ayon_username()
core_settings = settings["core"] core_settings = settings["core"]
return { return {
"studio": { "studio": {
"name": core_settings["studio_name"], "name": core_settings["studio_name"],
"code": core_settings["studio_code"] "code": core_settings["studio_code"]
}, },
"user": get_ayon_username() "user": username
} }
@ -145,6 +150,7 @@ def get_template_data(
task_entity=None, task_entity=None,
host_name=None, host_name=None,
settings=None, settings=None,
username=None
): ):
"""Prepare data for templates filling from entered documents and info. """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. host_name (Optional[str]): Used to fill '{app}' key.
settings (Union[Dict, None]): Prepared studio or project settings. settings (Union[Dict, None]): Prepared studio or project settings.
They're queried if not passed (may be slower). They're queried if not passed (may be slower).
username (Optional[str]): AYON Username.
Returns: Returns:
Dict[str, Any]: Data prepared for filling workdir template. 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)) template_data.update(get_project_template_data(project_entity))
if folder_entity: if folder_entity:
template_data.update(get_folder_template_data( template_data.update(get_folder_template_data(

View file

@ -24,6 +24,7 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin):
# NOTE we should use 'context.data["user"]' but that has higher # NOTE we should use 'context.data["user"]' but that has higher
# order. # order.
("AYON_USERNAME", get_ayon_username()), ("AYON_USERNAME", get_ayon_username()),
("AYON_HOST_NAME", context.data["hostName"]),
): ):
if value: if value:
self.log.debug(f"Setting job env: {key}: {value}") self.log.debug(f"Setting job env: {key}: {value}")

View file

@ -80,8 +80,18 @@ class CollectOtioRanges(pyblish.api.InstancePlugin):
if "workfileFrameStart" in instance.data: if "workfileFrameStart" in instance.data:
self._collect_timeline_ranges(instance, otio_clip) self._collect_timeline_ranges(instance, otio_clip)
# Traypublisher Simple or Advanced editorial publishing is
# working with otio clips which are having no available range
# because they are not having any media references.
try:
otio_clip.available_range()
has_available_range = True
except otio._otio.CannotComputeAvailableRangeError:
self.log.info("Clip has no available range")
has_available_range = False
# Collect source ranges if clip has available range # Collect source ranges if clip has available range
if hasattr(otio_clip, 'available_range') and otio_clip.available_range(): if has_available_range:
self._collect_source_ranges(instance, otio_clip) self._collect_source_ranges(instance, otio_clip)
# Handle retimed ranges if source duration is available # Handle retimed ranges if source duration is available

View file

@ -93,8 +93,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
# now we can just add instances from json file and we are done # now we can just add instances from json file and we are done
any_staging_dir_persistent = False 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( self.log.debug(" - processing instance for {}".format(
instance_data.get("productName"))) instance_data.get("productName")))
instance = self._context.create_instance( instance = self._context.create_instance(
@ -105,7 +104,11 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
instance.data.update(instance_data) instance.data.update(instance_data)
# stash render job id for later validation # 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( staging_dir_persistent = instance.data.get(
"stagingDir_persistent", False "stagingDir_persistent", False
) )

View file

@ -66,7 +66,8 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
"yeticacheUE", "yeticacheUE",
"tycache", "tycache",
"usd", "usd",
"oxrig" "oxrig",
"sbsar",
] ]
def process(self, instance): def process(self, instance):

View file

@ -209,13 +209,9 @@ class ExtractOTIOReview(
# File sequence way # File sequence way
if is_sequence: if is_sequence:
# Remap processing range to input file 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( first, last = remap_range_on_file_sequence(
r_otio_cl, r_otio_cl,
processing_range_as_frames, processing_range,
) )
input_fps = processing_range.start_time.rate input_fps = processing_range.start_time.rate

View file

@ -37,7 +37,7 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin):
label = "Validate File Saved" label = "Validate File Saved"
order = pyblish.api.ValidatorOrder - 0.1 order = pyblish.api.ValidatorOrder - 0.1
hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter", hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter",
"cinema4d"] "cinema4d", "silhouette"]
actions = [SaveByVersionUpAction, ShowWorkfilesAction] actions = [SaveByVersionUpAction, ShowWorkfilesAction]
def process(self, context): def process(self, context):

View file

@ -23,6 +23,9 @@ Enabled vs Disabled logic in most of stylesheets
font-family: "Noto Sans"; font-family: "Noto Sans";
font-weight: 450; font-weight: 450;
outline: none; outline: none;
/* Define icon size to fix size issues for most of DCCs */
icon-size: 16px;
} }
QWidget { QWidget {
@ -1168,6 +1171,8 @@ ValidationArtistMessage QLabel {
#PublishLogMessage { #PublishLogMessage {
font-family: "Noto Sans Mono"; font-family: "Noto Sans Mono";
border: none;
padding: 0;
} }
#PublishInstanceLogsLabel { #PublishInstanceLogsLabel {

View file

@ -197,7 +197,7 @@ class ConvertorItemsGroupWidget(BaseGroupWidget):
else: else:
widget = ConvertorItemCardWidget(item, self) widget = ConvertorItemCardWidget(item, self)
widget.selected.connect(self._on_widget_selection) 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._widgets_by_id[item.id] = widget
self._content_layout.insertWidget(widget_idx, widget) self._content_layout.insertWidget(widget_idx, widget)
widget_idx += 1 widget_idx += 1

View file

@ -1117,6 +1117,57 @@ class LogIconFrame(QtWidgets.QFrame):
painter.end() 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): class LogItemWidget(QtWidgets.QWidget):
log_level_to_flag = { log_level_to_flag = {
10: LOG_DEBUG_VISIBLE, 10: LOG_DEBUG_VISIBLE,
@ -1132,12 +1183,7 @@ class LogItemWidget(QtWidgets.QWidget):
type_flag, level_n = self._get_log_info(log) type_flag, level_n = self._get_log_info(log)
icon_label = LogIconFrame( icon_label = LogIconFrame(
self, log["type"], level_n, log.get("is_validation_error")) self, log["type"], level_n, log.get("is_validation_error"))
message_label = QtWidgets.QLabel(log["msg"].rstrip(), self) message_label = LogItemMessage(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)
main_layout = QtWidgets.QHBoxLayout(self) main_layout = QtWidgets.QHBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setContentsMargins(0, 0, 0, 0)
@ -1290,6 +1336,7 @@ class InstanceLogsWidget(QtWidgets.QWidget):
label_widget = QtWidgets.QLabel(instance.label, self) label_widget = QtWidgets.QLabel(instance.label, self)
label_widget.setObjectName("PublishInstanceLogsLabel") label_widget.setObjectName("PublishInstanceLogsLabel")
label_widget.setWordWrap(True)
logs_grid = LogsWithIconsView(instance.logs, self) logs_grid = LogsWithIconsView(instance.logs, self)
layout = QtWidgets.QVBoxLayout(self) layout = QtWidgets.QVBoxLayout(self)
@ -1329,9 +1376,11 @@ class InstancesLogsView(QtWidgets.QFrame):
content_wrap_widget = QtWidgets.QWidget(scroll_area) content_wrap_widget = QtWidgets.QWidget(scroll_area)
content_wrap_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) content_wrap_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
content_wrap_widget.setMinimumWidth(80)
content_widget = QtWidgets.QWidget(content_wrap_widget) content_widget = QtWidgets.QWidget(content_wrap_widget)
content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout = QtWidgets.QVBoxLayout(content_widget)
content_layout.setContentsMargins(8, 8, 8, 8) content_layout.setContentsMargins(8, 8, 8, 8)
content_layout.setSpacing(15) content_layout.setSpacing(15)

View file

@ -28,7 +28,7 @@ def find_free_port(
exclude_ports (list, tuple, set): List of ports that won't be exclude_ports (list, tuple, set): List of ports that won't be
checked form entered range. checked form entered range.
host (str): Host where will check for free ports. Set to 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: if port_from is None:
port_from = 8079 port_from = 8079
@ -42,7 +42,7 @@ def find_free_port(
# Default host is localhost but it is possible to look for other hosts # Default host is localhost but it is possible to look for other hosts
if host is None: if host is None:
host = "localhost" host = "127.0.0.1"
found_port = None found_port = None
while True: while True:
@ -78,7 +78,7 @@ class WebServerManager:
self._log = None self._log = None
self.port = port or 8079 self.port = port or 8079
self.host = host or "localhost" self.host = host or "127.0.0.1"
self.on_stop_callbacks = [] self.on_stop_callbacks = []

View file

@ -1,7 +1,6 @@
from ayon_core.resources import get_image_path
from ayon_core.tools.flickcharm import FlickCharm from ayon_core.tools.flickcharm import FlickCharm
from qtpy import QtWidgets, QtCore, QtGui, QtSvg from qtpy import QtWidgets, QtCore, QtGui
class DeselectableTreeView(QtWidgets.QTreeView): class DeselectableTreeView(QtWidgets.QTreeView):
@ -19,48 +18,6 @@ class DeselectableTreeView(QtWidgets.QTreeView):
QtWidgets.QTreeView.mousePressEvent(self, event) 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): class TreeView(QtWidgets.QTreeView):
"""Ultimate TreeView with flick charm and double click signals. """Ultimate TreeView with flick charm and double click signals.

View file

@ -184,9 +184,10 @@ class WorkareaModel:
return items return items
for filename in os.listdir(workdir): 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) filepath = os.path.join(workdir, filename)
if not os.path.isfile(filepath):
continue
ext = os.path.splitext(filename)[1].lower() ext = os.path.splitext(filename)[1].lower()
if ext not in self._extensions: if ext not in self._extensions:

View file

@ -136,6 +136,8 @@ class FilesWidget(QtWidgets.QWidget):
# Initial setup # Initial setup
workarea_btn_open.setEnabled(False) workarea_btn_open.setEnabled(False)
workarea_btn_browse.setEnabled(False)
workarea_btn_save.setEnabled(False)
published_btn_copy_n_open.setEnabled(False) published_btn_copy_n_open.setEnabled(False)
published_btn_change_context.setEnabled(False) published_btn_change_context.setEnabled(False)
published_btn_cancel.setVisible(False) published_btn_cancel.setVisible(False)
@ -278,8 +280,9 @@ class FilesWidget(QtWidgets.QWidget):
self._published_btn_change_context.setEnabled(enabled) self._published_btn_change_context.setEnabled(enabled)
def _update_workarea_btns_state(self): 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_save.setEnabled(enabled)
self._workarea_btn_browse.setEnabled(self._valid_selected_context)
def _on_published_repre_changed(self, event): def _on_published_repre_changed(self, event):
self._valid_representation_id = event["representation_id"] is not None 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 and self._selected_task_id is not None
) )
self._update_published_btns_state() self._update_published_btns_state()
self._update_workarea_btns_state()
def _on_published_save_clicked(self): def _on_published_save_clicked(self):
result = self._exec_save_as_dialog() result = self._exec_save_as_dialog()

View file

@ -113,6 +113,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
main_layout = QtWidgets.QHBoxLayout(self) main_layout = QtWidgets.QHBoxLayout(self)
main_layout.addWidget(pages_widget, 1) main_layout.addWidget(pages_widget, 1)
main_layout.setContentsMargins(0, 0, 0, 0)
overlay_messages_widget = MessageOverlayObject(self) overlay_messages_widget = MessageOverlayObject(self)
overlay_invalid_host = InvalidHostOverlay(self) overlay_invalid_host = InvalidHostOverlay(self)

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Package declaring AYON addon 'core' version.""" """Package declaring AYON addon 'core' version."""
__version__ = "1.0.12+dev" __version__ = "1.0.15-dev"

View file

@ -1,6 +1,6 @@
name = "core" name = "core"
title = "Core" title = "Core"
version = "1.0.12+dev" version = "1.0.15-dev"
client_dir = "ayon_core" client_dir = "ayon_core"

View file

@ -5,7 +5,7 @@
[tool.poetry] [tool.poetry]
name = "ayon-core" name = "ayon-core"
version = "1.0.12+dev" version = "1.0.15-dev"
description = "" description = ""
authors = ["Ynput Team <team@ynput.io>"] authors = ["Ynput Team <team@ynput.io>"]
readme = "README.md" readme = "README.md"

View file

@ -1008,8 +1008,8 @@ DEFAULT_PUBLISH_VALUES = {
{"name": "model", "order": 100}, {"name": "model", "order": 100},
{"name": "assembly", "order": 150}, {"name": "assembly", "order": 150},
{"name": "groom", "order": 175}, {"name": "groom", "order": 175},
{"name": "look", "order": 300}, {"name": "look", "order": 200},
{"name": "rig", "order": 100}, {"name": "rig", "order": 300},
# Shot layers # Shot layers
{"name": "layout", "order": 200}, {"name": "layout", "order": 200},
{"name": "animation", "order": 300}, {"name": "animation", "order": 300},
@ -1033,7 +1033,8 @@ DEFAULT_PUBLISH_VALUES = {
"maya", "maya",
"nuke", "nuke",
"photoshop", "photoshop",
"substancepainter" "substancepainter",
"silhouette",
], ],
"enabled": True, "enabled": True,
"optional": False, "optional": False,
@ -1053,7 +1054,8 @@ DEFAULT_PUBLISH_VALUES = {
"harmony", "harmony",
"photoshop", "photoshop",
"aftereffects", "aftereffects",
"fusion" "fusion",
"silhouette",
], ],
"enabled": True, "enabled": True,
"optional": True, "optional": True,

View file

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

View file

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

View file

@ -252,48 +252,48 @@ def test_multiple_review_clips_no_gap():
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif ' 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 ' '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i '
f'C:\\with_tc{os.sep}output.%04d.exr ' 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 # Repeated 25fps tiff sequence multiple times till the end
'/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif ' 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 ' '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif ' 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 ' '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif ' 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 ' '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif ' 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 ' '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif ' 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 ' '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif ' 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 ' '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif ' 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 ' '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif ' 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 ' '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i '
f'C:\\no_tc{os.sep}output.%04d.tif ' 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 assert calls == expected

View file

@ -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(): def test_movie_retime_effect():
""" """
Movie clip (embedded timecode 1h) Movie clip (embedded timecode 1h)
@ -187,3 +209,29 @@ def test_img_sequence_conform_to_23_976fps():
handle_start=0, handle_start=0,
handle_end=8, 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,
)