czyoung commited on
Commit
9c2ec10
·
verified ·
1 Parent(s): 1ec6474

Documentation and comment update with minor code cleanup

Browse files
Files changed (1) hide show
  1. sonogram_utility.py +510 -47
sonogram_utility.py CHANGED
@@ -10,52 +10,88 @@ import pandas as pd
10
  import datetime as dt
11
 
12
  def colors(n):
13
- '''
14
- Creates a list size n of distinctive colors
15
- '''
16
- if n == 0:
17
- return []
18
- ret = []
19
- h = int(random.random() * 180)
20
- step = 180 / n
21
- for i in range(n):
22
- h += step
23
- h = int(h) % 180
24
- hsv = np.uint8([[[h,200,200]]])
25
- bgr = cv2.cvtColor(hsv,cv2.COLOR_HSV2BGR)
26
- ret.append((bgr[0][0][0].item()/255,bgr[0][0][1].item()/255,bgr[0][0][2].item()/255))
27
- return ret
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  def colorsCSS(n):
30
- '''
31
- Creates a list size n of distinctive colors based on CSS formatting
32
- '''
33
- if n == 0:
34
- return []
35
- ret = []
36
- h = int(random.random() * 180)
37
- step = 180 / n
38
- for i in range(n):
39
- h += step
40
- h = int(h) % 180
41
- hsv = np.uint8([[[h,200,200]]])
42
- bgr = cv2.cvtColor(hsv,cv2.COLOR_HSV2BGR)
43
- b = f'{bgr[0][0][0].item():02x}'
44
- g = f'{bgr[0][0][1].item():02x}'
45
- r = f'{bgr[0][0][2].item():02x}'
46
- ret.append('#'+b+g+r)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  return ret
48
 
49
  def extendSpeakers(mySpeakerList, fileLabel = 'NONE', maximumSecondDifference = 1, minimumSecondDuration = 0):
50
  '''
51
- Assumes mySpeakerList is already split into Speaker/Audience
 
52
  '''
53
  mySpeakerAnnotations = Annotation(uri=fileLabel)
54
  newSpeakerList = [[],[]]
 
55
  for i, speaker in enumerate(mySpeakerList):
 
56
  speaker.sort()
57
  lastEnd = -1
58
  tempSection = None
 
59
  for section in speaker:
60
  if lastEnd == -1:
61
  tempSection = copy.deepcopy(section)
