AY-4801-Added conversion of resources

Added similar configuration as for ExtractReview to control possible conversion from .mov to target format (.mp4)
This commit is contained in:
Petr Kalis 2024-05-07 13:51:36 +02:00
parent 345f5f31f1
commit 2facf91bcb
2 changed files with 230 additions and 10 deletions

View file

@ -1,16 +1,20 @@
import copy
import os.path
import subprocess
import opentimelineio
import pyblish.api
from ayon_core.lib import filter_profiles, get_ffmpeg_tool_args, run_subprocess
from ayon_core.pipeline import publish
class ExtractEditorialPackage(publish.Extractor):
class ExtractEditorialPckgConversion(publish.Extractor):
"""Replaces movie paths in otio file with publish rootless
Prepares movie resources for integration.
TODO introduce conversion to .mp4
Prepares movie resources for integration (adds them to `transfers`).
Converts .mov files according to output definition.
"""
label = "Extract Editorial Package"
@ -35,13 +39,22 @@ class ExtractEditorialPackage(publish.Extractor):
instance.data["representations"].append(editorial_pckg_repre)
publish_path = self._get_published_path(instance)
publish_folder = os.path.dirname(publish_path)
publish_resource_folder = os.path.join(publish_folder, "resources")
publish_resource_folder = self._get_publish_resource_folder(instance)
resource_paths = editorial_pckg_data["resource_paths"]
transfers = self._get_transfers(resource_paths,
publish_resource_folder)
project_settings = instance.context.data["project_settings"]
profiles = (project_settings["traypublisher"]
["publish"]
["ExtractEditorialPckgConversion"]
.get("profiles"))
output_def = None
if profiles:
output_def = self._get_output_definition(instance, profiles)
if output_def:
transfers = self._convert_resources(output_def, transfers)
if not "transfers" in instance.data:
instance.data["transfers"] = []
instance.data["transfers"] = transfers
@ -57,6 +70,36 @@ class ExtractEditorialPackage(publish.Extractor):
self.log.info("Added Editorial Package representation: {}".format(
editorial_pckg_repre))
def _get_publish_resource_folder(self, instance):
"""Calculates publish folder and create it."""
publish_path = self._get_published_path(instance)
publish_folder = os.path.dirname(publish_path)
publish_resource_folder = os.path.join(publish_folder, "resources")
if not os.path.exists(publish_resource_folder):
os.makedirs(publish_resource_folder, exist_ok=True)
return publish_resource_folder
def _get_output_definition(self, instance, profiles):
"""Return appropriate profile by context information."""
product_type = instance.data["productType"]
product_name = instance.data["productName"]
task_entity = instance.data["taskEntity"] or {}
task_name = task_entity.get("name")
task_type = task_entity.get("taskType")
filtering_criteria = {
"product_types": product_type,
"product_names": product_name,
"task_names": task_name,
"task_types": task_type,
}
profile = filter_profiles(
profiles,
filtering_criteria,
logger=self.log
)
return profile
def _get_resource_path_mapping(self, instance, transfers):
"""Returns dict of {source_mov_path: rootless_published_path}."""
replace_paths = {}
@ -68,7 +111,7 @@ class ExtractEditorialPackage(publish.Extractor):
return replace_paths
def _get_transfers(self, resource_paths, publish_resource_folder):
"""Returns list of tuples (source, destination) movie paths."""
"""Returns list of tuples (source, destination) with movie paths."""
transfers = []
for res_path in resource_paths:
res_basename = os.path.basename(res_path)
@ -77,7 +120,7 @@ class ExtractEditorialPackage(publish.Extractor):
return transfers
def _replace_target_urls(self, otio_data, replace_paths):
"""Replace original movie paths with published rootles ones."""
"""Replace original movie paths with published rootless ones."""
for track in otio_data.tracks:
for clip in track:
# Check if the clip has a media reference
@ -120,3 +163,93 @@ class ExtractEditorialPackage(publish.Extractor):
template = anatomy.get_template_item("publish", "default", "path")
template_filled = template.format_strict(template_data)
return os.path.normpath(template_filled)
def _convert_resources(self, output_def, transfers):
"""Converts all resource files to configured format."""
outputs = output_def["outputs"]
if not outputs:
self.log.warning("No output configured in "
"ayon+settings://traypublisher/publish/ExtractEditorialPckgConversion/profiles/0/outputs") # noqa
return transfers
final_transfers = []
# most likely only single output is expected
for output in outputs:
out_extension = output["ext"]
out_def_ffmpeg_args = output["ffmpeg_args"]
ffmpeg_input_args = [
value.strip()
for value in out_def_ffmpeg_args["input"]
if value.strip()
]
ffmpeg_video_filters = [
value.strip()
for value in out_def_ffmpeg_args["video_filters"]
if value.strip()
]
ffmpeg_audio_filters = [
value.strip()
for value in out_def_ffmpeg_args["audio_filters"]
if value.strip()
]
ffmpeg_output_args = [
value.strip()
for value in out_def_ffmpeg_args["output"]
if value.strip()
]
ffmpeg_input_args = self._split_ffmpeg_args(ffmpeg_input_args)
generic_args = [
subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg"))
]
generic_args.extend(ffmpeg_input_args)
if ffmpeg_video_filters:
generic_args.append("-filter:v")
generic_args.append(
"\"{}\"".format(",".join(ffmpeg_video_filters)))
if ffmpeg_audio_filters:
generic_args.append("-filter:a")
generic_args.append(
"\"{}\"".format(",".join(ffmpeg_audio_filters)))
for source, destination in transfers:
base_name = os.path.basename(destination)
file_name, ext = os.path.splitext(base_name)
dest_path = os.path.join(os.path.dirname(destination),
f"{file_name}.{out_extension}")
final_transfers.append((source, dest_path))
all_args = copy.deepcopy(generic_args)
all_args.append(f"-i {source}")
all_args.extend(ffmpeg_output_args) # order matters
all_args.append(f"{dest_path}")
subprcs_cmd = " ".join(all_args)
# run subprocess
self.log.debug("Executing: {}".format(subprcs_cmd))
run_subprocess(subprcs_cmd, shell=True, logger=self.log)
return final_transfers
def _split_ffmpeg_args(self, in_args):
"""Makes sure all entered arguments are separated in individual items.
Split each argument string with " -" to identify if string contains
one or more arguments.
"""
splitted_args = []
for arg in in_args:
sub_args = arg.split(" -")
if len(sub_args) == 1:
if arg and arg not in splitted_args:
splitted_args.append(arg)
continue
for idx, arg in enumerate(sub_args):
if idx != 0:
arg = "-" + arg
if arg and arg not in splitted_args:
splitted_args.append(arg)
return splitted_args

