SaltProphet commited on
Commit
e72722a
·
verified ·
1 Parent(s): 42e5a20

Update app.py

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