Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Update app.py
Browse files
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 550 |
print(f"Finished processing {file_paths[i]}")
|
| 551 |
-
st.session_state.results[i] = (annotations, totalSeconds
|
| 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 |
-
|
| 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 |
-
|
| 662 |
-
|
| 663 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
)
|
| 665 |
-
if
|
| 666 |
-
st.sidebar.divider()
|
| 667 |
-
st.sidebar.subheader("\U0001f3a7 Speaker Samples")
|
| 668 |
st.sidebar.caption(
|
| 669 |
-
"Listen to
|
| 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 |
-
|
| 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)
|