vargha commited on
Commit
7ee3636
·
1 Parent(s): 7a295c7

updating interface for trim, delete, jump

Browse files
components/dashboard_page.py CHANGED
@@ -1,9 +1,14 @@
1
  import gradio as gr
2
  import numpy as np
 
 
 
3
  from components.header import Header
4
  from utils.logger import Logger
5
- from utils.gdrive_downloader import PublicFolderAudioLoader
6
  from config import conf
 
 
7
 
8
  log = Logger()
9
  LOADER = PublicFolderAudioLoader(conf.GDRIVE_API_KEY)
@@ -21,145 +26,436 @@ class DashboardPage:
21
  with gr.Row():
22
  self.tts_id = gr.Textbox(label="ID", interactive=False)
23
  self.filename = gr.Textbox(label="Filename", interactive=False)
24
- self.sentence = gr.Textbox(
25
- label="Sentence", interactive=False, max_lines=5, rtl=True
26
- )
27
- self.ann_sentence = gr.Textbox(
28
- label="Annotated Sentence",
29
- interactive=True,
30
- max_lines=5,
31
- rtl=True,
32
- )
33
  with gr.Row():
34
- self.ann_at = gr.Textbox(
35
- label="Annotation Time", interactive=False
36
  )
 
 
 
 
 
 
 
 
 
 
37
  self.validated = gr.Checkbox(
38
- label="Validated", interactive=False
39
  )
40
  with gr.Row():
41
- self.btn_prev = gr.Button("⬅️ Previous")
42
- self.btn_next = gr.Button("Next ➡️")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  # ستون راست
45
  with gr.Column(scale=2):
46
- self.btn_load_voice = gr.Button("Load Audio")
47
- self.audio = gr.Audio(label="🔊 Audio", interactive=False)
 
 
48
 
49
  # stateها
50
  self.items_state = gr.State([])
51
  self.idx_state = gr.State(0)
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  # ---------------- wiring ---------------- #
54
  def register_callbacks(
55
  self, login_page, session_state: gr.State, root_blocks: gr.Blocks
56
  ):
57
-
58
  self.header.register_callbacks(login_page, self, session_state)
59
 
