duongthienz commited on
Commit
7f3fca2
·
verified ·
1 Parent(s): c39c912

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +62 -348
app.py CHANGED
@@ -2,37 +2,33 @@
2
  app.py — Streamlit entry point.
3
 
4
  Responsibilities:
5
- - App-level config and constants
6
- - Pipeline / device initialisation
7
- - Top-level UI layout and navigation flow
8
- - Delegates all logic to state.py (callbacks) and utils.py (pure helpers)
9
  """
10
 
11
  import os
12
- import time
13
  import tempfile
14
  from pathlib import Path
15
 
16
  import streamlit as st
17
  import torch
18
  import pandas as pd
19
- import plotly.express as px
20
  from pyannote.audio import Pipeline
21
 
22
  import sonogram_utility as su
23
  import utils
24
- import state
25
  from state import (
26
- init_session_state, printV,
27
  get_display_name, apply_speaker_renames_to_df, convert_df,
28
- addCategory, removeCategory, updateCategoryOptions,
29
- applyGlobalRenames, addGlobalRename, removeGlobalRename, _global_rename_key,
30
- updateMultiSelect, store_speaker_clips, randomize_speaker_clip,
31
- register_file, analyze,
32
  )
33
 
34
  # ---------------------------------------------------------------------------
35
- # App-level constants
36
  # ---------------------------------------------------------------------------
37
 
38
  SUPPORTED_FILE_TYPES = (".wav", ".mp3", ".mp4", ".txt", ".rttm", ".csv")
@@ -58,7 +54,7 @@ MULTI_DEMO_PATHS = [
58
  ]
59
 
60
  # ---------------------------------------------------------------------------
61
- # Device / pipeline initialisation (runs once per server process)
62
  # ---------------------------------------------------------------------------
63
 
64
  torch.classes.__path__ = [os.path.join(torch.__path__[0], torch.classes.__file__)]
@@ -78,11 +74,20 @@ pipeline = Pipeline.from_pretrained("pyannote/speaker-diarization-3.1")
78
  pipeline.to(device)
79
 
80
  # ---------------------------------------------------------------------------
81
- # Session state
82
  # ---------------------------------------------------------------------------
83
 
84
  init_session_state()
85
 
 
 
 
 
 
 
 
 
 
86
  # ---------------------------------------------------------------------------
87
  # Page header
88
  # ---------------------------------------------------------------------------
@@ -93,47 +98,23 @@ if not isGPU:
93
 
94
  st.write(
95
  'If you would like to see a sample result or multiple sample results generated from '
96
- 'real classroom audio, '
97
- 'select "Single File Demo" or "Multiple Files Demo" on the left sidebar.'
98
  )
99
  st.markdown(
100
  "<p style='margin-bottom:4px;'>Keep in mind that this is a very early draft of the tool. "
101
  "Please be patient with any bugs/errors, and email Connor Young at "
102
  "<a href='mailto:czyoung@ualr.edu'>czyoung@ualr.edu</a> if you need help using the tool!</p>"
103
- "<hr style='margin-top:8px; margin-bottom:8px;'>",
104
- unsafe_allow_html=True,
105
- )
106
-
107
- st.markdown(
108
- "<style>"
109
- "details > summary { font-size: 1rem; font-weight: 500; }"
110
- ".stTabs [data-baseweb='tab'] { font-size: 1rem; }"
111
- ".stFileUploader label { font-size: 1rem; }"
112
- "</style>",
113
  unsafe_allow_html=True,
114
  )
115
 
116
  with st.expander("Instructions and additional details"):
117
- st.write(
118
- "Thank you for viewing our experimental app! "
119
- "The overall presentations and features are expected to be improved over time."
120
- )
121
- st.write(
122
- "To use this app:\n"
123
- "1. Upload an audio file for live analysis. Alternatively, upload an already "
124
- "generated [rttm file](https://stackoverflow.com/questions/30975084/rttm-file-format)"
125
- )
126
  st.write("2. Press Analyze All. No data is saved on our side.")
127
- st.write(
128
- "3. Use the sidebar to select your file. "
129
- "Multiple files are supported for more comprehensive analysis."
130
- )
131
  st.write("4. Use the tabs to view different visualizations. Each can be downloaded.")
132
- st.write(
133
- "4a. Graphs are built with [plotly](https://plotly.com/). "
134
- "Double-click to reset. "
135
- "[More examples](https://plotly.com/python/basic-charts/)."
136
- )
137
 
138
  # ---------------------------------------------------------------------------
139
  # File upload
@@ -164,43 +145,18 @@ file_names = st.session_state.file_names
164
  file_paths_dict = st.session_state.file_paths
165
 
166
  # ---------------------------------------------------------------------------
167
- # Sidebar demo buttons
168
  # ---------------------------------------------------------------------------
169
 
170
  isDemo = False
171
 
172
  if st.sidebar.button("Single File Demo"):
173
- dname = DEMO_PATH.split("/")[-1]
174
- register_file(dname)
175
- st.session_state.file_paths[dname] = DEMO_PATH
176
- file_names = st.session_state.file_names
177
- start_time = time.time()
178
- with st.spinner("Loading Demo Sample"):
179
- _, annotations = su.loadAudioRTTM(DEMO_PATH)
180
- totalSeconds = max((seg.end for seg in annotations.itersegments()), default=0)
181
- st.session_state.results[dname] = (annotations, totalSeconds)
182
- st.session_state.summaries[dname] = {}
183
- st.session_state.unusedSpeakers[dname] = list(annotations.labels())
184
- with st.spinner("Analyzing Demo Data"):
185
- analyze(dname)
186
- st.success(f"Took {time.time() - start_time:.1f}s to analyze the demo file!")
187
- st.session_state.select_currFile = dname
188
  isDemo = True
189
 
190
  if st.sidebar.button("Multiple Files Demo"):
191
- for demo_path in MULTI_DEMO_PATHS:
192
- dname = demo_path.split("/")[-1]
193
- register_file(dname)
194
- st.session_state.file_paths[dname] = demo_path
195
- file_names = st.session_state.file_names
196
- with st.spinner(f"Loading: {dname}"):
197
- _, annotations = su.loadAudioRTTM(demo_path)
198
- totalSeconds = max((seg.end for seg in annotations.itersegments()), default=0)
199
- st.session_state.results[dname] = (annotations, totalSeconds)
200
- st.session_state.summaries[dname] = {}
201
- st.session_state.unusedSpeakers[dname] = list(annotations.labels())
202
- isDemo = True
203
- st.session_state.analyzeAllToggle = True
204
 
205
  # ---------------------------------------------------------------------------
206
  # Analyze All / Reset buttons
@@ -224,56 +180,12 @@ else:
224
  # ---------------------------------------------------------------------------
225
 
226
  if st.session_state.analyzeAllToggle:
227
- start_time = time.time()
228
- totalFiles = len(file_names)
229
-
230
- for i, fname in enumerate(file_names):
231
- fpath = file_paths_dict.get(fname, "")
232
- ext = fpath.lower()
233
-
234
- if ext.endswith(".txt"):
235
- with st.spinner(f"Loading TXT {i+1}/{totalFiles}"):
236
- _, annotations = su.loadAudioTXT(fpath)
237
- totalSeconds = max((s.end for s in annotations.itersegments()), default=0)
238
- st.session_state.results[fname] = (annotations, totalSeconds)
239
- st.session_state.summaries[fname] = {}
240
- st.session_state.unusedSpeakers[fname] = list(annotations.labels())
241
-
242
- elif ext.endswith(".rttm"):
243
- with st.spinner(f"Loading RTTM {i+1}/{totalFiles}"):
244
- _, annotations = su.loadAudioRTTM(fpath)
245
- totalSeconds = max((s.end for s in annotations.itersegments()), default=0)
246
- st.session_state.results[fname] = (annotations, totalSeconds)
247
- st.session_state.summaries[fname] = {}
248
- st.session_state.unusedSpeakers[fname] = list(annotations.labels())
249
-
250
- elif ext.endswith(".csv"):
251
- with st.spinner(f"Loading CSV {i+1}/{totalFiles}"):
252
- _, annotations = su.loadAudioCSV(fpath)
253
- totalSeconds = max((s.end for s in annotations.itersegments()), default=0)
254
- st.session_state.results[fname] = (annotations, totalSeconds)
255
- st.session_state.summaries[fname] = {}
256
- st.session_state.unusedSpeakers[fname] = list(annotations.labels())
257
-
258
- else:
259
- with st.spinner(f"Processing Audio {i+1}/{totalFiles}"):
260
- annotations, totalSeconds, waveform, sample_rate = utils.processFile(
261
- fpath, pipeline, ENABLE_DENOISE, EARLY_CLEANUP,
262
- GAIN_WINDOW, MINIMUM_GAIN, MAXIMUM_GAIN,
263
- dfModel, dfState, ATTEN_LIM_DB,
264
- )
265
- st.session_state.results[fname] = (annotations, totalSeconds)
266
- st.session_state.summaries[fname] = {}
267
- st.session_state.unusedSpeakers[fname] = list(annotations.labels())
268
- with st.spinner(f"Generating clips {i+1}/{totalFiles}"):
269
- store_speaker_clips(fname, annotations, waveform, sample_rate)
270
- del waveform
271
-
272
- with st.spinner(f"Analyzing {i+1}/{totalFiles}"):
273
- analyze(fname)
274
-
275
- st.success(f"Analyzed {totalFiles} file(s) in {time.time() - start_time:.1f}s")
276
- st.session_state.analyzeAllToggle = False
277
 
278
  # ---------------------------------------------------------------------------
279
  # File selector
@@ -320,8 +232,8 @@ try:
320
  unusedSpeakers = st.session_state.unusedSpeakers[currFile]
321
  categorySelections = st.session_state.categorySelect[currFile]
322
 
323
- _saved_renames = st.session_state.speakerRenames.get(currFile, {})
324
- raw_to_display = {sp: _saved_renames.get(sp, sp) for sp in speakerNames}
325
  all_speakers_display = [raw_to_display[sp] for sp in speakerNames]
326
 
327
  catTypeColors = su.colorsCSS(3)
@@ -329,50 +241,16 @@ try:
329
  speakerColors = allColors[:len(speakerNames)]
330
  catColors = allColors[len(speakerNames):]
331
 
332
- # Rebuild live df4 to reflect current category selections
333
  nameList = st.session_state.categories
334
  valueList = [su.sumTimes(currAnnotation.subset(s)) for s in categorySelections]
335
  extraNames = list(unusedSpeakers)
336
  extraValues = [su.sumTimes(currAnnotation.subset([sp])) for sp in unusedSpeakers]
337
- df4_live = pd.DataFrame({"names": nameList + extraNames, "values": valueList + extraValues})
338
- st.session_state.summaries[currFile]["df4"] = df4_live
339
-
340
- # -----------------------------------------------------------------------
341
- # Sidebar — categories
342
- # -----------------------------------------------------------------------
343
-
344
- for i, category in enumerate(st.session_state.categories):
345
- ms_key = f"multiselect_{category}"
346
- speakerSet = categorySelections[i]
347
- default_disp = [raw_to_display.get(sp, sp) for sp in speakerSet]
348
- if ms_key not in st.session_state:
349
- st.session_state[ms_key] = default_disp
350
- st.sidebar.multiselect(
351
- category, all_speakers_display,
352
- key=ms_key, on_change=updateCategoryOptions, args=(currFile,),
353
- )
354
- st.sidebar.button(
355
- f"Remove {category}", key=f"remove_{category}",
356
- on_click=removeCategory, args=(i,),
357
- )
358
-
359
- st.sidebar.text_input("Add category", key="categoryInput", on_change=addCategory)
360
-
361
- # -----------------------------------------------------------------------
362
- # Sidebar — rename speakers
363
- # -----------------------------------------------------------------------
364
-
365
- st.sidebar.divider()
366
- st.sidebar.subheader("Rename Speakers")
367
- st.sidebar.markdown(
368
- "<p style='font-size:0.85rem; color:gray; margin-bottom:2px;'>"
369
- "Assign a name and select which speaker labels (across all files) it applies to. "
370
- "Changes apply to all matched speakers instantly.</p>",
371
- unsafe_allow_html=True,
372
  )
373
 
374
-
375
-
376
  all_speaker_tokens = [
377
  f"{fn}: {sp}"
378
  for fn in st.session_state.file_names
@@ -380,57 +258,12 @@ try:
380
  for sp in st.session_state.results[fn][0].labels()
381
  ]
382
 
383
- st.sidebar.divider()
384
-
385
- def _on_grename_change(idx):
386
- st.session_state.globalRenames[idx]["speakers"] = list(
387
- st.session_state[_global_rename_key(idx)]
388
- )
389
- applyGlobalRenames()
390
-
391
- for idx, entry in enumerate(st.session_state.globalRenames):
392
- grkey = _global_rename_key(idx)
393
- if grkey not in st.session_state:
394
- st.session_state[grkey] = list(entry["speakers"])
395
- st.sidebar.markdown(f"**{entry['name']}**")
396
- st.sidebar.multiselect(
397
- f"Speakers for {entry['name']}", options=all_speaker_tokens,
398
- key=grkey, on_change=_on_grename_change, args=(idx,),
399
- label_visibility="collapsed",
400
- )
401
- st.sidebar.button(
402
- f"Remove '{entry['name']}'", key=f"remove_grename_{idx}",
403
- on_click=removeGlobalRename, args=(idx,),
404
- )
405
-
406
- st.sidebar.text_input(
407
- "Add rename", placeholder="e.g. John",
408
- key="globalRenameInput", on_change=addGlobalRename,
409
- )
410
-
411
  # -----------------------------------------------------------------------
412
- # Shared helper: render figure + PDF/SVG download buttons
413
  # -----------------------------------------------------------------------
414
 
415
- def _render_chart(fig, tab, pdf_path, svg_path, pdf_name, svg_name, pdf_key, svg_key):
416
- with tab:
417
- st.plotly_chart(fig, use_container_width=True, config=PLOTLY_CONFIG)
418
- col_l, col_r = st.columns(2)
419
- try:
420
- fig.write_image(pdf_path)
421
- fig.write_image(svg_path)
422
- except Exception:
423
- pass
424
- with col_l:
425
- if os.path.exists(pdf_path):
426
- with open(pdf_path, "rb") as f:
427
- st.download_button("Save As PDF", f, pdf_name, "application/pdf",
428
- key=pdf_key, on_click="ignore")
429
- with col_r:
430
- if os.path.exists(svg_path):
431
- with open(svg_path, "rb") as f:
432
- st.download_button("Save As SVG", f, svg_name, "image/svg+xml",
433
- key=svg_key, on_click="ignore")
434
 
435
  # -----------------------------------------------------------------------
436
  # Tab: Data
@@ -444,182 +277,66 @@ try:
444
  f"sonogram-analysis-{currPlainName}.csv", "text/csv",
445
  key="download-csv", on_click="ignore",
446
  )
447
-
448
- # Build the display version of the table:
449
- # - Rename "Resource" -> "Speaker"
450
- # - Drop "Task" if present
451
- # - Format Start / Finish as HH:MM:SS strings
452
- def _fmt_seconds(val):
453
- try:
454
- secs = float(val)
455
- except (TypeError, ValueError):
456
- return str(val)
457
- h = int(secs // 3600)
458
- m = int(secs % 3600 // 60)
459
- s = int(secs % 60)
460
- cs = round((secs % 1) * 100)
461
- return f"{h:02d}:{m:02d}:{s:02d}.{cs:02d}"
462
-
463
- tableDF = displayDF.copy()
464
- if "Task" in tableDF.columns:
465
- tableDF = tableDF.drop(columns=["Task"])
466
- if "Start" in tableDF.columns:
467
- tableDF["Start"] = tableDF["Start"].apply(_fmt_seconds)
468
- if "Finish" in tableDF.columns:
469
- tableDF["Finish"] = tableDF["Finish"].apply(_fmt_seconds)
470
- tableDF = tableDF.rename(columns={"Resource": "Speaker"})
471
-
472
- file_clips = st.session_state.speakerClips.get(currFile, {})
473
- has_clips = bool(file_clips)
474
- has_waveform = currFile in st.session_state.speakerWaveforms
475
-
476
- # Column layout: Speaker | remaining cols | [Clip | 🔀] if clips exist
477
- other_cols = [c for c in tableDF.columns if c != "Speaker"]
478
- has_extras = has_clips
479
-
480
- if has_extras:
481
- col_widths = [2] + [1] * len(other_cols) + [2, 1]
482
- else:
483
- col_widths = [2] + [1] * len(other_cols)
484
-
485
- # Header row
486
- header_cols = st.columns(col_widths)
487
- header_cols[0].markdown("**Speaker**")
488
- for i, col_name in enumerate(other_cols):
489
- header_cols[i + 1].markdown(f"**{col_name}**")
490
- if has_extras:
491
- header_cols[-2].markdown("**Clip**")
492
- header_cols[-1].markdown("**&nbsp;**", unsafe_allow_html=True)
493
-
494
- st.markdown("<hr style='margin-top:2px; margin-bottom:4px;'>", unsafe_allow_html=True)
495
-
496
- # Track which speakers have already had their clip rendered
497
- # so we only show it once (on the first row for that speaker).
498
- rendered_clip_for: set = set()
499
-
500
- # ~52px per row gives roughly 10 visible rows before scrolling
501
- with st.container(height=480, border=False):
502
- for _, row in tableDF.iterrows():
503
- row_cols = st.columns(col_widths)
504
- display_sp = row["Speaker"]
505
-
506
- # Map display name back to original SPEAKER_## for clip lookup
507
- raw_key = next(
508
- (sp for sp in speakerNames if raw_to_display.get(sp, sp) == display_sp),
509
- display_sp,
510
- )
511
-
512
- row_cols[0].write(display_sp)
513
- for i, col_name in enumerate(other_cols):
514
- row_cols[i + 1].write(row[col_name])
515
-
516
- if has_extras:
517
- if raw_key in file_clips and raw_key not in rendered_clip_for:
518
- rendered_clip_for.add(raw_key)
519
- row_cols[-2].audio(file_clips[raw_key], format="audio/wav")
520
- sp_segs = st.session_state.speakerSegments.get(currFile, {}).get(raw_key, [])
521
- if has_waveform and sp_segs:
522
- if row_cols[-1].button(
523
- "🔀",
524
- key=f"randomize_{currFile}_{raw_key}",
525
- help="Try a different clip for this speaker",
526
- ):
527
- randomize_speaker_clip(currFile, raw_key)
528
- st.rerun()
529
 
530
  # -----------------------------------------------------------------------
531
- # Charts (pie1, pie2, sunburst, treemap, timeline, bar)
532
  # -----------------------------------------------------------------------
533
 
534
- df3 = st.session_state.summaries[currFile]["df3"]
535
  df4 = st.session_state.summaries[currFile]["df4"].copy()
536
  df5 = st.session_state.summaries[currFile]["df5"].copy()
537
  df2 = st.session_state.summaries[currFile]["df2"].copy()
538
 
539
- _render_chart(
540
  utils.build_fig_pie2(df4, speakerNames, speakerColors, catColors, get_display_name, currFile),
541
  pie2,
542
  "ascn_pie2.pdf", "ascn_pie2.svg",
543
  f"sonogram-speaker-percent-{currPlainName}.pdf",
544
  f"sonogram-speaker-percent-{currPlainName}.svg",
545
- "download-pdf2", "download-svg2",
546
  )
547
- _render_chart(
548
  utils.build_fig_sunburst(df5, catTypeColors, speakerColors, get_display_name, currFile),
549
  sunburst1,
550
  "ascn_sunburst.pdf", "ascn_sunburst.svg",
551
  f"sonogram-speaker-categories-{currPlainName}.pdf",
552
  f"sonogram-speaker-categories-{currPlainName}.svg",
553
- "download-pdf3", "download-svg3",
554
  )
555
- _render_chart(
556
  utils.build_fig_treemap(df5, catTypeColors, speakerColors, get_display_name, currFile),
557
  treemap1,
558
  "ascn_treemap.pdf", "ascn_treemap.svg",
559
  f"sonogram-treemap-{currPlainName}.pdf",
560
  f"sonogram-treemap-{currPlainName}.svg",
561
- "download-pdf4", "download-svg4",
562
  )
563
- _render_chart(
564
  utils.build_fig_timeline(speakers_dataFrame, currTotalTime, speakerColors, get_display_name, currFile),
565
  timeline,
566
  "ascn_timeline.pdf", "ascn_timeline.svg",
567
  f"sonogram-timeline-{currPlainName}.pdf",
568
  f"sonogram-timeline-{currPlainName}.svg",
569
- "download-pdf5", "download-svg5",
570
  )
571
- _render_chart(
572
  utils.build_fig_bar(df2, catColors, speakerColors, get_display_name, currFile),
573
  bar1,
574
  "ascn_bar.pdf", "ascn_bar.svg",
575
  f"sonogram-speaker-time-{currPlainName}.pdf",
576
  f"sonogram-speaker-time-{currPlainName}.svg",
577
- "download-pdf6", "download-svg6",
578
  )
579
 
580
  except ValueError:
581
  pass
582
 
583
  # ---------------------------------------------------------------------------
584
- # Multi-file summary
585
  # ---------------------------------------------------------------------------
586
 
587
- if len(st.session_state.results) > 0:
588
- with st.expander("Multi-file Summary Data"):
589
- st.header("Multi-file Summary Data")
590
- with st.spinner("Processing summary results..."):
591
- validNames = [
592
- fn for fn in st.session_state.file_names
593
- if fn in st.session_state.results
594
- and len(st.session_state.results[fn]) == 2
595
- ]
596
- if len(validNames) > 1:
597
- df6, allCategories = utils.build_multifile_category_df(
598
- validNames, st.session_state.results, st.session_state.summaries,
599
- st.session_state.categories, st.session_state.categorySelect,
600
- )
601
- st.plotly_chart(
602
- px.bar(df6, x="files", y=allCategories,
603
- title="Time Spoken by Each Speaker in Each File"),
604
- use_container_width=True, config=PLOTLY_CONFIG,
605
- )
606
-
607
- df7, _ = utils.build_multifile_voice_df(validNames, st.session_state.summaries)
608
- for sort_cols, ascending, title in [
609
- (["One Voice", "Multi Voice"], True, "Cross-file Voice Categories sorted for One Voice"),
610
- (["Multi Voice","One Voice"], True, "Cross-file Voice Categories sorted for Multi Voice"),
611
- (["No Voice", "Multi Voice"], False, "Cross-file Voice Categories sorted for Any Voice"),
612
- ]:
613
- st.plotly_chart(
614
- px.bar(df7.sort_values(by=sort_cols, ascending=ascending),
615
- x="files", y=["One Voice", "Multi Voice", "No Voice"],
616
- title=title),
617
- use_container_width=True, config=PLOTLY_CONFIG,
618
- )
619
-
620
- # ---------------------------------------------------------------------------
621
- # FAQ
622
- # ---------------------------------------------------------------------------
623
 
624
  with st.expander("(Potentially) FAQ"):
625
  st.write("**1. I tried analyzing a file, but the page refreshed and nothing happened! Why?**")
@@ -634,8 +351,5 @@ with st.expander("(Potentially) FAQ"):
634
  st.write("We are securing funding for permanent GPU access. Until then, CPU analysis may take a very long time.")
635
 
636
  st.divider()
637
- st.write(
638
- "Would you like additional data, charts, or features? "
639
- "[Tell us about our project!](https://forms.gle/A32CdfGYSZoMPyyX9)"
640
- )
641
- st.write("If you would like to learn more or work with us, contact Dr. Mark Baillie at mtbaillie@ualr.edu")
 
2
  app.py — Streamlit entry point.
3
 
4
  Responsibilities:
5
+ - Constants and pipeline initialisation
6
+ - Page header and file upload
7
+ - Demo / Analyze buttons and analysis loop (via state.py)
8
+ - Per-file view: sidebar + tabs (via ui.py and utils.py)
9
  """
