avans06 commited on
Commit
d439dd1
·
1 Parent(s): fc8a81e

feat: Add timeline shifting and track start time editing

Browse files

Implements two new editing features to provide more granular control over the CUE sheet.

Files changed (1) hide show
  1. app.py +103 -25
app.py CHANGED
@@ -132,7 +132,7 @@ def parse_cue_and_update_ui(cue_text):
132
  return cue_text, audio_filename, times, audio_duration, gr.update(choices=track_labels, value=[]), gr.update(visible=True)
133
 
134
  def update_editing_tools(selected_tracks, current_times, audio_duration):
135
- """Dynamically shows/hides Merge or Split tools based on selection count."""
136
  num_selected = len(selected_tracks)
137
 
138
  if num_selected == 1:
@@ -149,33 +149,27 @@ def update_editing_tools(selected_tracks, current_times, audio_duration):
149
  new_min_time = start_time + padding
150
  new_max_time = end_time
151
 
152
- # --- 3. [CORRECTION] Check if the track is too short to be split ---
153
- if new_min_time >= new_max_time:
154
- # If the track is too short, splitting is not possible. Hide the tools.
155
- return (
156
- gr.update(visible=False), # Hide Merge button
157
- gr.update(visible=False), # Hide Split Group
158
- None,
159
- None
160
- )
161
-
162
  # --- 4. Configure and show the Split UI with the corrected range ---
163
  mid_point = start_time + (end_time - start_time) / 2
164
 
 
 
165
  return (
166
  gr.update(visible=False), # Hide Merge button
167
- gr.update(visible=True), # Show Split Group
168
- # Use the new padded min/max values for the slider
169
  gr.update(minimum=new_min_time, maximum=new_max_time, value=mid_point), # Configure Slider
170
- gr.update(value=f"Split at: {seconds_to_cue_time(mid_point)}") # Update slider label
 
171
  )
172
 
173
  elif num_selected > 1:
174
  # Show Merge UI
175
- return gr.update(visible=True), gr.update(visible=False), None, None
176
  else:
177
  # Hide everything
178
- return gr.update(visible=False), gr.update(visible=False), None, None
179
 
180
  def perform_manual_merge(selected_tracks, original_times, audio_duration, audio_filename):
181
  """Merges selected tracks. The internal logic is robust and unchanged."""
@@ -217,6 +211,51 @@ def perform_manual_split(split_time_sec, original_times, audio_duration, audio_f
217
  new_track_labels = generate_track_labels(new_times, audio_duration)
218
  return final_cue_text, new_times, gr.update(choices=new_track_labels, value=[])
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
  # --- Gradio User Interface Definition ---
222
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
@@ -233,9 +272,9 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
233
  audio_input = gr.Audio(type="filepath", label="Upload Audio File")
234
  with gr.Accordion("Analysis Parameters", open=False):
235
  threshold_slider = gr.Slider(10, 80, 40, step=1, label="Silence Threshold (dB)")
236
- min_length_slider = gr.Slider(0.5, 30, 2, step=0.1, label="Min. Segment Length (s)")
237
  merge_length_slider = gr.Slider(1, 60, 15, step=1, label="Auto-Merge Threshold (s)")
238
- min_silence_length_slider = gr.Slider(0.5, 60, 5, step=0.1, label="Merge Protection Length (s)")
239
  generate_button = gr.Button("Analyze Audio", variant="primary")
240
 
241
  with gr.TabItem("Start with CUE Text"):
@@ -251,19 +290,41 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
251
 
252
  with gr.Row(visible=False) as merge_tools:
253
  merge_button = gr.Button("Merge Selected Tracks", variant="secondary", size="lg")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
- with gr.Group(visible=False) as split_tools:
256
- split_slider_label = gr.Textbox(label="Current Split Time", interactive=False)
257
- split_slider = gr.Slider(label="Drag to select split point")
258
- split_button = gr.Button("Split Track at Selected Time", variant="secondary")
259
 
260
  # --- Event Wiring ---
261
 
 
 
 
 
 
262
  # Workflow 1: Audio analysis button now updates everything, including the editing tools.
263
  generate_button.click(
264
  fn=analyze_audio_to_cue,
265
  inputs=[audio_input, threshold_slider, min_length_slider, merge_length_slider, min_silence_length_slider],
266
- outputs=[output_text, audio_filename_state, track_times_state, audio_duration_state, track_checkboxes, manual_editing_group]
 
 
 
 
267
  )
268
 
269
  # Workflow 2: Pasting text in the dedicated input box populates the main output and enables tools.
@@ -271,14 +332,18 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
271
  cue_text_input_for_paste.change(
272
  fn=parse_cue_and_update_ui,
273
  inputs=[cue_text_input_for_paste],
274
- outputs=[output_text, audio_filename_state, track_times_state, audio_duration_state, track_checkboxes, manual_editing_group]
 
 
 
 
275
  )
276
 
277
  # Dynamic UI controller for showing/hiding Merge/Split tools
278
  track_checkboxes.change(
279
  fn=update_editing_tools,
280
  inputs=[track_checkboxes, track_times_state, audio_duration_state],
281
- outputs=[merge_tools, split_tools, split_slider, split_slider_label]
282
  )
283
 
284
  # Live update for the split slider's time display
@@ -300,6 +365,19 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
300
  inputs=[split_slider, track_times_state, audio_duration_state, audio_filename_state],
301
  outputs=[output_text, track_times_state, track_checkboxes]
302
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
  if __name__ == "__main__":
305
  demo.launch(inbrowser=True)
 
132
  return cue_text, audio_filename, times, audio_duration, gr.update(choices=track_labels, value=[]), gr.update(visible=True)
133
 
134
  def update_editing_tools(selected_tracks, current_times, audio_duration):
135
+ """Dynamically shows/hides editing tools based on selection count."""
136
  num_selected = len(selected_tracks)
137
 
138
  if num_selected == 1:
 
149
  new_min_time = start_time + padding
150
  new_max_time = end_time
151
 
152
+ split_possible = new_min_time < new_max_time
 
 
 
 
 
 
 
 
 
153
  # --- 4. Configure and show the Split UI with the corrected range ---
154
  mid_point = start_time + (end_time - start_time) / 2
155
 
156
+ current_start_time_str = seconds_to_cue_time(start_time)
157
+
158
  return (
159
  gr.update(visible=False), # Hide Merge button
160
+ gr.update(visible=split_possible), # Show/Hide Split Group
161
+ gr.update(visible=True), # Show Edit Time Group
162
  gr.update(minimum=new_min_time, maximum=new_max_time, value=mid_point), # Configure Slider
163
+ gr.update(value=f"Split at: {seconds_to_cue_time(mid_point)}"), # Update slider label
164
+ gr.update(value=current_start_time_str) # Set current start time in edit box
165
  )
166
 
167
  elif num_selected > 1:
168
  # Show Merge UI
169
+ return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), None, None, None
170
  else:
