SaltProphet commited on
Commit
14845ad
·
verified ·
1 Parent(s): 6c7318a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +155 -327
app.py CHANGED
@@ -3,52 +3,28 @@ import os
3
  import shutil
4
  import asyncio
5
  import librosa
 
6
  import soundfile as sf
7
  import numpy as np
8
- import zipfile
9
  import tempfile
 
10
  import matplotlib
11
  matplotlib.use('Agg')
12
 
13
- # ---------------------------
14
- # UI helpers
15
- # ---------------------------
16
-
17
- def update_output_visibility(choice):
18
- if "2 Stems" in choice:
19
- return (
20
- gr.update(visible=True),
21
- gr.update(visible=False),
22
- gr.update(visible=False),
23
- gr.update(visible=True, label="Instrumental (No Vocals)")
24
- )
25
- else: # 4 stems
26
- return (
27
- gr.update(visible=True),
28
- gr.update(visible=True),
29
- gr.update(visible=True),
30
- gr.update(visible=True, label="Other")
31
- )
32
-
33
- # ---------------------------
34
- # Stem separation (Demucs)
35
- # ---------------------------
36
-
37
  async def separate_stems(audio_file_path, stem_choice, progress=gr.Progress(track_tqdm=True)):
38
- # outputs: [vocals_out, drums_out, bass_out, other_out, status_log]
39
  if audio_file_path is None:
40
  raise gr.Error("No audio file uploaded!")
 
41
  log_history = "Starting separation...\n"
42
- yield (gr.update(), gr.update(), gr.update(), gr.update(), log_history)
43
 
44
  try:
45
  progress(0.05, desc="Preparing audio file...")
46
- log_history += "Preparing audio file...\n"
47
- yield (gr.update(), gr.update(), gr.update(), gr.update(), log_history)
48
-
49
  original_filename_base = os.path.basename(audio_file_path).rsplit('.', 1)[0]
50
  stable_input_path = f"stable_input_{original_filename_base}.wav"
51
- # Ensure a .wav input for Demucs (demucs can read many formats, but filepath stability helps)
52
  shutil.copy(audio_file_path, stable_input_path)
53
 
54
  model_arg = "--two-stems=vocals" if "2 Stems" in stem_choice else ""
