duongthienz commited on
Commit
a5aa376
Β·
verified Β·
1 Parent(s): c56a9e1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +124 -24
app.py CHANGED
@@ -114,19 +114,66 @@ def processFile(filePath):
114
  print("Time in seconds calculated")
115
  return annotations, totalTimeInSeconds, waveform_gain_adjusted, sampleRate
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  def generate_speaker_clips(annotations, waveform, sample_rate, file_index):
118
  """
119
- For each unique speaker in `annotations`, find their longest contiguous segment.
120
- If that segment is >= 5s, clip exactly 5s from its start.
121
- If < 5s, take the whole segment.
122
- Accepts the already-loaded waveform tensor and sample_rate from processFile,
123
- so this never needs to re-decode the audio file (avoids torchcodec/FFmpeg issues).
124
  Saves clips as WAV bytes in st.session_state.speakerClips[file_index].
125
  """
126
- import io
 
 
 
 
 
127
 
128
- # file_index is now a filename string
129
- clips = {}
130
  for speaker in annotations.labels():
131
  speaker_segments = [
132
  segment for segment, _, label in annotations.itertracks(yield_label=True)
@@ -135,26 +182,61 @@ def generate_speaker_clips(annotations, waveform, sample_rate, file_index):
135
  if not speaker_segments:
136
  continue
137
 
 
 
 
138
  longest = max(speaker_segments, key=lambda s: s.duration)
 
 
 
 
 
 
 
139
 
140
- clip_start = longest.start
141
- clip_duration = min(longest.duration, 5.0)
142
- clip_end = clip_start + clip_duration
143
 
144
- start_sample = int(clip_start * sample_rate)
145
- end_sample = min(int(clip_end * sample_rate), waveform.shape[-1])
 
 
 
 
 
146
 
147
- clip_waveform = waveform[:, start_sample:end_sample]
 
 
 
148
 
149
- import soundfile as sf
150
- clip_np = clip_waveform.numpy().T # (channels, samples) -> (samples, channels)
151
- buffer = io.BytesIO()
152
- sf.write(buffer, clip_np, sample_rate, format="WAV", subtype="PCM_16")
153
- buffer.seek(0)
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
@@ -418,7 +500,11 @@ if 'file_paths' not in st.session_state:
418
  if 'showSummary' not in st.session_state:
419
  st.session_state.showSummary = 'No'
420
  if 'speakerClips' not in st.session_state:
421
- st.session_state.speakerClips = {} # {filename: {speaker: wav_bytes}}
 
 
 
 
422
  if 'analyzeAllToggle' not in st.session_state:
423
  st.session_state.analyzeAllToggle = False
424
 
@@ -610,6 +696,8 @@ if st.session_state.analyzeAllToggle == True:
610
  st.session_state.unusedSpeakers[fname] = list(annotations.labels())
611
  with st.spinner(text=f'Generating speaker clips for File {i+1} of {totalFiles}'):
612
  generate_speaker_clips(annotations, waveform, sample_rate, fname)
 
 
613
  del waveform
614
  print(f"Speaker clips generated for {fpath}")
615
  with st.spinner(text=f'Analyzing File {i+1} of {totalFiles}'):
@@ -678,7 +766,8 @@ try:
678
  file_clips = st.session_state.speakerClips.get(currFile, {})
679
  if file_clips:
680
  st.sidebar.caption(
681
- "Listen to each speaker's longest clip (up to 5 s) to help identify them."
 
682
  )
683
 
684
  current_renames = st.session_state.speakerRenames[currFile]
@@ -692,6 +781,17 @@ try:
692
  st.sidebar.markdown(f"**{display_label}**")
693
  if sp in file_clips:
694
  st.sidebar.audio(file_clips[sp], format="audio/wav")
 
 
 
 
 
 
 
 
 
 
 
695
  # Label is always the fixed original sp so Streamlit never recreates the widget
696
  new_name = st.sidebar.text_input(
697
  sp,
 
114
  print("Time in seconds calculated")
115
  return annotations, totalTimeInSeconds, waveform_gain_adjusted, sampleRate
116
 
117
+ def _extract_clip_bytes(waveform, sample_rate, seg_start, seg_end):
118
+ """
119
+ Extract a 3–5 s clip from [seg_start, seg_end] by finding the loudest
120
+ RMS window within that range. Returns raw WAV bytes.
121
+ """
122
+ import io
123
+ import soundfile as sf
124
+
125
+ CLIP_MIN = 3.0
126
+ CLIP_MAX = 5.0
127
+ STEP = 0.5 # scanning step in seconds
128
+
129
+ total_samples = waveform.shape[-1]
130
+ seg_start_s = int(seg_start * sample_rate)
131
+ seg_end_s = min(int(seg_end * sample_rate), total_samples)
132
+ seg_len_s = seg_end_s - seg_start_s
133
+
134
+ # Duration of this segment in seconds
135
+ seg_dur = (seg_end_s - seg_start_s) / sample_rate
136
+
137
+ # Clip duration: between CLIP_MIN and CLIP_MAX, capped by segment length
138
+ clip_dur = min(max(min(seg_dur, CLIP_MAX), CLIP_MIN), seg_dur)
139
+ clip_samples = int(clip_dur * sample_rate)
140
+
141
+ best_start = seg_start_s
142
+ best_rms = -1.0
143
+
144
+ # Slide a window and pick the loudest position
145
+ step_samples = int(STEP * sample_rate)
146
+ pos = seg_start_s
147
+ while pos + clip_samples <= seg_end_s:
148
+ window = waveform[:, pos: pos + clip_samples].float()
149
+ rms = float(window.pow(2).mean().sqrt())
150
+ if rms > best_rms:
151
+ best_rms = rms
152
+ best_start = pos
153
+ pos += step_samples
154
+
155
+ clip_waveform = waveform[:, best_start: best_start + clip_samples]
156
+ clip_np = clip_waveform.numpy().T # (samples, channels)
157
+ buf = io.BytesIO()
158
+ sf.write(buf, clip_np, sample_rate, format="WAV", subtype="PCM_16")
159
+ buf.seek(0)
160
+ return buf.read()
161
+
162
+
163
  def generate_speaker_clips(annotations, waveform, sample_rate, file_index):
164
  """
165
+ For each unique speaker in `annotations`:
166
+ - Store all their segments in st.session_state.speakerSegments[file_index][speaker].
167
+ - Pick the loudest 3–5 s window within their longest segment as the default clip.
 
 
168
  Saves clips as WAV bytes in st.session_state.speakerClips[file_index].
169
  """
170
+ # Initialise speakerSegments store if needed
171
+ if 'speakerSegments' not in st.session_state:
172
+ st.session_state.speakerSegments = {}
173
+
174
+ clips = {}
175
+ segments = {}
176
 
 
 
177
  for speaker in annotations.labels():
178
  speaker_segments = [
179
  segment for segment, _, label in annotations.itertracks(yield_label=True)
 
182
  if not speaker_segments:
183
  continue
184
 
185
+ # Persist all segments so the randomize button can draw from them later
186
+ segments[speaker] = [(s.start, s.end) for s in speaker_segments]
187
+
188
  longest = max(speaker_segments, key=lambda s: s.duration)
189
+ clips[speaker] = _extract_clip_bytes(
190
+ waveform, sample_rate, longest.start, longest.end
191
+ )
192
+
193
+ st.session_state.speakerClips[file_index] = clips
194
+ st.session_state.speakerSegments[file_index] = segments
195
+ print(f"Generated {len(clips)} speaker clips for {file_index}")
196
 
 
 
 
197
 
198
+ def randomize_speaker_clip(file_index, speaker):
199
+ """
200
+ Pick a random segment (weighted by duration) for `speaker` and extract
201
+ a random 3–5 s window from it. Updates speakerClips in session_state.
202
+ Requires that st.session_state.speakerWaveforms[file_index] is present.
203
+ """
204
+ import random
205
 
206
+ segs = st.session_state.speakerSegments.get(file_index, {}).get(speaker)
207
+ waveform_data = st.session_state.speakerWaveforms.get(file_index)
208
+ if not segs or waveform_data is None:
209
+ return
210
 
211
+ waveform, sample_rate = waveform_data
 
 
 
 
 
212
 
213
+ CLIP_MIN = 3.0
214
+ CLIP_MAX = 5.0
215
+
216
+ # Weight selection by segment duration so longer segments are more likely
217
+ durations = [max(e - s, 0.01) for s, e in segs]
218
+ total_dur = sum(durations)
219
+ rand_val = random.random() * total_dur
220
+ cumulative = 0.0
221
+ chosen_start, chosen_end = segs[0]
222
+ for (seg_s, seg_e), dur in zip(segs, durations):
223
+ cumulative += dur
224
+ if rand_val <= cumulative:
225
+ chosen_start, chosen_end = seg_s, seg_e
226
+ break
227
+
228
+ seg_dur = chosen_end - chosen_start
229
+ clip_dur = min(max(min(seg_dur, CLIP_MAX), CLIP_MIN), seg_dur)
230
+
231
+ # Random offset within the chosen segment
232
+ max_offset = max(seg_dur - clip_dur, 0.0)
233
+ offset = random.uniform(0.0, max_offset)
234
+ clip_start = chosen_start + offset
235
+ clip_end = clip_start + clip_dur
236
+
237
+ new_clip = _extract_clip_bytes(waveform, sample_rate, clip_start, clip_end)
238
+ st.session_state.speakerClips[file_index][speaker] = new_clip
239
+ print(f"Randomized clip for {speaker} in {file_index}: {clip_start:.2f}–{clip_end:.2f}s")
240
 
241
  def addCategory():
242
  newCategory = st.session_state.categoryInput
 
500
  if 'showSummary' not in st.session_state:
501
  st.session_state.showSummary = 'No'
502
  if 'speakerClips' not in st.session_state:
503
+ st.session_state.speakerClips = {} # {filename: {speaker: wav_bytes}}
504
+ if 'speakerSegments' not in st.session_state:
505
+ st.session_state.speakerSegments = {} # {filename: {speaker: [(start,end), ...]}}
506
+ if 'speakerWaveforms' not in st.session_state:
507
+ st.session_state.speakerWaveforms = {} # {filename: (waveform_tensor, sample_rate)}
508
  if 'analyzeAllToggle' not in st.session_state:
509
  st.session_state.analyzeAllToggle = False
510
 
 
696
  st.session_state.unusedSpeakers[fname] = list(annotations.labels())
697
  with st.spinner(text=f'Generating speaker clips for File {i+1} of {totalFiles}'):
698
  generate_speaker_clips(annotations, waveform, sample_rate, fname)
699
+ # Keep a reference so the "Try Another Clip" button can re-sample later
700
+ st.session_state.speakerWaveforms[fname] = (waveform, sample_rate)
701
  del waveform
702
  print(f"Speaker clips generated for {fpath}")
703
  with st.spinner(text=f'Analyzing File {i+1} of {totalFiles}'):
 
766
  file_clips = st.session_state.speakerClips.get(currFile, {})
767
  if file_clips:
768
  st.sidebar.caption(
769
+ "Listen to a short clip (3–5 s) to help identify each speaker. "
770
+ "If a clip sounds silent or unclear, press πŸ”€ to try a different one."
771
  )
772
 
773
  current_renames = st.session_state.speakerRenames[currFile]
 
781
  st.sidebar.markdown(f"**{display_label}**")
782
  if sp in file_clips:
783
  st.sidebar.audio(file_clips[sp], format="audio/wav")
784
+ # Only show randomize button if there are multiple segments to draw from
785
+ sp_segs = st.session_state.speakerSegments.get(currFile, {}).get(sp, [])
786
+ has_waveform = currFile in st.session_state.speakerWaveforms
787
+ if has_waveform and len(sp_segs) >= 1:
788
+ if st.sidebar.button(
789
+ "πŸ”€ Try Another Clip",
790
+ key=f"randomize_{currFile}_{sp}",
791
+ help="Pick a random clip from a different part of this speaker's audio",
792
+ ):
793
+ randomize_speaker_clip(currFile, sp)
794
+ st.rerun()
795
  # Label is always the fixed original sp so Streamlit never recreates the widget
796
  new_name = st.sidebar.text_input(
797
  sp,