171
  # Hide everything
172
+ return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), None, None, None
173
 
174
  def perform_manual_merge(selected_tracks, original_times, audio_duration, audio_filename):
175
  """Merges selected tracks. The internal logic is robust and unchanged."""
 
211
  new_track_labels = generate_track_labels(new_times, audio_duration)
212
  return final_cue_text, new_times, gr.update(choices=new_track_labels, value=[])
213
 
214
+ # --- Timeline Shift ---
215
+ def shift_timeline(shift_amount_sec, original_times, audio_duration, audio_filename):
216
+ """Shifts all track start times by a specified amount."""
217
+ if not original_times:
218
+ raise gr.Error("No track times to shift.")
219
+
220
+ # Apply the shift, ensuring no time is negative or exceeds audio duration
221
+ new_times = [min(max(0, t + shift_amount_sec), audio_duration) for t in original_times]
222
+
223
+ # Clean up by sorting and removing duplicates (e.g., if multiple tracks are clamped to 0)
224
+ new_times = sorted(list(set(new_times)))
225
+
226
+ final_cue_text = format_cue_text(new_times, audio_filename)
227
+ new_track_labels = generate_track_labels(new_times, audio_duration)
228
+ return final_cue_text, new_times, gr.update(choices=new_track_labels, value=[])
229
+
230
+
231
+ # --- Edit Track Start Time ---
232
+ def edit_track_start_time(selected_track, new_time_str, original_times, audio_duration, audio_filename):
233
+ """Edits the start time of a single selected track."""
234
+ if not selected_track:
235
+ raise gr.Error("No track selected for editing.")
236
+
237
+ new_time_sec = parse_cue_time_to_seconds(new_time_str)
238
+ if new_time_sec is None:
239
+ raise gr.Error("Invalid time format. Please use MM:SS:FF.")
240
+
241
+ track_idx = int(selected_track[0].split(' ')[1]) - 1
242
+
243
+ # Boundary checks
244
+ prev_track_time = original_times[track_idx - 1] if track_idx > 0 else -1
245
+ next_track_time = original_times[track_idx + 1] if track_idx < len(original_times) - 1 else audio_duration + 1
246
+
247
+ if new_time_sec <= prev_track_time:
248
+ raise gr.Error(f"New time cannot be earlier than the previous track's start time ({seconds_to_cue_time(prev_track_time)}).")
249
+ if new_time_sec >= next_track_time:
250
+ raise gr.Error(f"New time cannot be later than the next track's start time ({seconds_to_cue_time(next_track_time)}).")
251
+
252
+ new_times = original_times[:]
253
+ new_times[track_idx] = new_time_sec
254
+
255
+ final_cue_text = format_cue_text(new_times, audio_filename)
256
+ new_track_labels = generate_track_labels(new_times, audio_duration)
257
+ return final_cue_text, new_times, gr.update(choices=new_track_labels, value=[])
258
+
259
 
