duongthienz commited on
Commit
551882a
·
verified ·
1 Parent(s): 7976817

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +158 -241
app.py CHANGED
@@ -39,19 +39,17 @@ def printV(message,verbosityLevel):
39
  if verbosity>=verbosityLevel:
40
  print(message)
41
 
42
- def get_display_name(speaker, fileIndex):
43
  """Return the user-assigned display name for a speaker, or the original label."""
44
  renames = st.session_state.speakerRenames
45
- if fileIndex < len(renames) and speaker in renames[fileIndex]:
46
- return renames[fileIndex][speaker]
47
- return speaker
48
 
49
- def apply_speaker_renames_to_df(df, fileIndex, column="task"):
50
  """Replace speaker_## labels in a DataFrame column with display names."""
51
  if column not in df.columns:
52
  return df
53
  df = df.copy()
54
- df[column] = df[column].apply(lambda s: get_display_name(s, fileIndex))
55
  return df
56
 
57
  @st.cache_data
@@ -127,10 +125,7 @@ def generate_speaker_clips(annotations, waveform, sample_rate, file_index):
127
  """
128
  import io
129
 
130
- # Extend speakerClips list to cover this file index
131
- while len(st.session_state.speakerClips) <= file_index:
132
- st.session_state.speakerClips.append({})
133
-
134
  clips = {}
135
  for speaker in annotations.labels():
136
  speaker_segments = [
@@ -159,7 +154,7 @@ def generate_speaker_clips(annotations, waveform, sample_rate, file_index):
159
  clips[speaker] = buffer.read()
160
 
161
  st.session_state.speakerClips[file_index] = clips
162
- print(f"Generated {len(clips)} speaker clips for file index {file_index}")
163
 
164
  def addCategory():
165
  newCategory = st.session_state.categoryInput
@@ -167,8 +162,8 @@ def addCategory():
167
  st.session_state[f'multiselect_{newCategory}'] = []
168
  st.session_state.categories.append(newCategory)
169
  st.session_state.categoryInput = ''
170
- for resultGroup in st.session_state.categorySelect:
171
- resultGroup.append([])
172
 
173
  def removeCategory(index):
174
  categoryName = st.session_state.categories[index]
@@ -176,55 +171,43 @@ def removeCategory(index):
176
  del st.session_state[f'multiselect_{categoryName}']
177
  del st.session_state[f'remove_{categoryName}']
178
  del st.session_state.categories[index]
179
- for resultGroup in st.session_state.categorySelect:
180
- del resultGroup[index]
181
 
182
- def updateCategoryOptions(resultIndex):
183
  if st.session_state.resetResult:
184
- #st.info(f"Skipping update of {resultIndex}")
185
  return
186
- #st.info(f"Updating result {resultIndex}")
187
- #st.info(f"In update: {st.session_state.categorySelect}")
188
- # Handle
189
- _, currAnnotation, _ = st.session_state.results[currFileIndex]
190
  speakerNames = currAnnotation.labels()
191
-
192
- # Handle speaker category sidebars
193
  unusedSpeakers = copy.deepcopy(speakerNames)
194
- # Remove used speakers
195
  for i, category in enumerate(st.session_state['categories']):
196
  category_choices = copy.deepcopy(st.session_state[f'multiselect_{category}'])
197
- st.session_state["categorySelect"][resultIndex][i] = category_choices
198
  for sp in category_choices:
199
  try:
200
  unusedSpeakers.remove(sp)
201
  except:
202
  continue
203
- st.session_state.unusedSpeakers[resultIndex] = unusedSpeakers
204
- #st.info(f"After update: {st.session_state.categorySelect}")
205
 
206
  def updateMultiSelect():
207
- currFileIndex = st.session_state.file_names.index(st.session_state["select_currFile"])
208
  st.session_state.resetResult = True
209
  for i, category in enumerate(st.session_state['categories']):
210
- st.session_state[f'multiselect_{category}'] = st.session_state['categorySelect'][currFileIndex][i]
211
 
212
  def analyze(inFileName):
213
  try:
214
  print(f"Start analyzing {inFileName}")
215
  st.session_state.resetResult = False
216
- currFileIndex = file_names.index(inFileName)
217
- print(f"Found at index {currFileIndex}")
218
- if len(st.session_state.results) > currFileIndex and len(st.session_state.summaries) > currFileIndex and len(st.session_state.results[currFileIndex]) > 0:
219
 
220
  printV(f'In if',4)
221
- # Handle
222
- currAnnotation, currTotalTime = st.session_state.results[currFileIndex]
223
  speakerNames = currAnnotation.labels()
224
  printV(f'Loaded results',4)
225
- # Update other categories
226
- unusedSpeakers = st.session_state.unusedSpeakers[currFileIndex]
227
- categorySelections = st.session_state["categorySelect"][currFileIndex]
228
  printV(f'Loaded speaker selections',4)
229
  noVoice, oneVoice, multiVoice = su.calcSpeakingTypes(currAnnotation,currTotalTime)
230
  sumNoVoice = su.sumTimes(noVoice)
@@ -241,7 +224,7 @@ def analyze(inFileName):
241
  }
242
  )
243
  df3.name = "df3"
244
- st.session_state.summaries[currFileIndex]["df3"] = df3
245
  printV(f'Set df3',4)
246
 
247
  # --- Build df4 ---
@@ -275,7 +258,7 @@ def analyze(inFileName):
275
  }
276
  df4 = pd.DataFrame(data=df4_dict)
277
  df4.name = "df4"
278
- st.session_state.summaries[currFileIndex]["df4"] = df4
279
  printV(f'Set df4', 4)
280
 
281
  # --- Build df5 ---
@@ -331,20 +314,20 @@ def analyze(inFileName):
331
  + [(t * 100) / summativeMultiSpeaker for t in multiTimeList],
332
  })
333
  df5.name = "df5"
334
- st.session_state.summaries[currFileIndex]["df5"] = df5
335
  printV(f'Set df5', 4)
336
 
337
  # --- Build speakers_dataFrame, df2 ---
338
  speakers_dataFrame, speakers_times = su.annotationToDataFrame(currAnnotation)
339
- st.session_state.summaries[currFileIndex]["speakers_dataFrame"] = speakers_dataFrame
340
- st.session_state.summaries[currFileIndex]["speakers_times"] = speakers_times
341
 
342
  df2_dict = {
343
  "values": [100 * t / currTotalTime for t in df4_dict["values"]],
344
  "names": df4_dict["names"],
345
  }
346
  df2 = pd.DataFrame(df2_dict)
347
- st.session_state.summaries[currFileIndex]["df2"] = df2
348
  printV(f'Set df2', 4)
349
  except Exception as e:
350
  import traceback
@@ -393,34 +376,32 @@ pipeline = Pipeline.from_pretrained("pyannote/speaker-diarization-3.1")
393
  pipeline.to(device)#torch.device("cuda"))
394
 
395
  # Store results for viewing and further processing
396
- # Long-range usage
397
  if 'results' not in st.session_state:
398
- st.session_state.results = []
399
  if 'speakerRenames' not in st.session_state:
400
- st.session_state.speakerRenames = []
401
  if 'summaries' not in st.session_state:
402
- st.session_state.summaries = []
403
  if 'categories' not in st.session_state:
404
  st.session_state.categories = []
405
- st.session_state.categorySelect = []
406
- # Single Use
407
  if 'removeCategory' not in st.session_state:
408
  st.session_state.removeCategory = None
409
  if 'resetResult' not in st.session_state:
410
  st.session_state.resetResult = False
411
- # Specific to target file
412
  if 'unusedSpeakers' not in st.session_state:
413
- st.session_state.unusedSpeakers = []
414
  if 'file_names' not in st.session_state:
415
  st.session_state.file_names = []
416
  if 'valid_files' not in st.session_state:
417
  st.session_state.valid_files = []
418
  if 'file_paths' not in st.session_state:
419
- st.session_state.file_paths = []
420
  if 'showSummary' not in st.session_state:
421
  st.session_state.showSummary = 'No'
422
  if 'speakerClips' not in st.session_state:
423
- st.session_state.speakerClips = [] # List of dicts: {speaker_label: wav_bytes} per file
424
 
425
 
426
 
@@ -447,127 +428,97 @@ temp_dir = tempfile.mkdtemp()
447
 
448
  if uploaded_file_paths is not None and len(uploaded_file_paths) > 0:
449
  print("Found file paths")
450
- valid_files = []
451
- file_paths = []
452
- file_names = []
453
  for uploaded_file in uploaded_file_paths:
454
  if not uploaded_file.name.lower().endswith(supported_file_types):
455
  st.error('File must be of type: {}'.format(supported_file_types))
456
- uploaded_file = None
457
  else:
458
- print(f"Valid file: {uploaded_file.name}")
459
- if uploaded_file not in valid_files:
460
- path = os.path.join(temp_dir, uploaded_file.name)
461
- with open(path, "wb") as f:
462
- f.write(uploaded_file.getvalue())
463
- valid_files.append(uploaded_file)
464
- file_paths.append(path)
465
- # Save valid file names
466
- if len(valid_files) > 0:
467
- file_names = [f.name for f in valid_files]
468
-
469
- # --- Reconcile per-file state lists against the new file set ---
470
- # For each new file, carry over existing state if the filename matches a
471
- # previously analyzed file, otherwise insert a fresh empty slot.
472
- prev_names = st.session_state.file_names
473
- def _carry(old_list, empty_val_fn):
474
- new_list = []
475
- for name in file_names:
476
- if name in prev_names:
477
- idx = prev_names.index(name)
478
- new_list.append(old_list[idx] if idx < len(old_list) else empty_val_fn())
479
- else:
480
- new_list.append(empty_val_fn())
481
- return new_list
482
-
483
- st.session_state.results = _carry(st.session_state.results, lambda: [])
484
- st.session_state.summaries = _carry(st.session_state.summaries, lambda: [])
485
- st.session_state.unusedSpeakers= _carry(st.session_state.unusedSpeakers, lambda: [])
486
- st.session_state.categorySelect= _carry(st.session_state.categorySelect,
487
- lambda: [[] for _ in st.session_state.categories])
488
- st.session_state.speakerRenames= _carry(st.session_state.speakerRenames, lambda: {})
489
- st.session_state.speakerClips = _carry(st.session_state.speakerClips, lambda: {})
490
-
491
- st.session_state.file_names = file_names
492
  st.session_state.valid_files = valid_files
493
- st.session_state.file_paths = file_paths
494
 
495
  file_names = st.session_state.file_names
496
-
 
497
  if len(file_names) == 0:
498
  st.text("Upload file(s) to enable analysis")
499
  else:
500
  if st.button("Analyze All New Audio",key=f"button_all"):
501
- if len(valid_files) == 0:
502
  st.error('Upload file(s) first!')
503
  else:
504
  print("Start analyzing")
505
  start_time = time.time()
506
- totalFiles = len(valid_files)
507
- for i in range(totalFiles):
508
- if len(st.session_state.results) > i and len(st.session_state.results[i]) > 0:
 
509
  continue
510
- # Text files use sample data
511
- if file_paths[i].lower().endswith('.txt'):
512
  with st.spinner(text=f'Loading Demo File {i+1} of {totalFiles}'):
513
- # RTTM load as filler
514
- speakerList, annotations = su.loadAudioTXT(file_paths[i])
515
  printV(annotations,4)
516
- # Approximate total seconds
517
  totalSeconds = 0
518
  for segment in annotations.itersegments():
519
  if segment.end > totalSeconds:
520
  totalSeconds = segment.end
521
- st.session_state.results[i] = (annotations, totalSeconds)
522
- st.session_state.summaries[i] = {}
523
- speakerNames = annotations.labels()
524
- st.session_state.unusedSpeakers[i] = speakerNames
525
- elif file_paths[i].lower().endswith('.rttm'):
526
  with st.spinner(text=f'Loading File {i+1} of {totalFiles}'):
527
- # RTTM load as filler
528
- speakerList, annotations = su.loadAudioRTTM(file_paths[i])
529
  printV(annotations,4)
530
- # Approximate total seconds
531
  totalSeconds = 0
532
  for segment in annotations.itersegments():
533
  if segment.end > totalSeconds:
534
  totalSeconds = segment.end
535
- st.session_state.results[i] = (annotations, totalSeconds)
536
- st.session_state.summaries[i] = {}
537
- speakerNames = annotations.labels()
538
- st.session_state.unusedSpeakers[i] = speakerNames
539
- elif file_paths[i].lower().endswith('.csv'):
540
  with st.spinner(text=f'Loading File {i+1} of {totalFiles}'):
541
- # RTTM load as filler
542
- speakerList, annotations = su.loadAudioCSV(file_paths[i])
543
  printV(annotations,4)
544
- # Approximate total seconds
545
  totalSeconds = 0
546
  for segment in annotations.itersegments():
547
  if segment.end > totalSeconds:
548
  totalSeconds = segment.end
549
- st.session_state.results[i] = (annotations, totalSeconds)
550
- st.session_state.summaries[i] = {}
551
- speakerNames = annotations.labels()
552
- st.session_state.unusedSpeakers[i] = speakerNames
553
  else:
554
  with st.spinner(text=f'Processing File {i+1} of {totalFiles}'):
555
- annotations, totalSeconds, waveform, sample_rate = processFile(file_paths[i])
556
- print(f"Finished processing {file_paths[i]}")
557
- st.session_state.results[i] = (annotations, totalSeconds)
558
- print("Results saved")
559
- st.session_state.summaries[i] = {}
560
- print("Summaries saved")
561
- speakerNames = annotations.labels()
562
- st.session_state.unusedSpeakers[i] = speakerNames
563
- print("Speakers saved")
564
  with st.spinner(text=f'Generating speaker clips for File {i+1} of {totalFiles}'):
565
- generate_speaker_clips(annotations, waveform, sample_rate, i)
566
- del waveform # free memory after clipping
567
- print(f"Speaker clips generated for {file_paths[i]}")
568
  with st.spinner(text=f'Analyzing File {i+1} of {totalFiles}'):
569
- analyze(file_names[i])
570
- print(f"Finished analyzing {file_paths[i]}")
571
  print(f"Took {time.time() - start_time} seconds to analyze {totalFiles} files!")
572
  st.success(f"Took {time.time() - start_time} seconds to analyze {totalFiles} files!")
573
 
@@ -579,80 +530,58 @@ class FakeUpload:
579
  demoPath = "sample.rttm"
580
  isDemo = False
581
  if st.sidebar.button("Load Demo Example"):
582
- sampleUpload = FakeUpload(demoPath)
583
- valid_files=[sampleUpload]
584
- file_paths=[sampleUpload.path]
585
- file_names=[sampleUpload.name]
586
- st.session_state.valid_files = valid_files
587
- st.session_state.file_paths = file_paths
588
  start_time = time.time()
589
- st.session_state.file_names = file_names
590
- # Save valid file names
591
- if len(valid_files) > 0:
592
- file_names = [f.name for f in valid_files]
593
- while (len(st.session_state.results) < len(valid_files)):
594
- st.session_state.results.append([])
595
- while (len(st.session_state.summaries) < len(valid_files)):
596
- st.session_state.summaries.append([])
597
- while (len(st.session_state.unusedSpeakers) < len(valid_files)):
598
- st.session_state.unusedSpeakers.append([])
599
- while (len(st.session_state.categorySelect) < len(valid_files)):
600
- tempCategories = [[] for cat in st.session_state.categories]
601
- st.session_state.categorySelect.append(tempCategories)
602
- while (len(st.session_state.summaries) < len(valid_files)):
603
- st.session_state.summaries.append([])
604
- while (len(st.session_state.speakerRenames) < len(valid_files)):
605
- st.session_state.speakerRenames.append({})
606
- while (len(st.session_state.speakerClips) < len(valid_files)):
607
- st.session_state.speakerClips.append({})
608
-
609
  with st.spinner(text=f'Loading Demo Sample'):
610
- # RTTM load as filler
611
- speakerList, annotations = su.loadAudioRTTM(file_paths[0])
612
- # Approximate total seconds
613
  totalSeconds = 0
614
  for segment in annotations.itersegments():
615
  if segment.end > totalSeconds:
616
  totalSeconds = segment.end
617
- st.session_state.results = [(annotations, totalSeconds)]
618
- st.session_state.summaries = [{}]
619
- while len(st.session_state.speakerRenames) < 1:
620
- st.session_state.speakerRenames.append({})
621
- speakerNames = annotations.labels()
622
- st.session_state.unusedSpeakers = [speakerNames]
623
  with st.spinner(text=f'Analyzing Demo Data'):
624
- analyze(file_names[0])
625
  st.success(f"Took {time.time() - start_time} seconds to analyze the demo file!")
626
- st.session_state.select_currFile=file_names[0]
627
  isDemo = True
628
 
629
- currFile = st.sidebar.selectbox('Current File', file_names,on_change=updateMultiSelect,key="select_currFile")
630
 
631
  if isDemo:
632
- currFile=file_names[0]
633
  isDemo = False
634
-
635
- if currFile is None and len(st.session_state.results) > 0 and len(st.session_state.results[0]) > 0:
636
  st.write("Select a file to view from the sidebar")
637
  try:
638
  st.session_state.resetResult = False
639
- currFileIndex = file_names.index(currFile)
640
  currPlainName = currFile.split('.')[0]
641
- if len(st.session_state.results) > currFileIndex and len(st.session_state.summaries) > currFileIndex and len(st.session_state.results[currFileIndex]) > 0:
642
  st.header(f"Analysis of file {currFile}")
643
  graphNames = ["Data","Voice Categories","Speaker Percentage","Speakers with Categories","Treemap","Timeline","Time Spoken"]
644
  dataTab, pie1, pie2, sunburst1, treemap1, timeline, bar1 = st.tabs(graphNames)
645
- # Handle
646
- currAnnotation, currTotalTime = st.session_state.results[currFileIndex]
647
  speakerNames = currAnnotation.labels()
648
-
649
- speakers_dataFrame = st.session_state.summaries[currFileIndex]["speakers_dataFrame"]
650
  currDF, _ = su.annotationToSimpleDataFrame(currAnnotation)
651
- speakers_times = st.session_state.summaries[currFileIndex]["speakers_times"]
652
-
653
- # Update other categories
654
- unusedSpeakers = st.session_state.unusedSpeakers[currFileIndex]
655
- categorySelections = st.session_state["categorySelect"][currFileIndex]
656
  for i,category in enumerate(st.session_state.categories):
657
  speakerSet = categorySelections[i]
658
  st.sidebar.multiselect(category,
@@ -660,7 +589,7 @@ try:
660
  default=speakerSet,
661
  key=f"multiselect_{category}",
662
  on_change=updateCategoryOptions,
663
- args=(currFileIndex,))
664
  st.sidebar.button(f"Remove {category}",key=f"remove_{category}",on_click=removeCategory,args=(i,))
665
 
666
 
@@ -672,38 +601,34 @@ try:
672
  st.sidebar.caption("Replace SPEAKER_## labels with real names.")
673
 
674
  # --- Speaker clip preview ---
675
- file_clips = (
676
- st.session_state.speakerClips[currFileIndex]
677
- if currFileIndex < len(st.session_state.speakerClips)
678
- else {}
679
- )
680
  if file_clips:
681
  st.sidebar.caption(
682
  "Listen to each speaker's longest clip (up to 5 s) to help identify them."
683
  )
684
 
685
- current_renames = st.session_state.speakerRenames[currFileIndex]
686
  for sp in speakerNames:
687
- widget_key = f"rename_{currFileIndex}_{sp}"
688
- # Seed the widget state once from speakerRenames (e.g. after a file switch).
689
- # After that, never pass value= so Streamlit never overwrites what the user typed.
690
  if widget_key not in st.session_state:
691
  st.session_state[widget_key] = current_renames.get(sp, "")
692
- # Use live widget value for the display label so it updates immediately
693
  live_name = st.session_state[widget_key].strip()
694
  display_label = live_name if live_name else sp
695
  st.sidebar.markdown(f"**{display_label}**")
696
  if sp in file_clips:
697
  st.sidebar.audio(file_clips[sp], format="audio/wav")
 
698
  new_name = st.sidebar.text_input(
699
- f"Rename {display_label}",
700
  placeholder="e.g. John",
701
- key=widget_key
 
702
  )
703
  if new_name.strip():
704
- st.session_state.speakerRenames[currFileIndex][sp] = new_name.strip()
705
- elif sp in st.session_state.speakerRenames[currFileIndex]:
706
- del st.session_state.speakerRenames[currFileIndex][sp]
707
 
708
  catTypeColors = su.colorsCSS(3)
709
  allColors = su.colorsCSS(len(speakerNames)+len(st.session_state.categories))
@@ -730,10 +655,10 @@ try:
730
  }
731
  df4 = pd.DataFrame(data=df4_dict)
732
  df4.name = "df4"
733
- st.session_state.summaries[currFileIndex]["df4"] = df4
734
 
735
  with dataTab:
736
- displayDF = apply_speaker_renames_to_df(currDF, currFileIndex, column="Resource")
737
  csv = convert_df(displayDF)
738
 
739
  st.download_button(
@@ -747,7 +672,7 @@ try:
747
  st.dataframe(displayDF)
748
  with pie1:
749
  printV("In Pie1",4)
750
- df3 = st.session_state.summaries[currFileIndex]["df3"]
751
  fig1 = go.Figure()
752
  fig1.update_layout(
753
  title_text="Percentage of each Voice Category",
@@ -796,14 +721,14 @@ try:
796
 
797
  with pie2:
798
  printV("In Pie2",4)
799
- df4 = st.session_state.summaries[currFileIndex]["df4"].copy()
800
 
801
  # Some speakers may be missing, so fix colors
802
  figColors = []
803
  for n in df4["names"]:
804
  if n in speakerNames:
805
  figColors.append(speakerColors[speakerNames.index(n)])
806
- df4["names"] = df4["names"].apply(lambda s: get_display_name(s, currFileIndex))
807
  fig2 = go.Figure()
808
  fig2.update_layout(
809
  title_text="Percentage of Speakers and Custom Categories",
@@ -845,9 +770,9 @@ try:
845
  )
846
 
847
  with sunburst1:
848
- df5 = st.session_state.summaries[currFileIndex]["df5"].copy()
849
- df5["labels"] = df5["labels"].apply(lambda s: get_display_name(s, currFileIndex))
850
- df5["parentNames"] = df5["parentNames"].apply(lambda s: get_display_name(s, currFileIndex))
851
  fig3_1 = px.sunburst(df5,
852
  branchvalues = 'total',
853
  names = "labels",
@@ -903,9 +828,9 @@ try:
903
  )
904
 
905
  with treemap1:
906
- df5 = st.session_state.summaries[currFileIndex]["df5"].copy()
907
- df5["labels"] = df5["labels"].apply(lambda s: get_display_name(s, currFileIndex))
908
- df5["parentNames"] = df5["parentNames"].apply(lambda s: get_display_name(s, currFileIndex))
909
  fig3 = px.treemap(df5,
910
  branchvalues = "total",
911
  names = "labels",
@@ -965,7 +890,7 @@ try:
965
 
966
  with timeline:
967
  timeline_df = speakers_dataFrame.copy()
968
- timeline_df["Resource"] = timeline_df["Resource"].apply(lambda s: get_display_name(s, currFileIndex))
969
  base = dt.datetime.combine(dt.date.today(), dt.time.min)
970
  def to_audio_datetime(s):
971
  # If already a datetime/Timestamp, extract seconds since midnight of that date
@@ -1034,8 +959,8 @@ try:
1034
  )
1035
 
1036
  with bar1:
1037
- df2 = st.session_state.summaries[currFileIndex]["df2"].copy()
1038
- df2["names"] = df2["names"].apply(lambda s: get_display_name(s, currFileIndex))
1039
  fig2_la = px.bar(df2, x="values", y="names", color="names", orientation='h',
1040
  custom_data=["names","values"],title="Time Spoken by each Speaker",
1041
  color_discrete_sequence=catColors+speakerColors)
@@ -1094,36 +1019,28 @@ if len(st.session_state.results) > 0:
1094
  st.header("Multi-file Summary Data")
1095
  with st.spinner(text='Processing summary results...'):
1096
  fileNames = st.session_state.file_names
1097
- results = []
1098
- indices = []
1099
- for i, resultTuple in enumerate(st.session_state.results):
1100
- if len(resultTuple) == 2:
1101
- results.append(resultTuple)
1102
- indices.append(i)
1103
- if len(indices) > 1:
1104
-
1105
- df6_dict = {
1106
- "files":fileNames,
1107
- }
1108
  allCategories = copy.deepcopy(st.session_state.categories)
1109
- for i in indices:
1110
- currAnnotation, currTotalTime = st.session_state.results[i]
1111
- categorySelections = st.session_state["categorySelect"][i]
1112
- catSummary,extraCats = su.calcCategories(currAnnotation,categorySelections)
1113
- st.session_state.summaries[i]["categories"] = (catSummary,extraCats)
1114
  for extra in extraCats:
1115
  df6_dict[extra] = []
1116
  if extra not in allCategories:
1117
  allCategories.append(extra)
1118
-
1119
-
1120
  for category in st.session_state.categories:
1121
  df6_dict[category] = []
1122
- for i in indices:
1123
- summary, extras = st.session_state.summaries[i]["categories"]
1124
  theseCategories = st.session_state.categories + extras
1125
  for j, timeSlots in enumerate(summary):
1126
- df6_dict[theseCategories[j]].append(sum([t.duration for _,t in timeSlots])/st.session_state.results[i][1])
1127
  for category in allCategories:
1128
  if category not in theseCategories:
1129
  df6_dict[category].append(0)
@@ -1138,8 +1055,8 @@ if len(st.session_state.results) > 0:
1138
  }
1139
  for category in voiceNames:
1140
  df7_dict[category] = []
1141
- for resultID,summary in enumerate(st.session_state.summaries):
1142
- partialDf = summary["df5"]
1143
  for i in range(len(voiceNames)):
1144
  df7_dict[voiceNames[i]].append(partialDf["percentiles"][i])
1145
  df7 = pd.DataFrame(df7_dict)
 
39
  if verbosity>=verbosityLevel:
40
  print(message)
41
 
42
+ def get_display_name(speaker, fileName):
43
  """Return the user-assigned display name for a speaker, or the original label."""
44
  renames = st.session_state.speakerRenames
45
+ return renames.get(fileName, {}).get(speaker, speaker)
 
 
46
 
47
+ def apply_speaker_renames_to_df(df, fileName, column="task"):
48
  """Replace speaker_## labels in a DataFrame column with display names."""
