duongthienz commited on
Commit
c823bc3
Β·
verified Β·
1 Parent(s): a980c3f

Update state.py

Browse files
Files changed (1) hide show
  1. state.py +144 -100
state.py CHANGED
@@ -5,7 +5,8 @@ Covers:
5
  - init_session_state()
6
  - Speaker rename helpers (get_display_name, apply_speaker_renames_to_df)
7
  - Category callbacks (addCategory, removeCategory, updateCategoryOptions)
8
- - Global rename callbacks (addGlobalRename, removeGlobalRename, applyGlobalRenames)
 
9
  - File-switch callback (updateMultiSelect)
10
  - analyze() β€” builds and caches all DataFrames for a single file
11
  - convert_df(), printV()
@@ -42,8 +43,8 @@ def init_session_state():
42
  "results": {}, # {filename: (annotations, totalSeconds)}
43
  "speakerRenames": {}, # {filename: {speaker: name}}
44
  "summaries": {}, # {filename: {df2, df3, ...}}
45
- "categories": [],
46
- "categorySelect": {}, # {filename: [[], [], ...]}
47
  "removeCategory": None,
48
  "resetResult": False,
49
  "unusedSpeakers": {}, # {filename: [speaker, ...]}
@@ -90,149 +91,194 @@ def convert_df(df):
90
  # ---------------------------------------------------------------------------
91
 
92
  def addCategory():
93
- new = st.session_state.categoryInput
 
 
94
  st.toast(f"Adding {new}")
95
- st.session_state[f"multiselect_{new}"] = []
96
  st.session_state.categories.append(new)
 
 
97
  st.session_state.categoryInput = ""
98
- for fname in st.session_state.categorySelect:
99
- st.session_state.categorySelect[fname].append([])
100
 
101
 
102
  def removeCategory(index):
103
  name = st.session_state.categories[index]
104
  st.toast(f"Removing {name}")
105
- del st.session_state[f"multiselect_{name}"]
106
- del st.session_state[f"remove_{name}"]
107
  del st.session_state.categories[index]
108
- for fname in st.session_state.categorySelect:
109
- del st.session_state.categorySelect[fname][index]
110
 
111
 
112
- def updateCategoryOptions(fileName):
 
113
  if st.session_state.resetResult:
114
  return
115
- currAnnotation, _ = st.session_state.results[fileName]
116
- speakerNames = list(currAnnotation.labels())
117
- saved_renames = st.session_state.speakerRenames.get(fileName, {})
118
- display_to_raw = {saved_renames.get(sp, sp): sp for sp in speakerNames}
119
- unusedSpeakers = copy.deepcopy(speakerNames)
120
  for i, category in enumerate(st.session_state.categories):
121
- display_choices = list(st.session_state[f"multiselect_{category}"])
122
- raw_choices = [display_to_raw.get(d, d) for d in display_choices]
123
- st.session_state.categorySelect[fileName][i] = raw_choices
124
- for sp in raw_choices:
125
- try:
126
- unusedSpeakers.remove(sp)
127
- except ValueError:
128
- pass
129
- st.session_state.unusedSpeakers[fileName] = unusedSpeakers
 
 
 
 
 
 
 
 
 
130
 
131
 
132
  # ---------------------------------------------------------------------------
133
  # Global rename callbacks
134
  # ---------------------------------------------------------------------------
135
 
 
 
 
 
136
  def _global_rename_key(index):
137
  return f"grename_speakers_{index}"
138
 
139
 
140
- def applyGlobalRenames():
141
- """Re-write speakerRenames from globalRenames and refresh widget keys."""
142
- for fname in st.session_state.speakerRenames:
143
- st.session_state.speakerRenames[fname] = {}
144
- for entry in st.session_state.globalRenames:
145
- display_name = entry["name"]
146
- for token in entry["speakers"]:
147
- if ": " not in token:
148
- continue
149
- fname, raw_sp = token.split(": ", 1)
150
- if fname in st.session_state.speakerRenames:
151
- st.session_state.speakerRenames[fname][raw_sp] = display_name
152
- curr = st.session_state.get("select_currFile")
153
- if curr and curr in st.session_state.speakerRenames:
154
- saved = st.session_state.speakerRenames[curr]
155
- results = st.session_state.results.get(curr)
156
- if results:
157
- for sp in results[0].labels():
158
- st.session_state[f"rename_{curr}_{sp}"] = saved.get(sp, "")
159
-
160
-
161
- def enforceGlobalRenameExclusivity(changed_idx):
162
- """Ensure a token assigned to entry[changed_idx] is removed from all others.
163
-
164
- Called by the sidebar multiselect on_change so that picking a speaker for
165
- one name automatically unassigns it from any previously-holding name.
166
- We pop (not overwrite) stale widget keys to avoid the Streamlit
167
- "cannot modify after instantiation" error.
168
  """
