tictacsync 0.98a0__py3-none-any.whl → 1.4.0b0__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.

Potentially problematic release.


This version of tictacsync might be problematic. Click here for more details.

tictacsync/mamsync.py ADDED
@@ -0,0 +1,343 @@
1
+
2
+ # I know, the following is ugly, but I need those try's to
3
+ # run the command in my dev setting AND from
4
+ # a deployment set-up... surely I'm setting
5
+ # things wrong [TODO]: find why and clean up this mess
6
+
7
+ try:
8
+ from . import yaltc
9
+ from . import device_scanner
10
+ from . import timeline
11
+ from . import multi2polywav
12
+ from . import mamconf
13
+ from . import entry
14
+ except:
15
+ import yaltc
16
+ import device_scanner
17
+ import timeline
18
+ import multi2polywav
19
+ import mamconf
20
+ import entry
21
+
22
+ import argparse, tempfile, configparser, re
23
+ from loguru import logger
24
+ from pathlib import Path
25
+ # import os, sys
26
+ import os, sys, sox, platformdirs, shutil, filecmp
27
+ from rich.progress import track
28
+ # from pprint import pprint
29
+ from rich.console import Console
30
+ # from rich.text import Text
31
+ from rich.table import Table
32
+ from rich import print
33
+ from pprint import pprint, pformat
34
+ import numpy as np
35
+
36
+ DEL_TEMP = False
37
+ # CONF_FILE = 'mamsync.cfg'
38
+ # LOG_FILE = 'mamdone.txt'
39
+
40
+ av_file_extensions = \
41
+ """MOV webm mkv flv flv vob ogv ogg drc gif gifv mng avi MTS M2TS TS mov qt
42
+ wmv yuv rm rmvb viv asf amv mp4 m4p m4v mpg mp2 mpeg mpe mpv mpg mpeg m2v
43
+ m4v svi 3gp 3g2 mxf roq nsv flv f4v f4p f4a f4b 3gp aa aac aax act aiff alac
44
+ amr ape au awb dss dvf flac gsm iklax ivs m4a m4b m4p mmf mp3 mpc msv nmf
45
+ ogg oga mogg opus ra rm raw rf64 sln tta voc vox wav wma wv webm 8svx cda""".split()
46
+
47
+ logger.remove()
48
+ logger.level("DEBUG", color="<yellow>")
49
+ # logger.add(sys.stdout, level="DEBUG")
50
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "__init__" and r["module"] == "yaltc")
51
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "_fit_length")
52
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "_write_ISOs")
53
+
54
+ def copy_to_syncedroot(raw_root, synced_root):
55
+ # args are str
56
+ # copy dirs and non AV files
57
+ logger.debug(f'raw_root {raw_root}')
58
+ logger.debug(f'synced_root {synced_root}')
59
+ for raw_path in Path(raw_root).rglob('*'):
60
+ ext = raw_path.suffix[1:]
61
+ is_DS_Store = raw_path.name == '.DS_Store'# mac os
62
+ if ext not in av_file_extensions and not is_DS_Store:
63
+ logger.debug(f'raw_path: {raw_path}')
64
+ # dont copy WAVs either, they will be in ISOs
65
+ rel = raw_path.relative_to(raw_root)
66
+ logger.debug(f'relative path {rel}')
67
+ synced_path = Path(synced_root)/Path(raw_root).name/rel
68
+ logger.debug(f'synced_path: {synced_path}')
69
+ if raw_path.is_dir():
70
+ synced_path.mkdir(parents=True, exist_ok=True)
71
+ continue
72
+ # if here, it's a file
73
+ if not synced_path.exists():
74
+ print(f'will mirror non AV file {synced_path}')
75
+ logger.debug(f'will mirror non AV file at {synced_path}')
76
+ shutil.copy2(raw_path, synced_path)
77
+ continue
78
+ # file exists, check if same
79
+ same = filecmp.cmp(raw_path, synced_path, shallow=False)
80
+ logger.debug(f'copy exists of:\n{raw_path}\n{synced_path}')
81
+ if not same:
82
+ print(f'file changed, copying again\n{raw_path}')
83
+ shutil.copy2(raw_path, synced_path)
84
+ else:
85
+ logger.debug('same content, next')
86
+ continue # next raw_path in loop
87
+
88
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "clean_synced")
89
+ # def clean_synced(raw_root, synced_root):
90
+ # # removes <>v<capitalLetter>.mov if any
91
+ # # returns a list of deleted pathlib.Path
92
+ # project = Path(raw_root).name
93
+ # logger.debug(f'project {project}')
94
+ # synced_proj = Path(synced_root)/project
95
+ # if not synced_proj.exists():
96
+ # synced_proj.mkdir()
97
+ # deleted = []
98
+ # for raw_path in Path(synced_proj).rglob('*'):
99
+ # m = re.match(r'.*(v[A-Z])\..+', raw_path.name)
100
+ # if m != None:
101
+ # logger.debug(f'cleaning {raw_path}')
102
+ # raw_path.unlink()
103
+ # deleted.append(raw_path)
104
+ # return deleted
105
+
106
+ def copy_raw_root_tree_to_sndroot(raw_root, snd_root):
107
+ # args are str
108
+ # copy only tree structure, no files
109
+ for raw_path in Path(raw_root).rglob('*'):
110
+ synced_path = Path(snd_root)/str(raw_path)[1:] # cant join abs. paths
111
+ if raw_path.is_dir():
112
+ synced_path.mkdir(parents=True, exist_ok=True)
113
+
114
+ def new_parser():
115
+ parser = argparse.ArgumentParser()
116
+ parser.add_argument('--resync',
117
+ action='store_true',
118
+ dest='resync',
119
+ help='Resync previously done clips.')
120
+ parser.add_argument(
121
+ "sub_dir",
122
+ type=str,
123
+ nargs='?',
124
+ help="Sub directory to scan, should under RAWROOT."
125
+ )
126
+ parser.add_argument('--terse',
127
+ action='store_true',
128
+ dest='terse',
129
+ help='Terse output')
130
+ # parser.add_argument('--isos', # default True in mamsync
131
+ # action='store_true',
132
+ # dest='write_ISOs',
133
+ # help='Cut ISO sound files')
134
+ parser.add_argument('-t','--timelineoffset',
135
+ nargs=1,
136
+ default=['00:00:00:00'],
137
+ dest='timelineoffset',
138
+ help='When processing multicam, where to place clips on NLE timeline (HH:MM:SS:FF)')
139
+ return parser
140
+
141
+ def clear_log():
142
+ # clear the file logging clips already synced
143
+ data_dir = platformdirs.user_data_dir('mamsync', 'plutz', ensure_exists=True)
144
+ log_file = Path(data_dir)/mamconf.LOG_FILE
145
+ print('Clearing log file of synced clips: "%s"'%log_file)
146
+ with open(log_file, 'w') as fh:
147
+ fh.write('done:\n')
148
+
149
+ def main():
150
+ parser = new_parser()
151
+ args = parser.parse_args()
152
+ logger.debug(f'arguments from argparse {args}')
153
+ roots_strings = mamconf.get_proj(False)
154
+ roots_pathlibPaths = [Path(s) for s in mamconf.get_proj(False)]
155
+ logger.debug(f'roots_strings from mamconf.get_proj {roots_strings}')
156
+ logger.debug(f'roots_pathlibPaths from mamconf.get_proj {roots_pathlibPaths}')
157
+ # check all have values, except for PROXIES, the last one
158
+ if any([r == '' for r in roots_strings][:-1]):
159
+ print("Can't sync if some folders are not set:")
160
+ mamconf.print_out_conf(*mamconf.get_proj())
161
+ print('Bye.')
162
+ sys.exit(0)
163
+ # because optional PROXIES folder '' yields a '.' path, exclude it
164
+ for r in [rp for rp in roots_pathlibPaths if rp != Path('.')]:
165
+ if not r.is_absolute():
166
+ print(f'\rError: folder {r} must be an absolute path. Bye')
167
+ sys.exit(0)
168
+ if not r.exists():
169
+ print(f'\rError: folder {r} does not exist. Bye')
170
+ sys.exit(0)
171
+ if not r.is_dir():
172
+ print(f'\rError: path {r} is not a folder. Bye')
173
+ sys.exit(0)
174
+ raw_root, synced_root, snd_root, _ = roots_pathlibPaths
175
+ if args.sub_dir != None:
176
+ top_dir = args.sub_dir
177
+ logger.debug(f'sub _dir: {args.sub_dir}')
178
+ if not Path(top_dir).exists():
179
+ print(f"\rError: folder {top_dir} doesn't exist, bye.")
180
+ sys.exit(0)
181
+ else:
182
+ top_dir = raw_root
183
+ if args.resync:
184
+ clear_log()
185
+ # deleted = clean_synced(raw_root, synced_root)
186
+ # logger.debug(f'deleted older clip versions: {deleted}')
187
+ # go, mamsync!
188
+ copy_to_syncedroot(raw_root, synced_root)
189
+ # copy_raw_root_tree_to_sndroot(raw_root, snd_root) # why?
190
+ multi2polywav.poly_all(top_dir)
191
+ scanner = device_scanner.Scanner(top_dir, stay_silent=args.terse)
192
+ scanner.scan_media_and_build_devices_UID(synced_root=synced_root)
193
+ for m in scanner.found_media_files:
194
+ if m.device.tracks:
195
+ if not all([lv == None for lv in m.device.tracks.lag_values]):
196
+ logger.debug('%s has lag_values %s'%(
197
+ m.path, m.device.tracks.lag_values))
198
+ # any lag for a channel is specified by user in tracks.txt
199
+ entry.process_lag_adjustement(m)
200
+ audio_REC_only = all([m.device.dev_type == 'REC' for m
201
+ in scanner.found_media_files])
202
+ if not args.terse:
203
+ if scanner.input_structure == 'ordered':
204
+ print('\nDetected structured folders')
205
+ # if scanner.top_dir_has_multicam:
206
+ # print(', multicam')
207
+ # else:
208
+ # print()
209
+ else:
210
+ print('\nDetected loose structure')
211
+ if scanner.CAM_numbers() > 1:
212
+ print('\nNote: different CAMs are present, will sync audio for each of them but if you want to set their')
213
+ print('respective timecode for NLE timeline alignement you should regroup clips by CAM under their own DIR.')
214
+ print('\nFound [gold1]%i[/gold1] media files '%(
215
+ len(scanner.found_media_files)), end='')
216
+ print('from [gold1]%i[/gold1] devices:\n'%(
217
+ scanner.get_devices_number()))
218
+ all_devices = scanner.get_devices()
219
+ for dev in all_devices:
220
+ dt = 'Camera' if dev.dev_type == 'CAM' else 'Recorder'
221
+ print('%s [gold1]%s[/gold1] with files:'%(dt, dev.name), end = ' ')
222
+ medias = scanner.get_media_for_device(dev)
223
+ for m in medias[:-1]: # last printed out of loop
224
+ print('[gold1]%s[/gold1]'%m.path.name, end=', ')
225
+ print('[gold1]%s[/gold1]'%medias[-1].path.name)
226
+ a_media = medias[0]
227
+ # check if all audio recorders have same sampling freq
228
+ freqs = [dev.sampling_freq for dev in all_devices if dev.dev_type == 'REC']
229
+ same = np.isclose(np.std(freqs),0)
230
+ logger.debug('sampling freqs from audio recorders %s, same:%s'%(freqs, same))
231
+ if not same:
232
+ print('some audio recorders have different sampling frequencies:')
233
+ print(freqs)
234
+ print('resulting in undefined results: quitting...')
235
+ quit()
236
+ print()
237
+ recordings = [yaltc.Recording(m, do_plots=False) for m
238
+ in scanner.found_media_files]
239
+ recordings_with_time = [
240
+ rec
241
+ for rec in recordings
242
+ if rec.get_start_time()
243
+ ]
244
+ [r.load_track_info() for r in recordings_with_time if r.is_audio()]
245
+ if not args.terse:
246
+ table = Table(title="tictacsync results")
247
+ table.add_column("Recording\n", justify="center", style='gold1')
248
+ table.add_column("TTC chan\n (1st=#0)", justify="center", style='gold1')
249
+ # table.add_column("Device\n", justify="center", style='gold1')
250
+ table.add_column("UTC times\nstart:end", justify="center", style='gold1')
251
+ table.add_column("Clock drift\n(ppm)", justify="right", style='gold1')
252
+ # table.add_column("SN ratio\n(dB)", justify="center", style='gold1')
253
+ table.add_column("Date\n", justify="center", style='gold1')
254
+ rec_WO_time = [
255
+ rec.AVpath.name
256
+ for rec in recordings
257
+ if rec not in recordings_with_time]
258
+ if rec_WO_time:
259
+ print('No time found for: ',end='')
260
+ [print(rec, end=' ') for rec in rec_WO_time]
261
+ print('\n')
262
+ for r in recordings_with_time:
263
+ date = r.get_start_time().strftime("%y-%m-%d")
264
+ start_HHMMSS = r.get_start_time().strftime("%Hh%Mm%Ss")
265
+ end_MMSS = r.get_end_time().strftime("%Mm%Ss")
266
+ times_range = start_HHMMSS + ':' + end_MMSS
267
+ table.add_row(
268
+ str(r.AVpath.name),
269
+ str(r.TicTacCode_channel),
270
+ # r.device,
271
+ times_range,
272
+ # '%.6f'%(r.true_samplerate/1e3),
273
+ '%2i'%(r.get_samplerate_drift()),
274
+ # '%.0f'%r.decoder.SN_ratio,
275
+ date
276
+ )
277
+ console = Console()
278
+ console.print(table)
279
+ print()
280
+ n_devices = scanner.get_devices_number()
281
+ if len(recordings_with_time) < 2:
282
+ if not args.terse:
283
+ print('\nNothing to sync, exiting.\n')
284
+ sys.exit(1)
285
+ matcher = timeline.Matcher(recordings_with_time)
286
+ matcher.scan_audio_for_each_videoclip()
287
+ if not matcher.mergers:
288
+ if not args.terse:
289
+ print('\nNothing to sync, bye.\n')
290
+ sys.exit(1)
291
+ if scanner.input_structure != 'ordered':
292
+ print('Warning, can\'t run mamsync without structured folders: [gold1]--isos[/gold1] option ignored.\n')
293
+ asked_ISOs = True # par defaut
294
+ dont_write_cam_folder = False # write them
295
+ for merger in track(matcher.mergers, description="Merging..."):
296
+ merger._build_audio_and_write_video(top_dir,
297
+ dont_write_cam_folder,
298
+ asked_ISOs,
299
+ synced_root = synced_root,
300
+ snd_root = snd_root,
301
+ raw_root = raw_root)
302
+ if not args.terse:
303
+ print("\n")
304
+ # find out where files were written
305
+ # a_merger = matcher.mergers[0]
306
+ # log file
307
+ p = Path(platformdirs.user_data_dir('mamsync', 'plutz'))/mamconf.LOG_FILE
308
+ log_filehandle = open(p, 'a')
309
+ for merger in matcher.mergers:
310
+ print('[gold1]%s[/gold1]'%merger.videoclip.AVpath.name, end='')
311
+ for audio in merger.get_matched_audio_recs():
312
+ print(' + [gold1]%s[/gold1]'%audio.AVpath.name, end='')
313
+ new_file = merger.videoclip.final_synced_file.parts
314
+ final_p = merger.videoclip.final_synced_file
315
+ nameAnd2Parents = Path('').joinpath(*final_p.parts[-2:])
316
+ print(' became [gold1]%s[/gold1]'%nameAnd2Parents)
317
+ # add full path to log file
318
+ log_filehandle.write(f'{merger.videoclip.AVpath}\n')
319
+ # matcher._build_otio_tracks_for_cam()
320
+ log_filehandle.close()
321
+ matcher.set_up_clusters() # multicam
322
+ matcher.shrink_gaps_between_takes(args.timelineoffset)
323
+ logger.debug('matcher.multicam_clips_clusters %s'%
324
+ pformat(matcher.multicam_clips_clusters))
325
+ # clusters is list of {'end': t1, 'start': t2, 'vids': [r1,r3]}
326
+ # really_clusters is True if one of them has len() > 1
327
+ really_clusters = any([len(cl['vids']) > 1 for cl
328
+ in matcher.multicam_clips_clusters])
329
+ if really_clusters:
330
+ if scanner.input_structure == 'loose':
331
+ print('\nThere are synced multicam clips but without structured folders')
332
+ print('they were not grouped together under the same folder.')
333
+ else:
334
+ matcher.move_multicam_to_dir(raw_root=raw_root, synced_root=synced_root)
335
+ else:
336
+ logger.debug('not really a multicam cluster, nothing to move')
337
+ sys.exit(0)
338
+
339
+ if __name__ == '__main__':
340
+ main()
341
+
342
+
343
+
@@ -31,7 +31,7 @@ def print_grby(grby):
31
31
  def wav_recursive_scan(top_directory):