@@ -56,357 +32,209 @@ async def separate_stems(audio_file_path, stem_choice, progress=gr.Progress(trac
56
  if os.path.exists(output_dir):
57
  shutil.rmtree(output_dir)
58
 
59
- command = f"python -m demucs {model_arg} -o \"{output_dir}\" \"{stable_input_path}\""
60
- log_history += f"Running Demucs command: {command}\n"
61
- log_history += "(This may take a minute or more depending on track length)\n"
62
- yield (gr.update(), gr.update(), gr.update(), gr.update(), log_history)
63
 
64
- progress(0.2, desc="Running Demucs...")
65
  process = await asyncio.create_subprocess_shell(
66
  command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
67
  )
68
  stdout, stderr = await process.communicate()
69
-
70
  if process.returncode != 0:
71
- raise gr.Error(f"Demucs failed. Error: {stderr.decode(errors='ignore')[:800]}")
72
 
73
  log_history += "Demucs finished. Locating stem files...\n"
74
- progress(0.8, desc="Locating stems...")
75
- yield (gr.update(), gr.update(), gr.update(), gr.update(), log_history)
76
 
77
- stable_filename_base = os.path.basename(stable_input_path).rsplit('.', 1)[0]
78
  subfolders = [f.name for f in os.scandir(output_dir) if f.is_dir()]
79
  if not subfolders:
80
  raise gr.Error("Demucs output folder structure not found!")
 
81
  model_folder_name = subfolders[0]
82
- stems_path = os.path.join(output_dir, model_folder_name, stable_filename_base)
83
 
84
- if not os.path.exists(stems_path):
85
- raise gr.Error(f"Demucs output directory was not found! Looked for: {stems_path}")
 
86
 
87
- vocals_path = os.path.join(stems_path, "vocals.wav") if os.path.exists(os.path.join(stems_path, "vocals.wav")) else None
88
- drums_path = os.path.join(stems_path, "drums.wav") if os.path.exists(os.path.join(stems_path, "drums.wav")) else None
89
- bass_path = os.path.join(stems_path, "bass.wav") if os.path.exists(os.path.join(stems_path, "bass.wav")) else None
90
- other_name = "no_vocals.wav" if "2 Stems" in stem_choice else "other.wav"
91
- other_path = os.path.join(stems_path, other_name) if os.path.exists(os.path.join(stems_path, other_name)) else None
92
 
93
- try:
94
- os.remove(stable_input_path)
95
- except Exception:
96
- pass
97
 
98
  log_history += "✅ Stem separation complete!\n"
99
- yield (
100
- gr.update(value=vocals_path),
101
- gr.update(value=drums_path),
102
- gr.update(value=bass_path),
103
- gr.update(value=other_path),
104
- log_history
105
- )
 
106
 
107
  except Exception as e:
108
- err = f"❌ ERROR: {e}"
109
- print(f"Separation error: {e}")
110
- yield (gr.update(), gr.update(), gr.update(), gr.update(), log_history + err)
111
-
112
- # ---------------------------
113
- # Slicing (BPM override + Quantized grid)
114
- # ---------------------------
115
-
116
- def _grid_beats_per_step(grid_label: str) -> float:
117
- # "1/16 (Sixteenth)" -> "1/16"
118
- tok = grid_label.split(" ")[0].strip()
119
- mapping = {"1/1": 4.0, "1/2": 2.0, "1/4": 1.0, "1/8": 0.5, "1/16": 0.25}
120
- return mapping.get(tok, 0.25)
121
-
122
- def _load_audio_any(stem_input):
123
- """
124
- Accepts a filepath string OR (sr, numpy_array) and returns (sr, mono_float_array).
125
- We use filepath everywhere in this app to keep memory stable.
126
- """
127
- if stem_input is None:
128
- return None, None
129
- if isinstance(stem_input, str):
130
- y, sr = librosa.load(stem_input, sr=None, mono=False)
131
- else:
132
- sr, y = stem_input
133
- y = librosa.util.buf_to_float(y)
134
- y_mono = librosa.to_mono(y) if y.ndim > 1 else y
135
- return sr, y_mono
136
-
137
- def slice_stem_real(
138
- stem_input,
139
- loop_choice,
140
- sensitivity,
141
- stem_name,
142
- progress_fn=None,
143
- bpm_override_val: int = 0,
144
- quantize: bool = True,
145
- grid_label: str = "1/16 (Sixteenth)",
146
- ):
147
- sr, y_mono = _load_audio_any(stem_input)
148
- if sr is None or y_mono is None:
149
  return None, None
150
 
 
 
 
 
151
  if progress_fn: progress_fn(0.1, desc="Detecting BPM...")
152
  tempo, _ = librosa.beat.beat_track(y=y_mono, sr=sr)
153
- det_bpm = int(np.round(tempo)) if tempo and tempo > 0 else 120
154
- bpm = int(bpm_override_val) if bpm_override_val and bpm_override_val > 0 else det_bpm
155
- if bpm <= 0:
156
- bpm = 120
157
 
158
- output_files = []
159
  loops_dir = tempfile.mkdtemp()
 
160
 
161
  if "One-Shots" in loop_choice:
162
  if progress_fn: progress_fn(0.3, desc="Finding transients...")
163
- onset_frames = librosa.onset.onset_detect(
164
- y=y_mono, sr=sr, delta=sensitivity, wait=1, pre_avg=1, post_avg=1, post_max=1
165
- )
166
  onset_samples = librosa.frames_to_samples(onset_frames)
167
 
168
- # Quantize onsets to musical grid if enabled
169
- if quantize and len(onset_samples) > 0:
170
- beats_per_step = _grid_beats_per_step(grid_label)
171
- samples_per_beat = sr * (60.0 / bpm)
172
- step = max(1, int(round(samples_per_beat * beats_per_step)))
173
- q = np.clip(np.round(onset_samples / step) * step, 0, len(y_mono) - 1).astype(int)
174
- q = np.unique(q) # dedupe after snapping
175
- onset_samples = q
176
-
177
  if progress_fn: progress_fn(0.5, desc="Slicing one-shots...")
 
 
 
 
 
 
 
178
 
179
- if len(onset_samples) > 0:
180
- num_onsets = len(onset_samples)
181
- min_len = max(1, int(0.02 * sr)) # 20ms min slice guard
182
- for i, start_sample in enumerate(onset_samples):
183
- end_sample = onset_samples[i+1] if i+1 < num_onsets else len(y_mono)
184
- if end_sample - start_sample < min_len:
185
- continue
186
- slice_data = y_mono[start_sample:end_sample]
187
- filename = os.path.join(loops_dir, f"{stem_name}_one_shot_{i+1:03d}.wav")
188
- sf.write(filename, slice_data, sr, subtype='PCM_16')
189
- output_files.append(filename)
190
- if progress_fn and num_onsets > 1:
191
- progress_fn(0.5 + (i / (num_onsets - 1) * 0.5), desc=f"Exporting slice {i+1}/{num_onsets}...")
192
  else:
193
- # Grid-true by construction; honor BPM override
194
  bars = int(loop_choice.split(" ")[0])
195
- seconds_per_beat = 60.0 / bpm
196
- seconds_per_bar = seconds_per_beat * 4
197
- loop_duration_seconds = seconds_per_bar * bars
198
- loop_duration_samples = int(loop_duration_seconds * sr)
199
- if progress_fn: progress_fn(0.4, desc=f"Slicing into {bars}-bar loops @ {bpm} BPM...")
200
- num_loops = len(y_mono) // loop_duration_samples
201
- if num_loops == 0:
202
- return None, None
203
- for i in range(num_loops):
204
- start_sample = i * loop_duration_samples
205
- end_sample = start_sample + loop_duration_samples
206
- slice_data = y_mono[start_sample:end_sample]
207
- filename = os.path.join(loops_dir, f"{stem_name}_{bars}bar_loop_{i+1:03d}_{bpm}bpm.wav")
208
- sf.write(filename, slice_data, sr, subtype='PCM_16')
209
- output_files.append(filename)
210
- if progress_fn and num_loops > 1:
211
- progress_fn(0.4 + (i / (num_loops - 1) * 0.6), desc=f"Exporting loop {i+1}/{num_loops}...")
212
-
213
- if not output_files:
214
- return None, None
215
- return output_files, loops_dir
216
 
217
- # ---------------------------
218
- # Batch wrapper
219
- # ---------------------------
220
 
221
- async def slice_all_and_zip_real(vocals, drums, bass, other, loop_choice, sensitivity,
222
- bpm_over, quantize_on, grid_sel,
223
- progress=gr.Progress(track_tqdm=True)):
224
- # outputs: [status_log, download_zip_file]
225
- log_history = "Starting batch slice...\n"
226
- yield (log_history, gr.update(visible=False))
227
 
228
- await asyncio.sleep(0.05)
229
- stems_to_process = {"vocals": vocals, "drums": drums, "bass": bass, "other": other}
230
- present = {k: v for k, v in stems_to_process.items() if v}
231
- if not present:
232
- raise gr.Error("No stems to process! Please separate stems first.")
 
233
 
 
234
  zip_path = "Loop_Architect_Pack.zip"
235
- all_temp_dirs = []
236
- try:
237
- with zipfile.ZipFile(zip_path, 'w') as zf:
238
- processed, total = 0, len(present)
239
- for name, data in present.items():
240
- log_history += f"--- Slicing {name} stem ---\n"
241
- yield (log_history, gr.update(visible=False))
242
-
243
- def update_main_progress(p, desc=""):
244
- progress((processed + max(0.01, p)) / total, desc=f"{name}: {desc}")
245
-
246
- sliced_files, temp_dir = slice_stem_real(
247
- data, loop_choice, sensitivity, name,
248
- progress_fn=update_main_progress,
249
- bpm_override_val=int(bpm_over),
250
- quantize=bool(quantize_on),
251
- grid_label=grid_sel
252
- )
253
- if temp_dir:
254
- all_temp_dirs.append(temp_dir)
255
- if sliced_files:
256
- log_history += f"Generated {len(sliced_files)} slices for {name}.\n"
257
- for loop_file in sliced_files:
258
- zf.write(loop_file, os.path.join(name, os.path.basename(loop_file)))
259
- else:
260
- log_history += f"No slices generated for {name}.\n"
261
-
262
- processed += 1
263
- yield (log_history, gr.update(visible=False))
264
-
265
- log_history += "Packaging complete! ✅ Pack ready for download.\n"
266
- yield (log_history, gr.update(value=zip_path, visible=True))
267
 
268
- except Exception as e:
269
- err = f" ERROR: {e}"
270
- print(f"Batch slice error: {e}")
271
- yield (log_history + err, gr.update(visible=False))
272
- finally:
273
- for d in all_temp_dirs:
274
- if d and os.path.exists(d):
275
- shutil.rmtree(d)
276
-
277
- # ---------------------------
278
- # Gradio UI
279
- # ---------------------------
 
 
 
280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="red")) as demo:
282
  gr.Markdown("# 🎵 Loop Architect — SALT PRØPHET Edition")