169
- changed_tokens = set(st.session_state.globalRenames[changed_idx]["speakers"])
170
- for idx, entry in enumerate(st.session_state.globalRenames):
171
- if idx == changed_idx:
172
- continue
173
- new_speakers = [t for t in entry["speakers"] if t not in changed_tokens]
174
- if new_speakers != entry["speakers"]:
175
- entry["speakers"] = new_speakers
176
- st.session_state.pop(_global_rename_key(idx), None)
177
- applyGlobalRenames()
178
 
179
 
180
  def addGlobalRename():
181
  new_name = st.session_state.globalRenameInput.strip()
182
  if not new_name:
183
  return
 
 
 
 
 
184
  st.toast(f"Adding rename '{new_name}'")
185
  st.session_state.globalRenames.append({"name": new_name, "speakers": []})
186
- st.session_state[_global_rename_key(len(st.session_state.globalRenames) - 1)] = []
187
  st.session_state.globalRenameInput = ""
188
 
189
 
190
  def removeGlobalRename(index):
191
  entry = st.session_state.globalRenames[index]
192
  st.toast(f"Removing rename '{entry['name']}'")
 
 
 
 
193
  del st.session_state.globalRenames[index]
 
194
  for i in range(index, len(st.session_state.globalRenames)):
195
  st.session_state[_global_rename_key(i)] = list(
196
  st.session_state.globalRenames[i]["speakers"]
197
  )
198
- applyGlobalRenames()
199
 
200
 
201
  def apply_inline_rename(currFile, raw_sp, new_name):
202
- """Write a single inline rename into both speakerRenames and globalRenames.
203
-
204
- Keeps the sidebar global-rename list in sync so applyGlobalRenames()
205
- won't overwrite the change on the next rerun.
206
- """
207
  new_name = new_name.strip()
208
  token = f"{currFile}: {raw_sp}"
209
 
210
- # Always remove this token from every existing entry first so it can't
211
- # be claimed by more than one name at the same time.
212
- # We delete the widget key (rather than overwriting it) to avoid the
213
- # Streamlit "cannot modify after widget is instantiated" error β€” the
214
- # render loop will reseed it from globalRenames data on the next rerun.
215
  for idx, entry in enumerate(st.session_state.globalRenames):
216
  if token in entry["speakers"]:
217
  entry["speakers"].remove(token)
218
  st.session_state.pop(_global_rename_key(idx), None)
219
 
220
  if new_name:
221
- # Write into speakerRenames immediately
222
- st.session_state.speakerRenames.setdefault(currFile, {})[raw_sp] = new_name
223
- # Find an existing globalRenames entry for this name, or create one
224
  for idx, entry in enumerate(st.session_state.globalRenames):
225
  if entry["name"] == new_name:
226
  entry["speakers"].append(token)
227
  st.session_state.pop(_global_rename_key(idx), None)
228
  return
229
- # No existing entry β€” create a new one
230
- idx = len(st.session_state.globalRenames)
231
  st.session_state.globalRenames.append({"name": new_name, "speakers": [token]})
232
- # Don't pre-set the widget key here β€” render loop will seed it
233
  else:
