duongthienz commited on
Commit
c07f4bc
Β·
verified Β·
1 Parent(s): fc3f305

Move UI stuff out of app.py

Browse files
Files changed (1) hide show
  1. ui.py +212 -0
ui.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ui.py β€” Reusable Streamlit UI rendering helpers.
3
+
4
+ Keeps all layout/widget code that isn't tied to a single page out of app.py,
5
+ while keeping utils.py pure-Python and state.py focused on callbacks.
6
+ """
7
+
8
+ import os
9
+
10
+ import streamlit as st
11
+ import plotly.express as px
12
+
13
+ import utils
14
+ from state import (
15
+ updateCategoryOptions, removeCategory, addCategory,
16
+ applyGlobalRenames, addGlobalRename, removeGlobalRename,
17
+ _global_rename_key, randomize_speaker_clip,
18
+ )
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Chart rendering
23
+ # ---------------------------------------------------------------------------
24
+
25
+ def render_chart(fig, tab, pdf_path, svg_path, pdf_name, svg_name, pdf_key, svg_key,
26
+ plotly_config=None):
27
+ """Render a Plotly figure inside a tab with PDF/SVG download buttons."""
28
+ cfg = plotly_config or {"displayModeBar": True, "modeBarButtonsToRemove": []}
29
+ with tab:
30
+ st.plotly_chart(fig, use_container_width=True, config=cfg)
31
+ col_l, col_r = st.columns(2)
32
+ try:
33
+ fig.write_image(pdf_path)
34
+ fig.write_image(svg_path)
35
+ except Exception:
36
+ pass
37
+ with col_l:
38
+ if os.path.exists(pdf_path):
39
+ with open(pdf_path, "rb") as f:
40
+ st.download_button("Save As PDF", f, pdf_name, "application/pdf",
41
+ key=pdf_key, on_click="ignore")
42
+ with col_r:
43
+ if os.path.exists(svg_path):
44
+ with open(svg_path, "rb") as f:
45
+ st.download_button("Save As SVG", f, svg_name, "image/svg+xml",
46
+ key=svg_key, on_click="ignore")
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Sidebar β€” categories section
51
+ # ---------------------------------------------------------------------------
52
+
53
+ def render_categories_sidebar(currFile, categorySelections, all_speakers_display,
54
+ raw_to_display):
55
+ """Render category multiselects, remove buttons, and Add category input."""
56
+ for i, category in enumerate(st.session_state.categories):
57
+ ms_key = f"multiselect_{category}"
58
+ speakerSet = categorySelections[i]
59
+ default_disp = [raw_to_display.get(sp, sp) for sp in speakerSet]
60
+ if ms_key not in st.session_state:
61
+ st.session_state[ms_key] = default_disp
62
+ st.sidebar.multiselect(
63
+ category, all_speakers_display,
64
+ key=ms_key, on_change=updateCategoryOptions, args=(currFile,),
65
+ )
66
+ st.sidebar.button(
67
+ f"Remove {category}", key=f"remove_{category}",
68
+ on_click=removeCategory, args=(i,),
69
+ )
70
+ st.sidebar.text_input("Add category", key="categoryInput", on_change=addCategory)
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Sidebar β€” rename speakers section
75
+ # ---------------------------------------------------------------------------
76
+
77
+ def render_rename_sidebar(currFile, speakerNames, all_speaker_tokens):
78
+ """Render the full Rename Speakers sidebar section."""
79
+ st.sidebar.divider()
80
+ st.sidebar.subheader("Rename Speakers")
81
+ st.sidebar.markdown(
82
+ "<p style='font-size:0.85rem; color:gray; margin-bottom:2px;'>"
83
+ "Assign a name and select which speaker labels (across all files) it applies to. "
84
+ "Changes apply to all matched speakers instantly.</p>",
85
+ unsafe_allow_html=True,
86
+ )
87
+
88
+ st.sidebar.divider()
89
+
90
+ def _on_grename_change(idx):
91
+ st.session_state.globalRenames[idx]["speakers"] = list(
92
+ st.session_state[_global_rename_key(idx)]
93
+ )
94
+ applyGlobalRenames()
95
+
96
+ for idx, entry in enumerate(st.session_state.globalRenames):
97
+ grkey = _global_rename_key(idx)
98
+ if grkey not in st.session_state:
99
+ st.session_state[grkey] = list(entry["speakers"])
100
+ st.sidebar.markdown(f"**{entry['name']}**")
101
+ st.sidebar.multiselect(
102
+ f"Speakers for {entry['name']}", options=all_speaker_tokens,
103
+ key=grkey, on_change=_on_grename_change, args=(idx,),
104
+ label_visibility="collapsed",
105
+ )
106
+ st.sidebar.button(
107
+ f"Remove '{entry['name']}'", key=f"remove_grename_{idx}",
108
+ on_click=removeGlobalRename, args=(idx,),
109
+ )
110
+
111
+ st.sidebar.text_input(
112
+ "Add rename", placeholder="e.g. John",
113
+ key="globalRenameInput", on_change=addGlobalRename,
114
+ )
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # Data tab β€” custom scrollable table
119
+ # ---------------------------------------------------------------------------
120
+
121
+ def render_data_table(tableDF, speakerNames, raw_to_display, currFile):
122
+ """Render the custom column-based table with optional clip audio + randomize."""
123
+ file_clips = st.session_state.speakerClips.get(currFile, {})
124
+ has_clips = bool(file_clips)
125
+ has_waveform = currFile in st.session_state.speakerWaveforms
126
+ has_extras = has_clips
127
+
128
+ other_cols = [c for c in tableDF.columns if c != "Speaker"]
129
+ col_widths = [2] + [1] * len(other_cols) + ([2, 1] if has_extras else [])
130
+
131
+ # Header
132
+ header_cols = st.columns(col_widths)
133
+ header_cols[0].markdown("**Speaker**")
134
+ for i, col_name in enumerate(other_cols):
135
+ header_cols[i + 1].markdown(f"**{col_name}**")
136
+ if has_extras:
137
+ header_cols[-2].markdown("**Clip**")
138
+ header_cols[-1].markdown("**&nbsp;**", unsafe_allow_html=True)
139
+
140
+ st.markdown("<hr style='margin-top:2px; margin-bottom:4px;'>", unsafe_allow_html=True)
141
+
142
+ rendered_clip_for: set = set()
143
+
144
+ with st.container(height=480, border=False):
145
+ for _, row in tableDF.iterrows():
146
+ row_cols = st.columns(col_widths)
147
+ display_sp = row["Speaker"]
148
+ raw_key = next(
149
+ (sp for sp in speakerNames if raw_to_display.get(sp, sp) == display_sp),
150
+ display_sp,
151
+ )
152
+
153
+ row_cols[0].write(display_sp)
154
+ for i, col_name in enumerate(other_cols):
155
+ row_cols[i + 1].write(row[col_name])
156
+
157
+ if has_extras and raw_key in file_clips and raw_key not in rendered_clip_for:
158
+ rendered_clip_for.add(raw_key)
159
+ row_cols[-2].audio(file_clips[raw_key], format="audio/wav")
160
+ sp_segs = st.session_state.speakerSegments.get(currFile, {}).get(raw_key, [])
161
+ if has_waveform and sp_segs:
162
+ if row_cols[-1].button(
163
+ "πŸ”€", key=f"randomize_{currFile}_{raw_key}",
164
+ help="Try a different clip for this speaker",
165
+ ):
166
+ randomize_speaker_clip(currFile, raw_key)
167
+ st.rerun()
168
+
169
+
170
+ # ---------------------------------------------------------------------------
171
+ # Multi-file summary expander
172
+ # ---------------------------------------------------------------------------
173
+
174
+ def render_multifile_summary(plotly_config=None):
175
+ """Render the Multi-file Summary Data expander if enough files are analyzed."""
176
+ cfg = plotly_config or {"displayModeBar": True, "modeBarButtonsToRemove": []}
177
+ if not st.session_state.results:
178
+ return
179
+
180
+ with st.expander("Multi-file Summary Data"):
181
+ st.header("Multi-file Summary Data")
182
+ with st.spinner("Processing summary results..."):
183
+ validNames = [
184
+ fn for fn in st.session_state.file_names
185
+ if fn in st.session_state.results
186
+ and len(st.session_state.results[fn]) == 2
187
+ ]
188
+ if len(validNames) <= 1:
189
+ return
190
+
191
+ df6, allCategories = utils.build_multifile_category_df(
192
+ validNames, st.session_state.results, st.session_state.summaries,
193
+ st.session_state.categories, st.session_state.categorySelect,
194
+ )
195
+ st.plotly_chart(
196
+ px.bar(df6, x="files", y=allCategories,
197
+ title="Time Spoken by Each Speaker in Each File"),
198
+ use_container_width=True, config=cfg,
199
+ )
200
+
201
+ df7, _ = utils.build_multifile_voice_df(validNames, st.session_state.summaries)
202
+ for sort_cols, ascending, title in [
203
+ (["One Voice", "Multi Voice"], True, "Cross-file Voice Categories sorted for One Voice"),
204
+ (["Multi Voice","One Voice"], True, "Cross-file Voice Categories sorted for Multi Voice"),
205
+ (["No Voice", "Multi Voice"], False, "Cross-file Voice Categories sorted for Any Voice"),
206
+ ]:
207
+ st.plotly_chart(
208
+ px.bar(df7.sort_values(by=sort_cols, ascending=ascending),
209
+ x="files", y=["One Voice", "Multi Voice", "No Voice"],
210
+ title=title),
211
+ use_container_width=True, config=cfg,
212
+ )