10
 
11
  import os
 
12
  import tempfile
13
  from pathlib import Path
14
 
15
  import streamlit as st
16
  import torch
17
  import pandas as pd
 
18
  from pyannote.audio import Pipeline
19
 
20
  import sonogram_utility as su
21
  import utils
22
+ import ui
23
  from state import (
24
+ init_session_state,
25
  get_display_name, apply_speaker_renames_to_df, convert_df,
26
+ updateMultiSelect, store_speaker_clips, register_file, analyze,
27
+ load_demo_single, load_demo_multi, run_analysis_loop, build_table_df,
 
 
28
  )
29
 
30
  # ---------------------------------------------------------------------------
31
+ # Constants
32
  # ---------------------------------------------------------------------------
33
 
34
  SUPPORTED_FILE_TYPES = (".wav", ".mp3", ".mp4", ".txt", ".rttm", ".csv")
 
54
  ]
55
 
56
  # ---------------------------------------------------------------------------
57
+ # Pipeline initialisation (once per server process)
58
  # ---------------------------------------------------------------------------
59
 
60
  torch.classes.__path__ = [os.path.join(torch.__path__[0], torch.classes.__file__)]
 
74
  pipeline.to(device)
75
 
76
  # ---------------------------------------------------------------------------
77
+ # Session state + global styles
78
  # ---------------------------------------------------------------------------