View file

@ -1,4 +1,11 @@
from ayon_server.settings import BaseSettingsModel, SettingsField
from pydantic import validator
from ayon_server.settings import (
BaseSettingsModel,
SettingsField,
task_types_enum,
ensure_unique_names
)
class ValidatePluginModel(BaseSettingsModel):
@ -14,6 +21,74 @@ class ValidateFrameRangeModel(ValidatePluginModel):
'my_asset_to_publish.mov')"""
class ExtractEditorialPckgFFmpegModel(BaseSettingsModel):
video_filters: list[str] = SettingsField(
default_factory=list,
title="Video filters"
)
audio_filters: list[str] = SettingsField(
default_factory=list,
title="Audio filters"
)
input: list[str] = SettingsField(
default_factory=list,
title="Input arguments"
)
output: list[str] = SettingsField(
default_factory=list,
title="Output arguments"
)
class ExtractEditorialPckgOutputDefModel(BaseSettingsModel):
"""Set extension and ffmpeg arguments. See `ExtractReview` for example."""
_layout = "expanded"
name: str = SettingsField("", title="Name")
ext: str = SettingsField("", title="Output extension")
ffmpeg_args: ExtractEditorialPckgFFmpegModel = SettingsField(
default_factory=ExtractEditorialPckgFFmpegModel,
title="FFmpeg arguments"
)
class ExtractEditorialPckgProfileModel(BaseSettingsModel):
product_types: list[str] = SettingsField(
default_factory=list,
title="Product types"
)
task_types: list[str] = SettingsField(
default_factory=list,
title="Task types",
enum_resolver=task_types_enum
)
task_names: list[str] = SettingsField(
default_factory=list,
title="Task names"
)
product_names: list[str] = SettingsField(
default_factory=list,
title="Product names"
)
outputs: list[ExtractEditorialPckgOutputDefModel] = SettingsField(
default_factory=list,
title="Output Definitions",
)
@validator("outputs")
def validate_unique_outputs(cls, value):
ensure_unique_names(value)
return value
class ExtractEditorialPckgConversionModel(BaseSettingsModel):
"""Conversion of input movie files into expected format."""
enabled: bool = SettingsField(True)
profiles: list[ExtractEditorialPckgProfileModel] = SettingsField(
default_factory=list, title="Profiles"
)
class TrayPublisherPublishPlugins(BaseSettingsModel):
CollectFrameDataFromAssetEntity: ValidatePluginModel = SettingsField(
default_factory=ValidatePluginModel,
@ -28,6 +103,13 @@ class TrayPublisherPublishPlugins(BaseSettingsModel):
default_factory=ValidatePluginModel,
)
ExtractEditorialPckgConversion: ExtractEditorialPckgConversionModel = (
SettingsField(
default_factory=ExtractEditorialPckgConversionModel,
title="Extract Editorial Package Conversion"
)
)
DEFAULT_PUBLISH_PLUGINS = {
"CollectFrameDataFromAssetEntity": {
@ -44,5 +126,10 @@ DEFAULT_PUBLISH_PLUGINS = {
"enabled": True,
"optional": True,
"active": True
},
"ExtractEditorialPckgConversion": {
"enabled": True,
"optional": True,
"active": True
}
}