283
 
284
  with gr.Row():
285
  with gr.Column(scale=1):
286
- gr.Markdown("### 1) Separate Stems")
287
- audio_input = gr.Audio(type="filepath", label="Upload a Track")
288
- stem_options = gr.Radio(
289
  ["4 Stems (Vocals, Drums, Bass, Other)", "2 Stems (Vocals + Instrumental)"],
290
  label="Separation Type",
291
  value="4 Stems (Vocals, Drums, Bass, Other)"
292
  )
293
- submit_button = gr.Button("Separate Stems", variant="secondary")
294
-
295
- with gr.Accordion("2) Slicing Options", open=True):
296
- loop_options_radio = gr.Radio(
297
- ["One-Shots (All Transients)", "4 Bar Loops", "8 Bar Loops"],
298
- label="Slice Type",
299
- value="One-Shots (All Transients)"
300
- )
301
- sensitivity_slider = gr.Slider(
302
- minimum=0.01, maximum=0.5, value=0.05, step=0.01,
303
- label="One-Shot Sensitivity", info="Lower values = more slices"
304
- )
305
- bpm_override = gr.Slider(
306
- minimum=0, maximum=240, value=0, step=1,
307
- label="BPM Override (0 = auto-detect)"
308
- )
309
- quantize_toggle = gr.Checkbox(
310
- label="Quantize One-Shots to Grid", value=True
311
- )
312
- grid_select = gr.Dropdown(
313
- choices=["1/1 (Whole)", "1/2 (Half)", "1/4 (Quarter)", "1/8 (Eighth)", "1/16 (Sixteenth)"],
314
- value="1/16 (Sixteenth)",
315
- label="Grid Resolution"
316
- )
317
-
318
- gr.Markdown("### 3) Create Pack")
319
- slice_all_button = gr.Button("Slice All Stems & Create Pack", variant="primary")
320
- download_zip_file = gr.File(label="Download Your Loop Pack", visible=False)
321
-
322
- gr.Markdown("### Status")
323
- status_log = gr.Textbox(label="Status Log", lines=12, interactive=False)
324
 