79
 
80
  init_session_state()
81
 
82
+ st.markdown(
83
+ "<style>"
84
+ "details > summary { font-size: 1rem; font-weight: 500; }"
85
+ ".stTabs [data-baseweb='tab'] { font-size: 1rem; }"
86
+ ".stFileUploader label { font-size: 1rem; }"
87
+ "</style>",
88
+ unsafe_allow_html=True,
89
+ )
90
+
91
  # ---------------------------------------------------------------------------
92
  # Page header
93
  # ---------------------------------------------------------------------------
 
98
 
99
  st.write(
100
  'If you would like to see a sample result or multiple sample results generated from '
101
+ 'real classroom audio, select "Single File Demo" or "Multiple Files Demo" on the left sidebar.'
 
102
  )
103
  st.markdown(
104
  "<p style='margin-bottom:4px;'>Keep in mind that this is a very early draft of the tool. "
105
  "Please be patient with any bugs/errors, and email Connor Young at "
106
  "<a href='mailto:czyoung@ualr.edu'>czyoung@ualr.edu</a> if you need help using the tool!</p>"
107
+ "<hr style='margin-top:16px; margin-bottom:16px;'>",
 
 
 
 
 
 
 
 
 
108
  unsafe_allow_html=True,
109
  )
110
 
111
  with st.expander("Instructions and additional details"):
