From 5bb3d2f407714d259cdc364c7a34ca1878fbbbdd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Apr 2025 16:34:58 +0200 Subject: [PATCH 01/61] Refactored existing logic for reusing last rendered frame --- .../plugins/publish/extract_review.py | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index df87abba91..f3adfdcf74 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -403,12 +403,25 @@ class ExtractReview(pyblish.api.InstancePlugin): files_to_clean = [] if temp_data["input_is_sequence"]: self.log.debug("Checking sequence to fill gaps in sequence..") - files_to_clean = self.fill_sequence_gaps( - files=temp_data["origin_repre"]["files"], - staging_dir=new_repre["stagingDir"], - start_frame=temp_data["frame_start"], - end_frame=temp_data["frame_end"] - ) + + files = temp_data["origin_repre"]["files"] + collections = clique.assemble( + files, + patterns=[clique.PATTERNS["frames"]], + minimum_items=1 + )[0] + if len(collections) != 1: + raise KnownPublishError( + "Multiple collections {} found.".format(collections)) + + collection = collections[0] + if fill_type == "existing": + files_to_clean = self.fill_sequence_gaps_from_existing( + collection=collection, + staging_dir=new_repre["stagingDir"], + start_frame=temp_data["frame_start"], + end_frame=temp_data["frame_end"], + ) # create or update outputName output_name = new_repre.get("outputName", "") @@ -883,6 +896,13 @@ class ExtractReview(pyblish.api.InstancePlugin): def fill_sequence_gaps(self, files, staging_dir, start_frame, end_frame): # type: (list, str, int, int) -> list + def fill_sequence_gaps_from_existing( + self, + collection, + staging_dir: str, + start_frame: int, + end_frame: int + ) -> list: """Fill missing files in sequence by duplicating existing ones. This will take nearest frame file and copy it with so as to fill @@ -890,7 +910,7 @@ class ExtractReview(pyblish.api.InstancePlugin): hole ahead. Args: - files (list): List of representation files. + collection (clique.collection) staging_dir (str): Path to staging directory. start_frame (int): Sequence start (no matter what files are there) end_frame (int): Sequence end (no matter what files are there) @@ -903,19 +923,12 @@ class ExtractReview(pyblish.api.InstancePlugin): KnownPublishError: if more than one collection is obtained. """ - collections = clique.assemble(files)[0] - if len(collections) != 1: - raise KnownPublishError( - "Multiple collections {} found.".format(collections)) - - col = collections[0] - # Prepare which hole is filled with what frame # - the frame is filled only with already existing frames - prev_frame = next(iter(col.indexes)) + prev_frame = next(iter(collection.indexes)) hole_frame_to_nearest = {} for frame in range(int(start_frame), int(end_frame) + 1): - if frame in col.indexes: + if frame in collection.indexes: prev_frame = frame else: # Use previous frame as source for hole @@ -923,7 +936,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # Calculate paths added_files = [] - col_format = col.format("{head}{padding}{tail}") + col_format = collection.format("{head}{padding}{tail}") for hole_frame, src_frame in hole_frame_to_nearest.items(): hole_fpath = os.path.join(staging_dir, col_format % hole_frame) src_fpath = os.path.join(staging_dir, col_format % src_frame) From 25a94412396175d3e8934ceb0f838b5c934aa751 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Apr 2025 16:35:50 +0200 Subject: [PATCH 02/61] Added extension to temp_data --- client/ayon_core/plugins/publish/extract_review.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index f3adfdcf74..5c1de70c24 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -591,6 +591,8 @@ class ExtractReview(pyblish.api.InstancePlugin): ext = os.path.splitext(repre["files"][0])[1].replace(".", "") if ext.lower() in self.alpha_exts: input_allow_bg = True + else: + ext = os.path.splitext(repre["files"])[1].replace(".", "") return { "fps": float(instance.data["fps"]), @@ -611,7 +613,8 @@ class ExtractReview(pyblish.api.InstancePlugin): "input_allow_bg": input_allow_bg, "with_audio": with_audio, "without_handles": without_handles, - "handles_are_set": handles_are_set + "handles_are_set": handles_are_set, + "ext": ext } def _ffmpeg_arguments( From b5170670065e76baff535076c409d429765ac5ac Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Apr 2025 16:38:37 +0200 Subject: [PATCH 03/61] Implemented new blank frame fills --- .../plugins/publish/extract_review.py | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 5c1de70c24..83893443d9 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -422,6 +422,15 @@ class ExtractReview(pyblish.api.InstancePlugin): start_frame=temp_data["frame_start"], end_frame=temp_data["frame_end"], ) + elif fill_type == "blank": + files_to_clean = self.fill_sequence_gaps_with_blanks( + collection=collection, + staging_dir=new_repre["stagingDir"], + resolution_width=temp_data["resolution_width"], + resolution_height=temp_data["resolution_height"], + extension=temp_data["ext"], + ) + # create or update outputName output_name = new_repre.get("outputName", "") @@ -897,8 +906,57 @@ class ExtractReview(pyblish.api.InstancePlugin): return all_args - def fill_sequence_gaps(self, files, staging_dir, start_frame, end_frame): - # type: (list, str, int, int) -> list + def fill_sequence_gaps_with_blanks( + self, + collection: str, + staging_dir: str, + resolution_width: int, + resolution_height: int, + extension: str, + ): + """Fills missing files by blank frame. + + Args: + collection (clique.collection) + staging_dir (str): Path to staging directory. + resolution_width (int): width of source frame + resolution_height (int): height of source frame + extension (str) + + Returns: + list of added files. Those should be cleaned after work + is done. + + """ + blank_frame_path = os.path.join(staging_dir, f"blank.{extension}") + command = get_ffmpeg_tool_args("ffmpeg") + + command.extend([ + "-f", "lavfi", + "-i", "color=c=black:s={}x{}:d=1".format( + resolution_width, resolution_height + ), + "-tune", "stillimage", + "-frames: v" , 1, + blank_frame_path + ]) + + self.log.debug("Executing: {}".format(" ".join(command))) + output = run_subprocess( + command, logger=self.log + ) + self.log.debug("Output: {}".format(output)) + + added_files = [blank_frame_path] + + for missing_frame_name in collection.holes(): + hole_fpath = os.path.join(staging_dir, missing_frame_name) + speedcopy.copyfile(blank_frame_path, hole_fpath) + added_files.append(hole_fpath) + + return added_files + + def fill_sequence_gaps_from_existing( self, collection, From f66ff742f7171994e289cdb3b7a9a9a0501bf3c4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 9 Apr 2025 15:38:19 +0200 Subject: [PATCH 04/61] Updates review extract to improve quality Improves review extraction by: - Switches output extension to '.png' for better image quality. - Adds compression level to ffmpeg command. - Adds scaling to the video filter. - Forces re-encoding for lossy formats. --- .../plugins/publish/extract_otio_review.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 7a9a020ff0..e96c1a1b6b 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -54,7 +54,7 @@ class ExtractOTIOReview( # plugin default attributes to_width = 1280 to_height = 720 - output_ext = ".jpg" + output_ext = ".png" def process(self, instance): # Not all hosts can import these modules. @@ -474,6 +474,7 @@ class ExtractOTIOReview( command.extend([ "-start_number", str(in_frame_start), + "-compression_level", "5", "-framerate", str(sequence_fps), "-i", input_path ]) @@ -510,6 +511,11 @@ class ExtractOTIOReview( "-tune", "stillimage" ]) + if video or sequence: + command.extend([ + "-vf", f"scale={self.to_width}:{self.to_height}:flags=lanczos" + ]) + # add output attributes command.extend([ "-start_number", str(out_frame_start) @@ -520,9 +526,12 @@ class ExtractOTIOReview( input_extension and self.output_ext == input_extension ): - command.extend([ - "-c", "copy" - ]) + if input_extension.lower() in [ + '.png', '.tif', '.tiff', '.dpx', '.exr']: + command.extend(["-c", "copy"]) + else: + # For lossy formats, force re-encode + command.extend(["-pix_fmt", "rgba"]) # add output path at the end command.append(output_path) From d076f152fd4c13cf4b66ef668537d2ce41cfcecb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Apr 2025 18:22:07 +0200 Subject: [PATCH 05/61] Renamed files_to_clean as now its dictionary --- client/ayon_core/plugins/publish/extract_review.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 83893443d9..ec2569e3f6 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -416,14 +416,14 @@ class ExtractReview(pyblish.api.InstancePlugin): collection = collections[0] if fill_type == "existing": - files_to_clean = self.fill_sequence_gaps_from_existing( + added_frames_and_files = self.fill_sequence_gaps_from_existing( collection=collection, staging_dir=new_repre["stagingDir"], start_frame=temp_data["frame_start"], end_frame=temp_data["frame_end"], ) elif fill_type == "blank": - files_to_clean = self.fill_sequence_gaps_with_blanks( + added_frames_and_files = self.fill_sequence_gaps_with_blanks( collection=collection, staging_dir=new_repre["stagingDir"], resolution_width=temp_data["resolution_width"], @@ -487,8 +487,8 @@ class ExtractReview(pyblish.api.InstancePlugin): run_subprocess(subprcs_cmd, shell=True, logger=self.log) # delete files added to fill gaps - if files_to_clean: - for f in files_to_clean: + if added_frames_and_files: + for f in added_frames_and_files.values(): os.unlink(f) new_repre.update({ From 6c404c88e970c872ba4acf860d1d1850d91e80fb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Apr 2025 18:23:43 +0200 Subject: [PATCH 06/61] Added filling by previous published version --- .../plugins/publish/extract_review.py | 124 +++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index ec2569e3f6..1cffcaf68c 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import re import copy @@ -5,6 +7,7 @@ import json import shutil import subprocess from abc import ABC, abstractmethod +from typing import Dict, Any import clique import speedcopy @@ -29,6 +32,7 @@ from ayon_core.pipeline.publish import ( get_publish_instance_label, ) from ayon_core.pipeline.publish.lib import add_repre_files_for_cleanup +from ayon_api import get_last_version_by_product_name, get_representations def frame_to_timecode(frame: int, fps: float) -> str: @@ -430,7 +434,24 @@ class ExtractReview(pyblish.api.InstancePlugin): resolution_height=temp_data["resolution_height"], extension=temp_data["ext"], ) - + elif fill_type == "previous": + added_frames_and_files = self.fill_sequence_gaps_with_previous( + collection=collection, + staging_dir=new_repre["stagingDir"], + instance=instance, + current_repre=repre, + start_frame=temp_data["frame_start"], + end_frame=temp_data["frame_end"], + ) + # fallback to original workflow + if added_frames_and_files is None: + added_frames_and_files = self.fill_sequence_gaps_from_existing( + collection=collection, + staging_dir=new_repre["stagingDir"], + start_frame=temp_data["frame_start"], + end_frame=temp_data["frame_end"], + ) + temp_data["filled_files"] = added_frames_and_files # create or update outputName output_name = new_repre.get("outputName", "") @@ -906,6 +927,99 @@ class ExtractReview(pyblish.api.InstancePlugin): return all_args + def fill_sequence_gaps_with_previous( + self, + collection: str, + staging_dir: str, + instance: pyblish.plugin.Instance, + current_repre: Dict[Any, Any], + start_frame: int, + end_frame: int + ) -> Dict[int, str] | None: + """Tries to replace missing frames from ones from last version""" + repre_file_paths = self._get_last_version_files( + instance, current_repre) + if repre_file_paths is None: + # issues in getting last version files, falling back + return None + + prev_collection = clique.assemble( + repre_file_paths, + patterns=[clique.PATTERNS["frames"]], + minimum_items=1 + )[0][0] + prev_col_format = prev_collection.format("{head}{padding}{tail}") + + added_files = {} + anatomy = instance.context.data["anatomy"] + col_format = collection.format("{head}{padding}{tail}") + for frame in range(start_frame, end_frame + 1): + if frame in collection.indexes: + continue + hole_fpath = os.path.join(staging_dir, col_format % frame) + + previous_version_path = prev_col_format % frame + # limits too large padding coming from Anatomy + previous_version_path = ( + os.path.join( + anatomy.fill_root(os.path.dirname(previous_version_path)), + os.path.basename(previous_version_path) + ) + ) + if not os.path.exists(previous_version_path): + self.log.warning( + "Missing frame should be replaced from " + f"'{previous_version_path}' but that doesn't exist. " + "Falling back to filling from currently last rendered." + ) + return None + + self.log.warning( + f"Replacing missing '{hole_fpath}' with " + f"'{previous_version_path}'" + ) + speedcopy.copyfile(previous_version_path, hole_fpath) + added_files[frame] = hole_fpath + + return added_files + + def _get_last_version_files( + self, + instance: pyblish.plugin.Instance, + current_repre: Dict[Any, Any], + ): + product_name = instance.data["productName"] + project_name = instance.data["projectEntity"]["name"] + folder_entity = instance.data["folderEntity"] + + version_entity = get_last_version_by_product_name( + project_name, + product_name, + folder_entity["id"], + fields={"id"} + ) + if not version_entity: + return None + + repres = get_representations( + project_name, + version_ids=[version_entity["id"]] + ) + matching_repre = None + for repre in repres: + if repre["name"] == current_repre["name"]: + matching_repre = repre + break + if not matching_repre: + return None + + repre_file_paths = [ + file_info["path"] + for file_info in matching_repre["files"] + ] + + return repre_file_paths + def fill_sequence_gaps_with_blanks( self, collection: str, @@ -1052,6 +1166,14 @@ class ExtractReview(pyblish.api.InstancePlugin): # Make sure to have full path to one input file full_input_path_single_file = full_input_path + filled_files = temp_data.get("filled_files", {}) + if filled_files: + first_frame, first_file = list(filled_files.items())[0] + if first_file < full_input_path_single_file: + self.log.warning(f"Using filled frame: '{first_file}'") + full_input_path_single_file = first_file + temp_data["first_sequence_frame"] = first_frame + filename_suffix = output_def["filename_suffix"] output_ext = output_def.get("ext") From be54d9deb815aa03c615bf5629af8bd6f708f0bd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Apr 2025 18:24:27 +0200 Subject: [PATCH 07/61] Missed renamed variable initialization --- client/ayon_core/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 1cffcaf68c..61fbf2d90d 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -404,7 +404,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ) temp_data = self.prepare_temp_data(instance, repre, output_def) - files_to_clean = [] + added_frames_and_files = {} if temp_data["input_is_sequence"]: self.log.debug("Checking sequence to fill gaps in sequence..") From 3fe0c251dfdf449dc037706368cc87585c5771dc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Apr 2025 18:29:20 +0200 Subject: [PATCH 08/61] Refactored filling by blanks --- .../plugins/publish/extract_review.py | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 61fbf2d90d..8c3186d390 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -430,6 +430,8 @@ class ExtractReview(pyblish.api.InstancePlugin): added_frames_and_files = self.fill_sequence_gaps_with_blanks( collection=collection, staging_dir=new_repre["stagingDir"], + start_frame=temp_data["frame_start"], + end_frame=temp_data["frame_end"], resolution_width=temp_data["resolution_width"], resolution_height=temp_data["resolution_height"], extension=temp_data["ext"], @@ -1024,24 +1026,13 @@ class ExtractReview(pyblish.api.InstancePlugin): self, collection: str, staging_dir: str, + start_frame: int, + end_frame: int, resolution_width: int, resolution_height: int, extension: str, - ): - """Fills missing files by blank frame. - - Args: - collection (clique.collection) - staging_dir (str): Path to staging directory. - resolution_width (int): width of source frame - resolution_height (int): height of source frame - extension (str) - - Returns: - list of added files. Those should be cleaned after work - is done. - - """ + ) -> Dict[int, str] | None: + """Fills missing files by blank frame.""" blank_frame_path = os.path.join(staging_dir, f"blank.{extension}") command = get_ffmpeg_tool_args("ffmpeg") @@ -1063,14 +1054,16 @@ class ExtractReview(pyblish.api.InstancePlugin): added_files = [blank_frame_path] - for missing_frame_name in collection.holes(): - hole_fpath = os.path.join(staging_dir, missing_frame_name) + col_format = collection.format("{head}{padding}{tail}") + for frame in range(start_frame, end_frame + 1): + if frame in collection.indexes: + continue + hole_fpath = os.path.join(staging_dir, col_format % frame) speedcopy.copyfile(blank_frame_path, hole_fpath) - added_files.append(hole_fpath) + added_files[frame] = hole_fpath return added_files - def fill_sequence_gaps_from_existing( self, collection, From 0860fd130254a7a93916f50155e2370a5285d753 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Apr 2025 18:29:45 +0200 Subject: [PATCH 09/61] Updated return type --- client/ayon_core/plugins/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 8c3186d390..fdb7a887f8 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1084,7 +1084,7 @@ class ExtractReview(pyblish.api.InstancePlugin): end_frame (int): Sequence end (no matter what files are there) Returns: - list of added files. Those should be cleaned after work + dict[int, str] of added files. Those should be cleaned after work is done. Raises: @@ -1113,7 +1113,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "Missing previously detected file: {}".format(src_fpath)) speedcopy.copyfile(src_fpath, hole_fpath) - added_files.append(hole_fpath) + added_files[hole_frame] = hole_fpath return added_files From f14a00b51024d876076c0c6b1a1d534d16ddb13d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Apr 2025 14:59:36 +0200 Subject: [PATCH 10/61] Implemented review from explicit frames --- .../plugins/publish/extract_review.py | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index fdb7a887f8..1d6aec6d1d 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -453,6 +453,14 @@ class ExtractReview(pyblish.api.InstancePlugin): start_frame=temp_data["frame_start"], end_frame=temp_data["frame_end"], ) + elif fill_type == "only_rendered": + temp_data["explicit_frames"] = [ + os.path.join( + new_repre["stagingDir"], file + ).replace("\\", "/") + for file in files + ] + temp_data["filled_files"] = added_frames_and_files # create or update outputName @@ -514,6 +522,10 @@ class ExtractReview(pyblish.api.InstancePlugin): for f in added_frames_and_files.values(): os.unlink(f) + if (temp_data["explicit_frames_metadata_path"] + and os.path.exists(temp_data["explicit_frames_metadata_path"])): + os.unlink(temp_data["explicit_frames_metadata_path"]) + new_repre.update({ "fps": temp_data["fps"], "name": "{}_{}".format(output_name, output_ext), @@ -646,7 +658,9 @@ class ExtractReview(pyblish.api.InstancePlugin): "with_audio": with_audio, "without_handles": without_handles, "handles_are_set": handles_are_set, - "ext": ext + "ext": ext, + "explicit_frames": [], # absolute paths to rendered files + "explicit_frames_metadata_path": None # abs path to metadata file } def _ffmpeg_arguments( @@ -728,7 +742,8 @@ class ExtractReview(pyblish.api.InstancePlugin): if layer_name: ffmpeg_input_args.extend(["-layer", layer_name]) - if temp_data["input_is_sequence"]: + explicit_frames = temp_data["explicit_frames"] + if temp_data["input_is_sequence"] and not explicit_frames: # Set start frame of input sequence (just frame in filename) # - definition of input filepath # - add handle start if output should be without handles @@ -755,7 +770,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "-to", "{:0.10f}".format(duration_seconds) ]) - if temp_data["output_is_sequence"]: + if temp_data["output_is_sequence"] and not explicit_frames: # Set start frame of output sequence (just frame in filename) # - this is definition of an output ffmpeg_output_args.extend([ @@ -786,10 +801,29 @@ class ExtractReview(pyblish.api.InstancePlugin): "-frames:v", str(output_frames_len) ]) - # Add video/image input path - ffmpeg_input_args.extend([ - "-i", path_to_subprocess_arg(temp_data["full_input_path"]) - ]) + if not explicit_frames: + # Add video/image input path + ffmpeg_input_args.extend([ + "-i", path_to_subprocess_arg(temp_data["full_input_path"]) + ]) + else: + staging_dir = os.path.dirname(temp_data["full_input_path"]) + explicit_frames_path = os.path.join( + staging_dir, "explicit_frames.txt") + with open(explicit_frames_path, "w") as fp: + lines = [ + f"file {file}" + for file in temp_data["explicit_frames"] + ] + fp.write("\n".join(lines)) + temp_data["explicit_frames_metadata_path"] = explicit_frames_path + + # let ffmpeg use only rendered files, might have gaps + ffmpeg_input_args.extend([ + "-f", "concat", + "-safe", "0", + "-i", path_to_subprocess_arg(explicit_frames_path) + ]) # Add audio arguments if there are any. Skipped when output are images. if not temp_data["output_ext_is_image"] and temp_data["with_audio"]: From c210f62e1980a0784d231477d7488be45b38d09d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Apr 2025 18:12:15 +0200 Subject: [PATCH 11/61] Added fill_missing_frames to settings --- server/settings/publish_plugins.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 39a9c028f9..3a0e932606 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -12,6 +12,14 @@ from ayon_server.settings import ( from ayon_server.types import ColorRGBA_uint8 +def _handle_missing_frames_enum(): + return [ + {"value": "closest_existing", "label": "Use closest existing"}, + {"value": "blank", "label": "Generate blank frame"}, + {"value": "previous_version", "label": "Use previous version"}, + {"value": "only_rendered", "label": "Use only rendered"}, + ] + class EnabledModel(BaseSettingsModel): enabled: bool = SettingsField(True) @@ -642,6 +650,12 @@ class ExtractReviewOutputDefModel(BaseSettingsModel): default_factory=ExtractReviewLetterBox, title="Letter Box" ) + fill_missing_frames:str = SettingsField( + title="Handle missing frames", + description="How to handle frames that are missing from entity frame " + "range.", + enum_resolver=_handle_missing_frames_enum + ) @validator("name") def validate_name(cls, value): @@ -1261,7 +1275,8 @@ DEFAULT_PUBLISH_VALUES = { "fill_color": [0, 0, 0, 1.0], "line_thickness": 0, "line_color": [255, 0, 0, 1.0] - } + }, + "fill_missing_frames": "closest_existing" }, { "name": "h264", @@ -1311,7 +1326,8 @@ DEFAULT_PUBLISH_VALUES = { "fill_color": [0, 0, 0, 1.0], "line_thickness": 0, "line_color": [255, 0, 0, 1.0] - } + }, + "fill_missing_frames": "closest_existing" } ] } From deea9366bc4fde68efcdccf30d78a59015573548 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Apr 2025 18:20:51 +0200 Subject: [PATCH 12/61] Fix initialization --- client/ayon_core/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 1d6aec6d1d..66841203b6 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1137,7 +1137,7 @@ class ExtractReview(pyblish.api.InstancePlugin): hole_frame_to_nearest[frame] = prev_frame # Calculate paths - added_files = [] + added_files = {} col_format = collection.format("{head}{padding}{tail}") for hole_frame, src_frame in hole_frame_to_nearest.items(): hole_fpath = os.path.join(staging_dir, col_format % hole_frame) From 8a9c95a69b150f5b40db44d9f6bea7d2ed2609ce Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Apr 2025 18:38:49 +0200 Subject: [PATCH 13/61] Use fill_missing_frames from Settings --- client/ayon_core/plugins/publish/extract_review.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 66841203b6..1299b16f84 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -420,13 +420,16 @@ class ExtractReview(pyblish.api.InstancePlugin): collection = collections[0] if fill_type == "existing": + + fill_missing_frames = _output_def["fill_missing_frames"] + if fill_missing_frames == "closest_existing": added_frames_and_files = self.fill_sequence_gaps_from_existing( collection=collection, staging_dir=new_repre["stagingDir"], start_frame=temp_data["frame_start"], end_frame=temp_data["frame_end"], ) - elif fill_type == "blank": + elif fill_missing_frames == "blank": added_frames_and_files = self.fill_sequence_gaps_with_blanks( collection=collection, staging_dir=new_repre["stagingDir"], @@ -436,7 +439,7 @@ class ExtractReview(pyblish.api.InstancePlugin): resolution_height=temp_data["resolution_height"], extension=temp_data["ext"], ) - elif fill_type == "previous": + elif fill_missing_frames == "previous_version": added_frames_and_files = self.fill_sequence_gaps_with_previous( collection=collection, staging_dir=new_repre["stagingDir"], @@ -453,7 +456,7 @@ class ExtractReview(pyblish.api.InstancePlugin): start_frame=temp_data["frame_start"], end_frame=temp_data["frame_end"], ) - elif fill_type == "only_rendered": + elif fill_missing_frames == "only_rendered": temp_data["explicit_frames"] = [ os.path.join( new_repre["stagingDir"], file From 050db01c82eb18af31d7cd0adf18d095c3e41ed7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Apr 2025 18:39:45 +0200 Subject: [PATCH 14/61] Remove blank frame --- client/ayon_core/plugins/publish/extract_review.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 1299b16f84..a3892bec62 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -438,6 +438,7 @@ class ExtractReview(pyblish.api.InstancePlugin): resolution_width=temp_data["resolution_width"], resolution_height=temp_data["resolution_height"], extension=temp_data["ext"], + temp_data=temp_data ) elif fill_missing_frames == "previous_version": added_frames_and_files = self.fill_sequence_gaps_with_previous( @@ -525,9 +526,8 @@ class ExtractReview(pyblish.api.InstancePlugin): for f in added_frames_and_files.values(): os.unlink(f) - if (temp_data["explicit_frames_metadata_path"] - and os.path.exists(temp_data["explicit_frames_metadata_path"])): - os.unlink(temp_data["explicit_frames_metadata_path"]) + for f in temp_data["paths_to_remove"]: + os.unlink(f) new_repre.update({ "fps": temp_data["fps"], @@ -663,7 +663,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "handles_are_set": handles_are_set, "ext": ext, "explicit_frames": [], # absolute paths to rendered files - "explicit_frames_metadata_path": None # abs path to metadata file + "paths_to_remove": [] } def _ffmpeg_arguments( @@ -819,7 +819,7 @@ class ExtractReview(pyblish.api.InstancePlugin): for file in temp_data["explicit_frames"] ] fp.write("\n".join(lines)) - temp_data["explicit_frames_metadata_path"] = explicit_frames_path + temp_data["paths_to_remove"].append(explicit_frames_path) # let ffmpeg use only rendered files, might have gaps ffmpeg_input_args.extend([ @@ -1068,9 +1068,11 @@ class ExtractReview(pyblish.api.InstancePlugin): resolution_width: int, resolution_height: int, extension: str, + temp_data: Dict[str, Any] ) -> Dict[int, str] | None: """Fills missing files by blank frame.""" blank_frame_path = os.path.join(staging_dir, f"blank.{extension}") + temp_data["paths_to_remove"].append(blank_frame_path) command = get_ffmpeg_tool_args("ffmpeg") command.extend([ From 9415668912a43b96de29c77e34607f64af4ae2d0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Apr 2025 18:40:07 +0200 Subject: [PATCH 15/61] Fix initialization --- client/ayon_core/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index a3892bec62..9b4e36ca8e 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1091,7 +1091,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ) self.log.debug("Output: {}".format(output)) - added_files = [blank_frame_path] + added_files = {} col_format = collection.format("{head}{padding}{tail}") for frame in range(start_frame, end_frame + 1): From 488c29e97942a8107d65ca5697ce9c5ba2ba39a7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Apr 2025 18:40:19 +0200 Subject: [PATCH 16/61] Fix command --- client/ayon_core/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 9b4e36ca8e..06743d476c 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1081,7 +1081,7 @@ class ExtractReview(pyblish.api.InstancePlugin): resolution_width, resolution_height ), "-tune", "stillimage", - "-frames: v" , 1, + "-frames:v", "1", blank_frame_path ]) From 12b2ed84a3064fc541007a228a0e38d923ea021a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Apr 2025 18:57:19 +0200 Subject: [PATCH 17/61] Fix local rendering --- client/ayon_core/plugins/publish/extract_review.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 06743d476c..1370aa1fd1 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -411,15 +411,12 @@ class ExtractReview(pyblish.api.InstancePlugin): files = temp_data["origin_repre"]["files"] collections = clique.assemble( files, - patterns=[clique.PATTERNS["frames"]], - minimum_items=1 )[0] if len(collections) != 1: raise KnownPublishError( "Multiple collections {} found.".format(collections)) collection = collections[0] - if fill_type == "existing": fill_missing_frames = _output_def["fill_missing_frames"] if fill_missing_frames == "closest_existing": From 832beb1732ede03e7babc2ea2c497fb28cfe1a0a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Apr 2025 18:59:57 +0200 Subject: [PATCH 18/61] Refactor names --- .../plugins/publish/extract_review.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 1370aa1fd1..ed789ae895 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -404,7 +404,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ) temp_data = self.prepare_temp_data(instance, repre, output_def) - added_frames_and_files = {} + new_frame_files = {} if temp_data["input_is_sequence"]: self.log.debug("Checking sequence to fill gaps in sequence..") @@ -420,14 +420,14 @@ class ExtractReview(pyblish.api.InstancePlugin): fill_missing_frames = _output_def["fill_missing_frames"] if fill_missing_frames == "closest_existing": - added_frames_and_files = self.fill_sequence_gaps_from_existing( + new_frame_files = self.fill_sequence_gaps_from_existing( collection=collection, staging_dir=new_repre["stagingDir"], start_frame=temp_data["frame_start"], end_frame=temp_data["frame_end"], ) elif fill_missing_frames == "blank": - added_frames_and_files = self.fill_sequence_gaps_with_blanks( + new_frame_files = self.fill_sequence_gaps_with_blanks( collection=collection, staging_dir=new_repre["stagingDir"], start_frame=temp_data["frame_start"], @@ -438,7 +438,7 @@ class ExtractReview(pyblish.api.InstancePlugin): temp_data=temp_data ) elif fill_missing_frames == "previous_version": - added_frames_and_files = self.fill_sequence_gaps_with_previous( + new_frame_files = self.fill_sequence_gaps_with_previous( collection=collection, staging_dir=new_repre["stagingDir"], instance=instance, @@ -447,13 +447,14 @@ class ExtractReview(pyblish.api.InstancePlugin): end_frame=temp_data["frame_end"], ) # fallback to original workflow - if added_frames_and_files is None: - added_frames_and_files = self.fill_sequence_gaps_from_existing( + if new_frame_files is None: + new_frame_files = ( + self.fill_sequence_gaps_from_existing( collection=collection, staging_dir=new_repre["stagingDir"], start_frame=temp_data["frame_start"], end_frame=temp_data["frame_end"], - ) + )) elif fill_missing_frames == "only_rendered": temp_data["explicit_frames"] = [ os.path.join( @@ -462,7 +463,7 @@ class ExtractReview(pyblish.api.InstancePlugin): for file in files ] - temp_data["filled_files"] = added_frames_and_files + temp_data["filled_files"] = new_frame_files # create or update outputName output_name = new_repre.get("outputName", "") @@ -519,8 +520,8 @@ class ExtractReview(pyblish.api.InstancePlugin): run_subprocess(subprcs_cmd, shell=True, logger=self.log) # delete files added to fill gaps - if added_frames_and_files: - for f in added_frames_and_files.values(): + if new_frame_files: + for f in new_frame_files.values(): os.unlink(f) for f in temp_data["paths_to_remove"]: From e61266bc82be539911209cce6fc433fcc9c70ac7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 14 Apr 2025 12:14:00 +0200 Subject: [PATCH 19/61] Skip validation for explicit frames --- client/ayon_core/plugins/publish/extract_review.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index ed789ae895..31cb3763da 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -462,6 +462,13 @@ class ExtractReview(pyblish.api.InstancePlugin): ).replace("\\", "/") for file in files ] + frame_start = min(collection.indexes) + frame_end = max(collection.indexes) + # modify range for burnins + instance.data["frameStart"] = frame_start + instance.data["frameEnd"] = frame_end + temp_data["frame_start"] = frame_start + temp_data["frame_end"] = frame_end temp_data["filled_files"] = new_frame_files From a4db943903af482f0689f6574013042ff4051963 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 14 Apr 2025 14:22:24 +0200 Subject: [PATCH 20/61] Added default directly to enum Used if additional output defs are present. --- server/settings/publish_plugins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 3a0e932606..f9893add1d 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -652,6 +652,7 @@ class ExtractReviewOutputDefModel(BaseSettingsModel): ) fill_missing_frames:str = SettingsField( title="Handle missing frames", + default="closest_existing", description="How to handle frames that are missing from entity frame " "range.", enum_resolver=_handle_missing_frames_enum From f8ab13dd2aff1b5d13367c1b460305e5c4422a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 14 Apr 2025 15:50:20 +0200 Subject: [PATCH 21/61] Update client/ayon_core/plugins/publish/extract_otio_review.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/extract_otio_review.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index e96c1a1b6b..f7babc2b7f 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -527,7 +527,8 @@ class ExtractOTIOReview( and self.output_ext == input_extension ): if input_extension.lower() in [ - '.png', '.tif', '.tiff', '.dpx', '.exr']: + ".png", ".tif", ".tiff", ".dpx", ".exr" + ]: command.extend(["-c", "copy"]) else: # For lossy formats, force re-encode From c82537008f2439fc4591475939d281b651246ee6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Apr 2025 13:56:43 +0200 Subject: [PATCH 22/61] Refactor proper variable names --- client/ayon_core/plugins/publish/extract_review.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 31cb3763da..93f1098f4c 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -528,11 +528,11 @@ class ExtractReview(pyblish.api.InstancePlugin): # delete files added to fill gaps if new_frame_files: - for f in new_frame_files.values(): - os.unlink(f) + for filepath in new_frame_files.values(): + os.unlink(filepath) - for f in temp_data["paths_to_remove"]: - os.unlink(f) + for filepath in temp_data["paths_to_remove"]: + os.unlink(filepath) new_repre.update({ "fps": temp_data["fps"], From 1019eded3bd2dccd479c396f37ae83184d9530b0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Apr 2025 13:59:01 +0200 Subject: [PATCH 23/61] Refactor filled_files are always initialized --- client/ayon_core/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 93f1098f4c..f39bc0cff8 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1203,7 +1203,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # Make sure to have full path to one input file full_input_path_single_file = full_input_path - filled_files = temp_data.get("filled_files", {}) + filled_files = temp_data["filled_files"] if filled_files: first_frame, first_file = list(filled_files.items())[0] if first_file < full_input_path_single_file: From 4c305d9596ce1f8bc1907e83968b55a750940811 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Apr 2025 14:00:17 +0200 Subject: [PATCH 24/61] Refactor renamed explicit_frames to explicit_input_paths --- client/ayon_core/plugins/publish/extract_review.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index f39bc0cff8..6e31d11dce 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -456,7 +456,7 @@ class ExtractReview(pyblish.api.InstancePlugin): end_frame=temp_data["frame_end"], )) elif fill_missing_frames == "only_rendered": - temp_data["explicit_frames"] = [ + temp_data["explicit_input_paths"] = [ os.path.join( new_repre["stagingDir"], file ).replace("\\", "/") @@ -667,7 +667,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "without_handles": without_handles, "handles_are_set": handles_are_set, "ext": ext, - "explicit_frames": [], # absolute paths to rendered files + "explicit_input_paths": [], # absolute paths to rendered files "paths_to_remove": [] } @@ -750,8 +750,8 @@ class ExtractReview(pyblish.api.InstancePlugin): if layer_name: ffmpeg_input_args.extend(["-layer", layer_name]) - explicit_frames = temp_data["explicit_frames"] - if temp_data["input_is_sequence"] and not explicit_frames: + explicit_input_paths = temp_data["explicit_input_paths"] + if temp_data["input_is_sequence"] and not explicit_input_paths: # Set start frame of input sequence (just frame in filename) # - definition of input filepath # - add handle start if output should be without handles @@ -778,7 +778,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "-to", "{:0.10f}".format(duration_seconds) ]) - if temp_data["output_is_sequence"] and not explicit_frames: + if temp_data["output_is_sequence"] and not explicit_input_paths: # Set start frame of output sequence (just frame in filename) # - this is definition of an output ffmpeg_output_args.extend([ @@ -809,7 +809,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "-frames:v", str(output_frames_len) ]) - if not explicit_frames: + if not explicit_input_paths: # Add video/image input path ffmpeg_input_args.extend([ "-i", path_to_subprocess_arg(temp_data["full_input_path"]) @@ -821,7 +821,7 @@ class ExtractReview(pyblish.api.InstancePlugin): with open(explicit_frames_path, "w") as fp: lines = [ f"file {file}" - for file in temp_data["explicit_frames"] + for file in temp_data["explicit_input_paths"] ] fp.write("\n".join(lines)) temp_data["paths_to_remove"].append(explicit_frames_path) From 8b8d29042a6eb83b3c7973ee1d07fda4db552a62 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Apr 2025 14:01:26 +0200 Subject: [PATCH 25/61] Refactor reordered import --- client/ayon_core/plugins/publish/extract_review.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 6e31d11dce..0192964422 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -13,6 +13,8 @@ import clique import speedcopy import pyblish.api +from ayon_api import get_last_version_by_product_name, get_representations + from ayon_core.lib import ( get_ffmpeg_tool_args, filter_profiles, @@ -32,7 +34,6 @@ from ayon_core.pipeline.publish import ( get_publish_instance_label, ) from ayon_core.pipeline.publish.lib import add_repre_files_for_cleanup -from ayon_api import get_last_version_by_product_name, get_representations def frame_to_timecode(frame: int, fps: float) -> str: From efb3a01f4b4fe79de85e73e35b28ceab025a8594 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Apr 2025 14:02:15 +0200 Subject: [PATCH 26/61] Removed unnecessary import --- client/ayon_core/plugins/publish/extract_review.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 0192964422..0a1089ec9f 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import os import re import copy From 6df129b93f3460778f4fec02f8736efd1a2c62d4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Apr 2025 16:37:56 +0200 Subject: [PATCH 27/61] Optimizes review encoding for image sequences Simplifies the encoding process for image sequences by removing the conditional check for specific image formats when using the 'copy' codec. This ensures consistent and efficient handling of image sequence encoding for review purposes. --- .../ayon_core/plugins/publish/extract_otio_review.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index f7babc2b7f..908d78ca0d 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -526,13 +526,10 @@ class ExtractOTIOReview( input_extension and self.output_ext == input_extension ): - if input_extension.lower() in [ - ".png", ".tif", ".tiff", ".dpx", ".exr" - ]: - command.extend(["-c", "copy"]) - else: - # For lossy formats, force re-encode - command.extend(["-pix_fmt", "rgba"]) + command.extend(["-c", "copy"]) + else: + # For lossy formats, force re-encode + command.extend(["-pix_fmt", "rgba"]) # add output path at the end command.append(output_path) From cdf8764bb13c452eca607fa5aa9a05db14921724 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Apr 2025 17:18:28 +0200 Subject: [PATCH 28/61] Fix return type --- client/ayon_core/plugins/publish/extract_review.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 0a1089ec9f..3a7c6a6b1e 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -5,7 +5,7 @@ import json import shutil import subprocess from abc import ABC, abstractmethod -from typing import Dict, Any +from typing import Dict, Any, Union import clique import speedcopy @@ -978,7 +978,7 @@ class ExtractReview(pyblish.api.InstancePlugin): current_repre: Dict[Any, Any], start_frame: int, end_frame: int - ) -> Dict[int, str] | None: + ) -> Union[Dict[int, str], None]: """Tries to replace missing frames from ones from last version""" repre_file_paths = self._get_last_version_files( instance, current_repre) @@ -1073,7 +1073,7 @@ class ExtractReview(pyblish.api.InstancePlugin): resolution_height: int, extension: str, temp_data: Dict[str, Any] - ) -> Dict[int, str] | None: + ) -> Union[Dict[int, str], None]: """Fills missing files by blank frame.""" blank_frame_path = os.path.join(staging_dir, f"blank.{extension}") temp_data["paths_to_remove"].append(blank_frame_path) From ca12d13a40a8c3b03117c60a211828064e989bb2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 17 Apr 2025 12:50:28 +0200 Subject: [PATCH 29/61] Improves thumbnail extraction reliability Enhances thumbnail extraction by retrying without seeking if the initial attempt fails. This addresses issues where the generated thumbnail file is either missing or empty. It also calculates the seek position more accurately and avoid seeking for very short videos. --- .../plugins/publish/extract_thumbnail.py | 65 +++++++++++++++---- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index b72862ea22..89bb9a90ab 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -486,25 +486,37 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # Set video input attributes max_int = str(2147483647) video_data = get_ffprobe_data(video_file_path, logger=self.log) - # Use duration of the individual streams since it is returned with - # higher decimal precision than 'format.duration'. We need this - # more precise value for calculating the correct amount of frames - # for higher FPS ranges or decimal ranges, e.g. 29.97 FPS - duration = max( - float(stream.get("duration", 0)) - for stream in video_data["streams"] - if stream.get("codec_type") == "video" - ) + # Get duration or use a safe default (single frame) + duration = 0 + for stream in video_data["streams"]: + if stream.get("codec_type") == "video": + stream_duration = float(stream.get("duration", 0)) + if stream_duration > duration: + duration = stream_duration + + # For very short videos, just use the first frame + # Calculate seek position safely + seek_position = 0 + if duration > 0.1: # Only use timestamp calculation for videos longer than 0.1 seconds + seek_position = duration * self.duration_split + + # Build command args cmd_args = [ "-y", - "-ss", str(duration * self.duration_split), "-i", video_file_path, "-analyzeduration", max_int, "-probesize", max_int, - "-frames:v", "1" ] + # Only add -ss if we're seeking to a specific position + if seek_position > 0: + cmd_args.insert(1, "-ss") + cmd_args.insert(2, str(seek_position)) + + # Ensure we extract exactly one frame + cmd_args.extend(["-frames:v", "1"]) + # add output file path cmd_args.append(output_thumb_file_path) @@ -517,9 +529,34 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # run subprocess self.log.debug("Executing: {}".format(" ".join(cmd))) run_subprocess(cmd, logger=self.log) - self.log.debug( - "Thumbnail created: {}".format(output_thumb_file_path)) - return output_thumb_file_path + + # Verify the output file was created + if os.path.exists(output_thumb_file_path) and os.path.getsize(output_thumb_file_path) > 0: + self.log.debug( + "Thumbnail created: {}".format(output_thumb_file_path)) + return output_thumb_file_path + else: + self.log.warning( + "Output file was not created or is empty: {}".format(output_thumb_file_path)) + + # Fallback to extracting the first frame without seeking + if "-ss" in cmd_args: + self.log.debug("Trying fallback without seeking") + # Remove -ss and its value + ss_index = cmd_args.index("-ss") + cmd_args.pop(ss_index) # Remove -ss + cmd_args.pop(ss_index) # Remove the timestamp value + + # Create new command and try again + cmd = get_ffmpeg_tool_args("ffmpeg", *cmd_args) + self.log.debug("Fallback command: {}".format(" ".join(cmd))) + run_subprocess(cmd, logger=self.log) + + if os.path.exists(output_thumb_file_path) and os.path.getsize(output_thumb_file_path) > 0: + self.log.debug("Fallback thumbnail created: {}".format(output_thumb_file_path)) + return output_thumb_file_path + + return None except RuntimeError as error: self.log.warning( "Failed intermediate thumb source using ffmpeg: {}".format( From bf44622c057fd6d45c8a270ae24ee0f71eb89abc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 17 Apr 2025 12:55:02 +0200 Subject: [PATCH 30/61] Improves thumbnail extraction reliability Ensures thumbnail extraction falls back to the first frame if the initial attempt fails, handling potential issues with seek position calculation or output file creation. This enhances the robustness of the thumbnail creation process. --- .../plugins/publish/extract_thumbnail.py | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 89bb9a90ab..e5108444f7 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -498,7 +498,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # For very short videos, just use the first frame # Calculate seek position safely seek_position = 0 - if duration > 0.1: # Only use timestamp calculation for videos longer than 0.1 seconds + # Only use timestamp calculation for videos longer than 0.1 seconds + if duration > 0.1: seek_position = duration * self.duration_split # Build command args @@ -531,13 +532,17 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): run_subprocess(cmd, logger=self.log) # Verify the output file was created - if os.path.exists(output_thumb_file_path) and os.path.getsize(output_thumb_file_path) > 0: + if ( + os.path.exists(output_thumb_file_path) + and os.path.getsize(output_thumb_file_path) > 0 + ): self.log.debug( "Thumbnail created: {}".format(output_thumb_file_path)) return output_thumb_file_path else: self.log.warning( - "Output file was not created or is empty: {}".format(output_thumb_file_path)) + "Output file was not created or is empty: {}".format( + output_thumb_file_path)) # Fallback to extracting the first frame without seeking if "-ss" in cmd_args: @@ -549,11 +554,18 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # Create new command and try again cmd = get_ffmpeg_tool_args("ffmpeg", *cmd_args) - self.log.debug("Fallback command: {}".format(" ".join(cmd))) + self.log.debug("Fallback command: {}".format( + " ".join(cmd))) run_subprocess(cmd, logger=self.log) - if os.path.exists(output_thumb_file_path) and os.path.getsize(output_thumb_file_path) > 0: - self.log.debug("Fallback thumbnail created: {}".format(output_thumb_file_path)) + if ( + os.path.exists(output_thumb_file_path) + and os.path.getsize(output_thumb_file_path) > 0 + ): + self.log.debug( + "Fallback thumbnail created: {}".format( + output_thumb_file_path) + ) return output_thumb_file_path return None From ba80b3b9b459d74d5aface7b39509316cd6d808b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 17 Apr 2025 17:31:08 +0200 Subject: [PATCH 31/61] Added flags for ffmpeg not skipping some frames because DTS --- client/ayon_core/plugins/publish/extract_review.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index e8ed4c80fc..3615cc53c9 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -829,7 +829,9 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_input_args.extend([ "-f", "concat", "-safe", "0", - "-i", path_to_subprocess_arg(explicit_frames_path) + "-fflags", "+genpts+igndts", + "-i", path_to_subprocess_arg(explicit_frames_path), + "-r", "25" ]) # Add audio arguments if there are any. Skipped when output are images. From 71d37d8b59ee3be0b5dcb784b4b478d40c5d2288 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Apr 2025 14:04:41 +0200 Subject: [PATCH 32/61] Updated command creation Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/extract_review.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 3615cc53c9..a4eb1140ac 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1079,9 +1079,8 @@ class ExtractReview(pyblish.api.InstancePlugin): """Fills missing files by blank frame.""" blank_frame_path = os.path.join(staging_dir, f"blank.{extension}") temp_data["paths_to_remove"].append(blank_frame_path) - command = get_ffmpeg_tool_args("ffmpeg") - - command.extend([ + command = get_ffmpeg_tool_args( + "ffmpeg", "-f", "lavfi", "-i", "color=c=black:s={}x{}:d=1".format( resolution_width, resolution_height @@ -1089,7 +1088,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "-tune", "stillimage", "-frames:v", "1", blank_frame_path - ]) + ) self.log.debug("Executing: {}".format(" ".join(command))) output = run_subprocess( From c53de6d226ff48b8c30baaf4889563eb373e058f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Apr 2025 14:05:08 +0200 Subject: [PATCH 33/61] Formatting change Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/publish_plugins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index d487649f65..dbbbb9609d 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -20,6 +20,7 @@ def _handle_missing_frames_enum(): {"value": "only_rendered", "label": "Use only rendered"}, ] + class EnabledModel(BaseSettingsModel): enabled: bool = SettingsField(True) From 46d27ff7a4942fee39edf42ccab72cc05226fe40 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Apr 2025 14:05:24 +0200 Subject: [PATCH 34/61] Formatting change Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/publish_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index dbbbb9609d..cbe3894975 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -651,7 +651,7 @@ class ExtractReviewOutputDefModel(BaseSettingsModel): default_factory=ExtractReviewLetterBox, title="Letter Box" ) - fill_missing_frames:str = SettingsField( + fill_missing_frames: str = SettingsField( title="Handle missing frames", default="closest_existing", description="How to handle frames that are missing from entity frame " From 6d008edbada3d1ac3776b1ae51dc044c6454cb25 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Apr 2025 14:05:56 +0200 Subject: [PATCH 35/61] Refactor first file query Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index a4eb1140ac..265bbb0828 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1205,7 +1205,7 @@ class ExtractReview(pyblish.api.InstancePlugin): filled_files = temp_data["filled_files"] if filled_files: - first_frame, first_file = list(filled_files.items())[0] + first_frame, first_file = next(iter(filled_files.items())) if first_file < full_input_path_single_file: self.log.warning(f"Using filled frame: '{first_file}'") full_input_path_single_file = first_file From cc1ba078ed9f65608eb2a3035597bcf25c668d51 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Apr 2025 14:07:33 +0200 Subject: [PATCH 36/61] Changed variable name Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 265bbb0828..48bb2819ff 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -819,8 +819,8 @@ class ExtractReview(pyblish.api.InstancePlugin): staging_dir, "explicit_frames.txt") with open(explicit_frames_path, "w") as fp: lines = [ - f"file {file}" - for file in temp_data["explicit_input_paths"] + f"file {path}" + for path in temp_data["explicit_input_paths"] ] fp.write("\n".join(lines)) temp_data["paths_to_remove"].append(explicit_frames_path) From 24ec921ff6abce87a11f34a1162c11c310fbb634 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Apr 2025 14:08:18 +0200 Subject: [PATCH 37/61] Formatting change Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 48bb2819ff..cec6dd742c 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -829,7 +829,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_input_args.extend([ "-f", "concat", "-safe", "0", - "-fflags", "+genpts+igndts", + "-fflags", "+genpts+igndts", "-i", path_to_subprocess_arg(explicit_frames_path), "-r", "25" ]) From ed2d0baaf27d25053c7b755e8587815346f9ea63 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Apr 2025 14:12:43 +0200 Subject: [PATCH 38/61] Renamed key --- client/ayon_core/plugins/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index cec6dd742c..b8c75ff60a 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -433,7 +433,7 @@ class ExtractReview(pyblish.api.InstancePlugin): end_frame=temp_data["frame_end"], resolution_width=temp_data["resolution_width"], resolution_height=temp_data["resolution_height"], - extension=temp_data["ext"], + extension=temp_data["input_ext"], temp_data=temp_data ) elif fill_missing_frames == "previous_version": @@ -665,7 +665,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "with_audio": with_audio, "without_handles": without_handles, "handles_are_set": handles_are_set, - "ext": ext, + "input_ext": ext, "explicit_input_paths": [], # absolute paths to rendered files "paths_to_remove": [] } From 1836daad6251065e684b2bb4c7908475fecd2851 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Apr 2025 14:14:04 +0200 Subject: [PATCH 39/61] Refactor condition --- client/ayon_core/plugins/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index b8c75ff60a..252b3c7b6f 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -624,6 +624,8 @@ class ExtractReview(pyblish.api.InstancePlugin): input_is_sequence = self.input_is_sequence(repre) input_allow_bg = False first_sequence_frame = None + + ext = os.path.splitext(repre["files"])[1].replace(".", "") if input_is_sequence and repre["files"]: # Calculate first frame that should be used cols, _ = clique.assemble(repre["files"]) @@ -642,8 +644,6 @@ class ExtractReview(pyblish.api.InstancePlugin): ext = os.path.splitext(repre["files"][0])[1].replace(".", "") if ext.lower() in self.alpha_exts: input_allow_bg = True - else: - ext = os.path.splitext(repre["files"])[1].replace(".", "") return { "fps": float(instance.data["fps"]), From 3e042f4bcd1003793c1498822f88d37ca64538e2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Apr 2025 14:17:19 +0200 Subject: [PATCH 40/61] Fixed return type --- client/ayon_core/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 252b3c7b6f..9b2a139515 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1114,7 +1114,7 @@ class ExtractReview(pyblish.api.InstancePlugin): staging_dir: str, start_frame: int, end_frame: int - ) -> list: + ) -> Dict[int, str]: """Fill missing files in sequence by duplicating existing ones. This will take nearest frame file and copy it with so as to fill From 23bd9706d6b83d2b8b81d54d49d0111bf4579cda Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Apr 2025 14:20:45 +0200 Subject: [PATCH 41/61] Replaced Union with Optional --- client/ayon_core/plugins/publish/extract_review.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 9b2a139515..2577714675 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -5,7 +5,7 @@ import json import shutil import subprocess from abc import ABC, abstractmethod -from typing import Dict, Any, Union +from typing import Dict, Any, Optional import clique import speedcopy @@ -980,7 +980,7 @@ class ExtractReview(pyblish.api.InstancePlugin): current_repre: Dict[Any, Any], start_frame: int, end_frame: int - ) -> Union[Dict[int, str], None]: + ) -> Optional[Dict[int, str]]: """Tries to replace missing frames from ones from last version""" repre_file_paths = self._get_last_version_files( instance, current_repre) @@ -1075,7 +1075,7 @@ class ExtractReview(pyblish.api.InstancePlugin): resolution_height: int, extension: str, temp_data: Dict[str, Any] - ) -> Union[Dict[int, str], None]: + ) -> Optional[Dict[int, str]]: """Fills missing files by blank frame.""" blank_frame_path = os.path.join(staging_dir, f"blank.{extension}") temp_data["paths_to_remove"].append(blank_frame_path) From f73c6eccefe73ba61757231b5627cb6524959a8d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Apr 2025 14:29:17 +0200 Subject: [PATCH 42/61] Simplified argument --- client/ayon_core/plugins/publish/extract_review.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 2577714675..699bf42876 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -441,7 +441,7 @@ class ExtractReview(pyblish.api.InstancePlugin): collection=collection, staging_dir=new_repre["stagingDir"], instance=instance, - current_repre=repre, + current_repre_name=repre["name"], start_frame=temp_data["frame_start"], end_frame=temp_data["frame_end"], ) @@ -977,13 +977,13 @@ class ExtractReview(pyblish.api.InstancePlugin): collection: str, staging_dir: str, instance: pyblish.plugin.Instance, - current_repre: Dict[Any, Any], + current_repre_name: str, start_frame: int, end_frame: int ) -> Optional[Dict[int, str]]: """Tries to replace missing frames from ones from last version""" repre_file_paths = self._get_last_version_files( - instance, current_repre) + instance, current_repre_name) if repre_file_paths is None: # issues in getting last version files, falling back return None @@ -1031,7 +1031,7 @@ class ExtractReview(pyblish.api.InstancePlugin): def _get_last_version_files( self, instance: pyblish.plugin.Instance, - current_repre: Dict[Any, Any], + current_repre_name: str, ): product_name = instance.data["productName"] project_name = instance.data["projectEntity"]["name"] @@ -1052,7 +1052,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ) matching_repre = None for repre in repres: - if repre["name"] == current_repre["name"]: + if repre["name"] == current_repre_name: matching_repre = repre break if not matching_repre: From fe78983491556d82716d146a0e83f3e44de0e190 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Apr 2025 14:53:56 +0200 Subject: [PATCH 43/61] Fix condition --- client/ayon_core/plugins/publish/extract_review.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 699bf42876..312b594acd 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -625,7 +625,6 @@ class ExtractReview(pyblish.api.InstancePlugin): input_allow_bg = False first_sequence_frame = None - ext = os.path.splitext(repre["files"])[1].replace(".", "") if input_is_sequence and repre["files"]: # Calculate first frame that should be used cols, _ = clique.assemble(repre["files"]) @@ -644,6 +643,8 @@ class ExtractReview(pyblish.api.InstancePlugin): ext = os.path.splitext(repre["files"][0])[1].replace(".", "") if ext.lower() in self.alpha_exts: input_allow_bg = True + else: + ext = os.path.splitext(repre["files"])[1].replace(".", "") return { "fps": float(instance.data["fps"]), From bc509fcf0084d465ff4d51f0f3391a03298e799f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 24 Apr 2025 11:37:26 +0200 Subject: [PATCH 44/61] Simplified querying for old repre --- .../ayon_core/plugins/publish/extract_review.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 312b594acd..b56e5a2ac0 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1047,17 +1047,16 @@ class ExtractReview(pyblish.api.InstancePlugin): if not version_entity: return None - repres = get_representations( + matching_repres = get_representations( project_name, - version_ids=[version_entity["id"]] + version_ids=[version_entity["id"]], + representation_names=[current_repre_name], + fields={"files"} ) - matching_repre = None - for repre in repres: - if repre["name"] == current_repre_name: - matching_repre = repre - break - if not matching_repre: + + if not matching_repres: return None + matching_repre = list(matching_repres)[0] repre_file_paths = [ file_info["path"] From e9d3462da28bf114bef0b950f897f9ef740cf9a9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 24 Apr 2025 11:39:44 +0200 Subject: [PATCH 45/61] Fix description --- server/settings/publish_plugins.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index cbe3894975..47dd5ebfb0 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -654,8 +654,7 @@ class ExtractReviewOutputDefModel(BaseSettingsModel): fill_missing_frames: str = SettingsField( title="Handle missing frames", default="closest_existing", - description="How to handle frames that are missing from entity frame " - "range.", + description="How to handle gaps in sequence frame ranges.", enum_resolver=_handle_missing_frames_enum ) From 5410d69ad2137a91d51a1260f254470e605da125 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 24 Apr 2025 12:09:54 +0200 Subject: [PATCH 46/61] Simplified fill_root logic I must be wrong in my previous tests, it works even this simple way. --- client/ayon_core/plugins/publish/extract_review.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index b56e5a2ac0..a53e8eee8f 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1005,13 +1005,7 @@ class ExtractReview(pyblish.api.InstancePlugin): hole_fpath = os.path.join(staging_dir, col_format % frame) previous_version_path = prev_col_format % frame - # limits too large padding coming from Anatomy - previous_version_path = ( - os.path.join( - anatomy.fill_root(os.path.dirname(previous_version_path)), - os.path.basename(previous_version_path) - ) - ) + previous_version_path = anatomy.fill_root(previous_version_path) if not os.path.exists(previous_version_path): self.log.warning( "Missing frame should be replaced from " From 4a38e1175f4b6c9304ce233fe3bf8af58d2c41b8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 24 Apr 2025 13:20:33 +0200 Subject: [PATCH 47/61] Fix rendering explicit frames This seems only safe way --- .../plugins/publish/extract_review.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index a53e8eee8f..0c4b99cf66 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -526,12 +526,12 @@ class ExtractReview(pyblish.api.InstancePlugin): run_subprocess(subprcs_cmd, shell=True, logger=self.log) # delete files added to fill gaps - if new_frame_files: - for filepath in new_frame_files.values(): - os.unlink(filepath) - - for filepath in temp_data["paths_to_remove"]: - os.unlink(filepath) + # if new_frame_files: + # for filepath in new_frame_files.values(): + # # os.unlink(filepath) + # + # for filepath in temp_data["paths_to_remove"]: + # os.unlink(filepath) new_repre.update({ "fps": temp_data["fps"], @@ -818,9 +818,10 @@ class ExtractReview(pyblish.api.InstancePlugin): staging_dir = os.path.dirname(temp_data["full_input_path"]) explicit_frames_path = os.path.join( staging_dir, "explicit_frames.txt") + frame_duration = 1 / temp_data["fps"] with open(explicit_frames_path, "w") as fp: lines = [ - f"file {path}" + f"file '{path}'{os.linesep}duration {frame_duration}" for path in temp_data["explicit_input_paths"] ] fp.write("\n".join(lines)) @@ -830,9 +831,8 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_input_args.extend([ "-f", "concat", "-safe", "0", - "-fflags", "+genpts+igndts", "-i", path_to_subprocess_arg(explicit_frames_path), - "-r", "25" + "-r", str(temp_data["fps"]) ]) # Add audio arguments if there are any. Skipped when output are images. From e5d673c0209c065112bcd8e192c5889b68b5d16e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 24 Apr 2025 13:23:40 +0200 Subject: [PATCH 48/61] Reverted unwanted commenting out --- client/ayon_core/plugins/publish/extract_review.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 0c4b99cf66..daa58e0e93 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -525,13 +525,13 @@ class ExtractReview(pyblish.api.InstancePlugin): run_subprocess(subprcs_cmd, shell=True, logger=self.log) - # delete files added to fill gaps - # if new_frame_files: - # for filepath in new_frame_files.values(): - # # os.unlink(filepath) - # - # for filepath in temp_data["paths_to_remove"]: - # os.unlink(filepath) + #delete files added to fill gaps + if new_frame_files: + for filepath in new_frame_files.values(): + os.unlink(filepath) + + for filepath in temp_data["paths_to_remove"]: + os.unlink(filepath) new_repre.update({ "fps": temp_data["fps"], From 026ec6419673c8c896524bb83ce74fd315243050 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 24 Apr 2025 14:19:53 +0200 Subject: [PATCH 49/61] Fix typo Co-authored-by: Roy Nieterau --- client/ayon_core/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index daa58e0e93..f824e1db3c 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -525,7 +525,7 @@ class ExtractReview(pyblish.api.InstancePlugin): run_subprocess(subprcs_cmd, shell=True, logger=self.log) - #delete files added to fill gaps + # delete files added to fill gaps if new_frame_files: for filepath in new_frame_files.values(): os.unlink(filepath) From 8ef5c45eba8d074a01432c8f4373239baa33210d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 5 May 2025 16:05:26 +0200 Subject: [PATCH 50/61] Generate blank frame only if necessary --- .../plugins/publish/extract_review.py | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index f824e1db3c..de1c785475 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1071,8 +1071,35 @@ class ExtractReview(pyblish.api.InstancePlugin): temp_data: Dict[str, Any] ) -> Optional[Dict[int, str]]: """Fills missing files by blank frame.""" + + blank_frame_path = None + + added_files = {} + + col_format = collection.format("{head}{padding}{tail}") + for frame in range(start_frame, end_frame + 1): + if frame in collection.indexes: + continue + hole_fpath = os.path.join(staging_dir, col_format % frame) + if blank_frame_path is None: + blank_frame_path = self._create_blank_frame( + staging_dir, extension, resolution_width, resolution_height + ) + temp_data["paths_to_remove"].append(blank_frame_path) + speedcopy.copyfile(blank_frame_path, hole_fpath) + added_files[frame] = hole_fpath + + return added_files + + def _create_blank_frame( + self, + staging_dir, + extension, + resolution_width, + resolution_height + ): blank_frame_path = os.path.join(staging_dir, f"blank.{extension}") - temp_data["paths_to_remove"].append(blank_frame_path) + command = get_ffmpeg_tool_args( "ffmpeg", "-f", "lavfi", @@ -1090,17 +1117,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ) self.log.debug("Output: {}".format(output)) - added_files = {} - - col_format = collection.format("{head}{padding}{tail}") - for frame in range(start_frame, end_frame + 1): - if frame in collection.indexes: - continue - hole_fpath = os.path.join(staging_dir, col_format % frame) - speedcopy.copyfile(blank_frame_path, hole_fpath) - added_files[frame] = hole_fpath - - return added_files + return blank_frame_path def fill_sequence_gaps_from_existing( self, From 9bf848f1a81d5f6d0ec041f5c97f7df017ddd7e2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 9 May 2025 11:30:24 +0200 Subject: [PATCH 51/61] Create explicit_frames.txt as temp file --- client/ayon_core/plugins/publish/extract_review.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index de1c785475..87208f5574 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -6,6 +6,7 @@ import shutil import subprocess from abc import ABC, abstractmethod from typing import Dict, Any, Optional +import tempfile import clique import speedcopy @@ -815,10 +816,13 @@ class ExtractReview(pyblish.api.InstancePlugin): "-i", path_to_subprocess_arg(temp_data["full_input_path"]) ]) else: - staging_dir = os.path.dirname(temp_data["full_input_path"]) - explicit_frames_path = os.path.join( - staging_dir, "explicit_frames.txt") frame_duration = 1 / temp_data["fps"] + + explicit_frames_meta = tempfile.NamedTemporaryFile( + mode="w", prefix="explicit_frames", suffix=".txt", delete=False + ) + explicit_frames_meta.close() + explicit_frames_path = explicit_frames_meta.name with open(explicit_frames_path, "w") as fp: lines = [ f"file '{path}'{os.linesep}duration {frame_duration}" From 57b808e92477042a2caef41517dbd2284669c0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 12 May 2025 13:46:54 +0200 Subject: [PATCH 52/61] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/publish/extract_thumbnail.py | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index e5108444f7..3626c5f381 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -497,7 +497,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # For very short videos, just use the first frame # Calculate seek position safely - seek_position = 0 + seek_position = 0.0 # Only use timestamp calculation for videos longer than 0.1 seconds if duration > 0.1: seek_position = duration * self.duration_split @@ -539,36 +539,31 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): self.log.debug( "Thumbnail created: {}".format(output_thumb_file_path)) return output_thumb_file_path - else: - self.log.warning( - "Output file was not created or is empty: {}".format( - output_thumb_file_path)) - - # Fallback to extracting the first frame without seeking - if "-ss" in cmd_args: - self.log.debug("Trying fallback without seeking") - # Remove -ss and its value - ss_index = cmd_args.index("-ss") - cmd_args.pop(ss_index) # Remove -ss - cmd_args.pop(ss_index) # Remove the timestamp value - - # Create new command and try again - cmd = get_ffmpeg_tool_args("ffmpeg", *cmd_args) - self.log.debug("Fallback command: {}".format( - " ".join(cmd))) - run_subprocess(cmd, logger=self.log) - - if ( - os.path.exists(output_thumb_file_path) - and os.path.getsize(output_thumb_file_path) > 0 - ): - self.log.debug( - "Fallback thumbnail created: {}".format( - output_thumb_file_path) - ) - return output_thumb_file_path + self.log.warning("Output file was not created or is empty") + # Try to create thumbnail without offset + # - skip if offset did not happen + if "-ss" not in cmd_args: return None + + self.log.debug("Trying fallback without offset") + # Remove -ss and its value + ss_index = cmd_args.index("-ss") + cmd_args.pop(ss_index) # Remove -ss + cmd_args.pop(ss_index) # Remove the timestamp value + + # Create new command and try again + cmd = get_ffmpeg_tool_args("ffmpeg", *cmd_args) + self.log.debug("Fallback command: {}".format(" ".join(cmd))) + run_subprocess(cmd, logger=self.log) + + if ( + os.path.exists(output_thumb_file_path) + and os.path.getsize(output_thumb_file_path) > 0 + ): + self.log.debug(f"Fallback thumbnail created") + return output_thumb_file_path + return None except RuntimeError as error: self.log.warning( "Failed intermediate thumb source using ffmpeg: {}".format( From fe3995f07de627e9866e913b1054290c10d24847 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 12 May 2025 13:56:43 +0200 Subject: [PATCH 53/61] Applied suggestions from @iLLicit Simplifies the ffmpeg command construction by moving the seek position argument to the beginning of the command list if a seek position is specified, leading to a clearer and more maintainable structure. This also ensures that the output path is always the last argument passed to ffmpeg. --- .../plugins/publish/extract_thumbnail.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 3626c5f381..9f58be7d94 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -503,20 +503,19 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): seek_position = duration * self.duration_split # Build command args - cmd_args = [ - "-y", + cmd_args = [] + if seek_position > 0.0: + cmd_args.extend(["--ss", str(seek_position)]) + + # Add generic ffmpeg commands + cmd_args.extend([ "-i", video_file_path, "-analyzeduration", max_int, "-probesize", max_int, - ] - - # Only add -ss if we're seeking to a specific position - if seek_position > 0: - cmd_args.insert(1, "-ss") - cmd_args.insert(2, str(seek_position)) - - # Ensure we extract exactly one frame - cmd_args.extend(["-frames:v", "1"]) + "-y", + "-frames:v", "1", + output_thumb_file_path + ]) # add output file path cmd_args.append(output_thumb_file_path) From f0be8cd87704fb82ff946b50dde8418a88a95f5d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 12 May 2025 13:56:43 +0200 Subject: [PATCH 54/61] Applied suggestions from @iLLiCiTiT Simplifies the ffmpeg command construction by moving the seek position argument to the beginning of the command list if a seek position is specified, leading to a clearer and more maintainable structure. This also ensures that the output path is always the last argument passed to ffmpeg. --- .../plugins/publish/extract_thumbnail.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 3626c5f381..9f58be7d94 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -503,20 +503,19 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): seek_position = duration * self.duration_split # Build command args - cmd_args = [ - "-y", + cmd_args = [] + if seek_position > 0.0: + cmd_args.extend(["--ss", str(seek_position)]) + + # Add generic ffmpeg commands + cmd_args.extend([ "-i", video_file_path, "-analyzeduration", max_int, "-probesize", max_int, - ] - - # Only add -ss if we're seeking to a specific position - if seek_position > 0: - cmd_args.insert(1, "-ss") - cmd_args.insert(2, str(seek_position)) - - # Ensure we extract exactly one frame - cmd_args.extend(["-frames:v", "1"]) + "-y", + "-frames:v", "1", + output_thumb_file_path + ]) # add output file path cmd_args.append(output_thumb_file_path) From 9791fda4f6a4e8b898227700f9df4a119845c97b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 12 May 2025 14:02:45 +0200 Subject: [PATCH 55/61] Applied suggestions from @iLLiCiTiT Ensures that any generated thumbnail files that are empty are removed to prevent issues with subsequent processing or storage. --- client/ayon_core/plugins/publish/extract_thumbnail.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 9f58be7d94..4b93b6514e 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -569,6 +569,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): error) ) return None + finally: + # Remove output file if is empty + if ( + os.path.exists(output_thumb_file_path) + and os.path.getsize(output_thumb_file_path) == 0 + ): + os.remove(output_thumb_file_path) def _get_resolution_arg( self, From 2ac35d6dd8021c892c6c664b59066d29a9a950ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 12 May 2025 14:57:42 +0200 Subject: [PATCH 56/61] Apply suggestions from code review Co-authored-by: Robin De Lillo --- client/ayon_core/plugins/publish/extract_otio_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 908d78ca0d..f217be551c 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -474,7 +474,6 @@ class ExtractOTIOReview( command.extend([ "-start_number", str(in_frame_start), - "-compression_level", "5", "-framerate", str(sequence_fps), "-i", input_path ]) @@ -513,7 +512,8 @@ class ExtractOTIOReview( if video or sequence: command.extend([ - "-vf", f"scale={self.to_width}:{self.to_height}:flags=lanczos" + "-vf", f"scale={self.to_width}:{self.to_height}:flags=lanczos", + "-compression_level", "5", ]) # add output attributes From ce40d020d9a0c51f86066401e267ad3961fed91f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 12 May 2025 15:38:14 +0200 Subject: [PATCH 57/61] Updates image format to png and adds scaling Updates the image format for review outputs to PNG, adds scaling and compression to the ffmpeg calls, and includes pixel format specification for better compatibility and quality. --- .../editorial/test_extract_otio_review.py | 82 +++++++++++-------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py index 45191a2c53..a46ea149d7 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py +++ b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py @@ -103,17 +103,18 @@ def test_image_sequence_with_embedded_tc_and_handles_out_of_range(): # 10 head black handles generated from gap (991-1000) "/path/to/ffmpeg -t 0.4166666666666667 -r 24.0 -f lavfi -i " "color=c=black:s=1280x720 -tune stillimage -start_number 991 " - "C:/result/output.%04d.jpg", + "-pix_fmt rgba C:/result/output.%04d.png", # 10 tail black handles generated from gap (1102-1111) "/path/to/ffmpeg -t 0.4166666666666667 -r 24.0 -f lavfi -i " "color=c=black:s=1280x720 -tune stillimage -start_number 1102 " - "C:/result/output.%04d.jpg", + "-pix_fmt rgba C:/result/output.%04d.png", # Report from source exr (1001-1101) with enforce framerate "/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i " - f"C:\\exr_embedded_tc{os.sep}output.%04d.exr -start_number 1001 " - "C:/result/output.%04d.jpg" + f"C:\\exr_embedded_tc{os.sep}output.%04d.exr " + "-vf scale=1280:720:flags=lanczos -compression_level 5 " + "-start_number 1001 -pix_fmt rgba C:/result/output.%04d.png" ] assert calls == expected @@ -130,20 +131,22 @@ def test_image_sequence_and_handles_out_of_range(): expected = [ # 5 head black frames generated from gap (991-995) - "/path/to/ffmpeg -t 0.2 -r 25.0 -f lavfi -i color=c=black:s=1280x720" - " -tune stillimage -start_number 991 C:/result/output.%04d.jpg", + "/path/to/ffmpeg -t 0.2 -r 25.0 -f lavfi -i color=c=black:s=1280x720 " + "-tune stillimage -start_number 991 -pix_fmt rgba " + "C:/result/output.%04d.png", # 9 tail back frames generated from gap (1097-1105) - "/path/to/ffmpeg -t 0.36 -r 25.0 -f lavfi -i color=c=black:s=1280x720" - " -tune stillimage -start_number 1097 C:/result/output.%04d.jpg", + "/path/to/ffmpeg -t 0.36 -r 25.0 -f lavfi -i color=c=black:s=1280x720 " + "-tune stillimage -start_number 1097 -pix_fmt rgba C:/result/output.%04d.png", # Report from source tiff (996-1096) # 996-1000 = additional 5 head frames # 1001-1095 = source range conformed to 25fps # 1096-1096 = additional 1 tail frames "/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i " - f"C:\\tif_seq{os.sep}output.%04d.tif -start_number 996" - f" C:/result/output.%04d.jpg" + f"C:\\tif_seq{os.sep}output.%04d.tif " + "-vf scale=1280:720:flags=lanczos -compression_level 5 -start_number 996 " + "-pix_fmt rgba C:/result/output.%04d.png" ] assert calls == expected @@ -164,7 +167,7 @@ def test_movie_with_embedded_tc_no_gap_handles(): # - duration = 68fr (source) + 20fr (handles) = 88frames = 3.666s "/path/to/ffmpeg -ss 0.16666666666666666 -t 3.6666666666666665 " "-i C:\\data\\qt_embedded_tc.mov -start_number 991 " - "C:/result/output.%04d.jpg" + "-pix_fmt rgba C:/result/output.%04d.png" ] assert calls == expected @@ -181,12 +184,12 @@ def test_short_movie_head_gap_handles(): expected = [ # 10 head black frames generated from gap (991-1000) "/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi -i color=c=black:s=1280x720" - " -tune stillimage -start_number 991 C:/result/output.%04d.jpg", + " -tune stillimage -start_number 991 -pix_fmt rgba C:/result/output.%04d.png", # source range + 10 tail frames # duration = 50fr (source) + 10fr (tail handle) = 60 fr = 2.4s "/path/to/ffmpeg -ss 0.0 -t 2.4 -i C:\\data\\movie.mp4" - " -start_number 1001 C:/result/output.%04d.jpg" + " -start_number 1001 -pix_fmt rgba C:/result/output.%04d.png" ] assert calls == expected @@ -204,13 +207,13 @@ def test_short_movie_tail_gap_handles(): # 10 tail black frames generated from gap (1067-1076) "/path/to/ffmpeg -t 0.4166666666666667 -r 24.0 -f lavfi -i " "color=c=black:s=1280x720 -tune stillimage -start_number 1067 " - "C:/result/output.%04d.jpg", + "-pix_fmt rgba C:/result/output.%04d.png", # 10 head frames + source range # duration = 10fr (head handle) + 66fr (source) = 76fr = 3.16s "/path/to/ffmpeg -ss 1.0416666666666667 -t 3.1666666666666665 -i " "C:\\data\\qt_no_tc_24fps.mov -start_number 991" - " C:/result/output.%04d.jpg" + " -pix_fmt rgba C:/result/output.%04d.png" ] assert calls == expected @@ -239,62 +242,75 @@ def test_multiple_review_clips_no_gap(): # 10 head black frames generated from gap (991-1000) '/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi' ' -i color=c=black:s=1280x720 -tune ' - 'stillimage -start_number 991 C:/result/output.%04d.jpg', + 'stillimage -start_number 991 -pix_fmt rgba C:/result/output.%04d.png', # Alternance 25fps tiff sequence and 24fps exr sequence # for 100 frames each '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1001 C:/result/output.%04d.jpg', + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 1001 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' f'C:\\with_tc{os.sep}output.%04d.exr ' - '-start_number 1102 C:/result/output.%04d.jpg', + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 1102 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1198 C:/result/output.%04d.jpg', + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 1198 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' f'C:\\with_tc{os.sep}output.%04d.exr ' - '-start_number 1299 C:/result/output.%04d.jpg', + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 1299 -pix_fmt rgba C:/result/output.%04d.png', # 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 1395 C:/result/output.%04d.jpg', + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 1395 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1496 C:/result/output.%04d.jpg', + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 1496 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1597 C:/result/output.%04d.jpg', + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 1597 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1698 C:/result/output.%04d.jpg', + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 1698 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1799 C:/result/output.%04d.jpg', + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 1799 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1900 C:/result/output.%04d.jpg', + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 1900 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 2001 C:/result/output.%04d.jpg', + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 2001 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 2102 C:/result/output.%04d.jpg', + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 2102 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 2203 C:/result/output.%04d.jpg' + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 2203 -pix_fmt rgba C:/result/output.%04d.png' ] assert calls == expected @@ -323,15 +339,17 @@ def test_multiple_review_clips_with_gap(): # Gap on review track (12 frames) '/path/to/ffmpeg -t 0.5 -r 24.0 -f lavfi' ' -i color=c=black:s=1280x720 -tune ' - 'stillimage -start_number 991 C:/result/output.%04d.jpg', + 'stillimage -start_number 991 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' f'C:\\with_tc{os.sep}output.%04d.exr ' - '-start_number 1003 C:/result/output.%04d.jpg', + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 1003 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' f'C:\\with_tc{os.sep}output.%04d.exr ' - '-start_number 1091 C:/result/output.%04d.jpg' + '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-start_number 1091 -pix_fmt rgba C:/result/output.%04d.png' ] assert calls == expected From 9137d1c0bb7336a0d849f5488d30531aa16372b7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 12 May 2025 15:50:06 +0200 Subject: [PATCH 58/61] Adds scaling and compression to ffmpeg calls Updates the ffmpeg calls within the editorial extraction tests to include scaling and compression parameters. This ensures consistent image quality and size across different source media. --- .../editorial/test_extract_otio_review.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py index a46ea149d7..6a74df7f43 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py +++ b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py @@ -137,7 +137,8 @@ def test_image_sequence_and_handles_out_of_range(): # 9 tail back frames generated from gap (1097-1105) "/path/to/ffmpeg -t 0.36 -r 25.0 -f lavfi -i color=c=black:s=1280x720 " - "-tune stillimage -start_number 1097 -pix_fmt rgba C:/result/output.%04d.png", + "-tune stillimage -start_number 1097 -pix_fmt rgba " + "C:/result/output.%04d.png", # Report from source tiff (996-1096) # 996-1000 = additional 5 head frames @@ -145,8 +146,8 @@ def test_image_sequence_and_handles_out_of_range(): # 1096-1096 = additional 1 tail frames "/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i " f"C:\\tif_seq{os.sep}output.%04d.tif " - "-vf scale=1280:720:flags=lanczos -compression_level 5 -start_number 996 " - "-pix_fmt rgba C:/result/output.%04d.png" + "-vf scale=1280:720:flags=lanczos -compression_level 5 " + "-start_number 996 -pix_fmt rgba C:/result/output.%04d.png" ] assert calls == expected @@ -166,8 +167,9 @@ def test_movie_with_embedded_tc_no_gap_handles(): # - first_frame = 14 src - 10 (head tail) = frame 4 = 0.1666s # - duration = 68fr (source) + 20fr (handles) = 88frames = 3.666s "/path/to/ffmpeg -ss 0.16666666666666666 -t 3.6666666666666665 " - "-i C:\\data\\qt_embedded_tc.mov -start_number 991 " - "-pix_fmt rgba C:/result/output.%04d.png" + "-i C:\\data\\qt_embedded_tc.mov -vf scale=1280:720:flags=lanczos " + "-compression_level 5 -start_number 991 -pix_fmt rgba " + "C:/result/output.%04d.png" ] assert calls == expected @@ -184,12 +186,14 @@ def test_short_movie_head_gap_handles(): expected = [ # 10 head black frames generated from gap (991-1000) "/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi -i color=c=black:s=1280x720" - " -tune stillimage -start_number 991 -pix_fmt rgba C:/result/output.%04d.png", + " -tune stillimage -start_number 991 -pix_fmt rgba " + "C:/result/output.%04d.png", # source range + 10 tail frames # duration = 50fr (source) + 10fr (tail handle) = 60 fr = 2.4s - "/path/to/ffmpeg -ss 0.0 -t 2.4 -i C:\\data\\movie.mp4" - " -start_number 1001 -pix_fmt rgba C:/result/output.%04d.png" + "/path/to/ffmpeg -ss 0.0 -t 2.4 -i C:\\data\\movie.mp4 -vf " + "scale=1280:720:flags=lanczos -compression_level 5 " + "-start_number 1001 -pix_fmt rgba C:/result/output.%04d.png" ] assert calls == expected @@ -212,8 +216,9 @@ def test_short_movie_tail_gap_handles(): # 10 head frames + source range # duration = 10fr (head handle) + 66fr (source) = 76fr = 3.16s "/path/to/ffmpeg -ss 1.0416666666666667 -t 3.1666666666666665 -i " - "C:\\data\\qt_no_tc_24fps.mov -start_number 991" - " -pix_fmt rgba C:/result/output.%04d.png" + "C:\\data\\qt_no_tc_24fps.mov -vf scale=1280:720:flags=lanczos " + "-compression_level 5 -start_number 991 -pix_fmt rgba " + "C:/result/output.%04d.png" ] assert calls == expected From d01afd073a7501152bf0a94dd81b997bde5defde Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 May 2025 16:06:53 +0200 Subject: [PATCH 59/61] Simplifies debug log message Removes unnecessary f-string formatting in a debug log message within the thumbnail extraction process. This simplifies the log output and improves readability. --- client/ayon_core/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 7f698d3b2b..2b08d663de 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -580,7 +580,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): os.path.exists(output_thumb_file_path) and os.path.getsize(output_thumb_file_path) > 0 ): - self.log.debug(f"Fallback thumbnail created") + self.log.debug("Fallback thumbnail created") return output_thumb_file_path return None except RuntimeError as error: From f5f145287222802155088755269866e6bb42ce11 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 14 May 2025 10:20:21 +0200 Subject: [PATCH 60/61] Fixes ffmpeg seek argument Corrects the ffmpeg command-line argument for specifying the seek position. It changes from '--ss' to '-ss', which is the correct flag. --- client/ayon_core/plugins/publish/extract_thumbnail.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 2b08d663de..69bb9007f9 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -525,7 +525,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # Build command args cmd_args = [] if seek_position > 0.0: - cmd_args.extend(["--ss", str(seek_position)]) + cmd_args.extend(["-ss", str(seek_position)]) # Add generic ffmpeg commands cmd_args.extend([ @@ -537,9 +537,6 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): output_thumb_file_path ]) - # add output file path - cmd_args.append(output_thumb_file_path) - # create ffmpeg command cmd = get_ffmpeg_tool_args( "ffmpeg", From 9b8229fa811777c1794063de63ea7be8866b57ba Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 14 May 2025 16:29:49 +0200 Subject: [PATCH 61/61] Fixes: Uses correct fallback data key The code now uses the correct key ("fallback_type") to access the fallback type from the configuration data, ensuring the correct config path is retrieved when no product is found. --- client/ayon_core/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 8c4f97ab1c..4b1d14d570 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -834,7 +834,7 @@ def _get_global_config_data( if not product_entities_by_name: # in case no product was found we need to use fallback - fallback_type = fallback_data["type"] + fallback_type = fallback_data["fallback_type"] return _get_config_path_from_profile_data( fallback_data, fallback_type, template_data )