60
- # ---- helpers ----
61
- def show_current(items, idx):
62
- if not items:
63
- return ["", "", "", "", "", False, None]
64
- d = items[idx]
65
- return [
66
- d["id"],
67
- d["filename"],
68
- d["sentence"],
69
- d.get("annotated_sentence", ""),
70
- d.get("annotated_at", ""),
71
- bool(d.get("validated", False)),
72
- None, # Audio هنوز لود نشده
73
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- def next_idx(items, idx):
76
- return min(idx + 1, max(len(items) - 1, 0))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- def prev_idx(items, idx):
79
- return max(idx - 1, 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- def load_items(sess):
82
  items = sess.get("dashboard_items", [])
83
- return items, 0, *show_current(items, 0)
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
- # ---------- initial load ---------- #
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  root_blocks.load(
87
- fn=load_items,
 
 
 
88
  inputs=[session_state],
89
- outputs=[
90
- self.items_state,
91
- self.idx_state,
92
- self.tts_id,
93
- self.filename,
94
- self.sentence,
95
- self.ann_sentence,
96
- self.ann_at,
97
- self.validated,
98
- self.audio, # 9 اُمین خروجى
99
- ],
 
100
  )
101
 
102
- # ---------- navigation ---------- #
103
- for btn, nav_fn in [(self.btn_prev, prev_idx), (self.btn_next, next_idx)]:
104
- (
105
- btn.click(
106
- fn=nav_fn,
107
- inputs=[self.items_state, self.idx_state],
108
- outputs=self.idx_state,
109
- ).then(
110
- fn=show_current,
111
- inputs=[self.items_state, self.idx_state],
112
- outputs=[
113
- self.tts_id,
114
- self.filename,
115
- self.sentence,
116
- self.ann_sentence,
117
- self.ann_at,
118
- self.validated,
119
- self.audio,
120
  ],
 
121
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  )
123
 
124
- # ---------- load audio ---------- #
125
- def disable_buttons():
126
- return [
127
- gr.update(value="⏳ Waiting...", interactive=False),
128
- gr.update(interactive=False),
129
- gr.update(interactive=False),
130
- ]
 
 
 
 
 
 
 
 
 
131
 
132
- def enable_buttons():
133
- return [
134
- gr.update(value="Load Audio", interactive=True),
135
- gr.update(interactive=True),
136
- gr.update(interactive=True),
137
- ]
138
 
139
- def download_voice(folder_link, filename):
140
- if not filename:
141
- return None
142
- try:
143
- sr, wav = LOADER.load_audio(folder_link, filename)
144
- return (sr, wav)
145
- except Exception as e:
146
- log.error(f"GDrive download failed: {e}")
147
- return None # player خالي مى‌ماند
148
-
149
- (
150
- self.btn_load_voice.click(
151
- fn=disable_buttons,
152
- inputs=None,
153
- outputs=[self.btn_load_voice, self.btn_prev, self.btn_next],
154
- )
155
- .then(
156
- fn=download_voice,
157
- inputs=[gr.State(GDRIVE_FOLDER), self.filename],
158
- outputs=self.audio,
159
- )
160
- .then(
161
- fn=enable_buttons,
162
- inputs=None,
163
- outputs=[self.btn_load_voice, self.btn_prev, self.btn_next],
164
- )
 
 
 
 
 
 
 
 
 
 
165
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import numpy as np
3
+ import datetime
4
+ from sqlalchemy import orm
5
+
6
  from components.header import Header
7
  from utils.logger import Logger
8
+ from utils.gdrive_downloader import PublicFolderAudioLoader # Assuming LOADER uses this
9
  from config import conf
10
+ from utils.database import get_db # For DB operations
11
+ from data.models import Annotation, AudioTrim, TTSData # Import your models
12
 
13
  log = Logger()
14
  LOADER = PublicFolderAudioLoader(conf.GDRIVE_API_KEY)
 
26
  with gr.Row():
27
  self.tts_id = gr.Textbox(label="ID", interactive=False)
28
  self.filename = gr.Textbox(label="Filename", interactive=False)
 
 
 
 
 
 
 
 
 
29
  with gr.Row():
30
+ self.sentence = gr.Textbox(
31
+ label="Sentence", interactive=False, max_lines=5, rtl=True
32
  )
33
+ self.btn_copy = gr.Button("📋 Copy", interactive=True)
34
+ with gr.Row():
35
+ self.ann_sentence = gr.Textbox(
36
+ label="Annotated Sentence",
37
+ interactive=True,
38
+ max_lines=5,
39
+ rtl=True,
40
+ )
41
+ self.btn_paste = gr.Button("📥 Paste", interactive=True)
42
+ with gr.Row():
43
  self.validated = gr.Checkbox(
44
+ label="Validated", interactive=True
45
  )
46
  with gr.Row():
47
+ self.btn_prev = gr.Button("⬅️ Previous", interactive=True)
48
+ self.btn_next = gr.Button("Next ➡️", interactive=True)
49
+ self.btn_delete = gr.Button("🗑️ Delete", interactive=True)
50
+ with gr.Row():
51
+ self.jump_data_id_input = gr.Number(
52
+ label="Jump to Data ID", value=0, precision=0, interactive=True
53
+ )
54
+ self.btn_jump = gr.Button("Go", interactive=True)
55
+ with gr.Row():
56
+ self.trim_start_sec = gr.Number(
57
+ label="Trim Start (s)", value=0.0, precision=3, interactive=True
58
+ )
59
+ self.trim_end_sec = gr.Number(
60
+ label="Trim End (s)", value=0.0, precision=3, interactive=True
61
+ )
62
+ self.btn_trim = gr.Button("✂️ Trim", interactive=True)
63
+ self.btn_undo_trim = gr.Button("↩️ Undo Trim", interactive=True)
64
 
65
  # ستون راست
66
  with gr.Column(scale=2):
67
+ self.btn_load_voice = gr.Button("Load Audio", interactive=True)
68
+ self.audio = gr.Audio(
69
+ label="🔊 Audio", interactive=False, autoplay=True
70
+ )
71
 
72
  # stateها
73
  self.items_state = gr.State([])
74
  self.idx_state = gr.State(0)
75
+ self.clipboard_state = gr.State("")
76
+ self.original_audio_state = gr.State(None)
77
+ self.current_trim_params = gr.State(None)
78
+
79
+ # List of all interactive UI elements for enabling/disabling
80
+ self.interactive_ui_elements = [
81
+ self.btn_prev, self.btn_next, self.btn_delete, self.btn_jump,
82
+ self.jump_data_id_input, self.trim_start_sec, self.trim_end_sec,
83
+ self.btn_trim, self.btn_undo_trim, self.btn_load_voice,
84
+ self.ann_sentence, self.validated, self.btn_copy, self.btn_paste
85
+ ]
86
 
87
  # ---------------- wiring ---------------- #
88
  def register_callbacks(
89
  self, login_page, session_state: gr.State, root_blocks: gr.Blocks
90
  ):
 
91
  self.header.register_callbacks(login_page, self, session_state)
92
 
93
+ # Helper function to update UI interactive state
94
+ def update_ui_interactive_state(is_interactive: bool):
95
+ updates = []
96
+ for elem in self.interactive_ui_elements:
97
+ if elem == self.btn_load_voice and not is_interactive:
98
+ updates.append(gr.update(value="⏳ Loading...", interactive=False))
99
+ elif elem == self.btn_load_voice and is_interactive:
100
+ updates.append(gr.update(value="Load Audio", interactive=True))
101
+ else:
102
+ updates.append(gr.update(interactive=is_interactive))
103
+ return updates
104
+
105
+ # ---- All Helper Functions ----
106
+ def apply_loaded_trim_fn(audio_data_as_loaded, trim_params_from_state, original_audio_for_state):
107
+ """
108
+ Applies trim if trim_params_from_state are available to the audio_data_as_loaded.
109
+ This is used after loading an item and its original audio.
110
+ original_audio_for_state is preserved as the true original.
111
+ """
112
+ if audio_data_as_loaded and trim_params_from_state:
113
+ sr, wav = audio_data_as_loaded
114
+ start = trim_params_from_state.get("start")
115
+ end = trim_params_from_state.get("end")
116
+ operation = trim_params_from_state.get("operation")
117
+
118
+ if operation == "delete" and start is not None and end is not None and end > start and start >= 0:
119
+ start_sample = int(sr * start / 1000.0)
120
+ end_sample = int(sr * end / 1000.0)
121
+
122
+ audio_duration_samples = len(wav)
123
+ start_sample = max(0, min(start_sample, audio_duration_samples))
124
+ end_sample = max(start_sample, min(end_sample, audio_duration_samples))
125
+
126
+ if start_sample == 0 and end_sample == audio_duration_samples:
127
+ log.info(f"Applying saved trim: delete entire audio from {start}ms to {end}ms. Resulting in empty audio.")
128
+ return (sr, np.array([], dtype=wav.dtype)), original_audio_for_state
129
+
130
+ part1 = wav[:start_sample]
131
+ part2 = wav[end_sample:]
132
+ deleted_segment_wav = np.concatenate((part1, part2))
133
+
134
+ log.info(f"Applied saved trim (delete operation): {start}ms to {end}ms. Original shape: {wav.shape}, New shape: {deleted_segment_wav.shape}")
135
+ return (sr, deleted_segment_wav), original_audio_for_state
136
+ else:
137
+ if operation != "delete":
138
+ log.warning("Saved trim parameters do not specify a 'delete' operation. Using original audio.")
139
+ else:
140
+ log.warning("Invalid saved trim parameters for delete operation. Using original audio.")
141
+ return audio_data_as_loaded, original_audio_for_state
142
+ return audio_data_as_loaded, original_audio_for_state
143
 
144
+ def download_voice_fn(folder_link, filename_to_load):
145
+ if not filename_to_load:
146
+ return None, None
147
+ try:
148
+ log.info(f"Downloading voice: {filename_to_load}")
149
+ sr, wav = LOADER.load_audio(folder_link, filename_to_load)
150
+ return (sr, wav), (sr, wav.copy())
151
+ except Exception as e:
152
+ log.error(f"GDrive download failed for {filename_to_load}: {e}")
153
+ gr.Error(f"Failed to load audio: {filename_to_load}. Error: {e}")
154
+ return None, None
155
+
156
+ def save_annotation_db_fn(current_tts_id, session, ann_text_to_save, is_validated_ui, active_trim_params):
157
+ annotator_id = session.get("user_id")
158
+ if not current_tts_id or not annotator_id:
159
+ gr.Error("Cannot save: Missing TTS ID or User ID.")
160
+ return False
161
+ validated_to_save = bool(is_validated_ui)
162
+ with get_db() as db:
163
+ try:
164
+ annotation_obj = db.query(Annotation).filter_by(
165
+ tts_data_id=current_tts_id, annotator_id=annotator_id
166
+ ).first()
167
+ if not annotation_obj:
168
+ annotation_obj = Annotation(
169
+ tts_data_id=current_tts_id, annotator_id=annotator_id
170
+ )
171
+ db.add(annotation_obj)
172
+ annotation_obj.annotated_sentence = ann_text_to_save
173
+ annotation_obj.validated = validated_to_save
174
+ annotation_obj.annotated_at = datetime.datetime.utcnow()
175
+ if active_trim_params and active_trim_params.get("operation") == "delete" and active_trim_params.get("start") is not None:
176
+ start_to_save = active_trim_params["start"]
177
+ end_to_save = active_trim_params["end"]
178
+ if not annotation_obj.audio_trim:
179
+ db.flush()
180
+ if annotation_obj.id is None:
181
+ gr.Error("Failed to get annotation ID for saving trim.")
182
+ db.rollback()
183
+ return False
184
+ new_trim = AudioTrim(
185
+ annotation_id=annotation_obj.id,
186
+ original_tts_data_id=current_tts_id,
187
+ start=start_to_save,
188
+ end=end_to_save,
189
+ )
190
+ annotation_obj.audio_trim = new_trim
191
+ else:
192
+ annotation_obj.audio_trim.start = start_to_save
193
+ annotation_obj.audio_trim.end = end_to_save
194
+ elif annotation_obj.audio_trim:
195
+ db.delete(annotation_obj.audio_trim)
196
+ annotation_obj.audio_trim = None
197
+ db.commit()
198
+ gr.Info(f"Annotation for ID {current_tts_id} saved.")
199
+ return validated_to_save
200
+ except Exception as e:
201
+ db.rollback()
202
+ log.error(f"Failed to save annotation for {current_tts_id}: {e}")
203
+ gr.Error(f"Save failed: {e}")
204
+ return False
205
 
206
+ def show_current_item_fn(items, idx, session):
207
+ if not items or idx >= len(items):
208
+ return "", "", "", "", False, None, 0.0, 0.0, None
209
+ current_item = items[idx]
210
+ tts_data_id = current_item.get("id")
211
+ annotator_id = session.get("user_id")
212
+ ann_text, is_validated, trim_params_for_ui = "", False, None
213
+ start_sec_ui, end_sec_ui = 0.0, 0.0
214
+ if tts_data_id and annotator_id:
215
+ with get_db() as db:
216
+ try:
217
+ existing_annotation = db.query(Annotation).filter_by(
218
+ tts_data_id=tts_data_id, annotator_id=annotator_id
219
+ ).options(orm.joinedload(Annotation.audio_trim)).first() # Eager load audio_trim
220
+ if existing_annotation:
221
+ ann_text = existing_annotation.annotated_sentence or ""
222
+ is_validated = existing_annotation.validated
223
+ if existing_annotation.audio_trim:
224
+ trim_params_for_ui = {
225
+ "start": existing_annotation.audio_trim.start,
226
+ "end": existing_annotation.audio_trim.end,
227
+ "operation": "delete"
228
+ }
229
+ start_sec_ui = existing_annotation.audio_trim.start / 1000.0
230
+ end_sec_ui = existing_annotation.audio_trim.end / 1000.0
231
+ except Exception as e:
232
+ log.error(f"Database error in show_current_item_fn for TTS ID {tts_data_id}: {e}")
233
+ gr.Error(f"Error loading annotation details: {e}")
234
+ return (
235
+ current_item.get("id", ""), current_item.get("filename", ""),
236
+ current_item.get("sentence", ""), ann_text, is_validated, None,
237
+ start_sec_ui, end_sec_ui, trim_params_for_ui
238
+ )
239
+
240
+ def navigate_idx_fn(items, current_idx, direction):
241
+ if not items: return 0
242
+ new_idx = min(current_idx + 1, len(items) - 1) if direction == "next" else max(current_idx - 1, 0)
243
+ return new_idx
244
 
245
+ def load_all_items_fn(sess):
246
  items = sess.get("dashboard_items", [])
247
+ initial_ui_values = show_current_item_fn(items, 0, sess)
248
+ return items, 0, *initial_ui_values
249
+
250
+ def jump_by_data_id_fn(items, target_data_id_str, current_idx):
251
+ if not target_data_id_str: return current_idx
252
+ try:
253
+ target_id = int(target_data_id_str)
254
+ for i, item_dict in enumerate(items):
255
+ if item_dict.get("id") == target_id: return i
256
+ gr.Warning(f"Data ID {target_id} not found.")
257
+ except ValueError:
258
+ gr.Warning(f"Invalid Data ID format: {target_data_id_str}")
259
+ return current_idx
260
 
261
+ def perform_trim_fn(original_audio_data, start_sec, end_sec, current_audio_for_fallback):
262
+ log.info(f"perform_trim_fn called with start_sec: {start_sec}, end_sec: {end_sec}")
263
+ if original_audio_data is None:
264
+ gr.Warning("No original audio loaded. Cannot perform new trim.")
265
+ return current_audio_for_fallback, None
266
+ if start_sec is None or end_sec is None or start_sec < 0 or end_sec <= start_sec:
267
+ gr.Warning("Invalid trim times. Start must be >= 0 and End > Start.")
268
+ return original_audio_data, None
269
+ try:
270
+ sr, wav = original_audio_data
271
+ start_sample, end_sample = int(sr * start_sec), int(sr * end_sec)
272
+ audio_duration_samples = len(wav)
273
+ start_sample = max(0, min(start_sample, audio_duration_samples))
274
+ end_sample = max(start_sample, min(end_sample, audio_duration_samples))
275
+ trimmed_wav = np.concatenate((wav[:start_sample], wav[end_sample:]))
276
+ active_trim_params = {"start": start_sec * 1000.0, "end": end_sec * 1000.0, "operation": "delete"}
277
+ log.info(f"Audio segment deleted. New shape: {trimmed_wav.shape}")
278
+ if trimmed_wav.size == 0: gr.Warning("Trim resulted in empty audio.")
279
+ return (sr, trimmed_wav), active_trim_params
280
+ except Exception as e:
281
+ log.error(f"Error during audio trimming: {e}")
282
+ gr.Error(f"Failed to trim audio: {e}")
283
+ return original_audio_data, None
284
+
285
+ def delete_db_and_ui_fn(items, current_idx, session):
286
+ item_info = items[current_idx]
287
+ tts_data_id_to_delete = item_info.get("id")
288
+ annotator_id_for_delete = session.get("user_id")
289
+ if tts_data_id_to_delete and annotator_id_for_delete:
290
+ with get_db() as db:
291
+ try:
292
+ annotation_obj = db.query(Annotation).filter_by(
293
+ tts_data_id=tts_data_id_to_delete, annotator_id=annotator_id_for_delete
294
+ ).first()
295
+ if annotation_obj:
296
+ db.delete(annotation_obj) # Cascade should handle AudioTrim
297
+ db.commit()
298
+ gr.Info(f"Annotation for ID {tts_data_id_to_delete} deleted.")
299
+ else:
300
+ gr.Warning(f"No annotation found to delete for ID {tts_data_id_to_delete}.")
301
+ except Exception as e:
302
+ db.rollback()
303
+ log.error(f"Error deleting annotation {tts_data_id_to_delete}: {e}")
304
+ gr.Error(f"Failed to delete annotation: {e}")
305
+ else:
306
+ gr.Error("Cannot delete: Missing TTS ID or User ID.")
307
+ refreshed_ui_values = show_current_item_fn(items, current_idx, session)
308
+ return items, current_idx, *refreshed_ui_values
309
+
310
+ # ---- Callback Implementations ----
311
+ outputs_for_show_current = [
312
+ self.tts_id, self.filename, self.sentence, self.ann_sentence,
313
+ self.validated, self.audio, self.trim_start_sec,
314
+ self.trim_end_sec, self.current_trim_params,
315
+ ]
316
+
317
+ # Initial Load
318
  root_blocks.load(
319
+ fn=lambda: update_ui_interactive_state(False),
320
+ outputs=self.interactive_ui_elements
321
+ ).then(
322
+ fn=load_all_items_fn,
323
  inputs=[session_state],
324
+ outputs=[self.items_state, self.idx_state] + outputs_for_show_current,
325
+ ).then(
326
+ fn=download_voice_fn,
327
+ inputs=[gr.State(GDRIVE_FOLDER), self.filename],
328
+ outputs=[self.audio, self.original_audio_state],
329
+ ).then(
330
+ fn=apply_loaded_trim_fn,
331
+ inputs=[self.audio, self.current_trim_params, self.original_audio_state],
332
+ outputs=[self.audio, self.original_audio_state]
333
+ ).then(
334
+ fn=lambda: update_ui_interactive_state(True),
335
+ outputs=self.interactive_ui_elements
336
  )
337
 
338
+ # Navigation (Prev/Next)
339
+ for btn_widget, direction_str in [
340
+ (self.btn_prev, "prev"), (self.btn_next, "next"),
341
+ ]:
342
+ event_chain = btn_widget.click(
343
+ fn=lambda: update_ui_interactive_state(False),
344
+ outputs=self.interactive_ui_elements
345
+ )
346
+ if direction_str == "next":
347
+ event_chain = event_chain.then(
348
+ fn=save_annotation_db_fn,
349
+ inputs=[
350
+ self.tts_id, session_state, self.ann_sentence,
351
+ self.validated, self.current_trim_params,
 
 
 
 
352
  ],
353
+ outputs=[self.validated]
354
  )
355
+ event_chain.then(
356
+ fn=navigate_idx_fn,
357
+ inputs=[self.items_state, self.idx_state, gr.State(direction_str)],
358
+ outputs=self.idx_state,
359
+ ).then(
360
+ fn=show_current_item_fn,
361
+ inputs=[self.items_state, self.idx_state, session_state],
362
+ outputs=outputs_for_show_current,
363
+ ).then(
364
+ fn=download_voice_fn,
365
+ inputs=[gr.State(GDRIVE_FOLDER), self.filename],
366
+ outputs=[self.audio, self.original_audio_state],
367
+ ).then(
368
+ fn=apply_loaded_trim_fn,
369
+ inputs=[self.audio, self.current_trim_params, self.original_audio_state],
370
+ outputs=[self.audio, self.original_audio_state]
371
+ ).then(
372
+ fn=lambda: update_ui_interactive_state(True),
373
+ outputs=self.interactive_ui_elements
374
  )
375
 
376
+ # Manual Load Audio Button
377
+ self.btn_load_voice.click(
378
+ fn=lambda: update_ui_interactive_state(False),
379
+ outputs=self.interactive_ui_elements
380
+ ).then(
381
+ fn=download_voice_fn,
382
+ inputs=[gr.State(GDRIVE_FOLDER), self.filename],
383
+ outputs=[self.audio, self.original_audio_state],
384
+ ).then(
385
+ fn=apply_loaded_trim_fn,
386
+ inputs=[self.audio, self.current_trim_params, self.original_audio_state],
387
+ outputs=[self.audio, self.original_audio_state]
388
+ ).then(
389
+ fn=lambda: update_ui_interactive_state(True),
390
+ outputs=self.interactive_ui_elements
391
+ )
392
 
393
+ # Copy/Paste (Quick operations, no UI disable needed)
394
+ self.btn_copy.click(fn=lambda x: x, inputs=self.sentence, outputs=self.clipboard_state)
395
+ self.btn_paste.click(fn=lambda x: x, inputs=self.clipboard_state, outputs=self.ann_sentence)
 
 
 
396
 
397
+ # Jump to Data ID
398
+ self.btn_jump.click(
399
+ fn=lambda: update_ui_interactive_state(False),
400
+ outputs=self.interactive_ui_elements
401
+ ).then(
402
+ fn=jump_by_data_id_fn,
403
+ inputs=[self.items_state, self.jump_data_id_input, self.idx_state],
404
+ outputs=self.idx_state,
405
+ ).then(
406
+ fn=show_current_item_fn,
407
+ inputs=[self.items_state, self.idx_state, session_state],
408
+ outputs=outputs_for_show_current,
409
+ ).then(
410
+ fn=download_voice_fn,
411
+ inputs=[gr.State(GDRIVE_FOLDER), self.filename],
412
+ outputs=[self.audio, self.original_audio_state],
413
+ ).then(
414
+ fn=apply_loaded_trim_fn,
415
+ inputs=[self.audio, self.current_trim_params, self.original_audio_state],
416
+ outputs=[self.audio, self.original_audio_state]
417
+ ).then(
418
+ fn=lambda: update_ui_interactive_state(True),
419
+ outputs=self.interactive_ui_elements
420
+ )
421
+
422
+ # Trim Audio
423
+ self.btn_trim.click(
424
+ fn=lambda: update_ui_interactive_state(False),
425
+ outputs=self.interactive_ui_elements
426
+ ).then(
427
+ fn=perform_trim_fn,
428
+ inputs=[self.original_audio_state, self.trim_start_sec, self.trim_end_sec, self.audio],
429
+ outputs=[self.audio, self.current_trim_params],
430
+ ).then(
431
+ fn=lambda: update_ui_interactive_state(True),
432
+ outputs=self.interactive_ui_elements
433
  )
434
+
435
+ # Undo Trim
436
+ self.btn_undo_trim.click(
437
+ fn=lambda: update_ui_interactive_state(False),
438
+ outputs=self.interactive_ui_elements
439
+ ).then(
440
+ fn=lambda orig_audio: (orig_audio, None, 0.0, 0.0) if orig_audio else (None, None, 0.0, 0.0),
441
+ inputs=[self.original_audio_state],
442
+ outputs=[self.audio, self.current_trim_params, self.trim_start_sec, self.trim_end_sec],
443
+ ).then(
444
+ fn=lambda: update_ui_interactive_state(True),
445
+ outputs=self.interactive_ui_elements
446
+ )
447
+
448
+ # Delete Annotation
449
+ self.btn_delete.click(
450
+ fn=lambda: update_ui_interactive_state(False),
451
+ outputs=self.interactive_ui_elements
452
+ ).then(
453
+ fn=delete_db_and_ui_fn,
454
+ inputs=[self.items_state, self.idx_state, session_state],
455
+ outputs=[self.items_state, self.idx_state] + outputs_for_show_current,
456
+ ).then(
457
+ fn=lambda: update_ui_interactive_state(True),
458
+ outputs=self.interactive_ui_elements
459
+ )
460
+
461
+ return self.container
components/login_page.py CHANGED
@@ -48,7 +48,7 @@ class LoginPage:
48
  dashboard_page.filename,
49
  dashboard_page.sentence,
50
  dashboard_page.ann_sentence,
51
- dashboard_page.ann_at,
52
  dashboard_page.validated,
53
  ],
54
  )
 
48
  dashboard_page.filename,
49
  dashboard_page.sentence,
50
  dashboard_page.ann_sentence,
51
+ # dashboard_page.ann_at,
52
  dashboard_page.validated,
53
  ],
54
  )