112
+ st.write("Thank you for viewing our experimental app! The overall presentations and features are expected to be improved over time.")
113
+ st.write("To use this app:\n1. Upload an audio file for live analysis. Alternatively, upload an already generated [rttm file](https://stackoverflow.com/questions/30975084/rttm-file-format)")
 
 
 
 
 
 
 
114
  st.write("2. Press Analyze All. No data is saved on our side.")
115
+ st.write("3. Use the sidebar to select your file. Multiple files are supported for more comprehensive analysis.")
 
 
 
116
  st.write("4. Use the tabs to view different visualizations. Each can be downloaded.")
117
+ st.write("4a. Graphs are built with [plotly](https://plotly.com/). Double-click to reset. [More examples](https://plotly.com/python/basic-charts/).")
 
 
 
 
118
 
119
  # ---------------------------------------------------------------------------
120
  # File upload
 
145
  file_paths_dict = st.session_state.file_paths
146
 
147
  # ---------------------------------------------------------------------------
148
+ # Sidebar: demo buttons
149
  # ---------------------------------------------------------------------------
150
 
151
  isDemo = False
152
 
153
  if st.sidebar.button("Single File Demo"):
154
+ load_demo_single(DEMO_PATH)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  isDemo = True
156
 
157
  if st.sidebar.button("Multiple Files Demo"):