325
- with gr.Column(scale=2):
326
- with gr.Accordion("Separated Stems", open=True):
327
- with gr.Row():
328
- vocals_output = gr.Audio(type="filepath", label="Vocals", scale=4)
329
- slice_vocals_btn = gr.Button("Slice Vocals", scale=1)
330
- with gr.Row():
331
- drums_output = gr.Audio(type="filepath", label="Drums", scale=4)
332
- slice_drums_btn = gr.Button("Slice Drums", scale=1)
333
- with gr.Row():
334
- bass_output = gr.Audio(type="filepath", label="Bass", scale=4)
335
- slice_bass_btn = gr.Button("Slice Bass", scale=1)
336
- with gr.Row():
337
- other_output = gr.Audio(type="filepath", label="Other / Instrumental", scale=4)
338
- slice_other_btn = gr.Button("Slice Other", scale=1)
339
-
340
- gr.Markdown("### Sliced Files")
341
- loops_files = gr.Files(label="Generated Loops / One-Shots")
342
-
343
- # wire events
344
- submit_button.click(
345
- fn=separate_stems,
346
- inputs=[audio_input, stem_options],
347
- outputs=[vocals_output, drums_output, bass_output, other_output, status_log]
348
- )
349
 
350
- stem_options.change(
351
- fn=update_output_visibility,
352
- inputs=stem_options,
353
- outputs=[vocals_output, drums_output, bass_output, other_output]
354
- )
355
 
