duongthienz commited on
Commit
9979934
·
verified ·
1 Parent(s): 1405024

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +89 -86
app.py CHANGED
@@ -53,54 +53,7 @@ def apply_speaker_renames_to_df(df, fileIndex, column="task"):
53
  df = df.copy()
54
  df[column] = df[column].apply(lambda s: get_display_name(s, fileIndex))
55
  return df
56
-
57
- def extract_speaker_clip(annotation, speaker, waveform, sample_rate, clip_duration=5):
58
- """
59
- Slice a clip directly from the already-loaded waveform tensor.
60
- Avoids torchaudio.load (broken in this env due to torchcodec/libnppicc).
61
- Returns (wav_bytes, None) on success or (None, error_str) on failure.
62
- """
63
- import io, traceback as tb
64
- try:
65
- if waveform is None or sample_rate is None:
66
- return None, "No waveform stored (only audio uploads carry waveform data)"
67
-
68
- # Collect this speaker's segments
69
- speaker_segments = []
70
- try:
71
- for seg, _, label in annotation.itertracks(yield_label=True):
72
- if label == speaker:
73
- speaker_segments.append((seg.start, seg.end))
74
- except Exception as e:
75
- return None, f"itertracks failed: {e}"
76
-
77
- if not speaker_segments:
78
- return None, f"No segments found for speaker '{speaker}'"
79
-
80
- # Prefer first segment >= clip_duration; else take longest
81
- chosen_start, chosen_end = None, None
82
- for start, end in speaker_segments:
83
- if (end - start) >= clip_duration:
84
- chosen_start, chosen_end = start, start + clip_duration
85
- break
86
- if chosen_start is None:
87
- longest = max(speaker_segments, key=lambda s: s[1] - s[0])
88
- chosen_start, chosen_end = longest
89
-
90
- # Slice waveform
91
- start_frame = int(chosen_start * sample_rate)
92
- end_frame = min(int(chosen_end * sample_rate), waveform.shape[-1])
93
- clip = waveform[:, start_frame:end_frame]
94
-
95
- # Encode to WAV bytes
96
- buf = io.BytesIO()
97
- torchaudio.save(buf, clip.cpu(), sample_rate, format="wav")
98
- buf.seek(0)
99
- return buf.read(), None
100
-
101
- except Exception as e:
102
- print(f"extract_speaker_clip error for {speaker}: {tb.format_exc()}")
103
- return None, str(e)
104
  @st.cache_data
105
  def convert_df(df):
106
  return df.to_csv(index=False).encode('utf-8')
@@ -161,7 +114,62 @@ def processFile(filePath):
161
  print("Speakers Detected")
162
  totalTimeInSeconds = int(waveform_gain_adjusted.shape[-1]/sampleRate)
163
  print("Time in seconds calculated")
164
- return annotations, totalTimeInSeconds, waveform_gain_adjusted, sampleRate
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  def addCategory():
167
  newCategory = st.session_state.categoryInput
@@ -188,7 +196,7 @@ def updateCategoryOptions(resultIndex):
188
  #st.info(f"Updating result {resultIndex}")
189
  #st.info(f"In update: {st.session_state.categorySelect}")
190
  # Handle
191
- _r = st.session_state.results[currFileIndex]; currAnnotation = _r[0]
192
  speakerNames = currAnnotation.labels()
193
 
194
  # Handle speaker category sidebars
@@ -221,7 +229,7 @@ def analyze(inFileName):
221
 
222
  printV(f'In if',4)
223
  # Handle
224
- _r = st.session_state.results[currFileIndex]; currAnnotation, currTotalTime = _r[0], _r[1]
225
  speakerNames = currAnnotation.labels()
226
  printV(f'Loaded results',4)
227
  # Update other categories
@@ -421,6 +429,8 @@ if 'file_paths' not in st.session_state:
421
  st.session_state.file_paths = []
422
  if 'showSummary' not in st.session_state:
423
  st.session_state.showSummary = 'No'
 
 
424
 
425
 
426
 