158
+ load_demo_multi(MULTI_DEMO_PATHS)
159
+ isDemo = True
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  # ---------------------------------------------------------------------------
162
  # Analyze All / Reset buttons
 
180
  # ---------------------------------------------------------------------------
181
 
182
  if st.session_state.analyzeAllToggle:
183
+ run_analysis_loop(
184
+ file_names, file_paths_dict, pipeline,
185
+ ENABLE_DENOISE, EARLY_CLEANUP,
186
+ GAIN_WINDOW, MINIMUM_GAIN, MAXIMUM_GAIN,
187
+ dfModel, dfState, ATTEN_LIM_DB,
188
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
  # ---------------------------------------------------------------------------
191
  # File selector
 
232
  unusedSpeakers = st.session_state.unusedSpeakers[currFile]
233
  categorySelections = st.session_state.categorySelect[currFile]
234
 
235
+ _saved_renames = st.session_state.speakerRenames.get(currFile, {})
236
+ raw_to_display = {sp: _saved_renames.get(sp, sp) for sp in speakerNames}
237
  all_speakers_display = [raw_to_display[sp] for sp in speakerNames]
238
 
239
  catTypeColors = su.colorsCSS(3)
 
241
  speakerColors = allColors[:len(speakerNames)]
242
  catColors = allColors[len(speakerNames):]
243
 
244
+ # Rebuild live df4
245
  nameList = st.session_state.categories
246
  valueList = [su.sumTimes(currAnnotation.subset(s)) for s in categorySelections]
247
  extraNames = list(unusedSpeakers)
248
  extraValues = [su.sumTimes(currAnnotation.subset([sp])) for sp in unusedSpeakers]
249
+ st.session_state.summaries[currFile]["df4"] = pd.DataFrame(
250
+ {"names": nameList + extraNames, "values": valueList + extraValues}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  )
252
 
253
+ # Build all_speaker_tokens for rename sidebar
 
254
  all_speaker_tokens = [
255
  f"{fn}: {sp}"
256
  for fn in st.session_state.file_names
 
258
  for sp in st.session_state.results[fn][0].labels()
259
  ]
260
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  # -----------------------------------------------------------------------
262
+ # Sidebar
263
  # -----------------------------------------------------------------------
264
 
265
+ ui.render_categories_sidebar(currFile, categorySelections, all_speakers_display, raw_to_display)
266
+ ui.render_rename_sidebar(currFile, speakerNames, all_speaker_tokens)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
  # -----------------------------------------------------------------------
269
  # Tab: Data
 
277
  f"sonogram-analysis-{currPlainName}.csv", "text/csv",
278
  key="download-csv", on_click="ignore",
279
  )