356
- def slice_and_display(stem_path, loop_choice, sensitivity, stem_name, bpm_over, quantize_on, grid_sel):
357
- log_history = f"Slicing {stem_name}...\n"
358
- def _p(p, desc=""):
359
- gr.Progress(track_tqdm=True)(p, desc=desc)
360
-
361
- files, temp_dir = slice_stem_real(
362
- stem_path, loop_choice, sensitivity, stem_name,
363
- progress_fn=_p,
364
- bpm_override_val=int(bpm_over),
365
- quantize=bool(quantize_on),
366
- grid_label=grid_sel
367
- )
368
- if temp_dir and os.path.exists(temp_dir):
369
- shutil.rmtree(temp_dir)
370
-
371
- if files:
372
- log_history += f"✅ Sliced {stem_name} into {len(files)} pieces."
373
- return gr.update(value=files), log_history
374
- else:
375
- log_history += f"⚠️ No slices generated for {stem_name}."
376
- return gr.update(value=None), log_history
377
-
378
- slice_vocals_btn.click(
379
- fn=slice_and_display,
380
- inputs=[vocals_output, loop_options_radio, sensitivity_slider, gr.Textbox("vocals", visible=False),
381
- bpm_override, quantize_toggle, grid_select],
382
- outputs=[loops_files, status_log]
383
- )
384
- slice_drums_btn.click(
385
- fn=slice_and_display,
386
- inputs=[drums_output, loop_options_radio, sensitivity_slider, gr.Textbox("drums", visible=False),
387
- bpm_override, quantize_toggle, grid_select],
388
- outputs=[loops_files, status_log]
389
- )
390
- slice_bass_btn.click(
391
- fn=slice_and_display,
392
- inputs=[bass_output, loop_options_radio, sensitivity_slider, gr.Textbox("bass", visible=False),
393
- bpm_override, quantize_toggle, grid_select],
394
- outputs=[loops_files, status_log]
395
- )
396
- slice_other_btn.click(
397
- fn=slice_and_display,
398
- inputs=[other_output, loop_options_radio, sensitivity_slider, gr.Textbox("other", visible=False),
399
- bpm_override, quantize_toggle, grid_select],
400
- outputs=[loops_files, status_log]
401
  )
402
 
403
- slice_all_button.click(
404
- fn=slice_all_and_zip_real,
405
- inputs=[vocals_output, drums_output, bass_output, other_output,
406
- loop_options_radio, sensitivity_slider,
407
- bpm_override, quantize_toggle, grid_select],
408
- outputs=[status_log, download_zip_file]
409
  )
