Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Update app.py
Browse files
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`
|
| 120 |
-
|
| 121 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
-
|
| 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 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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,
|