49
  if column not in df.columns:
50
  return df
51
  df = df.copy()
52
+ df[column] = df[column].apply(lambda s: get_display_name(s, fileName))
53
  return df
54
 
55
  @st.cache_data
 
125
  """
126
  import io
127
 
128
+ # file_index is now a filename string
 
 
 
129
  clips = {}
130
  for speaker in annotations.labels():
131
  speaker_segments = [
 
154
  clips[speaker] = buffer.read()
155
 
156
  st.session_state.speakerClips[file_index] = clips
157
+ print(f"Generated {len(clips)} speaker clips for {file_index}")
158
 
159
  def addCategory():
160
  newCategory = st.session_state.categoryInput
 
162
  st.session_state[f'multiselect_{newCategory}'] = []
163
  st.session_state.categories.append(newCategory)
164
  st.session_state.categoryInput = ''
165
+ for fname in st.session_state.categorySelect:
166
+ st.session_state.categorySelect[fname].append([])
167
 
168
  def removeCategory(index):
169
  categoryName = st.session_state.categories[index]
 
171
  del st.session_state[f'multiselect_{categoryName}']
172
  del st.session_state[f'remove_{categoryName}']
173
  del st.session_state.categories[index]
174
+ for fname in st.session_state.categorySelect:
175
+ del st.session_state.categorySelect[fname][index]
176
 
177
+ def updateCategoryOptions(fileName):
178
  if st.session_state.resetResult:
 
179
  return
180
+ currAnnotation, _ = st.session_state.results[fileName]
 
 
 
181
  speakerNames = currAnnotation.labels()
 
 
182
  unusedSpeakers = copy.deepcopy(speakerNames)
 
183
  for i, category in enumerate(st.session_state['categories']):
184
  category_choices = copy.deepcopy(st.session_state[f'multiselect_{category}'])
185
+ st.session_state["categorySelect"][fileName][i] = category_choices
186
  for sp in category_choices:
187
  try:
188
  unusedSpeakers.remove(sp)
189
  except:
190
  continue
191
+ st.session_state.unusedSpeakers[fileName] = unusedSpeakers
 
192
 
193
  def updateMultiSelect():
194
+ fileName = st.session_state["select_currFile"]
195
  st.session_state.resetResult = True
196
  for i, category in enumerate(st.session_state['categories']):
197
+ st.session_state[f'multiselect_{category}'] = st.session_state['categorySelect'][fileName][i]
198
 
199
  def analyze(inFileName):
200
  try:
201
  print(f"Start analyzing {inFileName}")
202
  st.session_state.resetResult = False
203
+ if inFileName in st.session_state.results and inFileName in st.session_state.summaries and len(st.session_state.results[inFileName]) > 0:
 
 
204
 
205
  printV(f'In if',4)
206
+ currAnnotation, currTotalTime = st.session_state.results[inFileName]
 
207
  speakerNames = currAnnotation.labels()
208
  printV(f'Loaded results',4)
209
+ unusedSpeakers = st.session_state.unusedSpeakers[inFileName]
210
+ categorySelections = st.session_state["categorySelect"][inFileName]
 
211
  printV(f'Loaded speaker selections',4)
212
  noVoice, oneVoice, multiVoice = su.calcSpeakingTypes(currAnnotation,currTotalTime)
213
  sumNoVoice = su.sumTimes(noVoice)
 
224
  }
225
  )
226
  df3.name = "df3"
227
+ st.session_state.summaries[inFileName]["df3"] = df3
228
  printV(f'Set df3',4)
229
 
230
  # --- Build df4 ---
 
258
  }
259
  df4 = pd.DataFrame(data=df4_dict)
260
  df4.name = "df4"
261
+ st.session_state.summaries[inFileName]["df4"] = df4
262
  printV(f'Set df4', 4)
263
 
264
  # --- Build df5 ---
 
314
  + [(t * 100) / summativeMultiSpeaker for t in multiTimeList],
315
  })
316
  df5.name = "df5"
317
+ st.session_state.summaries[inFileName]["df5"] = df5
318
  printV(f'Set df5', 4)
319
 
320
  # --- Build speakers_dataFrame, df2 ---
321
  speakers_dataFrame, speakers_times = su.annotationToDataFrame(currAnnotation)
322
+ st.session_state.summaries[inFileName]["speakers_dataFrame"] = speakers_dataFrame
323
+ st.session_state.summaries[inFileName]["speakers_times"] = speakers_times
324
 
325
  df2_dict = {
326
  "values": [100 * t / currTotalTime for t in df4_dict["values"]],
327
  "names": df4_dict["names"],
328
  }
329
  df2 = pd.DataFrame(df2_dict)
330
+ st.session_state.summaries[inFileName]["df2"] = df2
331
  printV(f'Set df2', 4)
332
  except Exception as e:
333
  import traceback
 
376
  pipeline.to(device)#torch.device("cuda"))
377
 
378
  # Store results for viewing and further processing
379
+ # All per-file state is keyed by filename (str) so it survives upload order changes.
380
  if 'results' not in st.session_state:
381
+ st.session_state.results = {} # {filename: (annotations, totalSeconds)}
382
  if 'speakerRenames' not in st.session_state:
383
+ st.session_state.speakerRenames = {} # {filename: {speaker: name}}
384
  if 'summaries' not in st.session_state:
385
+ st.session_state.summaries = {} # {filename: {df2, df3, ...}}
386
  if 'categories' not in st.session_state:
387
  st.session_state.categories = []
388
+ st.session_state.categorySelect = {} # {filename: [[], [], ...]}
 
389
  if 'removeCategory' not in st.session_state:
390
  st.session_state.removeCategory = None
391
  if 'resetResult' not in st.session_state:
392
  st.session_state.resetResult = False
 
393
  if 'unusedSpeakers' not in st.session_state:
394
+ st.session_state.unusedSpeakers = {} # {filename: [speaker, ...]}
395
  if 'file_names' not in st.session_state:
396
  st.session_state.file_names = []
397
  if 'valid_files' not in st.session_state:
398
  st.session_state.valid_files = []
399
  if 'file_paths' not in st.session_state:
400
+ st.session_state.file_paths = {} # {filename: path}
401
  if 'showSummary' not in st.session_state:
402
  st.session_state.showSummary = 'No'
403
  if 'speakerClips' not in st.session_state:
404
+ st.session_state.speakerClips = {} # {filename: {speaker: wav_bytes}}
405
 
406
 
407
 
 
428
 
429
  if uploaded_file_paths is not None and len(uploaded_file_paths) > 0:
430
  print("Found file paths")
 
 
 
431
  for uploaded_file in uploaded_file_paths:
432
  if not uploaded_file.name.lower().endswith(supported_file_types):
433
  st.error('File must be of type: {}'.format(supported_file_types))
 
434
  else:
435
+ fname = uploaded_file.name
436
+ print(f"Valid file: {fname}")
437
+ # Write to disk (always refresh so file bytes are current)
438
+ path = os.path.join(temp_dir, fname)
439
+ with open(path, "wb") as f:
440
+ f.write(uploaded_file.getvalue())
441
+ # Add to master lists only if not already tracked
442
+ if fname not in st.session_state.file_names:
443
+ st.session_state.file_names.append(fname)
444
+ st.session_state.results.setdefault(fname, [])
445
+ st.session_state.summaries.setdefault(fname, [])
446
+ st.session_state.unusedSpeakers.setdefault(fname, [])
447
+ st.session_state.categorySelect.setdefault(fname, [[] for _ in st.session_state.categories])
448
+ st.session_state.speakerRenames.setdefault(fname, {})
449
+ st.session_state.speakerClips.setdefault(fname, {})
450
+ st.session_state.file_paths[fname] = path
451
+ # Rebuild valid_files / file_paths lists from tracked state
452
+ valid_files = [f for f in st.session_state.file_names]
453
+ file_paths = [st.session_state.file_paths[f] for f in valid_files]
454
+ file_names = valid_files
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  st.session_state.valid_files = valid_files
456
+ st.session_state.file_paths = {f: st.session_state.file_paths[f] for f in valid_files}
457
 
458
  file_names = st.session_state.file_names
459
+ file_paths_dict = st.session_state.file_paths # dict {fname: path}
460
+
461
  if len(file_names) == 0:
462
  st.text("Upload file(s) to enable analysis")
463
  else:
464
  if st.button("Analyze All New Audio",key=f"button_all"):
465
+ if len(file_names) == 0:
466
  st.error('Upload file(s) first!')
467
  else:
468
  print("Start analyzing")
469
  start_time = time.time()
470
+ totalFiles = len(file_names)
471
+ for i, fname in enumerate(file_names):
472
+ fpath = file_paths_dict.get(fname, "")
473
+ if st.session_state.results.get(fname):
474
  continue
475
+ if fpath.lower().endswith('.txt'):
 
476
  with st.spinner(text=f'Loading Demo File {i+1} of {totalFiles}'):
477
+ speakerList, annotations = su.loadAudioTXT(fpath)
 
478
  printV(annotations,4)
 
479
  totalSeconds = 0
480
  for segment in annotations.itersegments():
481
  if segment.end > totalSeconds:
482
  totalSeconds = segment.end
483
+ st.session_state.results[fname] = (annotations, totalSeconds)
484
+ st.session_state.summaries[fname] = {}
485
+ st.session_state.unusedSpeakers[fname] = list(annotations.labels())
486
+ elif fpath.lower().endswith('.rttm'):
 
487
  with st.spinner(text=f'Loading File {i+1} of {totalFiles}'):
488
+ speakerList, annotations = su.loadAudioRTTM(fpath)
 
489
  printV(annotations,4)
 
490
  totalSeconds = 0
491
  for segment in annotations.itersegments():
492
  if segment.end > totalSeconds:
493
  totalSeconds = segment.end
494
+ st.session_state.results[fname] = (annotations, totalSeconds)
495
+ st.session_state.summaries[fname] = {}
496
+ st.session_state.unusedSpeakers[fname] = list(annotations.labels())
497
+ elif fpath.lower().endswith('.csv'):
 
498
  with st.spinner(text=f'Loading File {i+1} of {totalFiles}'):
499
+ speakerList, annotations = su.loadAudioCSV(fpath)
 
500
  printV(annotations,4)
 
501
  totalSeconds = 0
502
  for segment in annotations.itersegments():
503
  if segment.end > totalSeconds:
504
  totalSeconds = segment.end
505
+ st.session_state.results[fname] = (annotations, totalSeconds)
506
+ st.session_state.summaries[fname] = {}
507
+ st.session_state.unusedSpeakers[fname] = list(annotations.labels())
 
508
  else:
509
  with st.spinner(text=f'Processing File {i+1} of {totalFiles}'):
510
+ annotations, totalSeconds, waveform, sample_rate = processFile(fpath)
511
+ print(f"Finished processing {fpath}")
512
+ st.session_state.results[fname] = (annotations, totalSeconds)
513
+ st.session_state.summaries[fname] = {}
514
+ st.session_state.unusedSpeakers[fname] = list(annotations.labels())
 
 
 
 
515
  with st.spinner(text=f'Generating speaker clips for File {i+1} of {totalFiles}'):
516
+ generate_speaker_clips(annotations, waveform, sample_rate, fname)
517
+ del waveform
518
+ print(f"Speaker clips generated for {fpath}")
519
  with st.spinner(text=f'Analyzing File {i+1} of {totalFiles}'):
520
+ analyze(fname)
521
+ print(f"Finished analyzing {fpath}")
522
  print(f"Took {time.time() - start_time} seconds to analyze {totalFiles} files!")
523
  st.success(f"Took {time.time() - start_time} seconds to analyze {totalFiles} files!")
524
 
 
530
  demoPath = "sample.rttm"
531
  isDemo = False
532
  if st.sidebar.button("Load Demo Example"):
533
+ demoName = demoPath.split('/')[-1]
 
 
 
 
 
534
  start_time = time.time()
535
+ if demoName not in st.session_state.file_names:
536
+ st.session_state.file_names.append(demoName)
537
+ st.session_state.file_paths[demoName] = demoPath
538
+ st.session_state.results.setdefault(demoName, [])
539
+ st.session_state.summaries.setdefault(demoName, {})
540
+ st.session_state.unusedSpeakers.setdefault(demoName, [])
541
+ st.session_state.categorySelect.setdefault(demoName, [[] for _ in st.session_state.categories])
542
+ st.session_state.speakerRenames.setdefault(demoName, {})
543
+ st.session_state.speakerClips.setdefault(demoName, {})
544
+ file_names = st.session_state.file_names
545
+
 
 
 
 
 
 
 
 
 
546
  with st.spinner(text=f'Loading Demo Sample'):
547
+ speakerList, annotations = su.loadAudioRTTM(demoPath)
 
 
548
  totalSeconds = 0
549
  for segment in annotations.itersegments():
550
  if segment.end > totalSeconds:
551
  totalSeconds = segment.end
552
+ st.session_state.results[demoName] = (annotations, totalSeconds)
553
+ st.session_state.summaries[demoName] = {}
554
+ st.session_state.unusedSpeakers[demoName] = list(annotations.labels())
 
 
 
555
  with st.spinner(text=f'Analyzing Demo Data'):
556
+ analyze(demoName)
557
  st.success(f"Took {time.time() - start_time} seconds to analyze the demo file!")
558
+ st.session_state.select_currFile = demoName
559
  isDemo = True
560
 
561
+ currFile = st.sidebar.selectbox('Current File', file_names, on_change=updateMultiSelect, key="select_currFile")
562
 
563
  if isDemo:
564
+ currFile = file_names[0]
565
  isDemo = False
566
+
567
+ if currFile is None and st.session_state.results:
568
  st.write("Select a file to view from the sidebar")
569
  try:
570
  st.session_state.resetResult = False
 
571
  currPlainName = currFile.split('.')[0]
572
+ if currFile in st.session_state.results and currFile in st.session_state.summaries and len(st.session_state.results[currFile]) > 0:
573
  st.header(f"Analysis of file {currFile}")
574
  graphNames = ["Data","Voice Categories","Speaker Percentage","Speakers with Categories","Treemap","Timeline","Time Spoken"]
575
  dataTab, pie1, pie2, sunburst1, treemap1, timeline, bar1 = st.tabs(graphNames)
576
+ currAnnotation, currTotalTime = st.session_state.results[currFile]
 
577
  speakerNames = currAnnotation.labels()
578
+
579
+ speakers_dataFrame = st.session_state.summaries[currFile]["speakers_dataFrame"]
580
  currDF, _ = su.annotationToSimpleDataFrame(currAnnotation)
581
+ speakers_times = st.session_state.summaries[currFile]["speakers_times"]
582
+
583
+ unusedSpeakers = st.session_state.unusedSpeakers[currFile]
584
+ categorySelections = st.session_state["categorySelect"][currFile]
 
585
  for i,category in enumerate(st.session_state.categories):
586
  speakerSet = categorySelections[i]
587
  st.sidebar.multiselect(category,
 
589
  default=speakerSet,
590
  key=f"multiselect_{category}",
591
  on_change=updateCategoryOptions,
592
+ args=(currFile,))
593
  st.sidebar.button(f"Remove {category}",key=f"remove_{category}",on_click=removeCategory,args=(i,))
594
 
595
 
 
601
  st.sidebar.caption("Replace SPEAKER_## labels with real names.")
602
 
603
  # --- Speaker clip preview ---
604
+ file_clips = st.session_state.speakerClips.get(currFile, {})
 
 
 
 
605
  if file_clips:
606
  st.sidebar.caption(
607
  "Listen to each speaker's longest clip (up to 5 s) to help identify them."
608
  )
609
 
610
+ current_renames = st.session_state.speakerRenames[currFile]
611
  for sp in speakerNames:
612
+ widget_key = f"rename_{currFile}_{sp}"
613
+ # Seed the widget state once; never pass value= so reruns don't overwrite it
 
614
  if widget_key not in st.session_state:
615
  st.session_state[widget_key] = current_renames.get(sp, "")
 
616
  live_name = st.session_state[widget_key].strip()
617
  display_label = live_name if live_name else sp
618
  st.sidebar.markdown(f"**{display_label}**")
619
  if sp in file_clips:
620
  st.sidebar.audio(file_clips[sp], format="audio/wav")
621
+ # Label is always the fixed original sp so Streamlit never recreates the widget
622
  new_name = st.sidebar.text_input(
623
+ sp,
624
  placeholder="e.g. John",
625
+ key=widget_key,
626
+ label_visibility="collapsed"
627
  )
628
  if new_name.strip():
629
+ st.session_state.speakerRenames[currFile][sp] = new_name.strip()
630
+ elif sp in st.session_state.speakerRenames[currFile]:
631
+ del st.session_state.speakerRenames[currFile][sp]
632
 
633
  catTypeColors = su.colorsCSS(3)
634
  allColors = su.colorsCSS(len(speakerNames)+len(st.session_state.categories))
 
655
  }
656
  df4 = pd.DataFrame(data=df4_dict)
657
  df4.name = "df4"
658
+ st.session_state.summaries[currFile]["df4"] = df4
659
 
660
  with dataTab:
661
+ displayDF = apply_speaker_renames_to_df(currDF, currFile, column="Resource")
662
  csv = convert_df(displayDF)
663
 
664
  st.download_button(
 
672
  st.dataframe(displayDF)
673
  with pie1:
674
  printV("In Pie1",4)
675
+ df3 = st.session_state.summaries[currFile]["df3"]
676
  fig1 = go.Figure()
677
  fig1.update_layout(
678
  title_text="Percentage of each Voice Category",
 
721
 
722
  with pie2:
723
  printV("In Pie2",4)
724
+ df4 = st.session_state.summaries[currFile]["df4"].copy()
725
 
726
  # Some speakers may be missing, so fix colors
727
  figColors = []
728
  for n in df4["names"]:
729
  if n in speakerNames:
730
  figColors.append(speakerColors[speakerNames.index(n)])
731
+ df4["names"] = df4["names"].apply(lambda s: get_display_name(s, currFile))
732
  fig2 = go.Figure()
733
  fig2.update_layout(
734
  title_text="Percentage of Speakers and Custom Categories",
 
770
  )
771
 
772
  with sunburst1:
773
+ df5 = st.session_state.summaries[currFile]["df5"].copy()
774
+ df5["labels"] = df5["labels"].apply(lambda s: get_display_name(s, currFile))
775
+ df5["parentNames"] = df5["parentNames"].apply(lambda s: get_display_name(s, currFile))
776
  fig3_1 = px.sunburst(df5,
777
  branchvalues = 'total',
778
  names = "labels",
 
828
  )
829
 
830
  with treemap1:
831
+ df5 = st.session_state.summaries[currFile]["df5"].copy()
832
+ df5["labels"] = df5["labels"].apply(lambda s: get_display_name(s, currFile))
833
+ df5["parentNames"] = df5["parentNames"].apply(lambda s: get_display_name(s, currFile))
834
  fig3 = px.treemap(df5,
835
  branchvalues = "total",
836
  names = "labels",
 
890
 
891
  with timeline:
892
  timeline_df = speakers_dataFrame.copy()
893
+ timeline_df["Resource"] = timeline_df["Resource"].apply(lambda s: get_display_name(s, currFile))
894
  base = dt.datetime.combine(dt.date.today(), dt.time.min)
895
  def to_audio_datetime(s):
896
  # If already a datetime/Timestamp, extract seconds since midnight of that date
 
959
  )
960
 
961
  with bar1:
962
+ df2 = st.session_state.summaries[currFile]["df2"].copy()
963
+ df2["names"] = df2["names"].apply(lambda s: get_display_name(s, currFile))
964
  fig2_la = px.bar(df2, x="values", y="names", color="names", orientation='h',
965
  custom_data=["names","values"],title="Time Spoken by each Speaker",
966
  color_discrete_sequence=catColors+speakerColors)
 
1019
  st.header("Multi-file Summary Data")
1020
  with st.spinner(text='Processing summary results...'):
1021
  fileNames = st.session_state.file_names
1022
+ validNames = [fn for fn in fileNames if fn in st.session_state.results and len(st.session_state.results[fn]) == 2]
1023
+ if len(validNames) > 1:
1024
+
1025
+ df6_dict = {"files": validNames}
 
 
 
 
 
 
 
1026
  allCategories = copy.deepcopy(st.session_state.categories)
1027
+ for fn in validNames:
1028
+ currAnnotation, currTotalTime = st.session_state.results[fn]
1029
+ categorySelections = st.session_state["categorySelect"][fn]
1030
+ catSummary, extraCats = su.calcCategories(currAnnotation, categorySelections)
1031
+ st.session_state.summaries[fn]["categories"] = (catSummary, extraCats)
1032
  for extra in extraCats:
1033
  df6_dict[extra] = []
1034
  if extra not in allCategories:
1035
  allCategories.append(extra)
1036
+
 
1037
  for category in st.session_state.categories:
1038
  df6_dict[category] = []
1039
+ for fn in validNames:
1040
+ summary, extras = st.session_state.summaries[fn]["categories"]
1041
  theseCategories = st.session_state.categories + extras
1042
  for j, timeSlots in enumerate(summary):
1043
+ df6_dict[theseCategories[j]].append(sum([t.duration for _,t in timeSlots])/st.session_state.results[fn][1])
1044
  for category in allCategories:
1045
  if category not in theseCategories:
1046
  df6_dict[category].append(0)
 
1055
  }
1056
  for category in voiceNames:
1057
  df7_dict[category] = []
1058
+ for fn in validNames:
1059
+ partialDf = st.session_state.summaries[fn]["df5"]
1060
  for i in range(len(voiceNames)):
1061
  df7_dict[voiceNames[i]].append(partialDf["percentiles"][i])
1062
  df7 = pd.DataFrame(df7_dict)