410
 
411
- # Spaces calls launch() automatically
412
- demo.queue(concurrency_count=1, max_size=10).launch()
 
 
 
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 tempfile
10
+ import zipfile
11
  import matplotlib
12
  matplotlib.use('Agg')
13
 
14
+ # ------------------------------------------------------------
15
+ # --- Stem Separation using Demucs ----------------------------
16
+ # ------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  async def separate_stems(audio_file_path, stem_choice, progress=gr.Progress(track_tqdm=True)):
 
18
  if audio_file_path is None:
19
  raise gr.Error("No audio file uploaded!")
20
+
21
  log_history = "Starting separation...\n"
22
+ yield { "status_log": log_history, "progress_bar": progress(0, desc="Starting...", visible=True) }
23
 
24
  try:
25
  progress(0.05, desc="Preparing audio file...")
 
 
 
26
  original_filename_base = os.path.basename(audio_file_path).rsplit('.', 1)[0]
27
  stable_input_path = f"stable_input_{original_filename_base}.wav"
 
28
  shutil.copy(audio_file_path, stable_input_path)
29
 
30
  model_arg = "--two-stems=vocals" if "2 Stems" in stem_choice else ""
 
32
  if os.path.exists(output_dir):
33
  shutil.rmtree(output_dir)
34
 
35
+ command = f"python3 -m demucs {model_arg} -o \"{output_dir}\" \"{stable_input_path}\""
36
+ log_history += f"Running Demucs: {command}\n"
37
+ yield { "status_log": log_history, "progress_bar": progress(0.2, desc="Running Demucs...") }
 
38
 
 
39
  process = await asyncio.create_subprocess_shell(
40
  command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
41
  )
42
  stdout, stderr = await process.communicate()
 
43
  if process.returncode != 0:
44
+ raise gr.Error(f"Demucs failed: {stderr.decode()[:500]}")
45
 
46
  log_history += "Demucs finished. Locating stem files...\n"
47
+ yield { "status_log": log_history, "progress_bar": progress(0.8, desc="Locating stems...") }
 
48
 
 
49
  subfolders = [f.name for f in os.scandir(output_dir) if f.is_dir()]
50
  if not subfolders:
51
  raise gr.Error("Demucs output folder structure not found!")
52
+
53
  model_folder_name = subfolders[0]
54
+ stems_path = os.path.join(output_dir, model_folder_name, original_filename_base)
55
 
56
+ def get_path(name):
57
+ p = os.path.join(stems_path, name)
58
+ return p if os.path.exists(p) else None
59
 
60
+ vocals_path = get_path("vocals.wav")
61
+ drums_path = get_path("drums.wav")
62
+ bass_path = get_path("bass.wav")
63
+ other_name = "no_vocals.wav" if "2 Stems" in stem_choice else "other.wav"
64
+ other_path = get_path(other_name)
65
 
66
+ os.remove(stable_input_path)
 
 
 
67
 
68
  log_history += "✅ Stem separation complete!\n"
69
+ yield {
70
+ "status_log": log_history,
71
+ "progress_bar": progress(1, desc="Complete!", visible=False),
72
+ "vocals_output": vocals_path,
73
+ "drums_output": drums_path,
74
+ "bass_output": bass_path,
75
+ "other_output": other_path
76
+ }
77
 
78
  except Exception as e:
79
+ yield { "status_log": log_history + f"❌ ERROR: {e}", "progress_bar": gr.update(visible=False) }
80
+
81
+
82
+ # ------------------------------------------------------------
83
+ # --- Slicing + Loop Generation -------------------------------
84
+ # ------------------------------------------------------------
85
+ def slice_stem_real(stem_audio_data, loop_choice, sensitivity, stem_name, bpm_override=None, quantize_grid=False, progress_fn=None):
86
+ if stem_audio_data is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  return None, None
88
 
