File size: 12,550 Bytes
c07f4bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5da9657
c07f4bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ec2153e
c07f4bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bfa5937
c07f4bc
bfa5937
c07f4bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bfa5937
 
 
 
 
 
c9a70a9
5da9657
 
 
bfa5937
5da9657
2902b0f
ad7eece
 
5da9657
2902b0f
5da9657
bfa5937
 
 
 
 
 
 
 
 
 
5da9657
 
 
 
 
 
 
c9a70a9
2902b0f
5da9657
 
5b6254c
c18c8f8
 
5b6254c
c9a70a9
c18c8f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c9a70a9
c18c8f8
c9a70a9
 
 
 
 
 
5da9657
 
ad7eece
c18c8f8
ad7eece
 
 
5da9657
 
 
 
 
 
 
 
 
 
2902b0f
5da9657
 
 
 
 
 
bfa5937
 
 
 
 
2902b0f
bfa5937
 
 
 
 
 
c07f4bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bfa5937
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
"""
ui.py β€” Reusable Streamlit UI rendering helpers.

Keeps all layout/widget code that isn't tied to a single page out of app.py,
while keeping utils.py pure-Python and state.py focused on callbacks.
"""

import os

import streamlit as st
import plotly.express as px

import utils
from state import (
    updateCategoryOptions, removeCategory, addCategory,
    applyGlobalRenames, addGlobalRename, removeGlobalRename,
    _global_rename_key, randomize_speaker_clip, apply_inline_rename,
)


# ---------------------------------------------------------------------------
# Chart rendering
# ---------------------------------------------------------------------------

def render_chart(fig, tab, pdf_path, svg_path, pdf_name, svg_name, pdf_key, svg_key,
                 plotly_config=None):
    """Render a Plotly figure inside a tab with PDF/SVG download buttons."""
    cfg = plotly_config or {"displayModeBar": True, "modeBarButtonsToRemove": []}
    with tab:
        st.plotly_chart(fig, use_container_width=True, config=cfg)
        col_l, col_r = st.columns(2)
        try:
            fig.write_image(pdf_path)
            fig.write_image(svg_path)
        except Exception:
            pass
        with col_l:
            if os.path.exists(pdf_path):
                with open(pdf_path, "rb") as f:
                    st.download_button("Save As PDF", f, pdf_name, "application/pdf",
                                       key=pdf_key, on_click="ignore")
        with col_r:
            if os.path.exists(svg_path):
                with open(svg_path, "rb") as f:
                    st.download_button("Save As SVG", f, svg_name, "image/svg+xml",
                                       key=svg_key, on_click="ignore")


# ---------------------------------------------------------------------------
# Sidebar β€” categories section
# ---------------------------------------------------------------------------

def render_categories_sidebar(currFile, categorySelections, all_speakers_display,
                               raw_to_display):
    """Render category multiselects, remove buttons, and Add category input."""
    for i, category in enumerate(st.session_state.categories):
        ms_key       = f"multiselect_{category}"
        speakerSet   = categorySelections[i]
        default_disp = [raw_to_display.get(sp, sp) for sp in speakerSet]
        if ms_key not in st.session_state:
            st.session_state[ms_key] = default_disp
        st.sidebar.multiselect(
            category, all_speakers_display,
            key=ms_key, on_change=updateCategoryOptions, args=(currFile,),
        )
        st.sidebar.button(
            f"Remove {category}", key=f"remove_{category}",
            on_click=removeCategory, args=(i,),
        )
    st.sidebar.text_input("Add Category", key="categoryInput", on_change=addCategory)


# ---------------------------------------------------------------------------
# Sidebar β€” rename speakers section
# ---------------------------------------------------------------------------