280
+ tableDF = build_table_df(displayDF)
281
+ ui.render_data_table(tableDF, speakerNames, raw_to_display, currFile)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
  # -----------------------------------------------------------------------
284
+ # Charts
285
  # -----------------------------------------------------------------------
286
 
 
287
  df4 = st.session_state.summaries[currFile]["df4"].copy()
288
  df5 = st.session_state.summaries[currFile]["df5"].copy()
289
  df2 = st.session_state.summaries[currFile]["df2"].copy()
290
 
291
+ ui.render_chart(
292
  utils.build_fig_pie2(df4, speakerNames, speakerColors, catColors, get_display_name, currFile),
293
  pie2,
294
  "ascn_pie2.pdf", "ascn_pie2.svg",
295
  f"sonogram-speaker-percent-{currPlainName}.pdf",
296
  f"sonogram-speaker-percent-{currPlainName}.svg",
297
+ "download-pdf2", "download-svg2", PLOTLY_CONFIG,
298
  )
299
+ ui.render_chart(
300
  utils.build_fig_sunburst(df5, catTypeColors, speakerColors, get_display_name, currFile),
301
  sunburst1,
302
  "ascn_sunburst.pdf", "ascn_sunburst.svg",
303
  f"sonogram-speaker-categories-{currPlainName}.pdf",
304
  f"sonogram-speaker-categories-{currPlainName}.svg",
305
+ "download-pdf3", "download-svg3", PLOTLY_CONFIG,
306
  )
