SaltProphet commited on
Commit
5da6c9d
·
verified ·
1 Parent(s): 819dfa0

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -457
app.py DELETED
@@ -1,457 +0,0 @@
1
- import gradio as gr
2
- import os
3
- import shutil
4
- import asyncio
5
- import librosa
6
- import librosa.display
7
- import soundfile as sf
8
- import numpy as np
9
- import time
10
- import zipfile
11
- import tempfile
12
- import matplotlib.pyplot as plt
13
- import matplotlib
14
-
15
- # Use a non-interactive backend for Matplotlib
16
- matplotlib.use('Agg')
17
-
18
- def update_output_visibility(choice):
19
- """Updates the visibility of stem outputs based on user's choice."""
20
- if "2 Stems" in choice:
21
- return {
22
- vocals_output: gr.update(visible=True),
23
- drums_output: gr.update(visible=False),
24
- bass_output: gr.update(visible=False),
25
- other_output: gr.update(visible=True, label="Instrumental (No Vocals)")
26
- }
27
- elif "4 Stems" in choice:
28
- return {
29
- vocals_output: gr.update(visible=True),
30
- drums_output: gr.update(visible=True),
31
- bass_output: gr.update(visible=True),
32
- other_output: gr.update(visible=True, label="Other")
33
- }
34
-
35
- async def separate_stems(audio_file_path, stem_choice):
36
- """
37
- Separates audio into stems using Demucs.
38
- This is an async generator function that yields updates to the UI.
39
- """
40
- if audio_file_path is None:
41
- raise gr.Error("No audio file uploaded!")
42
-
43
- log_history = "Starting separation...\n"
44
- yield {
45
- status_log: log_history,
46
- progress_bar: gr.Progress(0, desc="Starting...", visible=True)
47
- }
48
-
49
- try:
50
- log_history += "Preparing audio file...\n"
51
- yield {
52
- status_log: log_history,
53
- progress_bar: gr.Progress(0.05, desc="Preparing...")
54
- }
55
-
56
- original_filename_base = os.path.basename(audio_file_path).rsplit('.', 1)[0]
57
- # Create a stable path to avoid issues with temp files
58
- stable_input_path = f"stable_input_{original_filename_base}.wav"
59
- shutil.copy(audio_file_path, stable_input_path)
60
-
61
- model_arg = "--two-stems=vocals" if "2 Stems" in stem_choice else ""
62
- output_dir = "separated"
63
- if os.path.exists(output_dir):
64
- shutil.rmtree(output_dir)
65
-
66
- command = f"python3 -m demucs {model_arg} -o \"{output_dir}\" \"{stable_input_path}\""
67
-
68
- log_history += f"Running Demucs command: {command}\n(This may take a minute or more depending on track length)\n"
69
- yield {
70
- status_log: log_history,
71
- progress_bar: gr.Progress(0.2, desc="Running Demucs...")
72
- }
73
-
74
- process = await asyncio.create_subprocess_shell(
75
- command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
76
-
77
- stdout, stderr = await process.communicate()
78
-
79
- if process.returncode != 0:
80
- print(f"Demucs Error: {stderr.decode()}")
81
- raise gr.Error(f"Demucs failed. Is it installed? Error: {stderr.decode()[:500]}")
82
-
83
- log_history += "Demucs finished. Locating stem files...\n"
84
- yield {
85
- status_log: log_history,
86
- progress_bar: gr.Progress(0.8, desc="Locating stems...")
87
- }
88
-
89
- stable_filename_base = os.path.basename(stable_input_path).rsplit('.', 1)[0]
90
- subfolders = [f.name for f in os.scandir(output_dir) if f.is_dir()]
91
- if not subfolders:
92
- raise gr.Error("Demucs output folder structure not found!")
93
-
94
- # Assume the first subfolder is the one Demucs created
95
- model_folder_name = subfolders[0]
96
- stems_path = os.path.join(output_dir, model_folder_name, stable_filename_base)
97
-
98
- if not os.path.exists(stems_path):
99
- # Fallback: sometimes demucs doesn't create the innermost folder
100
- stems_path = os.path.join(output_dir, model_folder_name)
101
- if not (os.path.exists(os.path.join(stems_path, "vocals.wav")) or os.path.exists(os.path.join(stems_path, "no_vocals.wav"))):
102
- raise gr.Error(f"Demucs output directory was not found! Looked for: {stems_path}")
103
-
104
-
105
- vocals_path = os.path.join(stems_path, "vocals.wav") if os.path.exists(os.path.join(stems_path, "vocals.wav")) else None
106
- drums_path = os.path.join(stems_path, "drums.wav") if os.path.exists(os.path.join(stems_path, "drums.wav")) else None
107
- bass_path = os.path.join(stems_path, "bass.wav") if os.path.exists(os.path.join(stems_path, "bass.wav")) else None
108
-
109
- other_filename = "no_vocals.wav" if "2 Stems" in stem_choice else "other.wav"
110
- other_path = os.path.join(stems_path, other_filename) if os.path.exists(os.path.join(stems_path, other_filename)) else None
111
-
112
- # Clean up the copied input file
113
- os.remove(stable_input_path)
114
-
115
- log_history += "✅ Stem separation complete!\n"
116
- yield {
117
- status_log: log_history,
118
- progress_bar: gr.Progress(1, desc="Complete!", visible=False), # Hide progress bar when done
119
- vocals_output: gr.update(value=vocals_path),
120
- drums_output: gr.update(value=drums_path),
121
- bass_output: gr.update(value=bass_path),
122
- other_output: gr.update(value=other_path)
123
- }
124
-
125
- except Exception as e:
126
- print(f"An error occurred during separation: {e}")
127
- # Clean up input file on error too
128
- if 'stable_input_path' in locals() and os.path.exists(stable_input_path):
129
- os.remove(stable_input_path)
130
- yield {
131
- status_log: log_history + f"❌ ERROR: {e}",
132
- progress_bar: gr.update(visible=False) # Hide progress bar on error
133
- }
134
-
135
- def slice_stem_real(stem_audio_data, loop_choice, sensitivity, stem_name, progress_fn=None):
136
- """
137
- The core logic for slicing a single stem.
138
- Accepts an optional progress_fn callback for use in loops.
139
- """
140
- if stem_audio_data is None:
141
- return None, None
142
-
143
- sample_rate, y_int = stem_audio_data
144
- # Convert from int16 to float
145
- y = librosa.util.buf_to_float(y_int, dtype=np.float32)
146
-
147
- # Ensure y is 1D or 2D
148
- if y.ndim == 0:
149
- print(f"Warning: Empty audio data for {stem_name}.")
150
- return None, None
151
-
152
- y_mono = librosa.to_mono(y.T) if y.ndim > 1 else y
153
-
154
- if progress_fn: progress_fn(0.1, desc="Detecting BPM...")
155
-
156
- tempo, beats = librosa.beat.beat_track(y=y_mono, sr=sample_rate)
157
- bpm = 120 if tempo is None or tempo == 0 else np.round(tempo)
158
- bpm_int = int(bpm.item())
159
-
160
- if bpm_int == 0:
161
- bpm_int = 120 # Final fallback
162
- print("BPM detection failed, defaulting to 120 BPM.")
163
-
164
- print(f"Detected BPM for {stem_name}: {bpm_int}")
165
-
166
- output_files = []
167
- loops_dir = tempfile.mkdtemp()
168
-
169
- if "One-Shots" in loop_choice:
170
- if progress_fn: progress_fn(0.3, desc="Finding transients...")
171
- # Adjust onset detection parameters
172
- onset_frames = librosa.onset.onset_detect(
173
- y=y_mono, sr=sample_rate, delta=sensitivity,
174
- wait=1, pre_avg=1, post_avg=1, post_max=1, units='frames'
175
- )
176
- onset_samples = librosa.frames_to_samples(onset_frames)
177
-
178
- if progress_fn: progress_fn(0.5, desc="Slicing one-shots...")
179
-
180
- if len(onset_samples) > 0:
181
- num_onsets = len(onset_samples)
182
- for i, start_sample in enumerate(onset_samples):
183
- end_sample = onset_samples[i+1] if i+1 < num_onsets else len(y)
184
- # Use original stereo/mono data for the slice
185
- slice_data = y[start_sample:end_sample]
186
-
187
- filename = os.path.join(loops_dir, f"{stem_name}_one_shot_{i+1:03d}.wav")
188
- sf.write(filename, slice_data, sample_rate, subtype='PCM_16')
189
- output_files.append(filename)
190
-
191
- if progress_fn and num_onsets > 1 and i > 0:
192
- progress = 0.5 + (i / (num_onsets - 1) * 0.5)
193
- progress_fn(progress, desc=f"Exporting slice {i+1}/{num_onsets}...")
194
- else:
195
- # Loop slicing logic
196
- bars = int(loop_choice.split(" ")[0])
197
- seconds_per_beat = 60.0 / bpm_int
198
- seconds_per_bar = seconds_per_beat * 4 # Assuming 4/4 time
199
-
200
- loop_duration_seconds = seconds_per_bar * bars
201
- loop_duration_samples = int(loop_duration_seconds * sample_rate)
202
-
203
- if loop_duration_samples == 0:
204
- print(f"Loop duration is 0 for {stem_name}. BPM: {bpm_int}")
205
- return None, None
206
-
207
- if progress_fn: progress_fn(0.4, desc=f"Slicing into {bars}-bar loops...")
208
-
209
- num_loops = len(y) // loop_duration_samples
210
-
211
- if num_loops == 0:
212
- print(f"Audio for {stem_name} is too short for {bars}-bar loops at {bpm_int} BPM.")
213
- return None, None
214
-
215
- for i in range(num_loops):
216
- start_sample = i * loop_duration_samples
217
- end_sample = start_sample + loop_duration_samples
218
- # Use original stereo/mono data for the slice
219
- slice_data = y[start_sample:end_sample]
220
-
221
- filename = os.path.join(loops_dir, f"{stem_name}_{bars}bar_loop_{i+1:03d}_{bpm_int}bpm.wav")
222
- sf.write(filename, slice_data, sample_rate, subtype='PCM_16')
223
- output_files.append(filename)
224
-
225
- if progress_fn and num_loops > 1 and i > 0:
226
- progress = 0.4 + (i / (num_loops - 1) * 0.6)
227
- progress_fn(progress, desc=f"Exporting loop {i+1}/{num_loops}...")
228
-
229
- if not output_files:
230
- return None, None
231
-
232
- return output_files, loops_dir
233
-
234
- async def slice_all_and_zip_real(vocals, drums, bass, other, loop_choice, sensitivity):
235
- """
236
- Slices all available stems and packages them into a ZIP file.
237
- This is an async generator function that yields updates to the UI.
238
- """
239
- log_history = "Starting batch slice...\n"
240
- yield {
241
- status_log: log_history,
242
- progress_bar: gr.Progress(0, desc="Starting...", visible=True)
243
- }
244
- await asyncio.sleep(0.1) # Give UI time to update
245
-
246
- stems_to_process = {"vocals": vocals, "drums": drums, "bass": bass, "other": other}
247
- zip_path = "Loop_Architect_Pack.zip"
248
-
249
- num_stems = sum(1 for data in stems_to_process.values() if data is not None)
250
- if num_stems == 0:
251
- raise gr.Error("No stems to process! Please separate stems first.")
252
-
253
- all_temp_dirs = []
254
- try:
255
- with zipfile.ZipFile(zip_path, 'w') as zf:
256
- processed_count = 0
257
- for name, data in stems_to_process.items():
258
- if data is not None:
259
- log_history += f"--- Slicing {name} stem ---\n"
260
- yield { status_log: log_history }
261
-
262
- # This progress callback updates the main progress bar
263
- def update_main_progress(p, desc=""):
264
- overall_progress = (processed_count + p) / num_stems
265
- # We yield from the outer function's scope
266
- yield {
267
- progress_bar: gr.Progress(overall_progress, desc=f"Slicing {name}: {desc}", visible=True)
268
- }
269
-
270
- # We need a way to pass a generator-based progress fn
271
- # to a normal function. We can't.
272
- # Instead, we'll just update the progress bar *within* this loop
273
- # and pass a simple lambda that does nothing to slice_stem_real.
274
- # A more complex refactor would make slice_stem_real a generator.
275
-
276
- yield { progress_bar: gr.Progress(processed_count / num_stems, desc=f"Slicing {name}...", visible=True) }
277
-
278
- # This is a local callback just for slice_stem_real
279
- def simple_progress_callback(p, desc=""):
280
- # This doesn't update the UI, it's just a placeholder
281
- # We update the UI *around* this call.
282
- print(f"Slice Progress {name}: {p} - {desc}") # Log to console
283
-
284
- sliced_files, temp_dir = slice_stem_real(
285
- (data[0], data[1]), loop_choice, sensitivity, name,
286
- progress_fn=simple_progress_callback
287
- )
288
-
289
- if sliced_files:
290
- log_history += f"Generated {len(sliced_files)} slices for {name}.\n"
291
- all_temp_dirs.append(temp_dir)
292
- for loop_file in sliced_files:
293
- arcname = os.path.join(name, os.path.basename(loop_file))
294
- zf.write(loop_file, arcname)
295
- else:
296
- log_history += f"No slices generated for {name}.\n"
297
-
298
- processed_count += 1
299
- yield {
300
- status_log: log_history,
301
- progress_bar: gr.Progress(processed_count / num_stems, desc=f"Finished {name}", visible=True)
302
- }
303
-
304
- log_history += "Packaging complete!\n"
305
- yield {
306
- status_log: log_history + "✅ Pack ready for download!",
307
- progress_bar: gr.Progress(1, desc="Pack Ready!", visible=False),
308
- download_zip_file: gr.update(value=zip_path, visible=True)
309
- }
310
-
311
- except Exception as e:
312
- print(f"An error occurred during slice all: {e}")
313
- yield {
314
- status_log: log_history + f"❌ ERROR: {e}",
315
- progress_bar: gr.update(visible=False)
316
- }
317
- finally:
318
- # Clean up all temporary directories
319
- for d in all_temp_dirs:
320
- if d and os.path.exists(d):
321
- shutil.rmtree(d)
322
-
323
- # --- Create the full Gradio Interface ---
324
- with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="red")) as demo:
325
- gr.Markdown("# 🎵 Loop Architect")
326
- gr.Markdown("Upload any song to separate it into stems (vocals, drums, etc.) and then slice those stems into loops or one-shot samples.")
327
-
328
- with gr.Row():
329
- with gr.Column(scale=1):
330
- gr.Markdown("### 1. Separate Stems")
331
- audio_input = gr.Audio(type="filepath", label="Upload a Track")
332
- stem_options = gr.Radio(
333
- ["4 Stems (Vocals, Drums, Bass, Other)", "2 Stems (Vocals + Instrumental)"],
334
- label="Separation Type",
335
- value="4 Stems (Vocals, Drums, Bass, Other)"
336
- )
337
- submit_button = gr.Button("Separate Stems", variant="primary")
338
-
339
- gr.Markdown("### 2. Slicing Options")
340
- with gr.Group():
341
- loop_options_radio = gr.Radio(
342
- ["One-Shots (All Transients)", "4 Bar Loops", "8 Bar Loops"],
343
- label="Slice Type",
344
- value="One-Shots (All Transients)"
345
- )
346
- sensitivity_slider = gr.Slider(
347
- minimum=0.01, maximum=0.5, value=0.05, step=0.01,
348
- label="One-Shot Sensitivity",
349
- info="Lower values = more slices"
350
- )
351
-
352
- gr.Markdown("### 3. Create Pack")
353
- slice_all_button = gr.Button("Slice All Stems & Create Pack")
354
- download_zip_file = gr.File(label="Download Your Loop Pack", visible=False)
355
-
356
- gr.Markdown("### Status")
357
- # Initialize without any arguments
358
- progress_bar = gr.Progress()
359
- status_log = gr.Textbox(label="Status Log", lines=10, interactive=False)
360
-
361
- with gr.Column(scale=2):
362
- with gr.Accordion("Separated Stems (Preview & Slice)", open=True):
363
- with gr.Row():
364
- vocals_output = gr.Audio(label="Vocals", scale=4)
365
- slice_vocals_btn = gr.Button("Slice Vocals", scale=1)
366
- with gr.Row():
367
- drums_output = gr.Audio(label="Drums", scale=4)
368
- slice_drums_btn = gr.Button("Slice Drums", scale=1)
369
- with gr.Row():
370
- bass_output = gr.Audio(label="Bass", scale=4)
371
- slice_bass_btn = gr.Button("Slice Bass", scale=1)
372
- with gr.Row():
373
- other_output = gr.Audio(label="Other / Instrumental", scale=4)
374
- slice_other_btn = gr.Button("Slice Other", scale=1)
375
-
376
- gr.Markdown("### Sliced Loops / Samples (Preview)")
377
- loop_gallery = gr.Gallery(
378
- label="Generated Loops Preview",
379
- columns=8, object_fit="contain", height="auto", preview=True
380
- )
381
-
382
- # --- Define Event Listeners ---
383
- def slice_and_display(stem_data, loop_choice, sensitivity, stem_name):
384
- """
385
- Generator function to slice a single stem and update UI.
386
- """
387
- if stem_data is None:
388
- yield {
389
- status_log: f"No {stem_name} audio data to slice.",
390
- progress_bar: gr.update(visible=False)
391
- }
392
- return
393
-
394
- log_history = f"Slicing {stem_name}...\n"
395
- # Make progress bar visible when starting
396
- yield {
397
- status_log: log_history,
398
- progress_bar: gr.Progress(0, visible=True, desc=f"Slicing {stem_name}...")
399
- }
400
-
401
- # Define a callback for slice_stem_real to update this generator's progress
402
- def update_single_progress(p, desc=""):
403
- # This is a bit of a hack. We can't yield from here.
404
- # We will just rely on the yield before/after this call.
405
- # A full fix would make slice_stem_real a generator.
406
- print(f"Slice Progress {stem_name}: {p} - {desc}") # Log to console
407
-
408
- files, temp_dir = slice_stem_real(
409
- stem_data, loop_choice, sensitivity, stem_name,
410
- progress_fn=update_single_progress # Pass the callback
411
- )
412
-
413
- if temp_dir and os.path.exists(temp_dir):
414
- shutil.rmtree(temp_dir) # Clean up temp dir
415
-
416
- yield {
417
- loop_gallery: gr.update(value=files),
418
- status_log: log_history + f"✅ Sliced {stem_name} into {len(files) if files else 0} pieces.",
419
- progress_bar: gr.update(visible=False) # Hide progress bar when done
420
- }
421
-
422
- # --- MAIN EVENT LISTENERS (FIXED) ---
423
-
424
- # This event separates the stems
425
- submit_event = submit_button.click(
426
- fn=separate_stems,
427
- inputs=[audio_input, stem_options],
428
- # Add progress_bar to the outputs list
429
- outputs=[
430
- vocals_output, drums_output, bass_output, other_output,
431
- status_log # Removed progress_bar from outputs
432
- ]
433
- )
434
-
435
- # This event updates the UI based on the stem choice
436
- stem_options.change(
437
- fn=update_output_visibility,
438
- inputs=stem_options,
439
- outputs=[vocals_output, drums_output, bass_output, other_output]
440
- )
441
-
442
- # --- Single Slice Button Events ---
443
- slice_vocals_btn.click(fn=slice_and_display, inputs=[vocals_output, loop_options_radio, sensitivity_slider, gr.Textbox("vocals", visible=False)], outputs=[loop_gallery, status_log]) # Removed progress_bar
444
- slice_drums_btn.click(fn=slice_and_display, inputs=[drums_output, loop_options_radio, sensitivity_slider, gr.Textbox("drums", visible=False)], outputs=[loop_gallery, status_log]) # Removed progress_bar
445
- slice_bass_btn.click(fn=slice_and_display, inputs=[bass_output, loop_options_radio, sensitivity_slider, gr.Textbox("bass", visible=False)], outputs=[loop_gallery, status_log]) # Removed progress_bar
446
- slice_other_btn.click(fn=slice_and_display, inputs=[other_output, loop_options_radio, sensitivity_slider, gr.Textbox("other", visible=False)], outputs=[loop_gallery, status_log]) # Removed progress_bar
447
-
448
- # This event slices all stems and zips them
449
- slice_all_event = slice_all_button.click(
450
- fn=slice_all_and_zip_real,
451
- inputs=[vocals_output, drums_output, bass_output, other_output, loop_options_radio, sensitivity_slider],
452
- # Add progress_bar and fix the duplicate output
453
- outputs=[download_zip_file, status_log] # Removed progress_bar
454
- )
455
-
456
- if __name__ == "__main__":
457
- demo.launch(debug=True)