Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| """ | |
| app.py — Streamlit entry point. | |
| Responsibilities: | |
| - Constants and pipeline initialisation | |
| - Page header and file upload | |
| - Demo / Analyze buttons and analysis loop (via state.py) | |
| - Per-file view: sidebar + tabs (via ui.py and utils.py) | |
| """ | |
| import os | |
| import tempfile | |
| from pathlib import Path | |
| import streamlit as st | |
| import torch | |
| import pandas as pd | |
| from pyannote.audio import Pipeline | |
| import sonogram_utility as su | |
| import utils | |
| import ui | |
| from state import ( | |
| init_session_state, | |
| get_display_name, apply_speaker_renames_to_df, convert_df, | |
| updateMultiSelect, store_speaker_clips, register_file, analyze, | |
| load_demo_single, load_demo_multi, run_analysis_loop, build_table_df, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Constants | |
| # --------------------------------------------------------------------------- | |
| SUPPORTED_FILE_TYPES = (".wav", ".mp3", ".mp4", ".txt", ".rttm", ".csv") | |
| ENABLE_DENOISE = False | |
| EARLY_CLEANUP = True | |
| GAIN_WINDOW = 4 | |
| MINIMUM_GAIN = -45 | |
| MAXIMUM_GAIN = -5 | |
| ATTEN_LIM_DB = 3 | |
| PLOTLY_CONFIG = {"displayModeBar": True, "modeBarButtonsToRemove": []} | |
| PARQUET_DATASET_DIR = Path("parquet_dataset") | |
| PARQUET_DATASET_DIR.mkdir(parents=True, exist_ok=True) | |
| DEMO_PATH = "sample.rttm" | |
| MULTI_DEMO_PATHS = [ | |
| "audioSamples/media-afc-cal-afc1986022_sr01a05.rttm", | |
| "audioSamples/media-afc-cal-afc1986022_sr34a01.rttm", | |
| "audioSamples/media-afc-cal-afc1986022_sr14b02.rttm", | |
| "audioSamples/media-afc-cal-afc1986022_sr52a02.rttm", | |
| "audioSamples/media-afc-cal-afc1986022_sr14b01.rttm", | |
| ] | |
| # --------------------------------------------------------------------------- | |
| # Pipeline initialisation (once per server process) | |
| # --------------------------------------------------------------------------- | |
| torch.classes.__path__ = [os.path.join(torch.__path__[0], torch.classes.__file__)] | |
| isGPU = torch.cuda.is_available() | |
| device = torch.device("cuda" if isGPU else "cpu") | |
| print(f"Using {device}") | |
| if ENABLE_DENOISE: | |
| from df import init_df | |
| dfModel, dfState, _ = init_df(model_base_dir="DeepFilterNet3") | |
| dfModel.to(device) | |
| else: | |
| dfModel = dfState = None | |
| pipeline = Pipeline.from_pretrained("pyannote/speaker-diarization-3.1") | |
| pipeline.to(device) | |
| # --------------------------------------------------------------------------- | |
| # Session state + global styles | |
| # --------------------------------------------------------------------------- | |
| init_session_state() | |
| # Uploader key is rotated on reset to force the widget to clear | |
| if "uploader_key" not in st.session_state: | |
| st.session_state.uploader_key = 0 | |
| st.markdown( | |
| "<style>" | |
| "details > summary { font-size: 1rem; font-weight: 500; }" | |
| ".stTabs [data-baseweb='tab'] { font-size: 1rem; }" | |
| ".stFileUploader label { font-size: 1rem; }" | |
| "</style>", | |
| unsafe_allow_html=True, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Page header | |
| # --------------------------------------------------------------------------- | |
| st.title("Instructor Support Tool") | |
| if not isGPU: | |
| st.warning("TOOL CURRENTLY USING CPU, ANALYSIS EXTREMELY SLOW") | |
| st.write( | |
| 'If you would like to see a sample result or multiple sample results generated from ' | |
| 'real classroom audio, select "Single File Demo" or "Multiple Files Demo" on the left sidebar.' | |
| ) | |
| st.markdown( | |
| "<p style='margin-bottom:4px;'>Keep in mind that this is a very early draft of the tool. " | |
| "Please be patient with any bugs/errors, and email Connor Young at " | |
| "<a href='mailto:czyoung@ualr.edu'>czyoung@ualr.edu</a> if you need help using the tool!</p>" | |
| "<hr style='margin-top:16px; margin-bottom:16px;'>", | |
| unsafe_allow_html=True, | |
| ) | |
| with st.expander("Instructions and additional details"): | |
| st.write("Thank you for viewing our experimental app! The overall presentations and features are expected to be improved over time.") | |
| 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)") | |
| st.write("2. Press Analyze All. No data is saved on our side.") | |
| st.write("3. Use the sidebar to select your file. Multiple files are supported for more comprehensive analysis.") | |
| st.write("4. Use the tabs to view different visualizations. Each can be downloaded.") | |
| st.write("4a. Graphs are built with [plotly](https://plotly.com/). Double-click to reset. [More examples](https://plotly.com/python/basic-charts/).") | |
| # --------------------------------------------------------------------------- | |
| # File upload | |
| # --------------------------------------------------------------------------- | |
| uploaded_file_paths = st.file_uploader( | |
| "Upload an audio of classroom activity to analyze", | |
| accept_multiple_files=True, | |
| key=f"uploader_{st.session_state.uploader_key}", | |
| ) | |
| temp_dir = tempfile.mkdtemp() | |
| if uploaded_file_paths: | |
| for uploaded_file in uploaded_file_paths: | |
| if not uploaded_file.name.lower().endswith(SUPPORTED_FILE_TYPES): | |
| st.error(f"File must be of type: {SUPPORTED_FILE_TYPES}") | |
| continue | |
| fname = uploaded_file.name | |
| path = os.path.join(temp_dir, fname) | |
| with open(path, "wb") as f: | |
| f.write(uploaded_file.getvalue()) | |
| if fname not in st.session_state.file_names: | |
| register_file(fname) | |
| st.session_state.file_paths[fname] = path | |
| st.session_state.valid_files = list(st.session_state.file_names) | |
| file_names = st.session_state.file_names | |
| file_paths_dict = st.session_state.file_paths | |
| # --------------------------------------------------------------------------- | |
| # Sidebar: demo buttons | |
| # --------------------------------------------------------------------------- | |
| isDemo = False | |
| if st.sidebar.button("Single File Demo"): | |
| load_demo_single(DEMO_PATH) | |
| isDemo = True | |
| if st.sidebar.button("Multiple Files Demo"): | |
| load_demo_multi(MULTI_DEMO_PATHS) | |
| isDemo = True | |
| # --------------------------------------------------------------------------- | |
| # Analyze All / Reset buttons | |
| # --------------------------------------------------------------------------- | |
| if len(file_names) == 0: | |
| st.text("Upload file(s) to enable analysis") | |
| else: | |
| col_analyze, col_spacer, col_reset = st.columns([3, 5, 2]) | |
| with col_analyze: | |
| if st.button("Analyze All New Audio", key="button_all"): | |
| st.session_state.analyzeAllToggle = True | |
| with col_reset: | |
| if st.button("🗑️ Reset App", key="button_reset", type="secondary", use_container_width=True): | |
| next_key = st.session_state.uploader_key + 1 | |
| for key in list(st.session_state.keys()): | |
| del st.session_state[key] | |
| st.session_state.uploader_key = next_key | |
| st.rerun() | |
| # --------------------------------------------------------------------------- | |
| # Analysis loop | |
| # --------------------------------------------------------------------------- | |
| if st.session_state.analyzeAllToggle: | |
| run_analysis_loop( | |
| file_names, file_paths_dict, pipeline, | |
| ENABLE_DENOISE, EARLY_CLEANUP, | |
| GAIN_WINDOW, MINIMUM_GAIN, MAXIMUM_GAIN, | |
| dfModel, dfState, ATTEN_LIM_DB, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # File selector | |
| # --------------------------------------------------------------------------- | |
| currFile = st.sidebar.selectbox( | |
| "Current File", file_names, on_change=updateMultiSelect, key="select_currFile" | |
| ) | |
| if isDemo: | |
| currFile = file_names[0] | |
| st.sidebar.divider() | |
| if currFile is None: | |
| st.write("Select a file to view from the sidebar") | |
| # --------------------------------------------------------------------------- | |
| # Per-file analysis view | |
| # --------------------------------------------------------------------------- | |
| try: | |
| if currFile is None: | |
| raise ValueError("No file selected") | |
| st.session_state.resetResult = False | |
| currPlainName = currFile.split(".")[0] | |
| if not ( | |
| currFile in st.session_state.results | |
| and currFile in st.session_state.summaries | |
| and len(st.session_state.results[currFile]) > 0 | |
| ): | |
| raise ValueError("File not yet analyzed") | |
| st.header(f"Analysis of file {currFile}") | |
| TAB_NAMES = ["Data", "Rename Speaker", "Categories Percentage", | |
| "Speakers with Categories", "Treemap", "Timeline", "Time Spoken"] | |
| dataTab, renameTab, pie2, sunburst1, treemap1, timeline, bar1 = st.tabs(TAB_NAMES) | |
| currAnnotation, currTotalTime = st.session_state.results[currFile] | |
| speakerNames = currAnnotation.labels() | |
| speakers_dataFrame = st.session_state.summaries[currFile]["speakers_dataFrame"] | |
| currDF, _ = su.annotationToSimpleDataFrame(currAnnotation) | |
| unusedSpeakers = st.session_state.unusedSpeakers[currFile] | |
| categorySelections = st.session_state.categorySelect[currFile] | |
| _saved_renames = st.session_state.speakerRenames.get(currFile, {}) | |
| raw_to_display = {sp: _saved_renames.get(sp, sp) for sp in speakerNames} | |
| all_speakers_display = [raw_to_display[sp] for sp in speakerNames] | |
| catTypeColors = su.colorsCSS(3) | |
| allColors = su.colorsCSS(len(speakerNames) + len(st.session_state.categories)) | |
| speakerColors = allColors[:len(speakerNames)] | |
| catColors = allColors[len(speakerNames):] | |
| # Rebuild live df4 | |
| nameList = st.session_state.categories | |
| valueList = [su.sumTimes(currAnnotation.subset(s)) for s in categorySelections] | |
| extraNames = list(unusedSpeakers) | |
| extraValues = [su.sumTimes(currAnnotation.subset([sp])) for sp in unusedSpeakers] | |
| st.session_state.summaries[currFile]["df4"] = pd.DataFrame( | |
| {"names": nameList + extraNames, "values": valueList + extraValues} | |
| ) | |
| # Build all_speaker_tokens for rename sidebar | |
| all_speaker_tokens = [ | |
| f"{fn}: {sp}" | |
| for fn in st.session_state.file_names | |
| if fn in st.session_state.results and len(st.session_state.results[fn]) == 2 | |
| for sp in st.session_state.results[fn][0].labels() | |
| ] | |
| # ----------------------------------------------------------------------- | |
| # Sidebar | |
| # ----------------------------------------------------------------------- | |
| ui.render_categories_sidebar(currFile, categorySelections, all_speakers_display, raw_to_display) | |
| ui.render_rename_sidebar(currFile, speakerNames, all_speaker_tokens) | |
| # ----------------------------------------------------------------------- | |
| # Tab: Data | |
| # ----------------------------------------------------------------------- | |
| with dataTab: | |
| displayDF = apply_speaker_renames_to_df(currDF, currFile, column="Resource") | |
| csv = convert_df(displayDF) | |
| st.download_button( | |
| "Press to Download analysis data", csv, | |
| f"sonogram-analysis-{currPlainName}.csv", "text/csv", | |
| key="download-csv", on_click="ignore", | |
| ) | |
| tableDF = build_table_df(displayDF) | |
| ui.render_data_table(tableDF, speakerNames, raw_to_display, currFile) | |
| # ----------------------------------------------------------------------- | |
| # Tab: Rename Speaker | |
| # ----------------------------------------------------------------------- | |
| with renameTab: | |
| ui.render_speaker_samples_tab(speakerNames, raw_to_display, currFile) | |
| # ----------------------------------------------------------------------- | |
| # Charts | |
| # ----------------------------------------------------------------------- | |
| df4 = st.session_state.summaries[currFile]["df4"].copy() | |
| df5 = st.session_state.summaries[currFile]["df5"].copy() | |
| df2 = st.session_state.summaries[currFile]["df2"].copy() | |
| ui.render_chart( | |
| utils.build_fig_pie2(df4, speakerNames, speakerColors, catColors, get_display_name, currFile), | |
| pie2, | |
| "ascn_pie2.pdf", "ascn_pie2.svg", | |
| f"sonogram-speaker-percent-{currPlainName}.pdf", | |
| f"sonogram-speaker-percent-{currPlainName}.svg", | |
| "download-pdf2", "download-svg2", PLOTLY_CONFIG, | |
| ) | |
| ui.render_chart( | |
| utils.build_fig_sunburst(df5, catTypeColors, speakerColors, get_display_name, currFile), | |
| sunburst1, | |
| "ascn_sunburst.pdf", "ascn_sunburst.svg", | |
| f"sonogram-speaker-categories-{currPlainName}.pdf", | |
| f"sonogram-speaker-categories-{currPlainName}.svg", | |
| "download-pdf3", "download-svg3", PLOTLY_CONFIG, | |
| ) | |
| ui.render_chart( | |
| utils.build_fig_treemap(df5, catTypeColors, speakerColors, get_display_name, currFile), | |
| treemap1, | |
| "ascn_treemap.pdf", "ascn_treemap.svg", | |
| f"sonogram-treemap-{currPlainName}.pdf", | |
| f"sonogram-treemap-{currPlainName}.svg", | |
| "download-pdf4", "download-svg4", PLOTLY_CONFIG, | |
| ) | |
| ui.render_chart( | |
| utils.build_fig_timeline(speakers_dataFrame, currTotalTime, speakerColors, get_display_name, currFile), | |
| timeline, | |
| "ascn_timeline.pdf", "ascn_timeline.svg", | |
| f"sonogram-timeline-{currPlainName}.pdf", | |
| f"sonogram-timeline-{currPlainName}.svg", | |
| "download-pdf5", "download-svg5", PLOTLY_CONFIG, | |
| ) | |
| ui.render_chart( | |
| utils.build_fig_bar(df2, catColors, speakerColors, get_display_name, currFile), | |
| bar1, | |
| "ascn_bar.pdf", "ascn_bar.svg", | |
| f"sonogram-speaker-time-{currPlainName}.pdf", | |
| f"sonogram-speaker-time-{currPlainName}.svg", | |
| "download-pdf6", "download-svg6", PLOTLY_CONFIG, | |
| ) | |
| except ValueError: | |
| pass | |
| # --------------------------------------------------------------------------- | |
| # Multi-file summary + footer | |
| # --------------------------------------------------------------------------- | |
| ui.render_multifile_summary(PLOTLY_CONFIG) | |
| with st.expander("(Potentially) FAQ"): | |
| st.write("**1. I tried analyzing a file, but the page refreshed and nothing happened! Why?**") | |
| st.write("You may need to select a file using the sidebar on the left.") | |
| st.write("**2. I don't see a sidebar! Where is it?**") | |
| st.write("Press the '>' in the upper left to expand the sidebar.") | |
| st.write("**3. I still don't have a file to select in the dropdown! Why?**") | |
| st.write("Your file may be too large. We currently support approximately 1.5 hours of audio.") | |
| st.write("**4. I want to view my previously analyzed data. How?**") | |
| st.write("Download a CSV copy from the Data tab and re-upload it later.") | |
| st.write("**5. The app is extremely slow. What is wrong?**") | |
| st.write("We are securing funding for permanent GPU access. Until then, CPU analysis may take a very long time.") | |
| st.divider() | |
| st.write("Would you like additional data, charts, or features? [Tell us about our project!](https://forms.gle/A32CdfGYSZoMPyyX9)") | |
| st.write("If you would like to learn more or work with us, contact Dr. Mark Baillie at mtbaillie@ualr.edu") |