sprocket-systems.coda.sdk 1.3.3__py3-none-any.whl → 2.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coda/__init__.py +2 -31
- coda/sdk/__init__.py +27 -0
- coda/sdk/constants.py +22 -0
- coda/sdk/enums.py +270 -0
- coda/sdk/essence.py +498 -0
- coda/sdk/job.py +625 -0
- coda/sdk/preset.py +239 -0
- coda/sdk/utils.py +282 -0
- coda/sdk/workflow.py +1402 -0
- {sprocket_systems_coda_sdk-1.3.3.dist-info → sprocket_systems_coda_sdk-2.0.6.dist-info}/METADATA +7 -9
- sprocket_systems_coda_sdk-2.0.6.dist-info/RECORD +15 -0
- coda/sdk.py +0 -1663
- sprocket_systems_coda_sdk-1.3.3.dist-info/RECORD +0 -8
- {sprocket_systems_coda_sdk-1.3.3.dist-info → sprocket_systems_coda_sdk-2.0.6.dist-info}/WHEEL +0 -0
- {sprocket_systems_coda_sdk-1.3.3.dist-info → sprocket_systems_coda_sdk-2.0.6.dist-info}/entry_points.txt +0 -0
- {sprocket_systems_coda_sdk-1.3.3.dist-info → sprocket_systems_coda_sdk-2.0.6.dist-info}/licenses/LICENSE +0 -0
coda/sdk/essence.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Dict
|
|
9
|
+
from .enums import Format, SourceType, InputStemType
|
|
10
|
+
from .constants import (
|
|
11
|
+
ENV_CODA_CLI_EXE,
|
|
12
|
+
ENV_NO_CODA_EXE,
|
|
13
|
+
ENV_CODA_API_GROUP_ID,
|
|
14
|
+
DEFAULT_PROGRAM_ID,
|
|
15
|
+
DEFAULT_BIT_DEPTH,
|
|
16
|
+
DEFAULT_SAMPLE_RATE,
|
|
17
|
+
URL_PREFIX_S3,
|
|
18
|
+
URL_PREFIX_IO,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Essence:
|
|
23
|
+
"""Single source essence for a Coda job.
|
|
24
|
+
|
|
25
|
+
This class constructs the essence/s payload, which can
|
|
26
|
+
consist of one or more media files (e.g., an interleaved WAV or a
|
|
27
|
+
group of multi-mono WAVs).
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
payload (dict): The dictionary that holds the essence definition.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
format: Format,
|
|
37
|
+
stem_type: InputStemType = InputStemType.PRINTMASTER,
|
|
38
|
+
program: str = DEFAULT_PROGRAM_ID,
|
|
39
|
+
description: str = "",
|
|
40
|
+
timing_info: dict[str, str] | None = None
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Initialize the CodaEssence object.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
format (Format): The input audio format of the essence (e.g., "5.1", "7.1.2", "atmos").
|
|
46
|
+
stem_type (InputStemType, optional): The type of input source stem (e.g., "audio/pm", "audio/dx").
|
|
47
|
+
Defaults to InputStemType.PRINTMASTER.
|
|
48
|
+
program (str, optional): The program identifier for the essence definition.
|
|
49
|
+
Defaults to "program-1".
|
|
50
|
+
description (str, optional): A human-readable description of the essence.
|
|
51
|
+
Defaults to "".
|
|
52
|
+
timing_info (dict, optional): Framerate, FFOA and LFOA info.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If format is an empty string.
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
if not format or not isinstance(format, str):
|
|
59
|
+
raise ValueError("format must not be an empty string and must be a string type.")
|
|
60
|
+
|
|
61
|
+
self.payload = {
|
|
62
|
+
"type": "",
|
|
63
|
+
"definition": {
|
|
64
|
+
"format": format,
|
|
65
|
+
"program": program,
|
|
66
|
+
"description": description,
|
|
67
|
+
"type": stem_type,
|
|
68
|
+
},
|
|
69
|
+
"timing_info": timing_info
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def add_interleaved_resource(
|
|
73
|
+
self,
|
|
74
|
+
file: str | dict,
|
|
75
|
+
channel_selection: dict[str, int],
|
|
76
|
+
channel_count: int,
|
|
77
|
+
frames: int,
|
|
78
|
+
bit_depth: int = DEFAULT_BIT_DEPTH,
|
|
79
|
+
sample_rate: int = DEFAULT_SAMPLE_RATE,
|
|
80
|
+
io_location_id: str | None = None
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Configure the essence as a single interleaved resource.
|
|
83
|
+
|
|
84
|
+
This sets the essence type to 'interleaved' and populates the definition
|
|
85
|
+
with the properties of a single, multichannel media file.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
file (str | dict): The filepath of the interleaved media file, or a dict containing a 'url' key.
|
|
89
|
+
If a dict is passed in it will require a 'url' key to sepcify the location path.
|
|
90
|
+
The dict can contain the optional keys 'auth' and 'opts' for resource authorization.
|
|
91
|
+
channel_selection (dict[str, int]): A map of channel labels to their stream IDs.
|
|
92
|
+
channel_count (int): The total number of channels in the file.
|
|
93
|
+
frames (int): The duration of the file in frames.
|
|
94
|
+
bit_depth (int, optional): The bit depth. Defaults to 24.
|
|
95
|
+
sample_rate (int, optional): The sample rate. Defaults to 48000.
|
|
96
|
+
io_location_id (str, optional): The IO Location ID associated with the source files. Required for agent transfers.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
TypeError: If channel_selection is not a dictionary.
|
|
100
|
+
ValueError: If IO Location ID has not been provided for non-S3 sources.
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
if not isinstance(channel_selection, dict):
|
|
104
|
+
raise TypeError("channel_selection must be a dictionary.")
|
|
105
|
+
|
|
106
|
+
self.payload["type"] = SourceType.INTERLEAVED
|
|
107
|
+
|
|
108
|
+
if isinstance(file, str):
|
|
109
|
+
url = file
|
|
110
|
+
auth = None
|
|
111
|
+
opts = None
|
|
112
|
+
else:
|
|
113
|
+
url = file["url"]
|
|
114
|
+
auth = file.get("auth")
|
|
115
|
+
opts = file.get("opts")
|
|
116
|
+
|
|
117
|
+
if URL_PREFIX_S3 not in url:
|
|
118
|
+
if io_location_id is None:
|
|
119
|
+
raise ValueError("IO Location ID is required for non-S3 file sources.")
|
|
120
|
+
url = f"{URL_PREFIX_IO}{io_location_id}{url}"
|
|
121
|
+
|
|
122
|
+
resource_dict = {"url": url}
|
|
123
|
+
if auth is not None:
|
|
124
|
+
resource_dict["auth"] = auth
|
|
125
|
+
if opts is not None:
|
|
126
|
+
resource_dict["opts"] = opts
|
|
127
|
+
|
|
128
|
+
self.payload["definition"].update({
|
|
129
|
+
"resource": resource_dict,
|
|
130
|
+
"bit_depth": bit_depth,
|
|
131
|
+
"sample_rate": sample_rate,
|
|
132
|
+
"channel_count": channel_count,
|
|
133
|
+
"frames": frames,
|
|
134
|
+
"channel_selection": channel_selection.copy(),
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
def add_multi_mono_resources(
|
|
138
|
+
self, files: List, frames: int, bit_depth: int = DEFAULT_BIT_DEPTH, sample_rate: int = DEFAULT_SAMPLE_RATE, io_location_id: str | None = None
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Configure the essence as a group of multi-mono resources.
|
|
141
|
+
|
|
142
|
+
This sets the essence type to 'multi_mono' and populates the definition
|
|
143
|
+
with a list of single-channel media files that form a multichannel group.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
files (list): A list of file URLs (or dicts with 'url' keys).
|
|
147
|
+
frames (int): The duration of the files in frames.
|
|
148
|
+
bit_depth (int, optional): The bit depth. Defaults to 24.
|
|
149
|
+
sample_rate (int, optional): The sample rate. Defaults to 48000.
|
|
150
|
+
io_location_id (str, optional): The IO Location ID associated with the source files. Required for agent transfers.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
TypeError: If files is not a list.
|
|
154
|
+
ValueError: If the number of provided files does not match the
|
|
155
|
+
channel count implied by the essence's format.
|
|
156
|
+
ValueError: If IO Location ID has not been provided for non-S3 sources.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
if not isinstance(files, list):
|
|
160
|
+
raise TypeError("files must be provided as a list.")
|
|
161
|
+
|
|
162
|
+
stem_format = self.payload["definition"]["format"]
|
|
163
|
+
if stem_format != Format.ATMOS:
|
|
164
|
+
try:
|
|
165
|
+
expected_channels = sum(int(p) for p in stem_format.split('.'))
|
|
166
|
+
actual_channels = len(files)
|
|
167
|
+
if actual_channels != expected_channels:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"Channel count mismatch for format '{stem_format}': "
|
|
170
|
+
f"Expected {expected_channels} files for multi-mono resources, but received {actual_channels}."
|
|
171
|
+
)
|
|
172
|
+
except (ValueError, TypeError) as e:
|
|
173
|
+
if isinstance(e, ValueError) and "Channel count mismatch" in str(e):
|
|
174
|
+
raise e
|
|
175
|
+
print(
|
|
176
|
+
f"Warning: Could not validate channel count for format '{stem_format}'.",
|
|
177
|
+
file=sys.stderr,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
self.payload["type"] = SourceType.MULTI_MONO.value
|
|
181
|
+
self.payload["definition"]["resources"] = []
|
|
182
|
+
for file_entry in files:
|
|
183
|
+
if isinstance(file_entry, str):
|
|
184
|
+
url = file_entry
|
|
185
|
+
auth = None
|
|
186
|
+
opts = None
|
|
187
|
+
label = ""
|
|
188
|
+
else:
|
|
189
|
+
url = file_entry["url"]
|
|
190
|
+
auth = file_entry.get("auth")
|
|
191
|
+
opts = file_entry.get("opts")
|
|
192
|
+
label = file_entry.get("channel_label", "")
|
|
193
|
+
|
|
194
|
+
if not label:
|
|
195
|
+
ch_labels = [
|
|
196
|
+
"Lsr", "Rsr", "Lts", "Rts", "Lss", "Rss",
|
|
197
|
+
"Lfe", "Ls", "Rs", "L", "C", "R",
|
|
198
|
+
]
|
|
199
|
+
for ch in ch_labels:
|
|
200
|
+
if "." + ch.upper() + "." in url.upper():
|
|
201
|
+
label = "LFE" if ch == "Lfe" else ch.capitalize()
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
if URL_PREFIX_S3 not in url:
|
|
205
|
+
if io_location_id is None:
|
|
206
|
+
raise ValueError("IO Location ID is required for non-S3 file sources.")
|
|
207
|
+
url = f"{URL_PREFIX_IO}{io_location_id}{url}"
|
|
208
|
+
|
|
209
|
+
res = {
|
|
210
|
+
"resource": {"url": url},
|
|
211
|
+
"bit_depth": bit_depth,
|
|
212
|
+
"sample_rate": sample_rate,
|
|
213
|
+
"channel_count": 1,
|
|
214
|
+
"frames": frames,
|
|
215
|
+
"channel_label": label,
|
|
216
|
+
"bext_time_reference": 0,
|
|
217
|
+
}
|
|
218
|
+
if auth is not None:
|
|
219
|
+
res["resource"]["auth"] = auth
|
|
220
|
+
if opts is not None:
|
|
221
|
+
res["resource"]["opts"] = opts
|
|
222
|
+
self.payload["definition"]["resources"].append(res)
|
|
223
|
+
|
|
224
|
+
def override_stem_type(self, stem_type: InputStemType) -> None:
|
|
225
|
+
"""Override the stem type of the essence input source.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
stem_type: The type of input source stem (e.g., "audio/pm", "audio/dx").
|
|
229
|
+
|
|
230
|
+
"""
|
|
231
|
+
self.payload["definition"]["type"] = stem_type
|
|
232
|
+
|
|
233
|
+
def override_program(self, program: str) -> None:
|
|
234
|
+
"""Override the program identifier of the essence.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
program (str): The program identifier for the essence definition.
|
|
238
|
+
|
|
239
|
+
"""
|
|
240
|
+
self.payload["definition"]["program"] = program
|
|
241
|
+
|
|
242
|
+
def override_format(self, format: Format) -> None:
|
|
243
|
+
"""Override the audio format of the essence input source.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
format (Format): The input audio format of the essence (e.g., "5.1", "7.1.2", "atmos").
|
|
247
|
+
|
|
248
|
+
"""
|
|
249
|
+
self.payload["definition"]["format"] = format
|
|
250
|
+
|
|
251
|
+
def override_language(self, language) -> None:
|
|
252
|
+
"""Override the language of the essence.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
language: The language for the essence (e.g., Language.ENGLISH).
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
lang_value = language.value if hasattr(language, 'value') else language
|
|
259
|
+
self.payload["definition"]["language"] = lang_value
|
|
260
|
+
|
|
261
|
+
def override_timing_info(
|
|
262
|
+
self,
|
|
263
|
+
source_frame_rate=None,
|
|
264
|
+
ffoa_timecode: str | None = None,
|
|
265
|
+
lfoa_timecode: str | None = None
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Override timing information for the essence.
|
|
268
|
+
|
|
269
|
+
All parameters are optional. Only provided values will be set.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
source_frame_rate: The source frame rate (e.g., FrameRate.TWENTY_FOUR).
|
|
273
|
+
ffoa_timecode (str, optional): First frame of action timecode.
|
|
274
|
+
lfoa_timecode (str, optional): Last frame of action timecode.
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
# Initialize timing_info if it's None
|
|
278
|
+
if self.payload["timing_info"] is None:
|
|
279
|
+
self.payload["timing_info"] = {}
|
|
280
|
+
|
|
281
|
+
if source_frame_rate is not None:
|
|
282
|
+
fr_value = source_frame_rate.value if hasattr(source_frame_rate, 'value') else source_frame_rate
|
|
283
|
+
self.payload["timing_info"]["source_frame_rate"] = fr_value
|
|
284
|
+
if ffoa_timecode is not None:
|
|
285
|
+
self.payload["timing_info"]["ffoa_timecode"] = ffoa_timecode
|
|
286
|
+
if lfoa_timecode is not None:
|
|
287
|
+
self.payload["timing_info"]["lfoa_timecode"] = lfoa_timecode
|
|
288
|
+
|
|
289
|
+
def override_bext_time_reference(self, bext_time_reference: int) -> None:
|
|
290
|
+
"""Set BEXT time reference on all resources.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
bext_time_reference (int): The BEXT time reference value.
|
|
294
|
+
|
|
295
|
+
"""
|
|
296
|
+
for resource in self.payload["definition"]["resources"]:
|
|
297
|
+
resource["bext_time_reference"] = bext_time_reference
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def essences_from_files(
|
|
301
|
+
files: list,
|
|
302
|
+
io_location_id: str | None = None,
|
|
303
|
+
file_info: dict | None = None,
|
|
304
|
+
program: str = "",
|
|
305
|
+
forced_frame_rate: str | None = None,
|
|
306
|
+
no_file_scan: bool = False
|
|
307
|
+
) -> list["Essence"]:
|
|
308
|
+
"""Create a list of CodaEssence objects from files.
|
|
309
|
+
|
|
310
|
+
This method inspects local files using the 'coda inspect' command-line tool
|
|
311
|
+
to automatically determine their properties. For S3 files or when the CLI
|
|
312
|
+
is unavailable, it relies on the `file_info` dictionary for manual creation.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
files (List): A list of file paths.
|
|
316
|
+
io_location_id (str, optional): The IO Location ID associated with the source files. Required for agent transfers.
|
|
317
|
+
file_info (dict, optional): Manual override info for files. Required for S3.
|
|
318
|
+
Should contain keys like 'format', 'type', 'frames', 's3_auth'. Defaults to None.
|
|
319
|
+
Example: file_info = {
|
|
320
|
+
"frames": 720000,
|
|
321
|
+
"format": Format.SEVEN_ONE,
|
|
322
|
+
"type": InputStemType.PRINTMASTER,
|
|
323
|
+
"s3_auth": {
|
|
324
|
+
"role": os.getenv(ENV_S3_SRC_ROLE, "XXXX"),
|
|
325
|
+
"external_id": os.getenv(ENV_S3_SRC_EXTERNAL_ID, "XXXX"),
|
|
326
|
+
},
|
|
327
|
+
"s3_options": {"region": "us-west-2"},
|
|
328
|
+
}
|
|
329
|
+
program (str, optional): The program identifier to assign to the essences.
|
|
330
|
+
Defaults to "".
|
|
331
|
+
forced_frame_rate (str, optional): Specify the frame rate to calculate the FFOA and LFOA
|
|
332
|
+
no_file_scan (bool, optional): Do not use Coda CLI to scan files for metadata
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
ValueError: If `files` is not populated with a list of file paths.
|
|
336
|
+
ValueError: If the 'coda' CLI tool is required but not found, or if
|
|
337
|
+
the inspection process fails.
|
|
338
|
+
ValueError: If the 'coda inspect' CLI tool does not return sources info.
|
|
339
|
+
ValueError: If S3 files are provided without a `file_info` dictionary.
|
|
340
|
+
ValueError: If IO Location ID has not been provided for non-S3 sources.
|
|
341
|
+
ValueError: If CODA_API_GROUP_ID has not been set for using `coda inspect`.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
List[CodaEssence]: A list of CodaEssence objects.
|
|
345
|
+
|
|
346
|
+
"""
|
|
347
|
+
if not files:
|
|
348
|
+
raise ValueError("Files must contain a list of file paths.")
|
|
349
|
+
|
|
350
|
+
local_files = [f for f in files if URL_PREFIX_S3 not in str(f)]
|
|
351
|
+
s3_files = [f for f in files if URL_PREFIX_S3 in str(f)]
|
|
352
|
+
essences = []
|
|
353
|
+
|
|
354
|
+
if local_files:
|
|
355
|
+
if io_location_id is None:
|
|
356
|
+
raise ValueError("IO Location ID must be filled to use supplied files.")
|
|
357
|
+
|
|
358
|
+
absfiles = [str(Path(f).resolve()) for f in local_files]
|
|
359
|
+
codaexe = shutil.which("coda") or os.getenv(ENV_CODA_CLI_EXE)
|
|
360
|
+
if not codaexe or os.getenv(ENV_NO_CODA_EXE) is not None or no_file_scan is True:
|
|
361
|
+
if not file_info:
|
|
362
|
+
raise ValueError("Coda CLI not found and file_info was not provided for manual creation.")
|
|
363
|
+
|
|
364
|
+
print("Creating essence manually from file_info.", file=sys.stderr)
|
|
365
|
+
|
|
366
|
+
essence = Essence(file_info["format"], stem_type=file_info["type"], program=program)
|
|
367
|
+
res = [{"url": url} for url in absfiles]
|
|
368
|
+
essence.add_multi_mono_resources(res, frames=file_info["frames"], io_location_id=io_location_id)
|
|
369
|
+
essences.append(essence)
|
|
370
|
+
else:
|
|
371
|
+
try:
|
|
372
|
+
group_id = os.getenv(ENV_CODA_API_GROUP_ID)
|
|
373
|
+
if not group_id:
|
|
374
|
+
raise ValueError("CODA_API_GROUP_ID must be set. Use workflow.with_group() for convenience.")
|
|
375
|
+
|
|
376
|
+
print(f"coda inspect scanning {len(absfiles)} files", file=sys.stderr)
|
|
377
|
+
print(f"using coda cli: {codaexe}", file=sys.stderr)
|
|
378
|
+
|
|
379
|
+
args = [codaexe, "inspect", "--group-id", group_id, "--io-location-id", io_location_id]
|
|
380
|
+
if forced_frame_rate:
|
|
381
|
+
args = [
|
|
382
|
+
*args,
|
|
383
|
+
"--frame-rate",
|
|
384
|
+
forced_frame_rate,
|
|
385
|
+
]
|
|
386
|
+
args = [*args, *absfiles]
|
|
387
|
+
|
|
388
|
+
ret = subprocess.run(
|
|
389
|
+
args,
|
|
390
|
+
shell=False,
|
|
391
|
+
check=True,
|
|
392
|
+
capture_output=True,
|
|
393
|
+
text=True,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
j = json.loads(ret.stdout)
|
|
397
|
+
print(json.dumps(j, indent=2))
|
|
398
|
+
if not j.get("sources"):
|
|
399
|
+
raise ValueError("`coda inspect` was unable to retrieve the sources information.")
|
|
400
|
+
|
|
401
|
+
timing_info = {
|
|
402
|
+
"source_frame_rate": j.get("source_frame_rate"),
|
|
403
|
+
"ffoa_timecode": j.get("ffoa_timecode"),
|
|
404
|
+
"lfoa_timecode": j.get("lfoa_timecode")
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
for source in j.get("sources", []):
|
|
408
|
+
source_type = source.get("type")
|
|
409
|
+
if source_type in [SourceType.ADM, SourceType.IAB_MXF]:
|
|
410
|
+
format = Format.ATMOS
|
|
411
|
+
source_def = source.get("definition")
|
|
412
|
+
essence = Essence(
|
|
413
|
+
format=source_def.get("format") or format,
|
|
414
|
+
stem_type=source_def.get("type"),
|
|
415
|
+
program=source_def.get("program", program),
|
|
416
|
+
description=source_def.get("description"),
|
|
417
|
+
timing_info=timing_info,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
essence.payload["type"] = source_type
|
|
421
|
+
|
|
422
|
+
for key, value in source_def.items():
|
|
423
|
+
essence.payload["definition"][f"{key}"] = value
|
|
424
|
+
|
|
425
|
+
essences.append(essence)
|
|
426
|
+
|
|
427
|
+
except subprocess.CalledProcessError as e:
|
|
428
|
+
error_message = (
|
|
429
|
+
f"The 'coda inspect' command failed with exit code {e.returncode}.\n"
|
|
430
|
+
f"--- STDOUT ---\n{e.stdout}\n"
|
|
431
|
+
f"--- STDERR ---\n{e.stderr}\n"
|
|
432
|
+
)
|
|
433
|
+
raise ValueError(error_message) from e
|
|
434
|
+
except Exception as e:
|
|
435
|
+
raise ValueError(f"An unexpected error occurred during 'coda inspect': {e}") from e
|
|
436
|
+
|
|
437
|
+
if s3_files:
|
|
438
|
+
print(f"Adding {len(s3_files)} S3 files", file=sys.stderr)
|
|
439
|
+
if not file_info:
|
|
440
|
+
raise ValueError("file_info must be provided for S3 files.")
|
|
441
|
+
|
|
442
|
+
essence = Essence(
|
|
443
|
+
file_info["format"], stem_type=file_info["type"], program=program
|
|
444
|
+
)
|
|
445
|
+
res = [
|
|
446
|
+
{
|
|
447
|
+
"url": r,
|
|
448
|
+
"auth": file_info.get("s3_auth"),
|
|
449
|
+
"opts": file_info.get("s3_options"),
|
|
450
|
+
}
|
|
451
|
+
for r in s3_files
|
|
452
|
+
]
|
|
453
|
+
essence.add_multi_mono_resources(res, frames=file_info["frames"])
|
|
454
|
+
essences.append(essence)
|
|
455
|
+
|
|
456
|
+
return essences
|
|
457
|
+
|
|
458
|
+
def dict(self) -> dict:
|
|
459
|
+
"""Return the final payload dictionary for the essence.
|
|
460
|
+
|
|
461
|
+
Performs a validation check to ensure the number of resource files or
|
|
462
|
+
channel selections matches the channel count specified by the audio format.
|
|
463
|
+
|
|
464
|
+
Raises:
|
|
465
|
+
ValueError: If the number of files/channels doesn't match the format's channel count.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
dict: The essence payload dictionary.
|
|
469
|
+
|
|
470
|
+
"""
|
|
471
|
+
definition = self.payload["definition"]
|
|
472
|
+
if definition.get("format") in [Format.ATMOS, Format.IMAX5, Format.IMAX6, Format.IMAX12]:
|
|
473
|
+
return self.payload
|
|
474
|
+
|
|
475
|
+
try:
|
|
476
|
+
expected_channels = sum(int(e) for e in definition["format"].split("."))
|
|
477
|
+
actual_channels = 0
|
|
478
|
+
|
|
479
|
+
if "resources" in definition:
|
|
480
|
+
actual_channels = len(definition["resources"])
|
|
481
|
+
elif "channel_selection" in definition:
|
|
482
|
+
actual_channels = len(definition["channel_selection"])
|
|
483
|
+
|
|
484
|
+
if actual_channels > 0 and actual_channels != expected_channels:
|
|
485
|
+
raise ValueError(
|
|
486
|
+
f"Channel count mismatch for format '{definition['format']}': "
|
|
487
|
+
f"Expected {expected_channels} channels, but found {actual_channels}."
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
except (ValueError, TypeError) as e:
|
|
491
|
+
if isinstance(e, ValueError) and "Channel count mismatch" in str(e):
|
|
492
|
+
raise e
|
|
493
|
+
print(
|
|
494
|
+
f"Warning: Could not validate channel count for format '{definition['format']}'.",
|
|
495
|
+
file=sys.stderr,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
return self.payload
|