def render_rename_sidebar(currFile, speakerNames, all_speaker_tokens):
    """Render the full Rename Speakers sidebar section."""
    st.sidebar.divider()
    st.sidebar.subheader("Rename Speakers")
    st.sidebar.markdown(
        "<p style='font-size:0.85rem; color:gray; margin-bottom:2px;'>"
        "Assign a name and select which speaker labels (across all files) it applies to. "
        "Changes apply to all matched speakers instantly.</p>",
        unsafe_allow_html=True,
    )

    st.sidebar.divider()

    def _on_grename_change(idx):
        st.session_state.globalRenames[idx]["speakers"] = list(
            st.session_state[_global_rename_key(idx)]
        )
        applyGlobalRenames()

    for idx, entry in enumerate(st.session_state.globalRenames):
        grkey = _global_rename_key(idx)
        if grkey not in st.session_state:
            st.session_state[grkey] = list(entry["speakers"])
        st.sidebar.markdown(f"**{entry['name']}**")
        st.sidebar.multiselect(
            f"Speakers for {entry['name']}", options=all_speaker_tokens,
            key=grkey, on_change=_on_grename_change, args=(idx,),
            label_visibility="collapsed",
        )
        st.sidebar.button(
            f"Remove '{entry['name']}'", key=f"remove_grename_{idx}",
            on_click=removeGlobalRename, args=(idx,),
        )

    st.sidebar.text_input(
        "Add rename", placeholder="e.g. John",
        key="globalRenameInput", on_change=addGlobalRename,
    )


# ---------------------------------------------------------------------------
# Data tab β€” custom scrollable table
# ---------------------------------------------------------------------------

def render_data_table(tableDF, speakerNames, raw_to_display, currFile):
    """Render the custom column-based scrollable table (Speaker, Start, Finish)."""
    other_cols = [c for c in tableDF.columns if c != "Speaker"]
    col_widths = [2] + [1] * len(other_cols)

    # Header
    header_cols = st.columns(col_widths)
    header_cols[0].markdown("**Speaker**")
    for i, col_name in enumerate(other_cols):
        header_cols[i + 1].markdown(f"**{col_name}**")

    st.markdown("<hr style='margin-top:2px; margin-bottom:4px;'>", unsafe_allow_html=True)

    with st.container(height=480, border=False):
        for _, row in tableDF.iterrows():
            row_cols   = st.columns(col_widths)
            display_sp = row["Speaker"]
            row_cols[0].write(display_sp)
            for i, col_name in enumerate(other_cols):
                row_cols[i + 1].write(row[col_name])


# ---------------------------------------------------------------------------
# Rename Speaker tab β€” speaker / audio sample table
# ---------------------------------------------------------------------------