234
- # Empty name β€” clear the rename for this speaker (token already removed above)
235
- st.session_state.speakerRenames.setdefault(currFile, {}).pop(raw_sp, None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
 
238
  # ---------------------------------------------------------------------------
@@ -245,19 +291,12 @@ def updateMultiSelect():
245
  result = st.session_state.results.get(fileName)
246
  if not result:
247
  return
248
- currAnnotation, _ = result
249
- speakerNames = list(currAnnotation.labels())
250
- saved_renames = st.session_state.speakerRenames.get(fileName, {})
251
- raw_to_display = {}
252
- for sp in speakerNames:
253
- saved = saved_renames.get(sp, "")
254
- st.session_state[f"rename_{fileName}_{sp}"] = saved
255
- raw_to_display[sp] = saved if saved else sp
256
- for i, category in enumerate(st.session_state.categories):
257
- raw_choices = st.session_state.categorySelect[fileName][i]
258
- st.session_state[f"multiselect_{category}"] = [
259
- raw_to_display.get(sp, sp) for sp in raw_choices
260
- ]
261
 
262
 
263
  # ---------------------------------------------------------------------------
@@ -294,9 +333,9 @@ def register_file(fname):
294
  st.session_state.results.setdefault(fname, [])
295
  st.session_state.summaries.setdefault(fname, {})
296
  st.session_state.unusedSpeakers.setdefault(fname, [])
297
- st.session_state.categorySelect.setdefault(
298
- fname, [[] for _ in st.session_state.categories]
299
- )
300
  st.session_state.speakerRenames.setdefault(fname, {})
301
  st.session_state.speakerClips.setdefault(fname, {})
302
  if fname not in st.session_state.file_names:
@@ -437,7 +476,12 @@ def analyze(inFileName):
437
 
438
  currAnnotation, currTotalTime = st.session_state.results[inFileName]
439
  speakerNames = currAnnotation.labels()
440
- categorySelections = st.session_state.categorySelect[inFileName]
 
 
 
 
 
441
  printV("Loaded results", 4)
442
 
443
  noVoice, oneVoice, multiVoice = su.calcSpeakingTypes(currAnnotation, currTotalTime)
 
5
  - init_session_state()
6
  - Speaker rename helpers (get_display_name, apply_speaker_renames_to_df)
7
  - Category callbacks (addCategory, removeCategory, updateCategoryOptions)
8
+ - Global rename callbacks (addGlobalRename, removeGlobalRename, on_grename_change,
9
+ apply_inline_rename)
10
  - File-switch callback (updateMultiSelect)
11
  - analyze() β€” builds and caches all DataFrames for a single file
12
  - convert_df(), printV()
 
43
  "results": {}, # {filename: (annotations, totalSeconds)}
44
  "speakerRenames": {}, # {filename: {speaker: name}}
45
  "summaries": {}, # {filename: {df2, df3, ...}}
46
+ "categories": ["Instructor", "Students"],
47
+ "categorySelect": [[], []], # [[token, ...], ...] one list per category, tokens = "fname: SPEAKER_##"; starts with 2 lists for Instructor/Students
48
  "removeCategory": None,
49
  "resetResult": False,
50
  "unusedSpeakers": {}, # {filename: [speaker, ...]}
 
91
  # ---------------------------------------------------------------------------
92
 
93
  def addCategory():
94
+ new = st.session_state.categoryInput.strip()
95
+ if not new:
96
+ return
97
  st.toast(f"Adding {new}")
 
98
  st.session_state.categories.append(new)
99
+ st.session_state.categorySelect.append([])
100
+ st.session_state.pop(f"multiselect_{new}", None)
101
  st.session_state.categoryInput = ""
 
 
102
 
103
 
104
  def removeCategory(index):
105
  name = st.session_state.categories[index]
106
  st.toast(f"Removing {name}")
107
+ st.session_state.pop(f"multiselect_{name}", None)
 
108
  del st.session_state.categories[index]
109
+ del st.session_state.categorySelect[index]
 
110
 
111
 
112
+ def updateCategoryOptions():
113
+ """Store tokens ('fname: SPEAKER_##') in the global categorySelect lists."""
114
  if st.session_state.resetResult:
115
  return
116
+ # Rebuild unusedSpeakers for every analyzed file based on current token assignments
 
 
 
 
117
  for i, category in enumerate(st.session_state.categories):
118
+ st.session_state.categorySelect[i] = list(
119
+ st.session_state[f"multiselect_{category}"]
120
+ )
121
+ # Recompute unusedSpeakers for all files
122
+ all_assigned_tokens = {
123
+ token
124
+ for tokens in st.session_state.categorySelect
125
+ for token in tokens
126
+ }
127
+ for fname, result in st.session_state.results.items():
128
+ if len(result) != 2:
129
+ continue
130
+ annotation, _ = result
131
+ unused = [
132
+ sp for sp in annotation.labels()
133
+ if f"{fname}: {sp}" not in all_assigned_tokens
134
+ ]
135
+ st.session_state.unusedSpeakers[fname] = unused
136
 
137
 
138
  # ---------------------------------------------------------------------------
139
  # Global rename callbacks
140
  # ---------------------------------------------------------------------------
141
 
142
+ # ---------------------------------------------------------------------------
143
+ # Global rename helpers
144
+ # ---------------------------------------------------------------------------
145
+
146
  def _global_rename_key(index):
147
  return f"grename_speakers_{index}"
148
 
149
 
150
+ def _write_rename(token, name):
151
+ """Write name into speakerRenames for a single token (fname: SPEAKER_##).
152
+ If name is empty, clears the entry (revert to raw label).
153
+ Silently ignores tokens that don't match a known file.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  """
155
+ if ": " not in token:
156
+ return
157
+ fname, raw_sp = token.split(": ", 1)
158
+ if fname not in st.session_state.speakerRenames:
159
+ return # token references an unknown file β€” ignore
160
+ if name:
161
+ st.session_state.speakerRenames[fname][raw_sp] = name
162
+ else:
163
+ st.session_state.speakerRenames[fname].pop(raw_sp, None)
164
 
165
 
166
  def addGlobalRename():
167
  new_name = st.session_state.globalRenameInput.strip()
168
  if not new_name:
169
  return
170
+ for entry in st.session_state.globalRenames:
171
+ if entry["name"] == new_name:
172
+ st.toast(f"'{new_name}' already exists in the rename list")
173
+ st.session_state.globalRenameInput = ""
174
+ return
175
  st.toast(f"Adding rename '{new_name}'")
176
  st.session_state.globalRenames.append({"name": new_name, "speakers": []})
 
177
  st.session_state.globalRenameInput = ""
178
 
179
 
180
  def removeGlobalRename(index):
181
  entry = st.session_state.globalRenames[index]
182
  st.toast(f"Removing rename '{entry['name']}'")
183
+ # Revert every speaker that belonged to this entry
184
+ for token in entry["speakers"]:
185
+ _write_rename(token, "")
186
+ st.session_state.pop(_global_rename_key(index), None)
187
  del st.session_state.globalRenames[index]
188
+ # Shift remaining widget keys down
189
  for i in range(index, len(st.session_state.globalRenames)):
190
  st.session_state[_global_rename_key(i)] = list(
191
  st.session_state.globalRenames[i]["speakers"]
192
  )
 
193
 
194
 
195
  def apply_inline_rename(currFile, raw_sp, new_name):
196
+ """Write a rename from the Rename Speaker tab into speakerRenames and globalRenames."""
 
 
 
 
197
  new_name = new_name.strip()
198
  token = f"{currFile}: {raw_sp}"
199
 
 
 
 
 
 
200
  for idx, entry in enumerate(st.session_state.globalRenames):
201
  if token in entry["speakers"]:
202
  entry["speakers"].remove(token)
203
  st.session_state.pop(_global_rename_key(idx), None)
204
 
205
  if new_name:
206
+ _write_rename(token, new_name)
 
 
207
  for idx, entry in enumerate(st.session_state.globalRenames):
208
  if entry["name"] == new_name:
209
  entry["speakers"].append(token)
210
  st.session_state.pop(_global_rename_key(idx), None)
211
  return
 
 
212
  st.session_state.globalRenames.append({"name": new_name, "speakers": [token]})
 
213
  else:
214
+ _write_rename(token, "")
215
+ st.toast(f"Reverted {raw_sp} to original label")
216
+
217
+
218
+ def on_grename_change(idx):
219
+ """Callback for the sidebar rename multiselect at position idx.
220
+
221
+ Diffs old vs new speakers list:
222
+ - Newly added tokens β†’ enforce exclusivity, write name into speakerRenames.
223
+ - Removed tokens β†’ revert speakerRenames for those tokens.
224
+
225
+ Spurious-callback guard: Streamlit sometimes re-fires this callback with an
226
+ empty widget value when available_tokens changes (e.g. another entry just
227
+ claimed a token, shrinking options). We detect this by checking whether the
228
+ widget value went to empty while the data model still has speakers β€” if ALL
229
+ of the previous speakers are still valid options for this entry we treat the
230
+ empty report as a Streamlit glitch and restore the widget key from the data
231
+ model instead of acting on it.
232
+ """
233
+ grkey = _global_rename_key(idx)
234
+ entry = st.session_state.globalRenames[idx]
235
+ name = entry["name"]
236
+
237
+ prev = list(entry["speakers"]) # current data-model state (ordered)
238
+ reported = list(st.session_state.get(grkey, []))
239
+
240
+ # Build the set of tokens that are legitimately available for this entry
241
+ # right now (not claimed by any OTHER entry).
242
+ other_claimed = {
243
+ t
244
+ for other_idx, other_entry in enumerate(st.session_state.globalRenames)
245
+ if other_idx != idx
246
+ for t in other_entry["speakers"]
247
+ }
248
+
249
+ # Spurious-empty guard: if the widget reports [] but the data model has
250
+ # tokens and every one of them is still a valid (unclaimed) option, this is
251
+ # a Streamlit re-fire artefact β€” restore the key and do nothing.
252
+ if not reported and prev:
253
+ all_still_valid = all(t not in other_claimed for t in prev)
254
+ if all_still_valid:
255
+ st.session_state[grkey] = prev
256
+ return
257
+
258
+ prev_set = set(prev)
259
+ new_set = set(reported)
260
+ added = new_set - prev_set
261
+ removed = prev_set - new_set
262
+
263
+ # Write the new speakers list to the data model
264
+ kept = [t for t in prev if t in new_set]
265
+ entry["speakers"] = kept + [t for t in added]
266
+ st.session_state.pop(grkey, None)
267
+
268
+ # Enforce exclusivity and write renames for newly added tokens
269
+ for token in added:
270
+ for other_idx, other_entry in enumerate(st.session_state.globalRenames):
271
+ if other_idx == idx:
272
+ continue
273
+ if token in other_entry["speakers"]:
274
+ other_entry["speakers"].remove(token)
275
+ st.session_state.pop(_global_rename_key(other_idx), None)
276
+ _write_rename(token, "")
277
+ _write_rename(token, name)
278
+
279
+ # Revert speakerRenames for tokens genuinely removed from this entry
280
+ for token in removed:
281
+ _write_rename(token, "")
282
 
283
 
284
  # ---------------------------------------------------------------------------
 
291
  result = st.session_state.results.get(fileName)
292
  if not result:
293
  return
294
+ # Pop category widget keys so they re-seed from categorySelect data
295
+ for category in st.session_state.categories:
296
+ st.session_state.pop(f"multiselect_{category}", None)
297
+ # Pop globalRenames widget keys so they re-seed from entry["speakers"] data
298
+ for i in range(len(st.session_state.globalRenames)):
299
+ st.session_state.pop(_global_rename_key(i), None)
 
 
 
 
 
 
 
300
 
301
 
302
  # ---------------------------------------------------------------------------
 
333
  st.session_state.results.setdefault(fname, [])
334
  st.session_state.summaries.setdefault(fname, {})
335
  st.session_state.unusedSpeakers.setdefault(fname, [])
336
+ # Ensure categorySelect has one list per category (global, not per-file)
337
+ while len(st.session_state.categorySelect) < len(st.session_state.categories):
338
+ st.session_state.categorySelect.append([])
339
  st.session_state.speakerRenames.setdefault(fname, {})
340
  st.session_state.speakerClips.setdefault(fname, {})
341
  if fname not in st.session_state.file_names:
 
476
 
477
  currAnnotation, currTotalTime = st.session_state.results[inFileName]
478
  speakerNames = currAnnotation.labels()
479
+ # categorySelect is global tokens ("fname: SPEAKER_##"); extract raw IDs for this file
480
+ prefix = inFileName + ": "
481
+ categorySelections = [
482
+ [token[len(prefix):] for token in tokens if token.startswith(prefix)]
483
+ for tokens in st.session_state.categorySelect
484
+ ]
485
  printV("Loaded results", 4)
486
 
487
  noVoice, oneVoice, multiVoice = su.calcSpeakingTypes(currAnnotation, currTotalTime)