diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 23c49154ac..f382f91fec 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -218,8 +218,22 @@ def remap_range_on_file_sequence(otio_clip, in_out_range): raise ValueError("Invalid in_out_range provided.") from error media_ref = otio_clip.media_reference - media_in = otio_clip.available_range().start_time.value - available_range_rate = otio_clip.available_range().start_time.rate + 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 + + # Temporary. + # Some AYON custom OTIO exporter were implemented with relative + # source range for image sequence. Following code maintain + # backward-compatibility by adjusting media_in + # while we are updating those. + if ( + is_clip_from_media_sequence(otio_clip) + and otio_clip.available_range().start_time.to_frames() == media_ref.start_frame + and source_range.start_time.to_frames() < media_ref.start_frame + ): + media_in = 0 frame_in = otio.opentime.RationalTime.from_frames( media_in_trimmed - media_in + media_ref.start_frame, @@ -238,6 +252,27 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): available_range = otio_clip.available_range() available_range_rate = available_range.start_time.rate + # If media source is an image sequence, returned + # mediaIn/mediaOut have to correspond + # to frame numbers from source sequence. + 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 # @@ -335,9 +370,6 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # If media source is an image sequence, returned # mediaIn/mediaOut have to correspond # to frame numbers from source sequence. - media_ref = otio_clip.media_reference - is_input_sequence = is_clip_from_media_sequence(otio_clip) - if is_input_sequence: # preserve discrete frame numbers media_in_trimmed, media_out_trimmed = remap_range_on_file_sequence( diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index a7ef819c5d..2a0b51b123 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -133,6 +133,22 @@ class ExtractOTIOReview(publish.Extractor): start = src_range.start_time.rescaled_to(self.actual_fps) duration = src_range.duration.rescaled_to(self.actual_fps) + # 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. + media_ref = r_otio_cl.media_reference + if ( + is_clip_from_media_sequence(r_otio_cl) + and available_range.start_time.to_frames() == media_ref.start_frame + and src_range.start_time.to_frames() < media_ref.start_frame + ): + available_range = otio.opentime.TimeRange( + otio.opentime.RationalTime(0, rate=self.actual_fps), + available_range.duration, + ) + # Gap: no media, generate range based on source range else: available_range = processing_range = None diff --git a/tests/client/ayon_core/pipeline/editorial/resources/legacy_img_sequence.json b/tests/client/ayon_core/pipeline/editorial/resources/legacy_img_sequence.json new file mode 100644 index 0000000000..a50ca102fe --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/legacy_img_sequence.json @@ -0,0 +1,216 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "output.[1000-1100].exr", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 104.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 0.0 + } + }, + "effects": [], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "clip_index": "18b19490-21ea-4533-9e0c-03f434c66f14", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "18b19490-21ea-4533-9e0c-03f434c66f14", + "clip_source_resolution": { + "height": "720", + "pixelAspect": 1.0, + "width": "956" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/sq_old_otio/sh010 shot", + "vSyncOn": true, + "vSyncTrack": "Video 1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "/shots/sq_old_otio/sh010", + "folderPath": "/shots/sq_old_otio/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq_old_otio", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq_old_otio", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "d27c5f77-7218-44ca-8061-5b6d33f96116", + "label": "/shots/sq_old_otio/sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "24792946-9ac4-4c8d-922f-80a83dea4be1", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq_old_otio", + "folder_type": "sequence" + } + ], + "productName": "plateVideo_1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video 1", + "sequence": "sq_old_otio", + "shot": "sh###", + "sourceResolution": false, + "task": null, + "track": "{_track_}", + "uuid": "dec1a40b-7ce8-43cd-94b8-08a53056a171", + "variant": "Video_1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "18b19490-21ea-4533-9e0c-03f434c66f14", + "clip_source_resolution": { + "height": "720", + "pixelAspect": 1.0, + "width": "956" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 104, + "clipIn": 90000, + "clipOut": 90104, + "fps": "from_selection", + "frameEnd": 1105, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 0, + "sourceOut": 104, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "/shots/sq_old_otio/sh010", + "folderPath": "/shots/sq_old_otio/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/sq_old_otio", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq_old_otio", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "24792946-9ac4-4c8d-922f-80a83dea4be1", + "label": "/shots/sq_old_otio/sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "sq_old_otio", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video 1", + "sequence": "sq_old_otio", + "shot": "sh###", + "sourceResolution": false, + "task": null, + "track": "{_track_}", + "uuid": "dec1a40b-7ce8-43cd-94b8-08a53056a171", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AYONData", + "color": "MINT", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 52.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "height": 720, + "isSequence": true, + "padding": 4, + "pixelAspect": 1.0, + "width": 956 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\legacy\\", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 1000, + "frame_step": 1, + "rate": 24.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index 270b01a799..e5f0d335b5 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -146,3 +146,23 @@ def test_img_sequence_with_embedded_tc_and_handles(): "img_seq_embedded_tc.json", expected_data ) + + +def test_img_sequence_relative_source_range(): + """ + Img sequence clip (embedded timecode 1h) + available files = 1000-1100 + source_range = fps + """ + expected_data = { + 'mediaIn': 1000, + 'mediaOut': 1098, + 'handleStart': 0, + 'handleEnd': 2, + 'speed': 1.0 + } + + _check_expected_retimed_values( + "legacy_img_sequence.json", + expected_data + ) diff --git a/tools/manage.ps1 b/tools/manage.ps1 index 1fb57fe445..9a9a9a2eff 100755 --- a/tools/manage.ps1 +++ b/tools/manage.ps1 @@ -255,7 +255,7 @@ function Write-Help { Write-Info -Text " ruff-check ", "Run Ruff check for the repository" -Color White, Cyan Write-Info -Text " ruff-fix ", "Run Ruff fix for the repository" -Color White, Cyan Write-Info -Text " codespell ", "Run codespell check for the repository" -Color White, Cyan - Write-Info -Text " run ", "Run the repository" -Color White, Cyan + Write-Info -Text " run ", "Run a poetry command in the repository environment" -Color White, Cyan Write-Host "" } diff --git a/tools/manage.sh b/tools/manage.sh index 923953bf96..6b0a4d6978 100755 --- a/tools/manage.sh +++ b/tools/manage.sh @@ -157,6 +157,7 @@ default_help() { echo -e " ${BWhite}ruff-check${RST} ${BCyan}Run Ruff check for the repository${RST}" echo -e " ${BWhite}ruff-fix${RST} ${BCyan}Run Ruff fix for the repository${RST}" echo -e " ${BWhite}codespell${RST} ${BCyan}Run codespell check for the repository${RST}" + echo -e " ${BWhite}run${RST} ${BCyan}Run a poetry command in the repository environment${RST}" echo "" } @@ -175,6 +176,12 @@ run_codespell () { "$POETRY_HOME/bin/poetry" run codespell } +run_command () { + echo -e "${BIGreen}>>>${RST} Running ..." + shift; # will remove first arg ("run") from the "$@" + "$POETRY_HOME/bin/poetry" run "$@" +} + main () { detect_python || return 1 @@ -207,6 +214,10 @@ main () { run_codespell || return_code=$? exit $return_code ;; + "run") + run_command "$@" || return_code=$? + exit $return_code + ;; esac if [ "$function_name" != "" ]; then