@@ -443,9 +453,7 @@ viewChoices = ["Voice Categories","Custom Categories","Detailed Voice Categories
443
  valid_files = st.session_state.valid_files
444
  file_paths = st.session_state.file_paths
445
  currDF = None
446
- if 'temp_dir' not in st.session_state:
447
- st.session_state.temp_dir = tempfile.mkdtemp()
448
- temp_dir = st.session_state.temp_dir
449
 
450
  if uploaded_file_paths is not None and len(uploaded_file_paths) > 0:
451
  print("Found file paths")
@@ -481,6 +489,8 @@ if uploaded_file_paths is not None and len(uploaded_file_paths) > 0:
481
  st.session_state.summaries.append([])
482
  while (len(st.session_state.speakerRenames) < len(valid_files)):
483
  st.session_state.speakerRenames.append({})
 
 
484
 
485
  st.session_state.file_names = file_names
486
  st.session_state.valid_files = valid_files
@@ -546,15 +556,18 @@ else:
546
  st.session_state.unusedSpeakers[i] = speakerNames
547
  else:
548
  with st.spinner(text=f'Processing File {i+1} of {totalFiles}'):
549
- annotations, totalSeconds, wf_stored, sr_stored = processFile(file_paths[i])
550
  print(f"Finished processing {file_paths[i]}")
551
- st.session_state.results[i] = (annotations, totalSeconds, wf_stored, sr_stored)
552
  print("Results saved")
553
  st.session_state.summaries[i] = {}
554
  print("Summaries saved")
555
  speakerNames = annotations.labels()
556
  st.session_state.unusedSpeakers[i] = speakerNames
557
  print("Speakers saved")
 
 
 
558
  with st.spinner(text=f'Analyzing File {i+1} of {totalFiles}'):
559
  analyze(file_names[i])
560
  print(f"Finished analyzing {file_paths[i]}")
@@ -593,6 +606,8 @@ if st.sidebar.button("Load Demo Example"):
593
  st.session_state.summaries.append([])
594
  while (len(st.session_state.speakerRenames) < len(valid_files)):
595
  st.session_state.speakerRenames.append({})
 
 
596
 
597
  with st.spinner(text=f'Loading Demo Sample'):
598
  # RTTM load as filler
@@ -631,10 +646,7 @@ try:
631
  graphNames = ["Data","Voice Categories","Speaker Percentage","Speakers with Categories","Treemap","Timeline","Time Spoken"]
632
  dataTab, pie1, pie2, sunburst1, treemap1, timeline, bar1 = st.tabs(graphNames)
633
  # Handle
634
- _r = st.session_state.results[currFileIndex]
635
- currAnnotation, currTotalTime = _r[0], _r[1]
636
- currWaveform = _r[2] if len(_r) > 2 else None
637
- currSampleRate = _r[3] if len(_r) > 3 else None
638
  speakerNames = currAnnotation.labels()
639
 
640
  speakers_dataFrame = st.session_state.summaries[currFileIndex]["speakers_dataFrame"]
@@ -658,39 +670,30 @@ try:
658
 
659
  newCategory = st.sidebar.text_input('Add category', key='categoryInput',on_change=addCategory)
660
 
661
- # --- Speaker Sample Clips (audio files only) ---
662
- _is_audio_file = currFile is not None and not any(
663
- currFile.lower().endswith(ext) for ext in ('.rttm', '.txt', '.csv')
 
 
 
 
 
 
664
  )
665
- if _is_audio_file:
666
- st.sidebar.divider()
667
- st.sidebar.subheader("\U0001f3a7 Speaker Samples")
668
  st.sidebar.caption(
669
- "Listen to a 5-second clip for each speaker to help identify them."
670
  )
671
- _curr_audio_path = file_paths[currFileIndex] if currFileIndex < len(file_paths) else None
672
- if _curr_audio_path and os.path.exists(_curr_audio_path):
673
- for sp in speakerNames:
674
- _display = get_display_name(sp, currFileIndex)
675
- st.sidebar.markdown(f"**{_display}**")
676
- _clip_bytes, _clip_err = extract_speaker_clip(
677
- currAnnotation, sp, currWaveform, currSampleRate, clip_duration=5
678
- )
679
- if _clip_bytes:
680
- st.sidebar.audio(_clip_bytes, format="audio/wav")
681
- else:
682
- st.sidebar.error(f"Clip error: {_clip_err}")
683
- else:
684
- st.sidebar.warning(f"Audio path not found: {_curr_audio_path}")
685
 
686
- st.sidebar.divider()
687
- st.sidebar.subheader("Rename Speakers")
688
- st.sidebar.caption("Replace SPEAKER_## labels with real names.")
689
  current_renames = st.session_state.speakerRenames[currFileIndex]
690
  for sp in speakerNames:
 
 
 
 
691
  current_label = current_renames.get(sp, "")
692
  new_name = st.sidebar.text_input(
693
- f"{sp}",
694
  value=current_label,
695
  placeholder=f"e.g. John",
696
  key=f"rename_{currFileIndex}_{sp}"
@@ -1102,7 +1105,7 @@ if len(st.session_state.results) > 0:
1102
  }
1103
  allCategories = copy.deepcopy(st.session_state.categories)
1104
  for i in indices:
1105
- _ri = st.session_state.results[i]; currAnnotation, currTotalTime = _ri[0], _ri[1]
1106
  categorySelections = st.session_state["categorySelect"][i]
1107
  catSummary,extraCats = su.calcCategories(currAnnotation,categorySelections)
1108
  st.session_state.summaries[i]["categories"] = (catSummary,extraCats)
 
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
58
  def convert_df(df):
59
  return df.to_csv(index=False).encode('utf-8')
 
114
  print("Speakers Detected")
115
  totalTimeInSeconds = int(waveform_gain_adjusted.shape[-1]/sampleRate)
116
  print("Time in seconds calculated")
117
+ return annotations, totalTimeInSeconds
118
+
119
+ def generate_speaker_clips(annotations, audio_path, file_index):
120
+ """
121
+ For each unique speaker in `annotations`, find their longest contiguous segment.
122
+ If that segment is >= 5s, clip exactly 5s from its start.
123
+ If < 5s, take the whole segment.
124
+ Saves clips as WAV bytes in st.session_state.speakerClips[file_index].
125
+ Only works when a real audio file is provided (skips silently otherwise).
126
+ """
127
+ if audio_path is None or not os.path.exists(audio_path):
128
+ return
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
+ try:
135
+ waveform, sample_rate = torchaudio.load(audio_path)
136
+ except Exception as e:
137
+ print(f"generate_speaker_clips: could not load audio: {e}")
138
+ return
139
+
140
+ clips = {}
141
+ for speaker in annotations.labels():
142
+ # Collect all segments for this speaker
143
+ speaker_segments = [
144
+ segment for segment, _, label in annotations.itertracks(yield_label=True)
145
+ if label == speaker
146
+ ]
147
+ if not speaker_segments:
148
+ continue
149
+
150
+ # Find the longest segment
151
+ longest = max(speaker_segments, key=lambda s: s.duration)
152
+
153
+ # Determine clip boundaries
154
+ clip_start = longest.start
155
+ clip_duration = min(longest.duration, 5.0)
156
+ clip_end = clip_start + clip_duration
157
+
158
+ start_sample = int(clip_start * sample_rate)
159
+ end_sample = int(clip_end * sample_rate)
160
+ end_sample = min(end_sample, waveform.shape[-1])
161
+
162
+ clip_waveform = waveform[:, start_sample:end_sample]
163
+
164
+ # Write to an in-memory bytes buffer as WAV
165
+ import io
166
+ buffer = io.BytesIO()
167
+ torchaudio.save(buffer, clip_waveform, sample_rate, format="wav")
168
+ buffer.seek(0)
169
+ clips[speaker] = buffer.read()
170
+
171
+ st.session_state.speakerClips[file_index] = clips
172
+ print(f"Generated {len(clips)} speaker clips for file index {file_index}")
173
 
174
  def addCategory():
175
  newCategory = st.session_state.categoryInput
 
196
  #st.info(f"Updating result {resultIndex}")
197
  #st.info(f"In update: {st.session_state.categorySelect}")
198
  # Handle
199
+ _, currAnnotation, _ = st.session_state.results[currFileIndex]
200
  speakerNames = currAnnotation.labels()
201
 
202
  # Handle speaker category sidebars
 
229
 
230
  printV(f'In if',4)
231
  # Handle
232
+ currAnnotation, currTotalTime = st.session_state.results[currFileIndex]
233
  speakerNames = currAnnotation.labels()
234
  printV(f'Loaded results',4)
235
  # Update other categories
 
429
  st.session_state.file_paths = []
430
  if 'showSummary' not in st.session_state:
431
  st.session_state.showSummary = 'No'
432
+ if 'speakerClips' not in st.session_state:
433
+ st.session_state.speakerClips = [] # List of dicts: {speaker_label: wav_bytes} per file
434
 
435
 
436
 
 
453
  valid_files = st.session_state.valid_files
454
  file_paths = st.session_state.file_paths
455
  currDF = None
456
+ temp_dir = tempfile.mkdtemp()
 
 
457
 
458
  if uploaded_file_paths is not None and len(uploaded_file_paths) > 0:
459
  print("Found file paths")
 
489
  st.session_state.summaries.append([])
490
  while (len(st.session_state.speakerRenames) < len(valid_files)):
491
  st.session_state.speakerRenames.append({})
492
+ while (len(st.session_state.speakerClips) < len(valid_files)):
493
+ st.session_state.speakerClips.append({})
494
 
495
  st.session_state.file_names = file_names
496
  st.session_state.valid_files = valid_files
 
556
  st.session_state.unusedSpeakers[i] = speakerNames
557
  else:
558
  with st.spinner(text=f'Processing File {i+1} of {totalFiles}'):
559
+ annotations, totalSeconds = processFile(file_paths[i])
560
  print(f"Finished processing {file_paths[i]}")
561
+ st.session_state.results[i] = (annotations, totalSeconds)
562
  print("Results saved")
563
  st.session_state.summaries[i] = {}
564
  print("Summaries saved")
565
  speakerNames = annotations.labels()
566
  st.session_state.unusedSpeakers[i] = speakerNames
567
  print("Speakers saved")
568
+ with st.spinner(text=f'Generating speaker clips for File {i+1} of {totalFiles}'):
569
+ generate_speaker_clips(annotations, file_paths[i], i)
570
+ print(f"Speaker clips generated for {file_paths[i]}")
571
  with st.spinner(text=f'Analyzing File {i+1} of {totalFiles}'):
572
  analyze(file_names[i])
573
  print(f"Finished analyzing {file_paths[i]}")
 
606
  st.session_state.summaries.append([])
607
  while (len(st.session_state.speakerRenames) < len(valid_files)):
608
  st.session_state.speakerRenames.append({})
609
+ while (len(st.session_state.speakerClips) < len(valid_files)):
610
+ st.session_state.speakerClips.append({})
611
 
612
  with st.spinner(text=f'Loading Demo Sample'):
613
  # RTTM load as filler
 
646
  graphNames = ["Data","Voice Categories","Speaker Percentage","Speakers with Categories","Treemap","Timeline","Time Spoken"]
647
  dataTab, pie1, pie2, sunburst1, treemap1, timeline, bar1 = st.tabs(graphNames)
648
  # Handle
649
+ currAnnotation, currTotalTime = st.session_state.results[currFileIndex]
 
 
 
650
  speakerNames = currAnnotation.labels()
651
 
652
  speakers_dataFrame = st.session_state.summaries[currFileIndex]["speakers_dataFrame"]
 
670
 
671
  newCategory = st.sidebar.text_input('Add category', key='categoryInput',on_change=addCategory)
672
 
673
+ st.sidebar.divider()
674
+ st.sidebar.subheader("Rename Speakers")
675
+ st.sidebar.caption("Replace SPEAKER_## labels with real names.")
676
+
677
+ # --- Speaker clip preview ---
678
+ file_clips = (
679
+ st.session_state.speakerClips[currFileIndex]
680
+ if currFileIndex < len(st.session_state.speakerClips)
681
+ else {}
682
  )
683
+ if file_clips:
 
 
684
  st.sidebar.caption(
685
+ "Listen to each speaker's longest clip (up to 5 s) to help identify them."
686
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
 
 
 
 
688
  current_renames = st.session_state.speakerRenames[currFileIndex]
689
  for sp in speakerNames:
690
+ display_label = current_renames.get(sp, sp)
691
+ st.sidebar.markdown(f"**{display_label}**")
692
+ if sp in file_clips:
693
+ st.sidebar.audio(file_clips[sp], format="audio/wav")
694
  current_label = current_renames.get(sp, "")
695
  new_name = st.sidebar.text_input(
696
+ f"Rename {sp}",
697
  value=current_label,
698
  placeholder=f"e.g. John",
699
  key=f"rename_{currFileIndex}_{sp}"
 
1105
  }
1106
  allCategories = copy.deepcopy(st.session_state.categories)
1107
  for i in indices:
1108
+ currAnnotation, currTotalTime = st.session_state.results[i]
1109
  categorySelections = st.session_state["categorySelect"][i]
1110
  catSummary,extraCats = su.calcCategories(currAnnotation,categorySelections)
1111
  st.session_state.summaries[i]["categories"] = (catSummary,extraCats)