307
+ ui.render_chart(
308
  utils.build_fig_treemap(df5, catTypeColors, speakerColors, get_display_name, currFile),
309
  treemap1,
310
  "ascn_treemap.pdf", "ascn_treemap.svg",
311
  f"sonogram-treemap-{currPlainName}.pdf",
312
  f"sonogram-treemap-{currPlainName}.svg",
313
+ "download-pdf4", "download-svg4", PLOTLY_CONFIG,
314
  )
315
+ ui.render_chart(
316
  utils.build_fig_timeline(speakers_dataFrame, currTotalTime, speakerColors, get_display_name, currFile),
317
  timeline,
318
  "ascn_timeline.pdf", "ascn_timeline.svg",
319
  f"sonogram-timeline-{currPlainName}.pdf",
320
  f"sonogram-timeline-{currPlainName}.svg",
321
+ "download-pdf5", "download-svg5", PLOTLY_CONFIG,
322
  )
323
+ ui.render_chart(
324
  utils.build_fig_bar(df2, catColors, speakerColors, get_display_name, currFile),
325
  bar1,
326
  "ascn_bar.pdf", "ascn_bar.svg",
327
  f"sonogram-speaker-time-{currPlainName}.pdf",
328
  f"sonogram-speaker-time-{currPlainName}.svg",
329
+ "download-pdf6", "download-svg6", PLOTLY_CONFIG,
330
  )
331
 
332
  except ValueError:
333
  pass
334
 
335
  # ---------------------------------------------------------------------------
336
+ # Multi-file summary + footer
337
  # ---------------------------------------------------------------------------
338
 
339
+ ui.render_multifile_summary(PLOTLY_CONFIG)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
  with st.expander("(Potentially) FAQ"):
342
  st.write("**1. I tried analyzing a file, but the page refreshed and nothing happened! Why?**")
 
351
  st.write("We are securing funding for permanent GPU access. Until then, CPU analysis may take a very long time.")
352
 
353
  st.divider()
354
+ st.write("Would you like additional data, charts, or features? [Tell us about our project!](https://forms.gle/A32CdfGYSZoMPyyX9)")
355
+ st.write("If you would like to learn more or work with us, contact Dr. Mark Baillie at mtbaillie@ualr.edu")