Spaces:
Sleeping
Sleeping
feat: Add timeline shifting and track start time editing
Browse filesImplements two new editing features to provide more granular control over the CUE sheet.
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
|
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 |
-
|
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=
|
168 |
-
#
|
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,
|
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,
|
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
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
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,
|
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)
|