def render_speaker_samples_tab(speakerNames, raw_to_display, currFile):
    """Render a table: Speaker (with inline ✎ rename + history dropdown) | Audio Sample | β†Ί button."""
    file_samples = st.session_state.speakerClips.get(currFile, {})
    has_waveform = currFile in st.session_state.speakerWaveforms
    has_samples  = bool(file_samples)

    if "inline_rename_active" not in st.session_state:
        st.session_state.inline_rename_active = {}
    if "inline_rename_history" not in st.session_state:
        st.session_state.inline_rename_history = []

    # Header
    header_cols = st.columns([3, 3, 1])
    header_cols[0].markdown("**Speaker**")
    header_cols[1].markdown("**Audio Sample**")
    header_cols[2].markdown("**&nbsp;**", unsafe_allow_html=True)
    st.markdown("<hr style='margin-top:2px; margin-bottom:4px;'>", unsafe_allow_html=True)

    if not has_samples:
        st.info("Audio samples are only available for files analyzed from audio (not RTTM/CSV/TXT).")

    for sp in speakerNames:
        display_name = raw_to_display.get(sp, sp)
        edit_key     = (currFile, sp)
        is_editing   = st.session_state.inline_rename_active.get(edit_key, False)

        row_cols = st.columns([3, 3, 1])

        # --- Speaker cell ---
        if is_editing:
            select_key  = f"inline_rename_select_{currFile}_{sp}"
            input_key   = f"inline_rename_input_{currFile}_{sp}"
            confirm_key = f"inline_rename_confirm_{currFile}_{sp}"
            cancel_key  = f"inline_rename_cancel_{currFile}_{sp}"

            history     = st.session_state.inline_rename_history
            current_val = display_name if display_name != sp else ""

            with row_cols[0]:
                if history:
                    # Selectbox with index=None shows placeholder and lets user
                    # type to filter β€” single combined widget, no separate input.
                    options     = history
                    default_idx = history.index(current_val) if current_val in history else None
                    chosen = st.selectbox(
                        "Rename",
                        options=options,
                        index=default_idx,
                        key=select_key,
                        label_visibility="collapsed",
                        placeholder="Type or select a name…",
                    )
                    new_name = chosen or ""
                else:
                    # No history yet β€” plain text input
                    new_name = st.text_input(
                        "Rename",
                        value=current_val,
                        key=input_key,
                        label_visibility="collapsed",
                        placeholder=f"Rename {sp}…",
                    )

                btn_col1, btn_col2 = st.columns(2)
                if btn_col1.button("βœ“", key=confirm_key, help="Confirm rename"):
                    confirmed = new_name.strip()
                    if confirmed:
                        apply_inline_rename(currFile, sp, confirmed)
                        if confirmed not in st.session_state.inline_rename_history:
                            st.session_state.inline_rename_history.append(confirmed)
                    st.session_state.inline_rename_active[edit_key] = False
                    st.rerun()
                if btn_col2.button("βœ•", key=cancel_key, help="Cancel"):
                    st.session_state.inline_rename_active[edit_key] = False
                    st.rerun()
        else:
            with row_cols[0]:
                name_col, pencil_col = st.columns([4, 1])
                name_col.write(display_name)
                if pencil_col.button(
                    "✎", key=f"inline_rename_edit_{currFile}_{sp}",
                    help=f"Rename {sp}",
                ):
                    st.session_state.inline_rename_active[edit_key] = True
                    st.rerun()

        # --- Audio sample cell ---
        if sp in file_samples:
            row_cols[1].audio(file_samples[sp], format="audio/wav")
            sp_segs = st.session_state.speakerSegments.get(currFile, {}).get(sp, [])
            if has_waveform and sp_segs:
                if row_cols[2].button(
                    "β†Ί", key=f"sample_randomize_{currFile}_{sp}",
                    help="Try a different audio sample for this speaker",
                ):
                    randomize_speaker_clip(currFile, sp)
                    st.rerun()
        else:
            row_cols[1].write("β€”")


# ---------------------------------------------------------------------------
# Multi-file summary expander
# ---------------------------------------------------------------------------

def render_multifile_summary(plotly_config=None):
    """Render the Multi-file Summary Data expander if enough files are analyzed."""
    cfg = plotly_config or {"displayModeBar": True, "modeBarButtonsToRemove": []}
    if not st.session_state.results:
        return

    with st.expander("Multi-file Summary Data"):
        st.header("Multi-file Summary Data")
        with st.spinner("Processing summary results..."):
            validNames = [
                fn for fn in st.session_state.file_names
                if fn in st.session_state.results
                and len(st.session_state.results[fn]) == 2
            ]
            if len(validNames) <= 1:
                return

            df6, allCategories = utils.build_multifile_category_df(
                validNames, st.session_state.results, st.session_state.summaries,
                st.session_state.categories, st.session_state.categorySelect,
            )
            st.plotly_chart(
                px.bar(df6, x="files", y=allCategories,
                       title="Time Spoken by Each Speaker in Each File"),
                use_container_width=True, config=cfg,
            )

            df7, _ = utils.build_multifile_voice_df(validNames, st.session_state.summaries)
            for sort_cols, ascending, title in [
                (["One Voice",  "Multi Voice"], True,  "Cross-file Voice Categories sorted for One Voice"),
                (["Multi Voice","One Voice"],   True,  "Cross-file Voice Categories sorted for Multi Voice"),
                (["No Voice",   "Multi Voice"], False, "Cross-file Voice Categories sorted for Any Voice"),
            ]:
                st.plotly_chart(
                    px.bar(df7.sort_values(by=sort_cols, ascending=ascending),
                           x="files", y=["One Voice", "Multi Voice", "No Voice"],
                           title=title),
                    use_container_width=True, config=cfg,
                )