@@ -78,6 +114,10 @@ def extendSpeakers(mySpeakerList, fileLabel = 'NONE', maximumSecondDifference =
78
  return newSpeakerList,mySpeakerAnnotations
79
 
80
  def twoClassExtendAnnotation(myAnnotation,maximumSecondDifference = 1, minimumSecondDuration = 0):
 
 
 
 
81
  lecturerID = None
82
  lecturerLen = 0
83
 
@@ -103,46 +143,104 @@ def twoClassExtendAnnotation(myAnnotation,maximumSecondDifference = 1, minimumSe
103
  return newList, newAnnotation
104
 
105
  def loadAudioRTTM(sampleRTTM):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  # Read in prediction data
107
  # Data in list form, for convenient plotting
108
  speakerList = []
109
  # Data in Annotation form, for convenient error rate calculation
110
  prediction = Annotation(uri=sampleRTTM)
111
  with open(sampleRTTM, "r") as rttm:
 
112
  for line in rttm:
 
113
  speakerResult = line.split(' ')
 
114
  index = int(speakerResult[7][-2:])
 
115
  start = float(speakerResult[3])
116
  end = start + float(speakerResult[4])
 
117
  while len(speakerList) < index + 1:
118
  speakerList.append([])
 
119
  speakerList[index].append((float(speakerResult[3]),float(speakerResult[4])))
120
  prediction[Segment(start,end)] = speakerResult[7]
121
 
122
  return speakerList, prediction
123
 
124
  def loadAudioTXT(sampleTXT):
125
- # Read in prediction data
126
- # Data in list form, for convenient plotting
127
- speakerList = []
128
- # Data in Annotation form, for convenient error rate calculation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  prediction = Annotation(uri=sampleTXT)
130
  with open(sampleTXT, "r") as txt:
 
131
  for line in txt:
 
132
  speakerResult = line.split('\t')
 
133
  print(speakerResult)
 
134
  if len(speakerResult) < 3:
135
  continue
136
- index = -1
137
  start = float(speakerResult[0])
138
  end = float(speakerResult[1])
139
- duration = end - start
140
  prediction[Segment(start,end)] = speakerResult[2]
141
 
142
  return [], prediction
143
 
144
  def loadAudioCSV(sampleCSV):
145
- # Read in prediction data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  df = pd.read_csv(sampleCSV)
147
 
148
  df = df.reset_index() # make sure indexes pair with number of rows
@@ -159,49 +257,107 @@ def loadAudioCSV(sampleCSV):
159
  return [], prediction
160
 
161
  def splitIntoTimeSegments(testFile,maxDurationInSeconds=60):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  data, sample_rate = sf.read(testFile, dtype="float32", always_2d=True)
 
163
  waveform = torch.from_numpy(data.T) # shape: [channels, samples]
164
- audioSegments = []
165
 
 
166
  outOfBoundsIndex = waveform.shape[-1]
167
  currentStart = 0
 
168
  currentEnd = min(maxDurationInSeconds * sample_rate,outOfBoundsIndex)
169
  done = False
170
  while(not done):
 
171
  waveformSegment = waveform[:,currentStart:currentEnd]
172
  audioSegments.append(waveformSegment)
 
173
  if currentEnd >= outOfBoundsIndex:
174
  done = True
175
  break
176
  else:
 
177
  currentStart = currentEnd
178
  currentEnd = min(currentStart + maxDurationInSeconds * sample_rate,outOfBoundsIndex)
179
  return audioSegments, sample_rate
180
 
181
  def audioNormalize(waveform,sampleRate,stepSizeInSeconds = 2,dbThreshold = -50,dbTarget = -5):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  print("In audioNormalize")
 
183
  copyWaveform = waveform.clone().detach()
184
  print("Waveform copy made")
 
185
  transform = torchaudio.transforms.AmplitudeToDB(stype="amplitude", top_db=80)
 
186
  currStart = 0
187
  currEnd = int(min(currStart + stepSizeInSeconds * sampleRate, len(copyWaveform[0])-1))
188
  done = False
189
  while(not done):
 
190
  copyWaveform_db = waveform[:,currStart:currEnd].clone().detach()
191
  copyWaveform_db = transform(copyWaveform_db)
192
  if currStart == 0:
193
  print("First DB level calculated")
194
 
195
-
196
  if torch.max(copyWaveform_db[0]).item() > dbThreshold:
 
197
  gain = torch.min(dbTarget - copyWaveform_db[0])
198
  adjustGain = torchaudio.transforms.Vol(gain,'db')
 
199
  copyWaveform[0][currStart:currEnd] = adjustGain(copyWaveform[0][currStart:currEnd])
 
200
  if len(copyWaveform_db) > 1:
201
  if torch.max(copyWaveform_db[1]).item() > dbThreshold:
 
202
  gain = torch.min(dbTarget - copyWaveform_db[1])
203
  adjustGain = torchaudio.transforms.Vol(gain,'db')
 
204
  copyWaveform[1][currStart:currEnd] = adjustGain(copyWaveform[1][currStart:currEnd])
 
205
  currStart += int(stepSizeInSeconds * sampleRate)
206
  if currStart > currEnd:
207
  done = True
@@ -211,60 +367,162 @@ def audioNormalize(waveform,sampleRate,stepSizeInSeconds = 2,dbThreshold = -50,d
211
  return copyWaveform
212
 
213
  class equalizeVolume(torch.nn.Module):
 
 
 
214
  def forward(self, waveform,sampleRate,stepSizeInSeconds,dbThreshold,dbTarget):
215
  print("In equalizeVolume")
216
  waveformDifference = audioNormalize(waveform,sampleRate,stepSizeInSeconds,dbThreshold,dbTarget)
217
  return waveformDifference
218
 
219
  def combineWaveforms(waveformList):
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  return torch.cat(waveformList,1)
221
 
222
  def annotationToSpeakerList(myAnnotation):
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  tempSpeakerList = []
224
  tempSpeakerNames = []
 
225
  for speakerName in myAnnotation.labels():
226
  speakerIndex = None
 
227
  if speakerName not in tempSpeakerNames:
 
228
  speakerIndex = len(tempSpeakerNames)
229
  tempSpeakerNames.append(speakerName)
230
  tempSpeakerList.append([])
231
  else:
 
232
  speakerIndex = tempSpeakerNames.index(speakerName)
233
 
 
234
  for segmentItem in myAnnotation.label_support(speakerName):
235
  tempSpeakerList[speakerIndex].append((segmentItem.start,segmentItem.duration))
236
  return tempSpeakerList
237
 
238
  def speakerListToDataFrame(speakerList):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  dataList = []
 
240
  for j, row in enumerate(speakerList):
 
241
  for k, speakingPoint in enumerate(row):
 
242
  h0 = int(speakingPoint[0]//3600)
243
  m0 = int(speakingPoint[0]%3600//60)
244
  s0 = int(speakingPoint[0]%60)
245
  ms0 = int(speakingPoint[0]*1000000%1000000)
246
  time0 = dt.time(h0,m0,s0,ms0)
 
247
  dtStart = dt.datetime.combine(dt.date.today(), time0)
 
248
  endPoint = speakingPoint[0] + speakingPoint[1]
249
  h1 = int(endPoint//3600)
250
  m1 = int(endPoint%3600//60)
251
  s1 = int(endPoint%60)
252
  ms1 = int(endPoint*1000000%1000000)
253
  time1 = dt.time(h1,m1,s1,ms1)
 
254
  dtEnd = dt.datetime.combine(dt.date.today(), time1)
 
255
  dataList.append(dict(Task=f"Speaker {j}.{k}", Start=dtStart, Finish=dtEnd, Resource=f"Speaker {j+1}"))
256
  df = pd.DataFrame(dataList)
257
  return df
258
 
259
  def removeOverlap(timeSegment,overlap):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  times = []
 
261
  if timeSegment.start < overlap.start:
 
 
 
 
 
262
  times.append(Segment(timeSegment.start,min(overlap.start,timeSegment.end)))
 
263
  if timeSegment.end > overlap.end:
 
 
 
 
 
264
  times.append(Segment(max(timeSegment.start,overlap.end),timeSegment.end))
265
  return times
266
 
267
  def checkForOverlap(time1, time2):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  overlap = time1 & time2
269
  if overlap:
270
  return overlap
@@ -272,92 +530,221 @@ def checkForOverlap(time1, time2):
272
  return None
273
 
274
  def sumSegments(segmentList):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  total = 0
276
  for s in segmentList:
277
  total += s.duration
278
  return total
279
 
280
  def sumTimes(myAnnotation):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  return myAnnotation.get_timeline(False).duration()
282
 
283
  def sumTimesPerSpeaker(myAnnotation):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  speakerList = []
285
  timeList = []
 
286
  for speaker in myAnnotation.labels():
 
287
  if speaker not in speakerList:
288
  speakerList.append(speaker)
289
  timeList.append(0)
 
290
  timeList[speakerList.index(speaker)] += sumTimes(myAnnotation.subset([speaker]))
291
  return speakerList, timeList
292
 
293
  def sumMultiTimesPerSpeaker(myAnnotation):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  speakerList = []
295
  timeList = []
 
296
  sList,tList = sumTimesPerSpeaker(myAnnotation)
 
297
  for i,speakerGroup in enumerate(sList):
 
298
  speakerSplit = speakerGroup.split('+')
 
299
  for speaker in speakerSplit:
 
300
  if speaker not in speakerList:
301
  speakerList.append(speaker)
302
  timeList.append(0)
 
303
  timeList[speakerList.index(speaker)] += tList[i]
304
  return speakerList, timeList
305
 
306
  def annotationToDataFrame(myAnnotation):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  dataList = []
308
  speakerDict = {}
 
309
  for currSpeaker in myAnnotation.labels():
 
310
  if currSpeaker not in speakerDict.keys():
311
  speakerDict[currSpeaker] = []
 
312
  for currSegment in myAnnotation.subset([currSpeaker]).itersegments():
313
  speakerDict[currSpeaker].append(currSegment)
314
 
315
  timeSummary = {}
 
316
  for key in speakerDict.keys():
 
317
  if key not in timeSummary.keys():
318
  timeSummary[key] = 0
 
319
  for speakingSegment in speakerDict[key]:
320
  timeSummary[key] += speakingSegment.duration
321
-
 
322
  for key in speakerDict.keys():
 
323
  for k, speakingSegment in enumerate(speakerDict[key]):
 
324
  speakerName = key
325
  startPoint = speakingSegment.start
326
  endPoint = speakingSegment.end
 
327
  h0 = int(startPoint//3600)
328
  m0 = int(startPoint%3600//60)
329
  s0 = int(startPoint%60)
330
  ms0 = int(startPoint*1000000%1000000)
331
  time0 = dt.time(h0,m0,s0,ms0)
 
332
  dtStart = dt.datetime.combine(dt.date.today(), time0)
 
333
  h1 = int(endPoint//3600)
334
  m1 = int(endPoint%3600//60)
335
  s1 = int(endPoint%60)
336
  ms1 = int(endPoint*1000000%1000000)
337
  time1 = dt.time(h1,m1,s1,ms1)
 
338
  dtEnd = dt.datetime.combine(dt.date.today(), time1)
339
  dataList.append(dict(Task=speakerName + f".{k}", Start=dtStart, Finish=dtEnd, Resource=speakerName))
340
  df = pd.DataFrame(dataList)
341
  return df, timeSummary
342
 
343
  def annotationToSimpleDataFrame(myAnnotation):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  dataList = []
345
  speakerDict = {}
 
346
  for currSpeaker in myAnnotation.labels():
 
347
  if currSpeaker not in speakerDict.keys():
348
  speakerDict[currSpeaker] = []
 
349
  for currSegment in myAnnotation.subset([currSpeaker]).itersegments():
350
  speakerDict[currSpeaker].append(currSegment)
351
 
352
  timeSummary = {}
 
353
  for key in speakerDict.keys():
 
354
  if key not in timeSummary.keys():
355
  timeSummary[key] = 0
 
356
  for speakingSegment in speakerDict[key]:
357
  timeSummary[key] += speakingSegment.duration
358
-
 
359
  for key in speakerDict.keys():
 
360
  for k, speakingSegment in enumerate(speakerDict[key]):
 
361
  speakerName = key
362
  startPoint = speakingSegment.start
363
  endPoint = speakingSegment.end
@@ -366,54 +753,115 @@ def annotationToSimpleDataFrame(myAnnotation):
366
  return df, timeSummary
367
 
368
  def calcCategories(myAnnotation,categories):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  categorySlots = []
370
  extraCategories = []
 
371
  for category in categories:
372
  categorySlots.append([])
 
373
  for speaker in myAnnotation.labels():
 
374
  targetCategory = None
375
  for i, category in enumerate(categories):
376
  if speaker in category:
377
  targetCategory = i
 
378
  if targetCategory is None:
379
  targetCategory = len(categorySlots)
380
  categorySlots.append([])
381
  extraCategories.append(speaker)
382
-
383
  for timeSegment in myAnnotation.subset([speaker]).itersegments():
384
  categorySlots[targetCategory].append((speaker,timeSegment))
385
- # Clean up categories
 
386
  cleanCategories = []
 
387
  for category in categorySlots:
388
  newCategory = []
 
389
  catSorted = copy.deepcopy(sorted(category,key=lambda cSegment: cSegment[1].start))
390
  currID, currSegment = None, None
 
391
  if len(catSorted) > 0:
392
  currID, currSegment = catSorted[0]
 
393
  for sp, segmentSlot in catSorted[1:]:
 
394
  overlapTime = checkForOverlap(currSegment,segmentSlot)
 
395
  if overlapTime is None:
396
  newCategory.append((currID,currSegment))
397
  currID = sp
398
  currTime = segmentSlot
 
399
  else:
 
400
  currID = currID + "+" + sp
401
  # Union of segments
402
  currTime[1] = currSegment | segmentSlot
 
403
  if currSegment is not None:
404
  newCategory.append((currID,currSegment))
405
  cleanCategories.append(newCategory)
406
  return cleanCategories,extraCategories
407
 
408
  def calcSpeakingTypes(pipeline,myAnnotation,maxTime):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  nvAnnotation = Annotation()
410
  ovAnnotation = Annotation()
411
  mvAnnotation = Annotation()
412
-
 
413
  categorySegmentList, timeSteps = pipeline.annotationToNoiseList(myAnnotation,maxTime)
414
  # [group,individual,silence], each as (start,duration)
415
  print("MultiVoice")
 
416
  for seg in categorySegmentList[0]:
 
417
  if 'group' in seg[0] or seg[0] is None:
418
  print(f'unclear : {seg[1]}')
419
  mvAnnotation[seg[1]] = 'unclear'
@@ -421,19 +869,34 @@ def calcSpeakingTypes(pipeline,myAnnotation,maxTime):
421
  print(f'{seg[0]} : {seg[1]}')
422
  mvAnnotation[seg[1]] = seg[0]
423
  print("OneVoice")
 
424
  for seg in categorySegmentList[1]:
425
  print(f'{seg[0]} : {seg[1]}')
426
  ovAnnotation[seg[1]] = seg[0]
427
  print("NoVoice")
 
428
  for seg in categorySegmentList[2]:
429
  print(f'{seg[0]} : {seg[1]}')
 
430
  nvAnnotation[seg[1]] = 'silence'
431
  return nvAnnotation, ovAnnotation, mvAnnotation
432
 
433
  def timeToString(timeInSeconds):
 
 
 
 
 
 
 
 
 
 
 
434
  if isinstance(timeInSeconds,list):
435
  return [timeToString(t) for t in timeInSeconds]
436
  else:
 
437
  h = int(timeInSeconds//3600)
438
  m = int(timeInSeconds%3600//60)
439
  s = timeInSeconds%60
 
10
  import datetime as dt
11
 
12
  def colors(n):
13
+ '''
14
+ Creates a list size n of distinctive colors
15
+
16
+ Creates an arbitrary amount of distinctive colors in RGB format, evenly divided among hues (see HSV).
17
+ In practice, this proves to be fairly resistant to colorblindness as well.
18
+
19
+ Parameters
20
+ ----------
21
+ n : int
22
+ Number of distinctive colors required
23
+
24
+ Returns
25
+ -------
26
+ ret : list
27
+ List of colors in (BGR) format
28
+ '''
29
+ if n == 0:
30
+ return []
31
+ ret = []
32
+ # Random starting place
33
+ h = int(random.random() * 180)
34
+ # Calculate step size based on number needed. OpenCV supports Hue from 0-179
35
+ step = 180 / n
36
+ # Iterate across hue dimension to generate colors
37
+ for i in range(n):
38
+ h += step
39
+ h = int(h) % 180
40
+ hsv = np.uint8([[[h,200,200]]])
41
+ bgr = cv2.cvtColor(hsv,cv2.COLOR_HSV2BGR)
42
+ ret.append((bgr[0][0][0].item()/255,bgr[0][0][1].item()/255,bgr[0][0][2].item()/255))
43
+ return ret
44
 
45
  def colorsCSS(n):
46
+ '''
47
+ Creates a list size n of distinctive colors
48
+
49
+ Creates an arbitrary amount of distinctive colors in CSS format, evenly divided among hues (see HSV).
50
+ In practice, this proves to be fairly resistant to colorblindness as well.
51
+
52
+ Parameters
53
+ ----------
54
+ n : int
55
+ Number of distinctive colors required
56
+
57
+ Returns
58
+ -------
59
+ ret : list
60
+ List of colors in CSS format
61
+ '''
62
+ if n == 0:
63
+ return []
64
+ ret = []
65
+ # Random starting place
66
+ h = int(random.random() * 180)
67
+ # Calculate step size based on number needed. OpenCV supports Hue from 0-179
68
+ step = 180 / n
69
+ # Iterate across hue dimension to generate colors
70
+ for i in range(n):
71
+ h += step
72
+ h = int(h) % 180
73
+ hsv = np.uint8([[[h,200,200]]])
74
+ bgr = cv2.cvtColor(hsv,cv2.COLOR_HSV2BGR)
75
+ b = f'{bgr[0][0][0].item():02x}'
76
+ g = f'{bgr[0][0][1].item():02x}'
77
+ r = f'{bgr[0][0][2].item():02x}'
78
+ ret.append('#'+b+g+r)
79
  return ret
80
 
81
  def extendSpeakers(mySpeakerList, fileLabel = 'NONE', maximumSecondDifference = 1, minimumSecondDuration = 0):
82
  '''
83
+ (DEPRECATED)
84
+ Extends speaker Segments for Instructor/Audience split data stored as a list
85
  '''
86
  mySpeakerAnnotations = Annotation(uri=fileLabel)
87
  newSpeakerList = [[],[]]
88
+ # Iterate through individual speakers
89
  for i, speaker in enumerate(mySpeakerList):
90
+ # Rearrange times in chronological order
91
  speaker.sort()
92
  lastEnd = -1
93
  tempSection = None
94
+ # Iterate through sections
95
  for section in speaker:
96
  if lastEnd == -1:
97
  tempSection = copy.deepcopy(section)
 
114
  return newSpeakerList,mySpeakerAnnotations
115
 
116
  def twoClassExtendAnnotation(myAnnotation,maximumSecondDifference = 1, minimumSecondDuration = 0):
117
+ '''
118
+ (DEPRECATED)
119
+ Extends speaker Segments for Instructor/Audience split data stored as an Annotation
120
+ '''
121
  lecturerID = None
122
  lecturerLen = 0
123
 
 
143
  return newList, newAnnotation
144
 
145
  def loadAudioRTTM(sampleRTTM):
146
+ '''
147
+ Loads RTTM file in as list of (speaker times) and as Annotation
148
+
149
+ ...
150
+
151
+ Parameters
152
+ ----------
153
+ sampleRTTM : str
154
+ Full path to RTTM file to read
155
+
156
+ Returns
157
+ -------
158
+ speakerList : list
159
+ List of speakers as (List of times). Outer list represents speakers, inner list contains (start time, duration) speech segments
160
+ prediction : pyannote.core.Annotation
161
+ Annotation object containing RTTM data
162
+ '''
163
  # Read in prediction data
164
  # Data in list form, for convenient plotting
165
  speakerList = []
166
  # Data in Annotation form, for convenient error rate calculation
167
  prediction = Annotation(uri=sampleRTTM)
168
  with open(sampleRTTM, "r") as rttm:
169
+ # Process line by line
170
  for line in rttm:
171
+ # Delimited by ' '
172
  speakerResult = line.split(' ')
173
+ # Assume speaker is identified as number
174
  index = int(speakerResult[7][-2:])
175
+ # Collect speech time start and end
176
  start = float(speakerResult[3])
177
  end = start + float(speakerResult[4])
178
+ # Extend speakerList until a sublist exists for given speaker
179
  while len(speakerList) < index + 1:
180
  speakerList.append([])
181
+ # Add to speaker list and Annotation objects
182
  speakerList[index].append((float(speakerResult[3]),float(speakerResult[4])))
183
  prediction[Segment(start,end)] = speakerResult[7]
184
 
185
  return speakerList, prediction
186
 
187
  def loadAudioTXT(sampleTXT):
188
+ '''
189
+ Loads specially formatted TXT file in as list of (speaker times) and as Annotation
190
+
191
+ File to be read should be formatted with rows as:
192
+ (start time in seconds)\t(end time in seconds)\t(speaker ID)
193
+
194
+ Parameters
195
+ ----------
196
+ sampleTXT : str
197
+ Full path to specially formatted TXT file to read
198
+
199
+ Returns
200
+ -------
201
+ [] : list (DEPRECATED)
202
+ Empty list placeholder
203
+ prediction : pyannote.core.Annotation
204
+ Annotation object containing RTTM data
205
+ '''
206
  prediction = Annotation(uri=sampleTXT)
207
  with open(sampleTXT, "r") as txt:
208
+ # Iterate through rows
209
  for line in txt:
210
+ # Delimited with tabs '\t'
211
  speakerResult = line.split('\t')
212
+ # For debugging
213
  print(speakerResult)
214
+ # Expect 3 columns
215
  if len(speakerResult) < 3:
216
  continue
 
217
  start = float(speakerResult[0])
218
  end = float(speakerResult[1])
 
219
  prediction[Segment(start,end)] = speakerResult[2]
220
 
221
  return [], prediction
222
 
223
  def loadAudioCSV(sampleCSV):
224
+ '''
225
+ Loads specially formatted CSV file in as list of (speaker times) and as Annotation
226
+
227
+ File to be read should be formatted with first row containing:
228
+ Start,Finish,Resource
229
+ These headers represent start time, end time, and speaker ID.
230
+
231
+ Parameters
232
+ ----------
233
+ sampleCSV : str
234
+ Full path to specially formatted CSV file to read
235
+
236
+ Returns
237
+ -------
238
+ [] : list (DEPRECATED)
239
+ Empty list placeholder
240
+ prediction : pyannote.core.Annotation
241
+ Annotation object containing RTTM data
242
+ '''
243
+ # Read in prediction data using dataframes
244
  df = pd.read_csv(sampleCSV)
245
 
246
  df = df.reset_index() # make sure indexes pair with number of rows
 
257
  return [], prediction
258
 
259
  def splitIntoTimeSegments(testFile,maxDurationInSeconds=60):
260
+ '''
261
+ Read audio file and split into specified chunks of time
262
+
263
+ Reads in audio file and batches audio waveform based on time provided. Useful if the entire audio cannot be loaded simultaneously, as it can be processed in batches.
264
+
265
+ Parameters
266
+ ----------
267
+ testFile : str
268
+ Full path to audio file
269
+ maxDurationInSeconds : float or int
270
+ The max length of time for each chunk. Keep in mind that the final chunk will usually be smaller
271
+
272
+ Returns
273
+ -------
274
+ audioSegments : list
275
+ List of waveform values, chunked to time specified
276
+ sample_rate : int
277
+ Sample rate of the audio file
278
+ '''
279
+ # Read in data
280
  data, sample_rate = sf.read(testFile, dtype="float32", always_2d=True)
281
+ # Extract waveform data
282
  waveform = torch.from_numpy(data.T) # shape: [channels, samples]
 
283
 
284
+ audioSegments = []
285
  outOfBoundsIndex = waveform.shape[-1]
286
  currentStart = 0
287
+ # Determine the end of the current chunk being processed
288
  currentEnd = min(maxDurationInSeconds * sample_rate,outOfBoundsIndex)
289
  done = False
290
  while(not done):
291
+ # Chunk waveform and store
292
  waveformSegment = waveform[:,currentStart:currentEnd]
293
  audioSegments.append(waveformSegment)
294
+ # Check for end of audio
295
  if currentEnd >= outOfBoundsIndex:
296
  done = True
297
  break
298
  else:
299
+ # Move to next chunk
300
  currentStart = currentEnd
301
  currentEnd = min(currentStart + maxDurationInSeconds * sample_rate,outOfBoundsIndex)
302
  return audioSegments, sample_rate
303
 
304
  def audioNormalize(waveform,sampleRate,stepSizeInSeconds = 2,dbThreshold = -50,dbTarget = -5):
305
+ '''
306
+ Normalize audio loudness based on decibels
307
+
308
+ ...
309
+
310
+ Parameters
311
+ ----------
312
+ waveform : np.array
313
+ Audio waveform
314
+ sampleRate : int
315
+ Sample rate of source audio file
316
+ stepSizeInSeconds : float or int
317
+ Window to apply normalization to
318
+ dbThreshold : int
319
+ Minimum decibel level to consider below 80
320
+ dbTarget : int
321
+ Maximum decibel level for normalization below 80
322
+
323
+ Returns
324
+ -------
325
+ copyWaveform : np.array
326
+ Normalized audio waveform
327
+ '''
328
  print("In audioNormalize")
329
+ # Create copy of waveform and detach from CPU if necessary
330
  copyWaveform = waveform.clone().detach()
331
  print("Waveform copy made")
332
+ # Create transformation from waveform amplitude to decibel
333
  transform = torchaudio.transforms.AmplitudeToDB(stype="amplitude", top_db=80)
334
+ # Prepare start and end of each normalization chunk
335
  currStart = 0
336
  currEnd = int(min(currStart + stepSizeInSeconds * sampleRate, len(copyWaveform[0])-1))
337
  done = False
338
  while(not done):
339
+ # Create decibel representation of target chunk
340
  copyWaveform_db = waveform[:,currStart:currEnd].clone().detach()
341
  copyWaveform_db = transform(copyWaveform_db)
342
  if currStart == 0:
343
  print("First DB level calculated")
344
 
345
+ # Check first channel to see if above threshold for loudness enhancement
346
  if torch.max(copyWaveform_db[0]).item() > dbThreshold:
347
+ # Determine how much gain is required
348
  gain = torch.min(dbTarget - copyWaveform_db[0])
349
  adjustGain = torchaudio.transforms.Vol(gain,'db')
350
+ # Apply gain increase
351
  copyWaveform[0][currStart:currEnd] = adjustGain(copyWaveform[0][currStart:currEnd])
352
+ # Check second channel (when applicable) to see if above threshold for loudness enhancement
353
  if len(copyWaveform_db) > 1:
354
  if torch.max(copyWaveform_db[1]).item() > dbThreshold:
355
+ # Determine how much gain is required
356
  gain = torch.min(dbTarget - copyWaveform_db[1])
357
  adjustGain = torchaudio.transforms.Vol(gain,'db')
358
+ # Apply gain increase
359
  copyWaveform[1][currStart:currEnd] = adjustGain(copyWaveform[1][currStart:currEnd])
360
+ # Move to next chunk to process
361
  currStart += int(stepSizeInSeconds * sampleRate)
362
  if currStart > currEnd:
363
  done = True
 
367
  return copyWaveform
368
 
369
  class equalizeVolume(torch.nn.Module):
370
+ '''
371
+ Torch Module wrapper for equalization
372
+ '''
373
  def forward(self, waveform,sampleRate,stepSizeInSeconds,dbThreshold,dbTarget):
374
  print("In equalizeVolume")
375
  waveformDifference = audioNormalize(waveform,sampleRate,stepSizeInSeconds,dbThreshold,dbTarget)
376
  return waveformDifference
377
 
378
  def combineWaveforms(waveformList):
379
+ '''
380
+ Combines waveform that has been split into batches (see splitIntoTimeSegments())
381
+
382
+ Parameters
383
+ ----------
384
+ waveformList : list
385
+ List of waveform segments to merge
386
+
387
+ Returns
388
+ -------
389
+ : np.array
390
+ Concatenated waveform
391
+ '''
392
  return torch.cat(waveformList,1)
393
 
394
  def annotationToSpeakerList(myAnnotation):
395
+ '''
396
+ Converts pyannote.core.Annotation object into List of speakers with times for easy processing of matplotlib charts.
397
+
398
+ Parameters
399
+ ----------
400
+ myAnnotation : pyannote.core.Annotation
401
+ Diarization object
402
+
403
+ Returns
404
+ -------
405
+ tempSpeakerList : list
406
+ List of speakers with (list of (start time, duration)). Outer list represents speakers, inner list contains time start and end.
407
+ '''
408
  tempSpeakerList = []
409
  tempSpeakerNames = []
410
+ # Iterate through all speakers
411
  for speakerName in myAnnotation.labels():
412
  speakerIndex = None
413
+ # If never before seen speaker, add to both lists
414
  if speakerName not in tempSpeakerNames:
415
+ # Speaker ID is new index
416
  speakerIndex = len(tempSpeakerNames)
417
  tempSpeakerNames.append(speakerName)
418
  tempSpeakerList.append([])
419
  else:
420
+ # Lookup speaker ID based on name
421
  speakerIndex = tempSpeakerNames.index(speakerName)
422
 
423
+ # Iterate through Segments and add to speaker list
424
  for segmentItem in myAnnotation.label_support(speakerName):
425
  tempSpeakerList[speakerIndex].append((segmentItem.start,segmentItem.duration))
426
  return tempSpeakerList
427
 
428
  def speakerListToDataFrame(speakerList):
429
+ '''
430
+ Convert speaker list to pandas.DataFrame object
431
+
432
+ ...
433
+
434
+ Parameters
435
+ ----------
436
+ speakerList : list
437
+ List of speakers with (list of (start time, duration)). Outer list represents speakers, inner list contains time start and end.
438
+
439
+ Returns
440
+ -------
441
+ df : pandas.DataFrame
442
+ DataFrame representation of input
443
+ '''
444
  dataList = []
445
+ # Iterate through speakers
446
  for j, row in enumerate(speakerList):
447
+ # Iterate through times
448
  for k, speakingPoint in enumerate(row):
449
+ # Convert start time into HH:MM:SS:MS format
450
  h0 = int(speakingPoint[0]//3600)
451
  m0 = int(speakingPoint[0]%3600//60)
452
  s0 = int(speakingPoint[0]%60)
453
  ms0 = int(speakingPoint[0]*1000000%1000000)
454
  time0 = dt.time(h0,m0,s0,ms0)
455
+ # Set day as today, because plotly needs full datetime
456
  dtStart = dt.datetime.combine(dt.date.today(), time0)
457
+ # Convert end time into HH:MM:SS:MS format
458
  endPoint = speakingPoint[0] + speakingPoint[1]
459
  h1 = int(endPoint//3600)
460
  m1 = int(endPoint%3600//60)
461
  s1 = int(endPoint%60)
462
  ms1 = int(endPoint*1000000%1000000)
463
  time1 = dt.time(h1,m1,s1,ms1)
464
+ # Set day as today, because plotly needs full datetime
465
  dtEnd = dt.datetime.combine(dt.date.today(), time1)
466
+ # Add to formatted list for DataFrame
467
  dataList.append(dict(Task=f"Speaker {j}.{k}", Start=dtStart, Finish=dtEnd, Resource=f"Speaker {j+1}"))
468
  df = pd.DataFrame(dataList)
469
  return df
470
 
471
  def removeOverlap(timeSegment,overlap):
472
+ '''
473
+ Removes overlap (if any) from two segments of time
474
+
475
+ ...
476
+
477
+ Parameters
478
+ ----------
479
+ timeSegment : pyannote.core.Segment
480
+ Segment to remove overlap from
481
+ overlap : pyannote.core.Segment
482
+ Segment to apply as overlap mask
483
+
484
+ Returns
485
+ -------
486
+ times : list
487
+ List of up to two Segments
488
+ '''
489
  times = []
490
+ # If first Segment begins before overlap
491
  if timeSegment.start < overlap.start:
492
+ # Create new Segment which starts at first Segment but ends based on overlap
493
+ # Visual
494
+ # First ----------------
495
+ # Overlap -------
496
+ # Result -----
497
  times.append(Segment(timeSegment.start,min(overlap.start,timeSegment.end)))
498
+ # If first Segment ends after overlap
499
  if timeSegment.end > overlap.end:
500
+ # Create new Segment which starts based on overlap but ends when first Segment ends
501
+ # Visual
502
+ # First ----------------
503
+ # Overlap -------
504
+ # Result ----
505
  times.append(Segment(max(timeSegment.start,overlap.end),timeSegment.end))
506
  return times
507
 
508
  def checkForOverlap(time1, time2):
509
+ '''
510
+ Checks for overlap of two pyannote.core.Segments
511
+
512
+ ...
513
+
514
+ Parameters
515
+ ----------
516
+ time1 : pyannote.core.Segment
517
+ First Segment to check
518
+ time2 : pyannote.core.Segment
519
+ Second Segment to check
520
+
521
+ Returns
522
+ -------
523
+ overlap : Segment
524
+ Overlapping Segment, or None if none exists
525
+ '''
526
  overlap = time1 & time2
527
  if overlap:
528
  return overlap
 
530
  return None
531
 
532
  def sumSegments(segmentList):
533
+ '''
534
+ Adds up all durations of provided Segments in list
535
+
536
+ ...
537
+
538
+ Parameters
539
+ ----------
540
+ segmentList : list
541
+ List of pyannote.core.Segment
542
+
543
+ Returns
544
+ -------
545
+ total : float or int
546
+ Total duration of all Segments
547
+ '''
548
  total = 0
549
  for s in segmentList:
550
  total += s.duration
551
  return total
552
 
553
  def sumTimes(myAnnotation):
554
+ '''
555
+ Calculates duration of pyannote.core.Annotation
556
+
557
+ ...
558
+
559
+ Parameters
560
+ ----------
561
+ myAnnotation : pyannote.core.Annotation
562
+ Target Annotation
563
+
564
+ Returns
565
+ -------
566
+ : float
567
+ Duration in seconds of Annotation
568
+ '''
569
  return myAnnotation.get_timeline(False).duration()
570
 
571
  def sumTimesPerSpeaker(myAnnotation):
572
+ '''
573
+ Calculates duration of each speaker in pyannote.core.Annotation
574
+
575
+ ...
576
+
577
+ Parameters
578
+ ----------
579
+ myAnnotation : pyannote.core.Annotation
580
+ Target Annotation
581
+
582
+ Returns
583
+ -------
584
+ speakerList : list
585
+ List of speakers
586
+ timeList : list
587
+ List of times matching speakerList
588
+ '''
589
  speakerList = []
590
  timeList = []
591
+ # Iterate through speakers
592
  for speaker in myAnnotation.labels():
593
+ # If new speaker, then add to list
594
  if speaker not in speakerList:
595
  speakerList.append(speaker)
596
  timeList.append(0)
597
+ # Get duration of speaker
598
  timeList[speakerList.index(speaker)] += sumTimes(myAnnotation.subset([speaker]))
599
  return speakerList, timeList
600
 
601
  def sumMultiTimesPerSpeaker(myAnnotation):
602
+ '''
603
+ Calculates duration of each speaker in pyannote.core.Annotation, including multi-speaker labels
604
+
605
+ Multi-speaker labels can be identified as a str delimited with '+' for each speaker
606
+
607
+ Parameters
608
+ ----------
609
+ myAnnotation : pyannote.core.Annotation
610
+ Target Annotation
611
+
612
+ Returns
613
+ -------
614
+ speakerList : list
615
+ List of speakers
616
+ timeList : list
617
+ List of times matching speakerList
618
+ '''
619
  speakerList = []
620
  timeList = []
621
+ # Get top-level view of durations for speakers
622
  sList,tList = sumTimesPerSpeaker(myAnnotation)
623
+ # Iterate through speakers
624
  for i,speakerGroup in enumerate(sList):
625
+ # Split multi-group speakers, normal speakers are treated as list of 1
626
  speakerSplit = speakerGroup.split('+')
627
+ # For each speaker with associated duration
628
  for speaker in speakerSplit:
629
+ # If a new speaker, then add to list
630
  if speaker not in speakerList:
631
  speakerList.append(speaker)
632
  timeList.append(0)
633
+ # Add individual speaker duration (not group)
634
  timeList[speakerList.index(speaker)] += tList[i]
635
  return speakerList, timeList
636
 
637
  def annotationToDataFrame(myAnnotation):
638
+ '''
639
+ Convert pyannote.core.Annotation to specially formatted pandas.DataFrame object
640
+
641
+ ...
642
+
643
+ Parameters
644
+ ----------
645
+ myAnnotation : pyannote.core.Annotation
646
+ Diarization representation
647
+
648
+ Returns
649
+ -------
650
+ df : pandas.DataFrame
651
+ DataFrame representation of input
652
+ timeSummary : dict
653
+ Maps speakers to duration spoken
654
+ '''
655
  dataList = []
656
  speakerDict = {}
657
+ # Iterate through speakers
658
  for currSpeaker in myAnnotation.labels():
659
+ # If new speaker, then create entry
660
  if currSpeaker not in speakerDict.keys():
661
  speakerDict[currSpeaker] = []
662
+ # Collect individual segments for speaker
663
  for currSegment in myAnnotation.subset([currSpeaker]).itersegments():
664
  speakerDict[currSpeaker].append(currSegment)
665
 
666
  timeSummary = {}
667
+ # Iterate through speakers
668
  for key in speakerDict.keys():
669
+ # If new speaker (for time calculations), then create entry
670
  if key not in timeSummary.keys():
671
  timeSummary[key] = 0
672
+ # Add duration of all segments for speaker
673
  for speakingSegment in speakerDict[key]:
674
  timeSummary[key] += speakingSegment.duration
675
+
676
+ # Iterate through speakers
677
  for key in speakerDict.keys():
678
+ # Iterate through segments
679
  for k, speakingSegment in enumerate(speakerDict[key]):
680
+ # Create specially formatted DataFrame entry
681
  speakerName = key
682
  startPoint = speakingSegment.start
683
  endPoint = speakingSegment.end
684
+ # Convert to HH:MM:SS:MS format
685
  h0 = int(startPoint//3600)
686
  m0 = int(startPoint%3600//60)
687
  s0 = int(startPoint%60)
688
  ms0 = int(startPoint*1000000%1000000)
689
  time0 = dt.time(h0,m0,s0,ms0)
690
+ # Set day as today, because plotly needs full datetime
691
  dtStart = dt.datetime.combine(dt.date.today(), time0)
692
+ # Convert to HH:MM:SS:MS format
693
  h1 = int(endPoint//3600)
694
  m1 = int(endPoint%3600//60)
695
  s1 = int(endPoint%60)
696
  ms1 = int(endPoint*1000000%1000000)
697
  time1 = dt.time(h1,m1,s1,ms1)
698
+ # Set day as today, because plotly needs full datetime
699
  dtEnd = dt.datetime.combine(dt.date.today(), time1)
700
  dataList.append(dict(Task=speakerName + f".{k}", Start=dtStart, Finish=dtEnd, Resource=speakerName))
701
  df = pd.DataFrame(dataList)
702
  return df, timeSummary
703
 
704
  def annotationToSimpleDataFrame(myAnnotation):
705
+ '''
706
+ Convert pyannote.core.Annotation directly to pandas.DataFrame object
707
+
708
+ ...
709
+
710
+ Parameters
711
+ ----------
712
+ myAnnotation : pyannote.core.Annotation
713
+ Diarization representation
714
+
715
+ Returns
716
+ -------
717
+ df : pandas.DataFrame
718
+ DataFrame representation of input
719
+ timeSummary : dict
720
+ Maps speakers to duration spoken
721
+ '''
722
  dataList = []
723
  speakerDict = {}
724
+ # Iterate through speakers
725
  for currSpeaker in myAnnotation.labels():
726
+ # If new speaker, then add entry
727
  if currSpeaker not in speakerDict.keys():
728
  speakerDict[currSpeaker] = []
729
+ # Collect Segments for speaker
730
  for currSegment in myAnnotation.subset([currSpeaker]).itersegments():
731
  speakerDict[currSpeaker].append(currSegment)
732
 
733
  timeSummary = {}
734
+ # Iterate through speakers
735
  for key in speakerDict.keys():
736
+ # If new speaker, then add entry
737
  if key not in timeSummary.keys():
738
  timeSummary[key] = 0
739
+ # Calculate duration by summing all durations of Segments
740
  for speakingSegment in speakerDict[key]:
741
  timeSummary[key] += speakingSegment.duration
742
+
743
+ # Iterate through speakers
744
  for key in speakerDict.keys():
745
+ # Iterate through Segments
746
  for k, speakingSegment in enumerate(speakerDict[key]):
747
+ # Create simplified DataFrame entry
748
  speakerName = key
749
  startPoint = speakingSegment.start
750
  endPoint = speakingSegment.end
 
753
  return df, timeSummary
754
 
755
  def calcCategories(myAnnotation,categories):
756
+ '''
757
+ Combines speakers based on categories
758
+
759
+ ...
760
+
761
+ Parameters
762
+ ----------
763
+ myAnnotation : pyannote.core.Annotation
764
+ Target Annotation
765
+ categories : list
766
+ List of known categories, which contain a list of speakers. List(List(speaker))
767
+
768
+ Returns
769
+ -------
770
+ cleanCategories : List
771
+ List of all categories, which contains a list of (speaker,pyannote.core.Segment) pairs. List(List(speaker,Segment)).
772
+ Outer list length = categories + len(extraCategories)
773
+ extraCategories : List
774
+ List of speakers which fit in no category
775
+ '''
776
  categorySlots = []
777
  extraCategories = []
778
+ # Initialize categories
779
  for category in categories:
780
  categorySlots.append([])
781
+ # Iterate through speakers
782
  for speaker in myAnnotation.labels():
783
+ # Identify which category speaker belongs to
784
  targetCategory = None
785
  for i, category in enumerate(categories):
786
  if speaker in category:
787
  targetCategory = i
788
+ # If no category found, then add as "extra category"
789
  if targetCategory is None:
790
  targetCategory = len(categorySlots)
791
  categorySlots.append([])
792
  extraCategories.append(speaker)
793
+ # Add (speaker,Segment) pair to associated category
794
  for timeSegment in myAnnotation.subset([speaker]).itersegments():
795
  categorySlots[targetCategory].append((speaker,timeSegment))
796
+
797
+ # Clean up categories by merging Segments as necessary
798
  cleanCategories = []
799
+ # Iterate through categories + extra categories
800
  for category in categorySlots:
801
  newCategory = []
802
+ # Copy and sort current category based on start time of Segments
803
  catSorted = copy.deepcopy(sorted(category,key=lambda cSegment: cSegment[1].start))
804
  currID, currSegment = None, None
805
+ # If any Segments exist, start at the beginning
806
  if len(catSorted) > 0:
807
  currID, currSegment = catSorted[0]
808
+ # Iterate through remaining Segments
809
  for sp, segmentSlot in catSorted[1:]:
810
+ # Find overlaps
811
  overlapTime = checkForOverlap(currSegment,segmentSlot)
812
+ # If no overlap with previous Segment, add as normal
813
  if overlapTime is None:
814
  newCategory.append((currID,currSegment))
815
  currID = sp
816
  currTime = segmentSlot
817
+ # If overlapping previous Segment, then combine into one Segment
818
  else:
819
+ # Combine names
820
  currID = currID + "+" + sp
821
  # Union of segments
822
  currTime[1] = currSegment | segmentSlot
823
+ # If any Segments existed, then add "clean" category
824
  if currSegment is not None:
825
  newCategory.append((currID,currSegment))
826
  cleanCategories.append(newCategory)
827
  return cleanCategories,extraCategories
828
 
829
  def calcSpeakingTypes(pipeline,myAnnotation,maxTime):
830
+ '''
831
+ Calculates no voice, one voice, and multi voice for a given Annotation
832
+
833
+ ...
834
+
835
+ Parameters
836
+ ----------
837
+ pipeline : sonogram.Sonogram
838
+ Model object to use for analysis call
839
+ myAnnotation : pyannote.core.Annotation
840
+ Target Annotation
841
+ maxTime : float
842
+ The duration of the audio file. Note that Annotation does NOT strictly provide this.
843
+
844
+ Returns
845
+ -------
846
+ nvAnnotation : pyannote.core.Annotation
847
+ Annotation containing only 'no voice' labels
848
+ ovAnnotation : pyannote.core.Annotation
849
+ Annotation containing only 'one voice' labels
850
+ mvAnnotation : pyannote.core.Annotation
851
+ Annotation containing only 'multi voice' labels
852
+ '''
853
+ # Create 3 new Annotations to hold no voice, one voice, and multi voice
854
  nvAnnotation = Annotation()
855
  ovAnnotation = Annotation()
856
  mvAnnotation = Annotation()
857
+
858
+ # Generate categories
859
  categorySegmentList, timeSteps = pipeline.annotationToNoiseList(myAnnotation,maxTime)
860
  # [group,individual,silence], each as (start,duration)
861
  print("MultiVoice")
862
+ # Iterate through (speaker,Segment) pairs for multi voice
863
  for seg in categorySegmentList[0]:
864
+ # Rename 'group' to 'unclear' since group is implied already
865
  if 'group' in seg[0] or seg[0] is None:
866
  print(f'unclear : {seg[1]}')
867
  mvAnnotation[seg[1]] = 'unclear'
 
869
  print(f'{seg[0]} : {seg[1]}')
870
  mvAnnotation[seg[1]] = seg[0]
871
  print("OneVoice")
872
+ # Iterate through (speaker,Segment) pairs for one voice
873
  for seg in categorySegmentList[1]:
874
  print(f'{seg[0]} : {seg[1]}')
875
  ovAnnotation[seg[1]] = seg[0]
876
  print("NoVoice")
877
+ # Iterate through (speaker,Segment) pairs for no voice
878
  for seg in categorySegmentList[2]:
879
  print(f'{seg[0]} : {seg[1]}')
880
+ # Name speaker as 'silence' instead of None
881
  nvAnnotation[seg[1]] = 'silence'
882
  return nvAnnotation, ovAnnotation, mvAnnotation
883
 
884
  def timeToString(timeInSeconds):
885
+ '''
886
+ Convert time(s) into HH:MM:SS.MS format
887
+
888
+ ...
889
+
890
+ Parameters
891
+ ----------
892
+ timeInSeconds : float or int or list
893
+ Time to convert (in seconds). May contain a list of times to convert recursively
894
+ '''
895
+ # If list, then format time for each entry
896
  if isinstance(timeInSeconds,list):
897
  return [timeToString(t) for t in timeInSeconds]
898
  else:
899
+ # Format time
900
  h = int(timeInSeconds//3600)
901
  m = int(timeInSeconds%3600//60)
902
  s = timeInSeconds%60