32
32
  files_lower_case = Path(top_directory).rglob('*.wav')
33
33
  files_upper_case = Path(top_directory).rglob('*.WAV')
34
- files = list(files_lower_case) + list(files_upper_case)
34
+ files = set(list(files_lower_case) + list(files_upper_case))
35
35
  paths = [
36
36
  p
37
37
  for p in files
@@ -56,7 +56,6 @@ def nframes(path):
56
56
  return duration_ts
57
57
  return n_frames
58
58
 
59
-
60
59
  def build_poly_name(multifiles):
61
60
  """
62
61
  Returns string of polywav filename, constructed from similitudes between
@@ -83,7 +82,9 @@ def build_poly_name(multifiles):
83
82
  def jump_metadata(from_file, to_file):
84
83
  tempfile_for_metadata = tempfile.NamedTemporaryFile(suffix='.wav', delete=True)
85
84
  tempfile_for_metadata = tempfile_for_metadata.name
86
- process_list = ['ffmpeg', '-loglevel', 'quiet', '-nostats', '-hide_banner', '-i', from_file, '-i', to_file, '-map_metadata', '0', '-c', 'copy', tempfile_for_metadata]
85
+ # ffmpeg -i 32ch-44100-bwf.wav -i onechan.wav -map 1 -map_metadata 0 -c copy outmeta.wav
86
+ process_list = ['ffmpeg', '-loglevel', 'quiet', '-nostats', '-hide_banner', '-i', from_file, '-i', to_file, '-map', '1',
87
+ '-map_metadata', '0', '-c', 'copy', tempfile_for_metadata]
87
88
  # ss = shlex.split("ffmpeg -i %s -i %s -map_metadata 0 -c copy %s"%(from_file, to_file, tempfile_for_metadata))
88
89
  # print(ss)
89
90
  # logger.debug('process %s'%process_list)