260
  # --- Gradio User Interface Definition ---
261
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
 
272
  audio_input = gr.Audio(type="filepath", label="Upload Audio File")
273
  with gr.Accordion("Analysis Parameters", open=False):
274
  threshold_slider = gr.Slider(10, 80, 40, step=1, label="Silence Threshold (dB)")
275
+ min_length_slider = gr.Slider(0.5, 30, 1, step=0.1, label="Min. Segment Length (s)")
276
  merge_length_slider = gr.Slider(1, 60, 15, step=1, label="Auto-Merge Threshold (s)")
277
+ min_silence_length_slider = gr.Slider(0.5, 60, 2, step=0.1, label="Merge Protection Length (s)")
278
  generate_button = gr.Button("Analyze Audio", variant="primary")
279
 
280
  with gr.TabItem("Start with CUE Text"):
 
290
 
291
  with gr.Row(visible=False) as merge_tools:
292
  merge_button = gr.Button("Merge Selected Tracks", variant="secondary", size="lg")
293
+
294
+ # This group contains both Split and Edit tools, shown when one track is selected
295
+ with gr.Group(visible=False) as single_track_tools:
296
+ with gr.Accordion("Split Track", open=False):
297
+ split_slider_label = gr.Textbox(label="Current Split Time", interactive=False)
298
+ split_slider = gr.Slider(label="Drag to select split point")
299
+ split_button = gr.Button("Split Track at Selected Time", variant="secondary")
300
+
301
+ # --- Edit Start Time ---
302
+ with gr.Accordion("Edit Start Time", open=True):
303
+ edit_time_input = gr.Textbox(label="New Start Time (MM:SS:FF)", placeholder="e.g., 01:23:45")
304
+ edit_time_button = gr.Button("Update Start Time", variant="secondary")
305
+
306
+ # --- Global Timeline Shift ---
307
+ with gr.Accordion("Global Edits", open=False, visible=False) as global_editing_group:
308
+ shift_amount_input = gr.Number(label="Timeline Shift Amount (seconds, +/-)", value=0)
309
+ shift_button = gr.Button("Apply Timeline Shift", variant="secondary")
310
 
 
 
 
 
311
 
312
  # --- Event Wiring ---
313
 
314
+ # Combined update for enabling editing groups
315
+ def show_editing_groups(times):
316
+ is_visible = bool(times)
317
+ return gr.update(visible=is_visible), gr.update(visible=is_visible)
318
+
319
  # Workflow 1: Audio analysis button now updates everything, including the editing tools.
320
  generate_button.click(
321
  fn=analyze_audio_to_cue,
322
  inputs=[audio_input, threshold_slider, min_length_slider, merge_length_slider, min_silence_length_slider],
323
+ outputs=[output_text, audio_filename_state, track_times_state, audio_duration_state, track_checkboxes]
324
+ ).then(
325
+ fn=show_editing_groups,
326
+ inputs=[track_times_state],
327
+ outputs=[manual_editing_group, global_editing_group]
328
  )
329
 
330
  # Workflow 2: Pasting text in the dedicated input box populates the main output and enables tools.
 
332
  cue_text_input_for_paste.change(
333
  fn=parse_cue_and_update_ui,
334
  inputs=[cue_text_input_for_paste],
335
+ outputs=[output_text, audio_filename_state, track_times_state, audio_duration_state, track_checkboxes]
336
+ ).then(
337
+ fn=show_editing_groups,
338
+ inputs=[track_times_state],
339
+ outputs=[manual_editing_group, global_editing_group]
340
  )
341
 
342
  # Dynamic UI controller for showing/hiding Merge/Split tools
343
  track_checkboxes.change(
344
  fn=update_editing_tools,
345
  inputs=[track_checkboxes, track_times_state, audio_duration_state],
346
+ outputs=[merge_tools, single_track_tools, edit_time_input, split_slider, split_slider_label, edit_time_input]
347
  )
348
 
349
  # Live update for the split slider's time display
 
365
  inputs=[split_slider, track_times_state, audio_duration_state, audio_filename_state],
366
  outputs=[output_text, track_times_state, track_checkboxes]
367
  )
368
+
369
+ # --- Action Buttons for New Features ---
370
+ shift_button.click(
371
+ fn=shift_timeline,
372
+ inputs=[shift_amount_input, track_times_state, audio_duration_state, audio_filename_state],
373
+ outputs=[output_text, track_times_state, track_checkboxes]
374
+ )
375
+
376
+ edit_time_button.click(
377
+ fn=edit_track_start_time,
378
+ inputs=[track_checkboxes, edit_time_input, track_times_state, audio_duration_state, audio_filename_state],
379
+ outputs=[output_text, track_times_state, track_checkboxes]
380
+ )
381
 
382
  if __name__ == "__main__":
383
  demo.launch(inbrowser=True)