89
+ sr, y_int = stem_audio_data
90
+ y = librosa.util.buf_to_float(y_int)
91
+ y_mono = librosa.to_mono(y.T) if y.ndim > 1 else y
92
+
93
  if progress_fn: progress_fn(0.1, desc="Detecting BPM...")
94
  tempo, _ = librosa.beat.beat_track(y=y_mono, sr=sr)
95
+ bpm = bpm_override if bpm_override else int(np.round(tempo))
96
+ if bpm == 0: bpm = 120
 
 
97
 
 
98
  loops_dir = tempfile.mkdtemp()
99
+ output_files = []
100
 
101
  if "One-Shots" in loop_choice:
102
  if progress_fn: progress_fn(0.3, desc="Finding transients...")
103
+ onset_frames = librosa.onset.onset_detect(y=y_mono, sr=sr, delta=sensitivity, wait=1)
 
 
104
  onset_samples = librosa.frames_to_samples(onset_frames)
105
 
 
 
 
 
 
 
 
 
 
106
  if progress_fn: progress_fn(0.5, desc="Slicing one-shots...")
107
+ for i, start in enumerate(onset_samples):
108
+ end = onset_samples[i + 1] if i + 1 < len(onset_samples) else len(y)
109
+ slice_data = y[start:end]
110
+ fname = os.path.join(loops_dir, f"{stem_name}_one_shot_{i+1:03d}.wav")
111
+ sf.write(fname, slice_data, sr)
112
+ output_files.append(fname)
113
+ if progress_fn: progress_fn(0.5 + (i / len(onset_samples) * 0.5), desc=f"{i+1}/{len(onset_samples)}")
114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  else:
 
116
  bars = int(loop_choice.split(" ")[0])
117
+ spb = 60.0 / bpm
118
+ spbar = spb * 4
119
+ loop_len = int(spbar * bars * sr)
120
+
121
+ if progress_fn: progress_fn(0.4, desc=f"Slicing {bars}-bar loops...")
122
+ total = len(y) // loop_len
123
+ for i in range(total):
124
+ start = i * loop_len
125
+ end = start + loop_len
126
+ slice_data = y[start:end]
127
+
128
+ # Quantize slices if requested
129
+ if quantize_grid:
130
+ slice_data = librosa.effects.time_stretch(slice_data, rate=1.0)
131
+
132
+ fname = os.path.join(loops_dir, f"{stem_name}_{bars}bar_loop_{i+1:03d}_{bpm}bpm.wav")
133
+ sf.write(fname, slice_data, sr)
134
+ output_files.append(fname)
135
+ if progress_fn: progress_fn(0.4 + (i / total * 0.6), desc=f"Loop {i+1}/{total}")
 
 
136
 
137
+ return output_files, loops_dir
 
 
138
 
 
 
 
 
 
 
139
 
140
+ # ------------------------------------------------------------
141
+ # --- Slice All Stems & Zip ----------------------------------
142
+ # ------------------------------------------------------------
143
+ async def slice_all_and_zip(vocals, drums, bass, other, loop_choice, sensitivity, bpm_override, quantize_grid, progress=gr.Progress(track_tqdm=True)):
144
+ log = "Starting batch slice...\n"
145
+ yield { "status_log": log, "progress_bar": progress(0, desc="Starting...", visible=True) }
146
 
147
+ stems = {"vocals": vocals, "drums": drums, "bass": bass, "other": other}
148
  zip_path = "Loop_Architect_Pack.zip"
