sprocket-systems.coda.sdk 1.0.5__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/sdk.py ADDED
@@ -0,0 +1,1607 @@
1
+ import os
2
+ import sys
3
+ import json
4
+ import subprocess
5
+ import requests
6
+ import copy
7
+ import shutil
8
+
9
+ from .tc_tools import time_seconds_to_vid_frames,vid_frames_to_tc, tc_to_time_seconds
10
+
11
+ def make_request(func,port,route,payload=None):
12
+ url = f'http://localhost:{port}'
13
+ if port==38383 and os.getenv('CODA_API_BASE_URL'):
14
+ url = os.getenv('CODA_API_BASE_URL')
15
+ url += route
16
+ #print('request',url,file=sys.stderr)
17
+ auth = None
18
+ if os.getenv('CODA_API_TOKEN'):
19
+ auth= {'Authorization': f"Bearer {os.getenv('CODA_API_TOKEN')}"}
20
+ ret = func(url,json=payload,headers=auth)
21
+ return ret
22
+
23
+ def timingInfo(inputs,venue=None,fps=None,ffoa=None,lfoa=None,start_time=None):
24
+ if not inputs:
25
+ return None
26
+ tinfo = {}
27
+ if venue:
28
+ tinfo['venue'] = venue
29
+ else:
30
+ tinfo['venue'] = inputs['venue']
31
+ if not fps:
32
+ tinfo['source_frame_rate'] = inputs['source_frame_rate']
33
+ fps = tinfo['source_frame_rate']
34
+ else:
35
+ tinfo['source_frame_rate'] = fps
36
+ if not ffoa:
37
+ tinfo['ffoa_timecode'] = inputs['ffoa_timecode']
38
+ else:
39
+ tinfo['ffoa_timecode'] = ffoa
40
+ if not lfoa:
41
+ tinfo['lfoa_timecode'] = inputs['lfoa_timecode']
42
+ else:
43
+ tinfo['lfoa_timecode'] = lfoa
44
+ startt=-1
45
+ srate=-1
46
+ filelen = -1
47
+ sources = inputs['sources']
48
+ for i in sources:
49
+ if len(sources[i]):
50
+ if 'programme_timing' in sources[i][0]:
51
+ srate = sources[i][0]['sample_rate']
52
+ filelen = sources[i][0]['frames'] / srate
53
+ startt = sources[i][0]['programme_timing']['audio_programme_start_time_reference'] / srate
54
+ elif 'resources' in sources[i][0]:
55
+ startt = sources[i][0]['resources'][0]['bext_time_reference']/sources[i][0]['resources'][0]['sample_rate']
56
+ srate = sources[i][0]['resources'][0]['sample_rate']
57
+ filelen = sources[i][0]['resources'][0]['frames'] / srate
58
+ break
59
+ tinfo['start_time_sec'] = startt
60
+ tinfo['file_duration_sec'] = filelen
61
+ tinfo["file_duration"] =""
62
+ if fps!="":
63
+ tinfo['file_duration'] = vid_frames_to_tc(time_seconds_to_vid_frames(filelen,fps),fps)
64
+ tinfo["start_timecode"] =""
65
+ if fps!="":
66
+ tinfo['start_timecode'] = vid_frames_to_tc(time_seconds_to_vid_frames(startt,fps),fps)
67
+ tinfo["end_timecode"] =""
68
+ if fps!="":
69
+ tinfo['end_timecode'] = vid_frames_to_tc(time_seconds_to_vid_frames(startt+filelen,fps)-1,fps)
70
+ tinfo["ffoa_seconds"] = -1
71
+ tinfo["lfoa_seconds"] = -1
72
+ if fps!="":
73
+ tinfo["ffoa_seconds"] = tc_to_time_seconds(tinfo['ffoa_timecode'],fps)
74
+ tinfo["lfoa_seconds"] = tc_to_time_seconds(tinfo['lfoa_timecode'],fps)
75
+ tinfo['sample_rate'] = srate
76
+
77
+ return tinfo
78
+
79
+ class CodaPreset(object):
80
+
81
+ routes = {
82
+ 'groups': 'groups',
83
+ 'jobs': 'jobs',
84
+ 'workflows': 'workflows',
85
+ 'naming':'naming-conventions',
86
+ 'dolby': 'presets/encoding/dolby',
87
+ 'dts':'presets/encoding/dts' ,
88
+ 'loudness':'presets/loudness',
89
+ 'timecode':'presets/timecode',
90
+ 'super_session':'presets/super-session'
91
+ }
92
+
93
+ def __init__(self,preset_type,value):
94
+ self.preset = preset_type
95
+ self.value = value
96
+
97
+ def register(self):
98
+ # check if name exists and find preset id
99
+ assert(self.preset in CodaPreset.routes)
100
+ presets = CodaPreset.getPresets(self.preset)
101
+ foundid =None
102
+ if presets and len(presets)>0:
103
+ pf = [ p for p in presets if p['name']==self.value['name'] ]
104
+ if len(pf)>0:
105
+ assert(len(pf)==1)
106
+ if self.preset=='dolby' or self.preset=='dts':
107
+ foundid = pf[0]['encoding_preset_id']
108
+ elif self.preset == 'loudness':
109
+ foundid = pf[0]['loudness_preset_id']
110
+ elif self.preset == 'timecode':
111
+ foundid = pf[0]['timecode_preset_id']
112
+ elif self.preset == 'naming':
113
+ foundid = pf[0]['naming_convention_id']
114
+ elif self.preset == 'super_session':
115
+ foundid = pf[0]['super_session_preset_id']
116
+ elif self.preset == 'groups':
117
+ foundid = pf[0]['group_id']
118
+ if not foundid:
119
+ # add preset with that name for the first time
120
+ print(f"creating new preset {self.value['name']}",file=sys.stderr)
121
+ #ret = requests.post(f'http://localhost:38383/interface/v1/{CodaPreset.routes[self.preset]}',json=self.value)
122
+ ret = make_request(requests.post,38383,f'/interface/v1/{CodaPreset.routes[self.preset]}',self.value)
123
+ else:
124
+ # update found preset
125
+ print(f"updating preset {self.value['name']}, id={foundid}",file=sys.stderr)
126
+ #ret = requests.put(f'http://localhost:38383/interface/v1/{CodaPreset.routes[self.preset]}/{foundid}',json=self.value)
127
+ ret = make_request(requests.put,38383,f'/interface/v1/{CodaPreset.routes[self.preset]}/{foundid}',self.value)
128
+ J = ret.json()
129
+ return J
130
+
131
+ @staticmethod
132
+ def getPresets(preset_type):
133
+ assert(preset_type in CodaPreset.routes)
134
+ #ret = requests.get(f'http://localhost:38383/interface/v1/{CodaPreset.routes[preset_type]}')
135
+ ret = make_request(requests.get,38383,f'/interface/v1/{CodaPreset.routes[preset_type]}')
136
+ J = ret.json()
137
+ if 'error' in J:
138
+ return None
139
+ return J
140
+
141
+ class CodaEssence(object):
142
+ def __init__(self,stemformat,stemtype="audio/pm",program="program-1",description=""):
143
+ self.payload = {
144
+ 'type':stemtype,
145
+ 'format': stemformat,
146
+ 'resources':[],
147
+ 'program':program,
148
+ 'description':description
149
+ }
150
+ self.esstype = None
151
+ self.stemtype = stemtype
152
+
153
+ def addInterleavedResource(self,file,channel_selection,chans,samps,quant=24,srate=48000):
154
+ self.esstype='interleaved'
155
+ auth=None
156
+ opts=None
157
+ F = file
158
+ if type(F)==str:
159
+ f = F
160
+ else:
161
+ if 'auth' in F:
162
+ auth = F['auth']
163
+ if 'opts' in F:
164
+ opts = F['opts']
165
+ f = F['url']
166
+
167
+ res ={
168
+ 'bit_depth' : quant,
169
+ 'sample_rate' : srate,
170
+ 'url':f,
171
+ 'channel_count':chans,
172
+ 'frames':samps,
173
+ 'channel_selection':channel_selection.copy()
174
+ }
175
+
176
+ if auth is not None:
177
+ res['auth'] = auth
178
+ if opts is not None:
179
+ res['opts'] = opts
180
+ for r in res:
181
+ self.payload[r] = res[r]
182
+ del self.payload['resources']
183
+
184
+ def addMultiMonoResources(self,files,samps,quant=24,srate=48000):
185
+ self.esstype='multi_mono'
186
+ for F in files:
187
+ auth=None
188
+ opts=None
189
+ if type(F)==str:
190
+ f = F
191
+ else:
192
+ if 'auth' in F:
193
+ auth = F['auth']
194
+ if 'opts' in F:
195
+ opts = F['opts']
196
+ f = F['url']
197
+
198
+ label =""
199
+ chlabels = ['Lsr','Rsr','Lts','Rts','Lss','Rss','Lfe','Ls','Rs','L','C','R']
200
+ for ch in chlabels:
201
+ if '.'+ch.upper()+'.' in f.upper():
202
+ label = ch[0:1].upper()+ch[1:].lower()
203
+ if ch=='Lfe':
204
+ label = 'LFE'
205
+ break
206
+ res ={
207
+ 'bit_depth' : quant,
208
+ 'sample_rate' : srate,
209
+ 'url':f,
210
+ 'channel_count':1,
211
+ 'frames':samps,
212
+ 'channel_label':label,
213
+ 'bext_time_reference':0
214
+ }
215
+ if auth is not None:
216
+ res['auth'] = auth
217
+ if opts is not None:
218
+ res['opts'] = opts
219
+ self.payload['resources'] += [res]
220
+ return
221
+
222
+ def dict(self):
223
+ if self.payload['format']!='atmos':
224
+ if 'resources' in self.payload:
225
+ assert len(self.payload['resources'])==sum([ int(e) for e in self.payload['format'].split('.')])
226
+ else:
227
+ assert len(self.payload['channel_selection'])==sum([ int(e) for e in self.payload['format'].split('.')])
228
+ return self.payload
229
+
230
+
231
+ class CodaWorkflow(object):
232
+ def __init__(self,name):
233
+ self.name = name
234
+ self.packages = {}
235
+ self.agents = {}
236
+ self.processBlocks = {}
237
+ self.destinations= {}
238
+ self.wfparams= {}
239
+
240
+ @staticmethod
241
+ def getChannels(fmt):
242
+ if fmt=='7.1.4':
243
+ return ['L','R','C','LFE','Lss','Rss','Lsr','Rsr','Ltf','Rtf','Ltr','Rtr']
244
+ elif fmt=='7.1.2':
245
+ return ['L','R','C','LFE','Lss','Rss','Lsr','Rsr','Ltm','Rtm']
246
+ elif fmt=='7.1':
247
+ return ['L','R','C','LFE','Lss','Rss','Lsr','Rsr']
248
+ elif fmt=='5.1':
249
+ return ['L','R','C','LFE','Ls','Rs']
250
+ elif fmt=='2.0':
251
+ return ['L','R']
252
+
253
+ def setParameters(self,params):
254
+ self.wfparams = params.copy()
255
+
256
+ def importFromPreset(self,preset):
257
+ if preset and type(preset) is dict:
258
+ self.processBlocks = copy.deepcopy(preset['definition']['process_blocks'])
259
+ self.packages = copy.deepcopy(preset['definition']['packages'])
260
+ self.agents = copy.deepcopy(preset['definition']['agents'])
261
+ self.destinations = copy.deepcopy(preset['definition']['destinations'])
262
+ if 'name' in preset:
263
+ self.name = preset['name']
264
+ return 0
265
+ else:
266
+ wfpresets = CodaPreset.getPresets("workflows")
267
+ for J in wfpresets:
268
+ if preset and type(preset) is str and J['name']==preset:
269
+ self.processBlocks = copy.deepcopy(J['definition']['process_blocks'])
270
+ self.packages = copy.deepcopy(J['definition']['packages'])
271
+ self.agents = copy.deepcopy(J['definition']['agents'])
272
+ self.destinations = copy.deepcopy(J['definition']['destinations'])
273
+ self.name = J['name']
274
+ print('imported workflow',self.name,'id',J['workflow_id'],file=sys.stderr)
275
+ return J['workflow_id']
276
+ elif preset and type(preset) is int and J['workflow_id']==preset:
277
+ self.processBlocks = copy.deepcopy(J['definition']['process_blocks'])
278
+ self.packages = copy.deepcopy(J['definition']['packages'])
279
+ self.agents = copy.deepcopy(J['definition']['agents'])
280
+ self.destinations = copy.deepcopy(J['definition']['destinations'])
281
+ self.name = J['name']
282
+ print('imported workflow',self.name,'id',J['workflow_id'],file=sys.stderr)
283
+ return J['workflow_id']
284
+ return -1
285
+
286
+
287
+ def importFromJob(self,jobid,use_mne_definition=False):
288
+ print(f'importing workflow from job {jobid}',file=sys.stderr)
289
+ #ret = requests.get(f'http://localhost:38383/interface/v1/jobs/{jobid}')
290
+ ret = make_request(requests.get,38383,f'/interface/v1/jobs/{jobid}')
291
+ J = ret.json()
292
+ assert(J['status']=='COMPLETED')
293
+ if use_mne_definition and 'mne_workflow_definition' in J:
294
+ self.processBlocks = copy.deepcopy(J['mne_workflow_definition']['process_blocks'])
295
+ self.packages = copy.deepcopy(J['mne_workflow_definition']['packages'])
296
+ self.agents = copy.deepcopy(J['mne_workflow_definition']['agents'])
297
+ self.destinations = copy.deepcopy(J['mne_workflow_definition']['destinations'])
298
+ else:
299
+ if use_mne_definition:
300
+ print('** WARNING ** Mne workflow definition was not found. using normal workflow',file=sys.stderr)
301
+ self.processBlocks = copy.deepcopy(J['workflow_definition']['process_blocks'])
302
+ self.packages = copy.deepcopy(J['workflow_definition']['packages'])
303
+ self.agents = copy.deepcopy(J['workflow_definition']['agents'])
304
+ self.destinations = copy.deepcopy(J['workflow_definition']['destinations'])
305
+ return
306
+
307
+
308
+ def addProcessBlock(self,name, output_venue="nearfield",loudness=None,timecode=None,input_filter="all_stems"):
309
+
310
+ if not loudness:
311
+ loudness= {}
312
+ if not timecode:
313
+ timecode= {}
314
+
315
+ if timecode and type(timecode) is str:
316
+ presets = CodaPreset.getPresets('timecode')
317
+ pf = [ p for p in presets if p['name']==timecode ]
318
+ assert(len(pf)==1)
319
+ print('found timecode id',pf[0]['timecode_preset_id'],'for',timecode,file=sys.stderr)
320
+ timecode = pf[0]['definition']
321
+
322
+ if loudness and type(loudness) is str:
323
+ presets = CodaPreset.getPresets('loudness')
324
+ pf = [ p for p in presets if p['name']==loudness ]
325
+ assert(len(pf)==1)
326
+ print('found loudness id',pf[0]['loudness_preset_id'],'for',loudness,file=sys.stderr)
327
+ loudness = pf[0]['definition']
328
+
329
+ if 'tolerances' not in loudness:
330
+ loudness['tolerances']= {
331
+ "target_program_loudness": [
332
+ -0.5,
333
+ 0.4
334
+ ],
335
+ "target_dialog_loudness": [
336
+ -0.5,
337
+ 0.4
338
+ ],
339
+ "target_true_peak": [
340
+ -0.2,
341
+ 0.0
342
+ ]
343
+ }
344
+ pblock = {
345
+ "name":name,
346
+ "input_filter": input_filter,
347
+ "output_settings": {
348
+ "loudness": loudness,
349
+ "venue":output_venue,
350
+ },
351
+ "output_essences": {}
352
+ }
353
+ if timecode:
354
+ pblock["output_settings"][ "timecode"]=timecode
355
+
356
+ pid = f"my-process-block-{len(self.processBlocks)+1}"
357
+ self.processBlocks[pid] = pblock
358
+ return
359
+
360
+ def addDCPPackage(self,name,process_blocks,reels=False,naming_convention=None,naming_options=None,package_wide_uuid=False):
361
+
362
+ naming_convention_id=None
363
+ if naming_convention and type(naming_convention) is str:
364
+ presets = CodaPreset.getPresets('naming')
365
+ pf = [ p for p in presets if p['name']==naming_convention ]
366
+ assert(len(pf)==1)
367
+ print('found naming convention id',pf[0]['naming_convention_id'],'for',naming_convention,file=sys.stderr)
368
+ naming_convention_id = int(pf[0]['naming_convention_id'])
369
+ naming_convention=None
370
+ elif naming_convention and type(naming_convention) is dict:
371
+ naming_convention_id=None
372
+ #else:
373
+ elif naming_convention is not None:
374
+ naming_convention_id = int(naming_convention)
375
+ naming_convention=None
376
+
377
+ blist = []
378
+ for b in process_blocks:
379
+ block = [ B for B in self.processBlocks if self.processBlocks[B]['name']==b]
380
+ if len(block)==0:
381
+ print('process block not found',b,file=sys.stderr)
382
+ return -1
383
+ blist += block
384
+ block = self.processBlocks[block[0]]
385
+ assert(block['output_settings']['venue']=='theatrical')
386
+ fps = "24"
387
+ fmt = ["atmos"]
388
+ typ = ["printmaster"]
389
+ for F in fmt:
390
+ for t in typ:
391
+ block['output_essences'][t+'_'+fps+'_'+F]= {
392
+ 'audio_format': F,
393
+ 'frame_rate': fps,
394
+ 'type':t
395
+ }
396
+ if 'dcp_mxf' not in self.packages:
397
+ self.packages['dcp_mxf'] = {}
398
+ pid = f"my-dcp-mxf-package-{len(self.packages['dcp_mxf'])+1}"
399
+
400
+ self.packages['dcp_mxf'][pid] = {
401
+ "name": name,
402
+ "process_block_ids":blist,
403
+ "include_reel_splitting" : reels,
404
+ "include_package_wide_uuid" :package_wide_uuid,
405
+ }
406
+ if naming_convention_id:
407
+ self.packages['dcp_mxf'][pid]['naming_convention_id'] = naming_convention_id
408
+ if naming_convention:
409
+ self.packages['dcp_mxf'][pid]['naming_convention'] = naming_convention.copy()
410
+ if naming_options:
411
+ self.packages['dcp_mxf'][pid]["naming_convention_options"] = naming_options
412
+
413
+ def addSuperSessionPackage(self,name,process_blocks,essences,super_session_profile=None,naming_convention=None,naming_options=None,package_wide_uuid=False):
414
+
415
+ naming_convention_id=None
416
+ if naming_convention and type(naming_convention) is str:
417
+ presets = CodaPreset.getPresets('naming')
418
+ pf = [ p for p in presets if p['name']==naming_convention ]
419
+ assert(len(pf)==1)
420
+ print('found naming convention id',pf[0]['naming_convention_id'],'for',naming_convention,file=sys.stderr)
421
+ naming_convention_id = int(pf[0]['naming_convention_id'])
422
+ naming_convention=None
423
+ elif naming_convention and type(naming_convention) is dict:
424
+ naming_convention_id=None
425
+ #else:
426
+ elif naming_convention is not None:
427
+ naming_convention_id = int(naming_convention)
428
+ naming_convention=None
429
+
430
+ super_session_profile_id=None
431
+ if super_session_profile and type(super_session_profile) is str:
432
+ presets = CodaPreset.getPresets('super_session')
433
+ pf = [ p for p in presets if p['name']==super_session_profile ]
434
+ assert(len(pf)==1)
435
+ print('found super_session_profile id',pf[0]['super_session_preset_id'],'for',super_session_profile,file=sys.stderr)
436
+ super_session_profile_id = int(pf[0]['super_session_profile_id'])
437
+ super_session_profile=None
438
+ elif super_session_profile and type(super_session_profile) is dict:
439
+ super_session_profile_id=None
440
+ elif super_session_profile:
441
+ super_session_profile_id = int(super_session_profile)
442
+ super_session_profile=None
443
+ else:
444
+ assert(super_session_profile is None)
445
+ print('populating default session profile')
446
+ # populate default profile with all essences from all blocks
447
+ super_session_profile_id = None
448
+ super_session_profile= {'session_name_template': "{{TITLE}}_{{FRAME_RATE}}", 'tracks':[] }
449
+
450
+
451
+ tracks = []
452
+ blist = []
453
+ venues = []
454
+ for idx,b in enumerate(process_blocks):
455
+ block = [ B for B in self.processBlocks if self.processBlocks[B]['name']==b]
456
+ if len(block)==0:
457
+ print('process block not found',b,file=sys.stderr)
458
+ return -1
459
+ blist += block
460
+ block = self.processBlocks[block[0]]
461
+ venues += [ block['output_settings']['venue']]
462
+ fps = [essences[0]]
463
+ fmt = essences[1][idx][0]
464
+ typ = essences[1][idx][1]
465
+ ven = block['output_settings']['venue']
466
+ for fr in fps:
467
+ for F in fmt:
468
+ for t in typ:
469
+ block['output_essences'][t+'_'+fr+'_'+F]= {
470
+ 'audio_format': F,
471
+ 'frame_rate': fr,
472
+ 'type':t
473
+ }
474
+ if ven != 'same_as_input':
475
+ tracks += [ { 'element': t, 'format':F, 'venue': ven } ]
476
+ else:
477
+ tracks += [ { 'element': t, 'format':F, 'venue': 'theatrical' } ]
478
+ tracks += [ { 'element': t, 'format':F, 'venue': 'nearfield' } ]
479
+
480
+ if super_session_profile and len(super_session_profile['tracks'])==0:
481
+ T = []
482
+ for t in tracks:
483
+ if t['element']=='wides' or t['element']=='same_as_input':
484
+ stemlist = ['audio/dx','audio/fx','audio/mx','audio/vox','audio/fol','audio/fix']
485
+ for k in stemlist:
486
+ T += [ { 'element':k , 'format':t['format'], 'venue':t['venue']} ]
487
+ elif t['element']=='dme':
488
+ stemlist = ['audio/dx','audio/fx','audio/mx']
489
+ for k in stemlist:
490
+ T += [ { 'element':k , 'format':t['format'], 'venue':t['venue']} ]
491
+ elif t['element']=='printmaster':
492
+ T += [ { 'element':'audio/pm' , 'format':t['format'], 'venue':t['venue']} ]
493
+ else:
494
+ T += [t]
495
+ super_session_profile['tracks'] = T
496
+
497
+ if 'super_session' not in self.packages:
498
+ self.packages['super_session'] = {}
499
+ pid = f"my-super-session-package-{len(self.packages['super_session'])+1}"
500
+
501
+ self.packages['super_session'][pid] = {
502
+ "name": name,
503
+ "process_block_ids":blist,
504
+ "frame_rate" : essences[0],
505
+ "include_package_wide_uuid" :package_wide_uuid,
506
+ }
507
+ if super_session_profile_id:
508
+ self.packages['super_session'][pid]['super_session_profile_id'] = super_session_profile_id
509
+ if super_session_profile:
510
+ self.packages['super_session'][pid]['super_session_profile'] = super_session_profile.copy()
511
+ if naming_convention_id:
512
+ self.packages['super_session'][pid]['naming_convention_id'] = naming_convention_id
513
+ if naming_convention:
514
+ self.packages['super_session'][pid]['naming_convention'] = naming_convention.copy()
515
+ if naming_options:
516
+ self.packages['super_session'][pid]["naming_convention_options"] = naming_options
517
+
518
+
519
+ def addMultiMonoReelsPackage(self,name,process_blocks,essences=['same_as_input'],naming_convention=None,naming_options=None,package_wide_uuid=False):
520
+
521
+ naming_convention_id=None
522
+ if naming_convention and type(naming_convention) is str:
523
+ presets = CodaPreset.getPresets('naming')
524
+ pf = [ p for p in presets if p['name']==naming_convention ]
525
+ assert(len(pf)==1)
526
+ print('found naming convention id',pf[0]['naming_convention_id'],'for',naming_convention,file=sys.stderr)
527
+ naming_convention_id = int(pf[0]['naming_convention_id'])
528
+ naming_convention=None
529
+ elif naming_convention and type(naming_convention) is dict:
530
+ naming_convention_id=None
531
+ #else:
532
+ elif naming_convention is not None:
533
+ naming_convention_id = int(naming_convention)
534
+ naming_convention=None
535
+
536
+ blist = []
537
+ for b in process_blocks:
538
+ block = [ B for B in self.processBlocks if self.processBlocks[B]['name']==b]
539
+ if len(block)==0:
540
+ print('process block not found',b,file=sys.stderr)
541
+ return -1
542
+ blist += block
543
+ block = self.processBlocks[block[0]]
544
+ assert(block['output_settings']['venue']=='theatrical')
545
+ fps = "24"
546
+ fmt = essences
547
+ typ = ["printmaster"]
548
+ for F in fmt:
549
+ for t in typ:
550
+ block['output_essences'][t+'_'+fps+'_'+F]= {
551
+ 'audio_format': F,
552
+ 'frame_rate': fps,
553
+ 'type':t
554
+ }
555
+ if 'multi_mono_reels' not in self.packages:
556
+ self.packages['multi_mono_reels'] = {}
557
+ pid = f"my-multi-mono-reels-package-{len(self.packages['multi_mono_reels'])+1}"
558
+
559
+ #if 'same_as_input' in fmt:
560
+ #fmt = ['all_from_essence']
561
+
562
+ self.packages['multi_mono_reels'][pid] = {
563
+ "name": name,
564
+ "process_block_ids":blist,
565
+ "formats" : fmt,
566
+ #"include_package_wide_uuid" :package_wide_uuid,
567
+ }
568
+ if naming_convention_id:
569
+ self.packages['multi_mono_reels'][pid]['naming_convention_id'] = naming_convention_id
570
+ if naming_convention:
571
+ self.packages['multi_mono_reels'][pid]['naming_convention'] = naming_convention.copy()
572
+ if naming_options:
573
+ self.packages['multi_mono_reels'][pid]["naming_convention_options"] = naming_options
574
+
575
+ def addDolbyEncodePackage(self,name,process_blocks,encode_profile,essences=('same_as_input','same_as_input'),naming_convention=None,naming_options=None,package_wide_uuid=False):
576
+
577
+ naming_convention_id=None
578
+ if naming_convention and type(naming_convention) is str:
579
+ presets = CodaPreset.getPresets('naming')
580
+ pf = [ p for p in presets if p['name']==naming_convention ]
581
+ assert(len(pf)==1)
582
+ print('found naming convention id',pf[0]['naming_convention_id'],'for',naming_convention,file=sys.stderr)
583
+ naming_convention_id = int(pf[0]['naming_convention_id'])
584
+ naming_convention=None
585
+ elif naming_convention and type(naming_convention) is dict:
586
+ naming_convention_id=None
587
+ #else:
588
+ elif naming_convention is not None:
589
+ naming_convention_id = int(naming_convention)
590
+ naming_convention=None
591
+
592
+ if type(encode_profile) is str:
593
+ presets = CodaPreset.getPresets('dolby')
594
+ pf = [ p for p in presets if p['name']==encode_profile and essences[1] in p['formats'] ]
595
+ assert(len(pf)==1)
596
+ print('found encode profile id',pf[0]['encoding_preset_id'],'for',encode_profile,file=sys.stderr)
597
+ encode_profile = pf[0]['encoding_preset_id']
598
+ elif type(encode_profile) is dict:
599
+ assert(essences[1] in encode_profile['formats'])
600
+
601
+ blist = []
602
+ for b in process_blocks:
603
+ block = [ B for B in self.processBlocks if self.processBlocks[B]['name']==b]
604
+ if len(block)==0:
605
+ print('process block not found',b,file=sys.stderr)
606
+ return -1
607
+ blist += block
608
+ block = self.processBlocks[block[0]]
609
+ assert(block['output_settings']['venue']=='nearfield')
610
+ fps = essences[0]
611
+ fmt = essences[1]
612
+ typ = "printmaster"
613
+ block['output_essences'][typ+'_'+fps+'_'+fmt]= {
614
+ 'audio_format': fmt,
615
+ 'frame_rate': fps,
616
+ 'type':typ
617
+ }
618
+
619
+ if 'dolby' not in self.packages:
620
+ self.packages['dolby'] = {}
621
+ if fmt=='atmos':
622
+ pid = f"my-dolby-atmos-package-{len(self.packages['dolby'])+1}"
623
+ else:
624
+ pid = f"my-dolby-package-{len(self.packages['dolby'])+1}"
625
+
626
+ #if fmt=='same_as_input':
627
+ #fmt = 'all_from_essence'
628
+ #if fps=='same_as_input':
629
+ #fps = 'all_from_essence'
630
+
631
+ self.packages['dolby'][pid] = {
632
+ "name": name,
633
+ "process_block_ids":blist,
634
+ "format" : fmt,
635
+ "frame_rate": fps,
636
+ "include_package_wide_uuid" :package_wide_uuid,
637
+ }
638
+
639
+ if type(encode_profile) is dict:
640
+ self.packages['dolby'][pid]["encoding_profile"]=encode_profile
641
+ else:
642
+ self.packages['dolby'][pid]["encoding_profile_id"]=encode_profile
643
+
644
+ if naming_convention_id:
645
+ self.packages['dolby'][pid]['naming_convention_id'] = naming_convention_id
646
+ if naming_convention:
647
+ self.packages['dolby'][pid]['naming_convention'] = naming_convention.copy()
648
+ if naming_options:
649
+ self.packages['dolby'][pid]["naming_convention_options"] = naming_options
650
+
651
+ def addImaxEnhancedEncodePackage(self,name,process_blocks,encode_profile,essences=('same_as_input','same_as_input'),naming_convention=None,naming_options=None,package_wide_uuid=False):
652
+
653
+ assert(essences[1] in ['5.1','5.1.4','7.1.4','7.1.5','5.1.1','imax5','imax6','imax12'])
654
+ if type(encode_profile) is str:
655
+ presets = CodaPreset.getPresets('dts')
656
+ pf = [ p for p in presets if p['name']==encode_profile and essences[1] in p['formats'] and 't1cc' in p['definition'] and p['definition']['t1cc']]
657
+ assert(len(pf)==1)
658
+ print('found encode profile id',pf[0]['encoding_preset_id'],'for',encode_profile,file=sys.stderr)
659
+ encode_profile = pf[0]['encoding_preset_id']
660
+ else:
661
+ assert('t1cc' in encode_profile and encode_profile['t1cc'])
662
+
663
+ self.addDtsEncodePackage(name,process_blocks,encode_profile,essences,naming_convention,naming_options)
664
+
665
+ def addDtsEncodePackage(self,name,process_blocks,encode_profile,essences=('same_as_input','same_as_input'),naming_convention=None,naming_options=None,package_wide_uuid=False):
666
+
667
+ naming_convention_id=None
668
+ if naming_convention and type(naming_convention) is str:
669
+ presets = CodaPreset.getPresets('naming')
670
+ pf = [ p for p in presets if p['name']==naming_convention ]
671
+ assert(len(pf)==1)
672
+ print('found naming convention id',pf[0]['naming_convention_id'],'for',naming_convention,file=sys.stderr)
673
+ naming_convention_id = int(pf[0]['naming_convention_id'])
674
+ naming_convention=None
675
+ elif naming_convention and type(naming_convention) is dict:
676
+ naming_convention_id=None
677
+ #else:
678
+ elif naming_convention is not None:
679
+ naming_convention_id = int(naming_convention)
680
+ naming_convention=None
681
+
682
+ t1cc= False
683
+ if type(encode_profile) is str:
684
+ presets = CodaPreset.getPresets('dts')
685
+ pf = [ p for p in presets if p['name']==encode_profile and essences[1] in p['formats'] ]
686
+ assert(len(pf)==1)
687
+ t1cc =('t1cc' in pf[0]['definition'] and pf[0]['definition']['t1cc'])
688
+ print('found encode profile id',pf[0]['encoding_preset_id'],'for',encode_profile,file=sys.stderr)
689
+ encode_profile = pf[0]['encoding_preset_id']
690
+ elif type(encode_profile) is dict:
691
+ t1cc =('t1cc' in encode_profile and encode_profile['t1cc'])
692
+
693
+ #print('t1cc is',t1cc)
694
+
695
+ blist = []
696
+ for b in process_blocks:
697
+ block = [ B for B in self.processBlocks if self.processBlocks[B]['name']==b]
698
+ if len(block)==0:
699
+ print('process block not found',b,file=sys.stderr)
700
+ return -1
701
+ blist += block
702
+ block = self.processBlocks[block[0]]
703
+ #if not t1cc:
704
+ assert(block['output_settings']['venue']=='nearfield')
705
+ #else:
706
+ #assert(block['output_settings']['venue']=='theatrical')
707
+ fps = essences[0]
708
+ fmt = essences[1]
709
+ if t1cc and fmt!='same_as_input' and 'imax' not in fmt:
710
+ fmt += ';mode=imax_enhanced'
711
+ typ = "printmaster"
712
+ block['output_essences'][typ+'_'+fps+'_'+fmt.replace(';','_').replace('=','_')]= {
713
+ 'audio_format': fmt,
714
+ 'frame_rate': fps,
715
+ 'type':typ
716
+ }
717
+
718
+ packtype = 'dts'
719
+ if t1cc:
720
+ packtype = 'imax_enhanced'
721
+
722
+ if packtype not in self.packages:
723
+ self.packages[packtype] = {}
724
+ pid = f"my-{packtype.replace('_','-')}-package-{len(self.packages[packtype])+1}"
725
+
726
+ #if fmt=='same_as_input':
727
+ #fmt = 'all_from_essence'
728
+ #if fps=='same_as_input':
729
+ #fps = 'all_from_essence'
730
+
731
+ pfmt = fmt
732
+ if fmt=='imax12':
733
+ pfmt = '5.1.4;mode=imax_enhanced'
734
+ elif fmt=='imax6':
735
+ pfmt = '5.1.1;mode=imax_enhanced'
736
+ elif fmt=='imax5':
737
+ pfmt = '5.1;mode=imax_enhanced'
738
+
739
+ self.packages[packtype][pid] = {
740
+ "name": name,
741
+ "process_block_ids":blist,
742
+ "format" : pfmt,
743
+ "frame_rate": fps,
744
+ "include_package_wide_uuid" :package_wide_uuid,
745
+ }
746
+ if type(encode_profile) is dict:
747
+ self.packages[packtype][pid]["encoding_profile"]=encode_profile
748
+ else:
749
+ self.packages[packtype][pid]["encoding_profile_id"]=encode_profile
750
+
751
+ if naming_convention_id:
752
+ self.packages[packtype][pid]['naming_convention_id'] = naming_convention_id
753
+ if naming_convention:
754
+ self.packages[packtype][pid]['naming_convention'] = naming_convention.copy()
755
+ if naming_options:
756
+ self.packages[packtype][pid]["naming_convention_options"] = naming_options
757
+
758
+
759
+ def addInterleavedPackage(self,name,process_blocks,essences=('same_as_input',['same_as_input'],['same_as_input']),streams=None,naming_convention=None,naming_options=None,package_wide_uuid=False):
760
+
761
+ naming_convention_id=None
762
+ if naming_convention and type(naming_convention) is str:
763
+ presets = CodaPreset.getPresets('naming')
764
+ pf = [ p for p in presets if p['name']==naming_convention ]
765
+ assert(len(pf)==1)
766
+ print('found naming convention id',pf[0]['naming_convention_id'],'for',naming_convention,file=sys.stderr)
767
+ naming_convention_id = int(pf[0]['naming_convention_id'])
768
+ naming_convention=None
769
+ elif naming_convention and type(naming_convention) is dict:
770
+ naming_convention_id=None
771
+ #else:
772
+ elif naming_convention is not None:
773
+ naming_convention_id = int(naming_convention)
774
+ naming_convention=None
775
+
776
+ blist = []
777
+ for b in process_blocks:
778
+ block = [ B for B in self.processBlocks if self.processBlocks[B]['name']==b]
779
+ if len(block)==0:
780
+ print('process block not found',b,file=sys.stderr)
781
+ return -1
782
+ blist += block
783
+ block = self.processBlocks[block[0]]
784
+ fps = essences[0]
785
+ fmt = essences[1]
786
+ typ = essences[2]
787
+ for F in fmt:
788
+ for t in typ:
789
+ block['output_essences'][t+'_'+fps+'_'+F]= {
790
+ 'audio_format': F,
791
+ 'frame_rate': fps,
792
+ 'type':t
793
+ }
794
+
795
+ if 'interleaved' not in self.packages:
796
+ self.packages['interleaved'] = {}
797
+ pid = f"my-interleaved-package-{len(self.packages['interleaved'])+1}"
798
+
799
+ if not streams:
800
+ streams = []
801
+ for t in sorted(typ):
802
+ for f in fmt:
803
+ if t == "printmaster":
804
+ E = ['audio/pm']
805
+ elif t== "dme":
806
+ E = ['audio/dx','audio/fx','audio/mx']
807
+ else:
808
+ continue
809
+ for e in E:
810
+ for ch in CodaWorkflow.getChannels(f):
811
+ streams += [ {'format':f, 'element':e, 'channel':ch } ]
812
+
813
+ #if fps=='same_as_input':
814
+ #fps = 'all_from_essence'
815
+
816
+ self.packages['interleaved'][pid] = {
817
+ "name": name,
818
+ "frame_rate": fps,
819
+ "process_block_ids":blist,
820
+ "streams": streams,
821
+ "include_package_wide_uuid" :package_wide_uuid,
822
+ }
823
+ if naming_convention:
824
+ self.packages['interleaved'][pid]['naming_convention'] = naming_convention.copy()
825
+ if naming_convention_id:
826
+ self.packages['interleaved'][pid]['naming_convention_id'] = naming_convention_id
827
+ if naming_options:
828
+ self.packages['interleaved'][pid]["naming_convention_options"] = naming_options
829
+
830
+ return
831
+
832
+ def addMultiMonoPackage(self,name,process_blocks,essences=(['same_as_input'],['same_as_input'],['same_as_input']),naming_convention=None,naming_options=None,package_wide_uuid=False,include_pt_session=False):
833
+
834
+ naming_convention_id=None
835
+ if naming_convention and type(naming_convention) is str:
836
+ presets = CodaPreset.getPresets('naming')
837
+ pf = [ p for p in presets if p['name']==naming_convention ]
838
+ assert(len(pf)==1)
839
+ print('found naming convention id',pf[0]['naming_convention_id'],'for',naming_convention,file=sys.stderr)
840
+ naming_convention_id = int(pf[0]['naming_convention_id'])
841
+ naming_convention=None
842
+ elif naming_convention and type(naming_convention) is dict:
843
+ naming_convention_id=None
844
+ #else:
845
+ elif naming_convention is not None:
846
+ naming_convention_id = int(naming_convention)
847
+ naming_convention=None
848
+
849
+ blist = []
850
+ venues = []
851
+ for b in process_blocks:
852
+ block = [ B for B in self.processBlocks if self.processBlocks[B]['name']==b]
853
+ if len(block)==0:
854
+ print('process block not found',b,file=sys.stderr)
855
+ return -1
856
+ blist += block
857
+ block = self.processBlocks[block[0]]
858
+ venues += [ block['output_settings']['venue']]
859
+ fps = essences[0]
860
+ fmt = essences[1]
861
+ typ = essences[2]
862
+ for fr in fps:
863
+ for F in fmt:
864
+ for t in typ:
865
+ block['output_essences'][t+'_'+fr+'_'+F]= {
866
+ 'audio_format': F,
867
+ 'frame_rate': fr,
868
+ 'type':t
869
+ }
870
+
871
+ if 'multi_mono' not in self.packages:
872
+ self.packages['multi_mono'] = {}
873
+ pid = f"my-multi-mono-package-{len(self.packages['multi_mono'])+1}"
874
+
875
+ #if 'same_as_input' in fps:
876
+ #fps = ['all_from_essence']
877
+ #if 'same_as_input' in fmt:
878
+ #fmt = ['all_from_essence']
879
+ #if 'same_as_input' in typ:
880
+ #typ = ['all_from_essence']
881
+ #if 'same_as_input' in venues:
882
+ #venues = ['all_from_essence']
883
+
884
+ self.packages['multi_mono'][pid] = {
885
+ "name": name,
886
+ "frame_rates": fps,
887
+ "formats":fmt,
888
+ "elements":typ,
889
+ "venues": list(set(venues)),
890
+ "process_block_ids":blist,
891
+ "include_package_wide_uuid" :package_wide_uuid,
892
+ "include_pro_tools_session" :include_pt_session,
893
+ }
894
+ if naming_convention:
895
+ self.packages['multi_mono'][pid]['naming_convention'] = naming_convention.copy()
896
+ if naming_convention_id:
897
+ self.packages['multi_mono'][pid]['naming_convention_id'] = naming_convention_id
898
+ if naming_options:
899
+ self.packages['multi_mono'][pid]["naming_convention_options"] = naming_options
900
+
901
+ return
902
+
903
+ def addAdmPackage(self,name,process_blocks,essences=("same_as_input","same_as_input"),naming_convention=None,naming_options=None,package_wide_uuid=False):
904
+
905
+ naming_convention_id=None
906
+ if naming_convention and type(naming_convention) is str:
907
+ presets = CodaPreset.getPresets('naming')
908
+ pf = [ p for p in presets if p['name']==naming_convention ]
909
+ assert(len(pf)==1)
910
+ print('found naming convention id',pf[0]['naming_convention_id'],'for',naming_convention,file=sys.stderr)
911
+ naming_convention_id = int(pf[0]['naming_convention_id'])
912
+ naming_convention=None
913
+ elif naming_convention and type(naming_convention) is dict:
914
+ naming_convention_id=None
915
+ #else:
916
+ elif naming_convention is not None:
917
+ naming_convention_id = int(naming_convention)
918
+ naming_convention=None
919
+
920
+ blist = []
921
+ venues = []
922
+ assert(len(process_blocks)==1)
923
+ for b in process_blocks:
924
+ block = [ B for B in self.processBlocks if self.processBlocks[B]['name']==b]
925
+ if len(block)==0:
926
+ print('process block not found',b,file=sys.stderr)
927
+ return -1
928
+ blist += block
929
+ block = self.processBlocks[block[0]]
930
+ venues += [ block['output_settings']['venue']]
931
+ fps = essences[0]
932
+ fmt = "atmos"
933
+ typ = essences[1]
934
+ block['output_essences'][typ+'_'+fps+'_'+fmt]= {
935
+ 'audio_format': fmt,
936
+ 'frame_rate': fps,
937
+ 'type':typ
938
+ }
939
+
940
+ if 'adm' not in self.packages:
941
+ self.packages['adm'] = {}
942
+ pid = f"my-adm-package-{len(self.packages['adm'])+1}"
943
+
944
+ #if fps=='same_as_input':
945
+ #fps = 'all_from_essence'
946
+ #if typ=='same_as_input':
947
+ #typ = 'all_from_essence'
948
+
949
+ self.packages['adm'][pid] = {
950
+ "name": name,
951
+ "frame_rate": fps,
952
+ "format":fmt,
953
+ "element":typ,
954
+ "venue": list(set(venues))[0],
955
+ "process_block_ids":blist,
956
+ "include_package_wide_uuid" :package_wide_uuid,
957
+ }
958
+ if naming_convention:
959
+ self.packages['adm'][pid]['naming_convention'] = naming_convention.copy()
960
+ if naming_convention_id:
961
+ self.packages['adm'][pid]['naming_convention_id'] = naming_convention_id
962
+ if naming_options:
963
+ self.packages['adm'][pid]["naming_convention_options"] = naming_options
964
+
965
+ return
966
+
967
+ def addDestination(self,name,url,auth=None, options=None):
968
+ self.destinations[name] = {
969
+ 'url':url,
970
+ 'auth':auth,
971
+ 'opts':options
972
+ }
973
+ return
974
+
975
+ def sendPackagesToDestination(self,dest,packageList):
976
+ assert dest in self.destinations
977
+ plist = []
978
+ for pname in packageList:
979
+ found = False
980
+ for t in self.packages:
981
+ for p in self.packages[t]:
982
+ if self.packages[t][p]['name']==pname:
983
+ plist += [p]
984
+ found=True
985
+ if not found:
986
+ print(f'warning !!! package {pname} not found',file=sys.stderr)
987
+
988
+ assert(len(plist)>0)
989
+
990
+ for p in plist:
991
+ if 'package_ids' not in self.destinations[dest]:
992
+ self.destinations[dest]['package_ids'] =[]
993
+ self.destinations[dest]['package_ids'] += [p]
994
+ return
995
+
996
+ def sendPackagesToAgent(self,client,packageList):
997
+
998
+ #aid=0
999
+ #if (os.getenv('DATAIO_AGENT_ID')):
1000
+ #aid = int(os.getenv('DATAIO_AGENT_ID'))
1001
+ #else:
1002
+ #try:
1003
+ #ret = requests.get("http://localhost:38384/info")
1004
+ #J = ret.json()
1005
+ #if 'id' in J:
1006
+ #print('got src agent from data-io client',J['id'],file=sys.stderr)
1007
+ #aid= J['id']
1008
+ #except:
1009
+ #pass
1010
+
1011
+ if type(client)== str:
1012
+ if client!='Origin':
1013
+ #ret = requests.get('http://localhost:38383/interface/v1/agents')
1014
+ ret = make_request(requests.get,38383,'/interface/v1/agents')
1015
+ allclients = ret.json()
1016
+ C = [ k for k in allclients if k['hostname']==client]
1017
+ #for c in C:
1018
+ #print(c['hostname'],c['id'],file=sys.stderr)
1019
+ assert(len(C)==1)
1020
+ aid = C[0]['id']
1021
+ print('fetched agent id',client,'->',aid,file=sys.stderr)
1022
+ else:
1023
+ aid=0
1024
+ else:
1025
+ aid = int(client)
1026
+ #print('sending to agent',aid,file=sys.stderr)
1027
+
1028
+ # check if agent already exists or use new
1029
+ pid = f"my-agent-{len(self.agents)+1}"
1030
+ for a in self.agents:
1031
+ if self.agents[a]['agent_id']==aid:
1032
+ pid = a
1033
+
1034
+ plist = []
1035
+ for pname in packageList:
1036
+ found = False
1037
+ for t in self.packages:
1038
+ for p in self.packages[t]:
1039
+ if self.packages[t][p]['name']==pname:
1040
+ plist += [p]
1041
+ found = True
1042
+ if not found:
1043
+ print(f'warning !!! package {pname} not found',file=sys.stderr)
1044
+
1045
+ assert(len(plist)>0)
1046
+
1047
+ if pid not in self.agents:
1048
+ self.agents[pid] = {
1049
+ "agent_id":aid,
1050
+ "package_ids": plist
1051
+ }
1052
+ else:
1053
+ self.agents[pid]['package_ids'] += plist
1054
+ self.agents[pid]['package_ids'] = list(set(self.agents[pid]['package_ids'])) # enforce unique packages
1055
+ return
1056
+
1057
+ def getPackageList(self):
1058
+ packlist= []
1059
+ for t in self.packages:
1060
+ for n in self.packages[t]:
1061
+ p = copy.deepcopy(self.packages[t][n])
1062
+ p['type'] =t
1063
+ packlist += [p]
1064
+ return packlist
1065
+
1066
+ def dict(self):
1067
+
1068
+ dests = {}
1069
+ for d in self.destinations:
1070
+ if len(self.destinations[d]['package_ids'])>0:
1071
+ if 's3://' in self.destinations[d]['url']:
1072
+ if 's3' not in dests:
1073
+ dests['s3']= {}
1074
+ dests['s3'][d] = self.destinations[d]
1075
+ else:
1076
+ dests['s3'][d] = self.destinations[d]
1077
+
1078
+ _wfDef = {
1079
+ "name":self.name,
1080
+ "process_blocks" : copy.deepcopy(self.processBlocks),
1081
+ "packages" : copy.deepcopy(self.packages),
1082
+ "workflow_parameters": {
1083
+ "dme_stem_mapping": {
1084
+ #"audio/fx1": "audio/fx;contents=comp",
1085
+ "audio/nar": "audio/dx;contents=comp",
1086
+ "audio/vox": "audio/mx;contents=comp",
1087
+ "audio/fx": "audio/fx;contents=comp",
1088
+ #"audio/fx2": "audio/fx;contents=comp",
1089
+ #"audio/dx1": "audio/dx;contents=comp",
1090
+ "audio/arch": "audio/dx;contents=comp",
1091
+ #"audio/dx2": "audio/dx;contents=comp",
1092
+ "audio/fffx": "audio/fx;contents=comp",
1093
+ #"audio/fol2": "audio/fx;contents=comp",
1094
+ "audio/dx": "audio/dx;contents=comp",
1095
+ #"audio/mx2": "audio/mx;contents=comp",
1096
+ #"audio/fol1": "audio/fx;contents=comp",
1097
+ #"audio/mx1": "audio/mx;contents=comp",
1098
+ #"audio/fx3": "audio/fx;contents=comp",
1099
+ #"audio/fx4": "audio/fx;contents=comp",
1100
+ "audio/scr": "audio/mx;contents=comp",
1101
+ "audio/adr": "audio/dx;contents=comp",
1102
+ #"audio/dxcomp": "audio/dx;contents=comp",
1103
+ "audio/sng": "audio/mx;contents=comp",
1104
+ #"audio/wla": "audio/fx;contents=comp",
1105
+ #"audio/mxcomp": "audio/mx;contents=comp",
1106
+ "audio/mnemx": "audio/mx;contents=comp",
1107
+ "audio/fol": "audio/fx;contents=comp",
1108
+ #"audio/fxcomp": "audio/fx;contents=comp",
1109
+ "audio/mx": "audio/mx;contents=comp",
1110
+ "audio/pfx": "audio/fx;contents=comp",
1111
+ "audio/bg": "audio/fx;contents=comp",
1112
+ #"audio/fix4": "audio/fx;contents=comp",
1113
+ "audio/audiodescription": "audio/dx;contents=comp",
1114
+ #"audio/fix2": "audio/fx;contents=comp",
1115
+ "audio/vo": "audio/dx;contents=comp",
1116
+ #"audio/fix3": "audio/fx;contents=comp",
1117
+ "audio/crd": "audio/fx;contents=comp",
1118
+ "audio/fix": "audio/fx;contents=comp",
1119
+ #"audio/fix1": "audio/fx;contents=comp",
1120
+ "audio/lg": "audio/dx;contents=comp"
1121
+ },
1122
+ "enable_atmos_renders": [
1123
+ "7.1.4",
1124
+ "7.1"
1125
+ ]
1126
+ }
1127
+ }
1128
+
1129
+ for k in self.wfparams:
1130
+ _wfDef['workflow_parameters'][k] = self.wfparams[k]
1131
+
1132
+ if self.agents and len(self.agents):
1133
+ _wfDef["agents"]= copy.deepcopy(self.agents)
1134
+ if dests and len(dests):
1135
+ _wfDef["destinations"]= copy.deepcopy(dests)
1136
+
1137
+ return _wfDef
1138
+
1139
+
1140
+ class CodaJob(object):
1141
+
1142
+ def __init__(self,name,input_venue=None,input_time_options=None,sequence=None,output_language=None):
1143
+
1144
+ self.programFPS = None
1145
+ self.programLFOA = None
1146
+ self.programFFOA = None
1147
+ self.programStart = None
1148
+
1149
+ if input_time_options:
1150
+ inputFramerate,ffoa,lfoa,start_time = input_time_options
1151
+ if inputFramerate is not None:
1152
+ inputFramerate = inputFramerate.upper()
1153
+ if start_time is not None:
1154
+ if type(start_time) is str:
1155
+ """
1156
+ start_time = [float(s) for s in start_time.split(':')]
1157
+ dt = {
1158
+ "23": 24000./1001.,
1159
+ "24" : 1./24.,
1160
+ "25" : 1./25,
1161
+ "29" : 24000./1001. * 1.25,
1162
+ "30" :1./30.
1163
+ }
1164
+ hmsf = [ 3600., 60.,1.0, dt[inputFramerate] ]
1165
+ start_time = sum ( [ a[0]*a[1] for a in zip(start_time,hmsf) ])
1166
+ """
1167
+ start_time =tc_to_time_seconds(start_time,inputFramerate)
1168
+ self.programFPS = inputFramerate
1169
+ self.programLFOA = lfoa
1170
+ self.programFFOA = ffoa
1171
+ self.programStart = start_time
1172
+
1173
+ self.reference_run = None
1174
+ self.programName = name
1175
+ self.programVenue = input_venue
1176
+ self.sequence = sequence
1177
+ self.language = output_language
1178
+ self.inputs = { "sources" : {"interleaved" : [], "multi_mono_groups": [] ,"adms":[] ,"iab_mxfs": [] }}
1179
+ self.wfDef = None
1180
+ self.edits = None
1181
+ if not self.sequence:
1182
+ self.sequence = ""
1183
+ if not self.language:
1184
+ self.language= "UND"
1185
+
1186
+ self.src_agent=None
1187
+ if (os.getenv('DATAIO_AGENT_ID')):
1188
+ self.src_agent = int(os.getenv('DATAIO_AGENT_ID'))
1189
+ else:
1190
+ try:
1191
+ #ret = requests.get("http://localhost:38384/api/agent")
1192
+ ret = make_request(requests.get,38384,'/api/agent')
1193
+ J = ret.json()
1194
+ if 'id' in J:
1195
+ print('got src agent from data-io client',J['id'],file=sys.stderr)
1196
+ self.src_agent= J['id']
1197
+ except:
1198
+ pass
1199
+
1200
+ if (os.getenv('CODA_GROUP_ID')):
1201
+ self.groupId = int(os.getenv('CODA_GROUP_ID'))
1202
+ else:
1203
+ self.groupId = 1
1204
+ return
1205
+
1206
+ def forceImax5(self):
1207
+ fmts = []
1208
+ for t in self.inputs['sources']:
1209
+ for k in self.inputs['sources'][t]:
1210
+ fmts += [ not (k['format']=='5.0' or k['format']=='imax5') ]
1211
+ if any(fmts):
1212
+ return False
1213
+ for t in self.inputs['sources']:
1214
+ for k in self.inputs['sources'][t]:
1215
+ k['format'] = 'imax5'
1216
+ return True
1217
+
1218
+ def setInputLanguage(self,lang):
1219
+ for t in self.inputs['sources']:
1220
+ for k in self.inputs['sources'][t]:
1221
+ k['language'] = lang
1222
+
1223
+ def setProgramForType(self,typ,prog="program-1"):
1224
+ for t in self.inputs['sources']:
1225
+ for k in self.inputs['sources'][t]:
1226
+ if typ in k['type']:
1227
+ k['program'] = prog
1228
+ return
1229
+
1230
+ def setProgramForFormat(self,fmt,prog="program-1"):
1231
+ for t in self.inputs['sources']:
1232
+ for k in self.inputs['sources'][t]:
1233
+ if fmt == k['format']:
1234
+ k['program'] = prog
1235
+ return
1236
+
1237
+ def setUniqueProgram(self,prog="program-1"):
1238
+ for t in self.inputs['sources']:
1239
+ for k in self.inputs['sources'][t]:
1240
+ k['program'] = prog
1241
+ return
1242
+
1243
+ def addInputEssences(self,essences):
1244
+ if len(essences)==0:
1245
+ return
1246
+ for e in essences:
1247
+ assert(e.esstype)
1248
+ self.inputs['sources']['multi_mono_groups'] += [e.dict() for e in essences if isinstance(e,CodaEssence) and e.esstype=='multi_mono']
1249
+ self.inputs['sources']['interleaved'] += [e.dict() for e in essences if isinstance(e,CodaEssence) and e.esstype=='interleaved']
1250
+ return
1251
+
1252
+ def addInputFiles(self,files,file_info=None,program=None,force_fps=None):
1253
+ alls3ins = all(['s3://' in f for f in files])
1254
+ if not alls3ins and not self.src_agent:
1255
+ print('** ERROR !!!! : you need a data-io agent ID to transfer local files. install data-io or export DATAIO_AGENT_ID=<id>',file=sys.stderr)
1256
+ return -1 #assert(self.src_agent)
1257
+
1258
+ absfiles = [os.path.abspath(f) for f in files if 's3://' not in f] ### make sure to use absolute paths for coda inspect...
1259
+
1260
+ if len(absfiles)>0:
1261
+ print('coda inspect scanning',len(absfiles),'files',file=sys.stderr)
1262
+ codaexe = shutil.which('coda') #'/usr/local/bin/coda'
1263
+ if os.getenv('CODA_CLI_EXE'):
1264
+ codaexe = os.getenv('CODA_CLI_EXE')
1265
+ if not codaexe:
1266
+ if not file_info:
1267
+ print('ERRROR ! you need coda cli installed to autoscan local files',file=sys.stderr)
1268
+ return -1 #assert(codaexe)
1269
+ else:
1270
+ # manual essence creation from local files
1271
+ essences = [ CodaEssence(file_info['format'],stemtype=file_info['type']) ]
1272
+ for e in essences:
1273
+ res = []
1274
+ for r in absfiles:
1275
+ res += [
1276
+ {
1277
+ 'url' : r
1278
+ }
1279
+ ]
1280
+ e.addMultiMonoResources(res,samps=file_info['samps'])
1281
+ self.addInputEssences(essences)
1282
+ else:
1283
+ try:
1284
+ print('using coda cli', codaexe,file=sys.stderr)
1285
+ try:
1286
+ ret = subprocess.run([codaexe] +['checkin'],shell=False,check=True,stdout = subprocess.PIPE)
1287
+ except:
1288
+ print('ERRROR ! you need coda agent to be running to autoscan local files',file=sys.stderr)
1289
+ return -1
1290
+
1291
+ fpsflag = []
1292
+ if force_fps is not None:
1293
+ fpsflag = [ '--frame-rate'] + [ force_fps ]
1294
+
1295
+ ret = subprocess.run([codaexe] +['inspect']+ ['-i'] + fpsflag + absfiles,shell=False,check=True,stdout = subprocess.PIPE)
1296
+ j = json.loads(ret.stdout)
1297
+ for t in j['sources']:
1298
+ for g in j['sources'][t]:
1299
+ if 'resources' in g:
1300
+ g['resources'] = sorted(g['resources'], key=lambda d: d['channel_label']) ## sort file list by channel label for repeatability
1301
+ if program is not None:
1302
+ for t in j['sources']:
1303
+ for g in j['sources'][t]:
1304
+ g['program']= program
1305
+ if 'multi_mono_groups' in j['sources']:
1306
+ for mmgroup in j['sources']['multi_mono_groups']:
1307
+ if mmgroup['format']=='7.1.5' or mmgroup['format']=='5.1.1':
1308
+ mmgroup['format']+= ';mode=imax_enhanced'
1309
+ for t in j['sources']:
1310
+ self.inputs['sources'][t] += j['sources'][t]
1311
+ self.inputs['ffoa_timecode'] = j['ffoa_timecode']
1312
+ self.inputs['lfoa_timecode'] = j['lfoa_timecode']
1313
+ self.inputs['source_frame_rate'] = j['source_frame_rate']
1314
+ except Exception as e:
1315
+ print(str(e),file=sys.stderr)
1316
+ return -1
1317
+
1318
+ s3files = [f for f in files if 's3://' in f]
1319
+
1320
+ if len(s3files)>0:
1321
+ print('adding',len(s3files),'s3 files',file=sys.stderr)
1322
+ assert(file_info is not None)
1323
+
1324
+ # manual essence creation from s3 bucket
1325
+ essences = [ CodaEssence(file_info['format'],stemtype=file_info['type']) ]
1326
+ for e in essences:
1327
+ res = []
1328
+ for r in s3files:
1329
+ res += [
1330
+ {
1331
+ 'url' : r,
1332
+ 'auth':file_info['s3_auth'],
1333
+ 'opts':file_info['s3_options'],
1334
+ }
1335
+ ]
1336
+ e.addMultiMonoResources(res,samps=file_info['samps'])
1337
+
1338
+ self.addInputEssences(essences)
1339
+
1340
+ return 0
1341
+
1342
+ def addEdits(self,edit_payload):
1343
+ self.edits = edit_payload
1344
+ return
1345
+
1346
+ def validate(self, skip_cloud_validation=True):
1347
+ # we could check for errors here, like adding a reel package but having no edits, etc...
1348
+ if self.wfDef:
1349
+ for f in self.wfDef.packages:
1350
+ if f=='dcp_mxf':
1351
+ for p in self.wfDef.packages[f]:
1352
+ if self.wfDef.packages[f][p]['include_reel_splitting']:
1353
+ assert(self.edits)
1354
+ elif f=='multi_mono_reels' and len(self.wfDef.packages[f])>0:
1355
+ assert(self.edits)
1356
+
1357
+ J = json.loads(self.json())
1358
+
1359
+ assert( 'agents' in J['workflow_definition'] or 'destinations' in J['workflow_definition'] )
1360
+ if 'agents' in J['workflow_definition']:
1361
+ for a in J['workflow_definition']['agents']:
1362
+ assert(len(J['workflow_definition']['agents'][a]['package_ids']) >0)
1363
+
1364
+ # check that all multi monos have same bext
1365
+ if 'multi_mono_groups' in J['workflow_input']['sources']:
1366
+ bext =None
1367
+ for t in J['workflow_input']['sources']['multi_mono_groups']:
1368
+ for f in t['resources']:
1369
+ if bext is None:
1370
+ bext = f['bext_time_reference']
1371
+ else:
1372
+ assert(bext==f['bext_time_reference'])
1373
+
1374
+ #ret = requests.post("http://localhost:38383/interface/v1/jobs?validate_only=true&skip_cloud_validation=true",json =J)
1375
+ ret = make_request(requests.post,38383,f"/interface/v1/jobs?validate_only=true&skip_cloud_validation={skip_cloud_validation}",J)
1376
+ print('validate :: ',ret.json(),file=sys.stderr)
1377
+ return ret.json()
1378
+
1379
+ def setWorkflow(self,wf):
1380
+ self.wfDef = copy.deepcopy(wf)
1381
+ return
1382
+
1383
+ def useReferenceJob(self,jobid):
1384
+ #ret = requests.get(f'http://localhost:38383/interface/v1/jobs/{jobid}')
1385
+ ret = make_request(requests.get,38383,f'/interface/v1/jobs/{jobid}')
1386
+ J = ret.json()
1387
+ assert(J['status']=='COMPLETED')
1388
+ wid = J['conductor_workflow_instance_id']
1389
+ print(f'referencing cache from job {jobid} --> {wid}',file=sys.stderr)
1390
+ self.reference_run = jobid
1391
+ return
1392
+
1393
+ def setGroup(self,gid):
1394
+ if type(gid) is str:
1395
+ #ret = requests.get(f'http://localhost:38383/interface/v1/groups')
1396
+ ret = make_request(requests.get,38383,'/interface/v1/groups')
1397
+ J = ret.json()
1398
+ if 'error' in J:
1399
+ return None
1400
+ pf = [ p for p in J if p['name']==gid ]
1401
+ assert(len(pf)==1)
1402
+ print('found group id',pf[0]['group_id'],'for',gid,file=sys.stderr)
1403
+ self.groupId = int(pf[0]['group_id'])
1404
+ else:
1405
+ self.groupId = int(gid)
1406
+ return
1407
+
1408
+ @staticmethod
1409
+ def run_raw_payload(J):
1410
+ CodaJob.validate_raw_payload(J)
1411
+ if 'errors' in ret or ('success' in ret and not ret['success']):
1412
+ return None
1413
+ if 'source_agent_id' in J['workflow_input']:
1414
+ assert(J['workflow_input']['source_agent_id']>0)
1415
+ print("run raw :: launching job.",file=sys.stderr)
1416
+ #ret = requests.post("http://localhost:38383/interface/v1/jobs",json =J)
1417
+ ret = make_request(requests.post,38383,'/interface/v1/jobs',J)
1418
+ print(ret.json(),file=sys.stderr)
1419
+ J = ret.json()
1420
+ if 'errors' in J:
1421
+ return None
1422
+ if 'job_id' not in J:
1423
+ return None
1424
+ return int(J['job_id'])
1425
+
1426
+ @staticmethod
1427
+ def validate_raw_payload(J):
1428
+ #ret = requests.post("http://localhost:38383/interface/v1/jobs?validate_only=true",json =J)
1429
+ ret = make_request(requests.post,38383,"/interface/v1/jobs?validate_only=true",J)
1430
+ print('validate raw :: ',ret.json(),file=sys.stderr)
1431
+ return ret.json()
1432
+
1433
+ def run(self):
1434
+ ret = self.validate()
1435
+ if 'errors' in ret or ('success' in ret and not ret['success']):
1436
+ return None
1437
+ J = json.loads(self.json())
1438
+ if 'source_agent_id' in J['workflow_input']:
1439
+ assert(J['workflow_input']['source_agent_id']>0)
1440
+ print("run :: launching job.",file=sys.stderr)
1441
+ #ret = requests.post("http://localhost:38383/interface/v1/jobs",json =J)
1442
+ ret = make_request(requests.post,38383,"/interface/v1/jobs",J)
1443
+ print(ret.json(),file=sys.stderr)
1444
+ J = ret.json()
1445
+ if 'errors' in J:
1446
+ return None
1447
+ if 'job_id' not in J:
1448
+ return None
1449
+ return int(J['job_id'])
1450
+
1451
+ @staticmethod
1452
+ def getStatus(jobid):
1453
+ #print(f'getting status for job {jobid}',file=sys.stderr)
1454
+ #ret = requests.get(f"http://localhost:38383/interface/v1/jobs/{jobid}")
1455
+ ret = make_request(requests.get,38383,f'/interface/v1/jobs/{jobid}')
1456
+ J = ret.json()
1457
+ #print(J['status'],file=sys.stderr)
1458
+ errorcnt=0
1459
+ while 'error' in J and errorcnt<3:
1460
+ print('error in getstatus::',ret.status_code,J['error'],file=sys.stderr)
1461
+ time.sleep(1)
1462
+ #ret = requests.get(f"http://localhost:38383/interface/v1/jobs/{jobid}")
1463
+ ret = make_request(requests.get,38383,f'/interface/v1/jobs/{jobid}')
1464
+ J = ret.json()
1465
+ errorcnt+=1
1466
+ if 'error' in J:
1467
+ return None
1468
+ return {'status':J['status'],'progress':J['progress']}
1469
+
1470
+ @staticmethod
1471
+ def getReport(jobid):
1472
+ #print(f'getting report for job {jobid}',file=sys.stderr)
1473
+ #ret = requests.get(f"http://localhost:38383/interface/v1/report/raw/{jobid}")
1474
+ ret = make_request(requests.get,38383,f'/interface/v1/report/raw/{jobid}')
1475
+ J = ret.json()
1476
+ #if 'error' in J:
1477
+ #print(J,file=sys.stderr)
1478
+ #return None
1479
+ return J
1480
+
1481
+
1482
+ def checkInputCompatibility(self,jobid):
1483
+ print(f'checking input compatibility against job {jobid}',file=sys.stderr)
1484
+ #ret = requests.get(f'http://localhost:38383/interface/v1/jobs/{jobid}')
1485
+ ret = make_request(requests.get,38383,f'/interface/v1/jobs/{jobid}')
1486
+ J = ret.json()
1487
+ current = self.getInputTimingInfo()
1488
+ remote = timingInfo(J['workflow_input'])
1489
+ print('local',current,file=sys.stderr)
1490
+ print(f'job {jobid}',remote,file=sys.stderr)
1491
+ return current==remote
1492
+
1493
+ def getInputTimingInfo(self):
1494
+ return timingInfo(self.inputs,self.programVenue,self.programFPS,self.programFFOA,self.programLFOA,self.programStart)
1495
+
1496
+ def json(self):
1497
+
1498
+ sources = copy.deepcopy(self.inputs['sources'])
1499
+ for s in self.inputs['sources']:
1500
+ if len(self.inputs['sources'][s])==0:
1501
+ del sources[s]
1502
+
1503
+ if self.programFPS is None:
1504
+ self.programFPS = self.inputs['source_frame_rate']
1505
+ else:
1506
+ self.inputs['source_frame_rate'] = self.programFPS
1507
+ if self.programFFOA is None:
1508
+ self.programFFOA = self.inputs['ffoa_timecode']
1509
+ else:
1510
+ self.inputs['ffoa_timecode'] = self.programFFOA
1511
+ if self.programLFOA is None:
1512
+ self.programLFOA = self.inputs['lfoa_timecode']
1513
+ else:
1514
+ self.inputs['lfoa_timecode'] = self.programLFOA
1515
+
1516
+ if self.programStart is None:
1517
+ for i in sources:
1518
+ if len(sources[i]):
1519
+ if 'resources' in sources[i][0]:
1520
+ self.programStart = sources[i][0]['resources'][0]['bext_time_reference']/sources[i][0]['resources'][0]['sample_rate']
1521
+ break
1522
+ print('setting prog start from sources',self.programStart,file=sys.stderr)
1523
+ else:
1524
+ print('punching prog start into sources',self.programStart,file=sys.stderr)
1525
+ for i in sources:
1526
+ for k in sources[i]:
1527
+ if 'resources' in k:
1528
+ for r in k['resources']:
1529
+ #print(r['url'],r['sample_rate'],int(self.programStart*r['sample_rate']),file=sys.stderr)
1530
+ r['bext_time_reference']= int(self.programStart*r['sample_rate'])
1531
+
1532
+ if self.programLFOA=="":
1533
+ print('invalid LFOA !!!!!',file=sys.stderr)
1534
+ if self.programFFOA=="":
1535
+ print('invalid FFOA !!!!!',file=sys.stderr)
1536
+ if self.programFPS=="":
1537
+ print('invalid FPS !!!!!',file=sys.stderr)
1538
+
1539
+ #print('prg start',self.programStart,file=sys.stderr)
1540
+
1541
+ wfIn = {
1542
+ "project": {"title":self.programName, "sequence":self.sequence,"language":self.language,"version":""},
1543
+ "source_frame_rate":self.programFPS,
1544
+ "venue":self.programVenue,
1545
+ "sources": sources,
1546
+ #"source_agent_id" : self.src_agent,
1547
+ "ffoa_timecode": self.inputs['ffoa_timecode'],
1548
+ "lfoa_timecode": self.inputs['lfoa_timecode']
1549
+ }
1550
+ if self.src_agent:
1551
+ wfIn['source_agent_id'] = self.src_agent
1552
+ if self.edits:
1553
+ wfIn["edits"] = self.edits
1554
+
1555
+ wdef = copy.deepcopy(self.wfDef.dict())
1556
+ for t in wdef['packages']:
1557
+ for p in wdef['packages'][t]:
1558
+
1559
+ for k in wdef['packages'][t][p]:
1560
+ #print(t,k,'::',wdef['packages'][t][p][k])
1561
+ if not ('venue' in k or 'element' in k or 'format' in k or 'frame' in k):
1562
+ continue
1563
+ if 'same_as_input' in wdef['packages'][t][p][k]:
1564
+ if 'venue' in k:
1565
+ if type(wdef['packages'][t][p][k]) is list:
1566
+ wdef['packages'][t][p][k] += [self.programVenue]
1567
+ wdef['packages'][t][p][k].remove('same_as_input')
1568
+ else:
1569
+ wdef['packages'][t][p][k] = self.programVenue
1570
+ if 'element' in k:
1571
+ if type(wdef['packages'][t][p][k]) is list:
1572
+ wdef['packages'][t][p][k] = ['all_from_essence']
1573
+ else:
1574
+ wdef['packages'][t][p][k] = self.programVenue
1575
+ if 'format' in k:
1576
+ if type(wdef['packages'][t][p][k]) is list:
1577
+ wdef['packages'][t][p][k] = ['all_from_essence'] #sources[0][0]['specifications']['audio_format']
1578
+ else:
1579
+ wdef['packages'][t][p][k] = 'all_from_essence' #sources[0][0]['specifications']['audio_format']
1580
+ if 'frame' in k:
1581
+ if type(wdef['packages'][t][p][k]) is list:
1582
+ wdef['packages'][t][p][k] += [self.programFPS]
1583
+ wdef['packages'][t][p][k].remove('same_as_input')
1584
+ else:
1585
+ wdef['packages'][t][p][k] = self.programFPS
1586
+
1587
+ #print(wdef['packages'][t][p])
1588
+
1589
+ if 'naming_convention' in wdef['packages'][t][p]:
1590
+ if 'package_data' not in wfIn:
1591
+ wfIn['package_data'] ={}
1592
+ wfIn['package_data'][p] = {'naming_convention':wdef['packages'][t][p]['naming_convention']}
1593
+ del wdef['packages'][t][p]['naming_convention']
1594
+
1595
+
1596
+ J = {
1597
+ "group_id":self.groupId,
1598
+ "workflow_input": wfIn,
1599
+ "workflow_definition": wdef
1600
+ }
1601
+
1602
+ if self.reference_run:
1603
+ J['parent_job_id'] = self.reference_run
1604
+
1605
+ return json.dumps(J,indent=2)
1606
+
1607
+