149
+ num_stems = sum(1 for v in stems.values() if v is not None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ if num_stems == 0:
152
+ raise gr.Error("No stems to process! Please separate first.")
153
+
154
+ tmp_dirs = []
155
+
156
+ with zipfile.ZipFile(zip_path, "w") as zf:
157
+ done = 0
158
+ for name, data in stems.items():
159
+ if data is not None:
160
+ log += f"--- Slicing {name} ---\n"
161
+ yield { "status_log": log }
162
+
163
+ def update_prog(p, desc=""):
164
+ overall = (done + p) / num_stems
165
+ progress(overall, desc=f"{name}: {desc}", visible=True)
166
 
167
+ files, tmp_dir = slice_stem_real((data[0], data[1]), loop_choice, sensitivity, name, bpm_override, quantize_grid, progress_fn=update_prog)
168
+ if files:
169
+ log += f"Generated {len(files)} slices for {name}.\n"
170
+ for f in files:
171
+ zf.write(f, os.path.join(name, os.path.basename(f)))
172
+ tmp_dirs.append(tmp_dir)
173
+ done += 1
174
+
175
+ for d in tmp_dirs:
176
+ shutil.rmtree(d, ignore_errors=True)
177
+
178
+ yield {
179
+ "status_log": log + "✅ Pack ready!",
180
+ "progress_bar": progress(1, desc="Done", visible=False),
181
+ "download_zip_file": zip_path
182
+ }
183
+
184
+
185
+ # ------------------------------------------------------------
186
+ # --- Gradio UI ----------------------------------------------
187
+ # ------------------------------------------------------------
188
  with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="red")) as demo:
189
  gr.Markdown("# 🎵 Loop Architect — SALT PRØPHET Edition")
190
 
191
  with gr.Row():
192
  with gr.Column(scale=1):
193
+ gr.Markdown("### 1️⃣ Separate Stems")
194
+ audio_input = gr.Audio(type="filepath", label="Upload Track")
195
+ stem_choice = gr.Radio(
196
  ["4 Stems (Vocals, Drums, Bass, Other)", "2 Stems (Vocals + Instrumental)"],
197
  label="Separation Type",
198
  value="4 Stems (Vocals, Drums, Bass, Other)"
199
  )
200
+ separate_btn = gr.Button("Separate Stems", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
+ gr.Markdown("### 2️⃣ Slicing Options")
203
+ loop_choice = gr.Radio(
204
+ ["One-Shots (All Transients)", "4 Bar Loops", "8 Bar Loops"],
205
+ label="Slice Type", value="One-Shots (All Transients)"
206
+ )
207
+ sensitivity = gr.Slider(0.01, 0.5, 0.05, step=0.01, label="One-Shot Sensitivity")
208
+ bpm_override = gr.Number(label="BPM Override (optional)", value=None)
209
+ quantize_grid = gr.Checkbox(label="Enable Quantized Slice Grid", value=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
+ gr.Markdown("### 3️⃣ Create Pack")
212
+ slice_btn = gr.Button("Slice All & Create Pack")
213
+ download_zip_file = gr.File(label="Download Pack", visible=False)
214
+ progress_bar = gr.HTML()
215
+ status_log = gr.Textbox(label="Status Log", lines=10)
216
 
217
+ with gr.Column(scale=2):
218
+ with gr.Accordion("Separated Stems", open=True):
219
+ vocals_output = gr.Audio(label="Vocals")
220
+ drums_output = gr.Audio(label="Drums")
221
+ bass_output = gr.Audio(label="Bass")
222
+ other_output = gr.Audio(label="Other / Instrumental")
223
+
224
+ # --- Actions ---
225
+ separate_btn.click(
226
+ separate_stems,
227
+ [audio_input, stem_choice],
228
+ [vocals_output, drums_output, bass_output, other_output, status_log]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  )
230
 
231
+ slice_btn.click(
232
+ slice_all_and_zip,
233
+ [vocals_output, drums_output, bass_output, other_output, loop_choice, sensitivity, bpm_override, quantize_grid],
234
+ [download_zip_file, status_log]
 
 
235
  )
236
 
237
+ # ------------------------------------------------------------
238
+ # --- Launch --------------------------------------------------
239
+ # ------------------------------------------------------------
240
+ demo.launch()