SaltProphet commited on
Commit
7272862
·
verified ·
1 Parent(s): e96edda

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +263 -69
app.py CHANGED
@@ -11,9 +11,12 @@ import zipfile
11
  import tempfile
12
  import matplotlib.pyplot as plt
13
  import matplotlib
 
 
14
  matplotlib.use('Agg')
15
 
16
  def update_output_visibility(choice):
 
17
  if "2 Stems" in choice:
18
  return {
19
  vocals_output: gr.update(visible=True),
@@ -29,149 +32,290 @@ def update_output_visibility(choice):
29
  other_output: gr.update(visible=True, label="Other")
30
  }
31
 
32
- async def separate_stems(audio_file_path, stem_choice, progress=gr.Progress(track_tqdm=True)):
33
- if audio_file_path is None: raise gr.Error("No audio file uploaded!")
 
 
 
 
 
 
34
  log_history = "Starting separation...\n"
35
- yield { status_log: log_history, progress_bar: progress(0, desc="Starting...", visible=True) }
 
 
 
 
36
  try:
37
- progress(0.05, desc="Preparing audio file...")
38
- log_history += "Preparing audio file...\n"; yield { status_log: log_history, progress_bar: progress(0.05, desc="Preparing...") }
 
 
 
 
39
  original_filename_base = os.path.basename(audio_file_path).rsplit('.', 1)[0]
 
40
  stable_input_path = f"stable_input_{original_filename_base}.wav"
41
  shutil.copy(audio_file_path, stable_input_path)
42
 
43
  model_arg = "--two-stems=vocals" if "2 Stems" in stem_choice else ""
44
  output_dir = "separated"
45
- if os.path.exists(output_dir): shutil.rmtree(output_dir)
 
46
 
47
  command = f"python3 -m demucs {model_arg} -o \"{output_dir}\" \"{stable_input_path}\""
48
- log_history += f"Running Demucs command: {command}\n(This may take a minute or more depending on track length)\n";
49
- yield { status_log: log_history, progress_bar: progress(0.2, desc="Running Demucs...") }
 
 
 
 
50
 
51
  process = await asyncio.create_subprocess_shell(
52
  command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
 
53
  stdout, stderr = await process.communicate()
54
 
55
  if process.returncode != 0:
56
- raise gr.Error(f"Demucs failed. Error: {stderr.decode()[:500]}")
 
57
 
58
- log_history += "Demucs finished. Locating stem files...\n"; yield { status_log: log_history, progress_bar: progress(0.8, desc="Locating stems...") }
59
- stable_filename_base = os.path.basename(stable_input_path).rsplit('.', 1)[0]
 
 
 
60
 
 
61
  subfolders = [f.name for f in os.scandir(output_dir) if f.is_dir()]
62
- if not subfolders: raise gr.Error("Demucs output folder structure not found!")
 
 
 
63
  model_folder_name = subfolders[0]
64
  stems_path = os.path.join(output_dir, model_folder_name, stable_filename_base)
65
 
66
  if not os.path.exists(stems_path):
67
- raise gr.Error(f"Demucs output directory was not found! Looked for: {stems_path}")
 
 
 
 
68
 
69
  vocals_path = os.path.join(stems_path, "vocals.wav") if os.path.exists(os.path.join(stems_path, "vocals.wav")) else None
70
  drums_path = os.path.join(stems_path, "drums.wav") if os.path.exists(os.path.join(stems_path, "drums.wav")) else None
71
  bass_path = os.path.join(stems_path, "bass.wav") if os.path.exists(os.path.join(stems_path, "bass.wav")) else None
 
72
  other_filename = "no_vocals.wav" if "2 Stems" in stem_choice else "other.wav"
73
  other_path = os.path.join(stems_path, other_filename) if os.path.exists(os.path.join(stems_path, other_filename)) else None
74
 
 
75
  os.remove(stable_input_path)
76
 
77
- log_history += "✅ Stem separation complete!\n";
78
  yield {
79
  status_log: log_history,
80
- progress_bar: progress(1, desc="Complete!", visible=False), # Hide progress bar when done
81
  vocals_output: gr.update(value=vocals_path),
82
  drums_output: gr.update(value=drums_path),
83
  bass_output: gr.update(value=bass_path),
84
  other_output: gr.update(value=other_path)
85
  }
 
86
  except Exception as e:
87
  print(f"An error occurred during separation: {e}")
88
- yield { status_log: log_history + f"❌ ERROR: {e}", progress_bar: gr.update(visible=False) } # Hide progress bar on error
 
 
 
 
 
 
89
 
90
  def slice_stem_real(stem_audio_data, loop_choice, sensitivity, stem_name, progress_fn=None):
91
- if stem_audio_data is None: return None, None
92
- sample_rate, y_int = stem_audio_data; y = librosa.util.buf_to_float(y_int)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  y_mono = librosa.to_mono(y.T) if y.ndim > 1 else y
 
94
  if progress_fn: progress_fn(0.1, desc="Detecting BPM...")
 
95
  tempo, beats = librosa.beat.beat_track(y=y_mono, sr=sample_rate)
96
- bpm = 120 if tempo is None else np.round(tempo); bpm_int = int(bpm.item())
97
- if bpm_int == 0: bpm_int = 120; print("BPM detection failed, defaulting to 120 BPM.")
 
 
 
 
 
98
  print(f"Detected BPM for {stem_name}: {bpm_int}")
99
- output_files = []; loops_dir = tempfile.mkdtemp()
 
 
 
100
  if "One-Shots" in loop_choice:
101
  if progress_fn: progress_fn(0.3, desc="Finding transients...")
102
- onset_frames = librosa.onset.onset_detect(y=y_mono, sr=sample_rate, delta=sensitivity, wait=1, pre_avg=1, post_avg=1, post_max=1)
 
 
 
 
103
  onset_samples = librosa.frames_to_samples(onset_frames)
 
104
  if progress_fn: progress_fn(0.5, desc="Slicing one-shots...")
 
105
  if len(onset_samples) > 0:
106
  num_onsets = len(onset_samples)
107
  for i, start_sample in enumerate(onset_samples):
108
  end_sample = onset_samples[i+1] if i+1 < num_onsets else len(y)
 
109
  slice_data = y[start_sample:end_sample]
 
110
  filename = os.path.join(loops_dir, f"{stem_name}_one_shot_{i+1:03d}.wav")
111
  sf.write(filename, slice_data, sample_rate, subtype='PCM_16')
112
  output_files.append(filename)
113
- if progress_fn and num_onsets > 1:
114
- progress_fn(0.5 + (i / (num_onsets - 1) * 0.5), desc=f"Exporting slice {i+1}/{num_onsets}...")
 
 
115
  else:
116
- bars = int(loop_choice.split(" ")[0]); seconds_per_beat = 60.0 / bpm_int; seconds_per_bar = seconds_per_beat * 4
117
- loop_duration_seconds = seconds_per_bar * bars; loop_duration_samples = int(loop_duration_seconds * sample_rate)
 
 
 
 
 
 
 
 
 
 
118
  if progress_fn: progress_fn(0.4, desc=f"Slicing into {bars}-bar loops...")
 
119
  num_loops = len(y) // loop_duration_samples
120
- if num_loops == 0: print(f"Audio for {stem_name} is too short for {bars}-bar loops at {bpm_int} BPM."); return None, None
 
 
 
 
121
  for i in range(num_loops):
122
- start_sample = i * loop_duration_samples; end_sample = start_sample + loop_duration_samples
 
 
123
  slice_data = y[start_sample:end_sample]
 
124
  filename = os.path.join(loops_dir, f"{stem_name}_{bars}bar_loop_{i+1:03d}_{bpm_int}bpm.wav")
125
  sf.write(filename, slice_data, sample_rate, subtype='PCM_16')
126
  output_files.append(filename)
127
- if progress_fn and num_loops > 1:
128
- progress_fn(0.4 + (i / (num_loops - 1) * 0.6), desc=f"Exporting loop {i+1}/{num_loops}...")
129
- if not output_files: return None, None
 
 
 
 
 
130
  return output_files, loops_dir
131
 
132
- async def slice_all_and_zip_real(vocals, drums, bass, other, loop_choice, sensitivity, progress=gr.Progress(track_tqdm=True)):
 
 
 
 
133
  log_history = "Starting batch slice...\n"
134
- yield { status_log: log_history, progress_bar: progress(0, desc="Starting...", visible=True) }
135
- await asyncio.sleep(0.1)
 
 
 
 
136
  stems_to_process = {"vocals": vocals, "drums": drums, "bass": bass, "other": other}
137
  zip_path = "Loop_Architect_Pack.zip"
 
138
  num_stems = sum(1 for data in stems_to_process.values() if data is not None)
139
- if num_stems == 0: raise gr.Error("No stems to process! Please separate stems first.")
 
 
140
  all_temp_dirs = []
141
  try:
142
  with zipfile.ZipFile(zip_path, 'w') as zf:
143
  processed_count = 0
144
  for name, data in stems_to_process.items():
145
  if data is not None:
146
- log_history += f"--- Slicing {name} stem ---\n"; yield { status_log: log_history }
 
 
 
147
  def update_main_progress(p, desc=""):
148
  overall_progress = (processed_count + p) / num_stems
149
- progress(overall_progress, desc=f"Slicing {name}: {desc}", visible=True)
150
-
151
- sliced_files, temp_dir = slice_stem_real((data[0], data[1]), loop_choice, sensitivity, name, progress_fn=update_main_progress)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
  if sliced_files:
154
- log_history += f"Generated {len(sliced_files)} slices for {name}.\n"; yield { status_log: log_history }
155
  all_temp_dirs.append(temp_dir)
156
  for loop_file in sliced_files:
157
  arcname = os.path.join(name, os.path.basename(loop_file))
158
  zf.write(loop_file, arcname)
159
  else:
160
- log_history += f"No slices generated for {name}.\n"; yield { status_log: log_history }
161
 
162
  processed_count += 1
163
- yield { status_log: log_history, progress_bar: progress(processed_count / num_stems, desc=f"Finished {name}", visible=True) }
 
 
 
164
 
165
- log_history += "Packaging complete!\n";
166
  yield {
167
  status_log: log_history + "✅ Pack ready for download!",
168
- progress_bar: progress(1, desc="Pack Ready!", visible=False),
169
  download_zip_file: gr.update(value=zip_path, visible=True)
170
  }
 
171
  except Exception as e:
172
  print(f"An error occurred during slice all: {e}")
173
- yield { status_log: log_history + f"❌ ERROR: {e}", progress_bar: gr.update(visible=False) }
 
 
 
174
  finally:
 
175
  for d in all_temp_dirs:
176
  if d and os.path.exists(d):
177
  shutil.rmtree(d)
@@ -179,28 +323,43 @@ async def slice_all_and_zip_real(vocals, drums, bass, other, loop_choice, sensit
179
  # --- Create the full Gradio Interface ---
180
  with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="red")) as demo:
181
  gr.Markdown("# 🎵 Loop Architect")
 
 
182
  with gr.Row():
183
  with gr.Column(scale=1):
184
  gr.Markdown("### 1. Separate Stems")
185
  audio_input = gr.Audio(type="filepath", label="Upload a Track")
186
- stem_options = gr.Radio(["4 Stems (Vocals, Drums, Bass, Other)", "2 Stems (Vocals + Instrumental)"], label="Separation Type", value="4 Stems (Vocals, Drums, Bass, Other)")
187
- submit_button = gr.Button("Separate Stems")
188
-
189
- with gr.Accordion("Slicing Options", open=True):
190
- loop_options_radio = gr.Radio(["One-Shots (All Transients)", "4 Bar Loops", "8 Bar Loops"], label="Slice Type", value="One-Shots (All Transients)")
191
- sensitivity_slider = gr.Slider(minimum=0.01, maximum=0.5, value=0.05, step=0.01, label="One-Shot Sensitivity", info="Lower values = more slices")
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
193
  gr.Markdown("### 3. Create Pack")
194
- slice_all_button = gr.Button("Slice All Stems & Create Pack", variant="primary")
195
  download_zip_file = gr.File(label="Download Your Loop Pack", visible=False)
196
 
197
  gr.Markdown("### Status")
198
- # --- THIS IS THE FIX --- Initialize without any arguments
199
  progress_bar = gr.Progress()
200
  status_log = gr.Textbox(label="Status Log", lines=10, interactive=False)
201
 
202
  with gr.Column(scale=2):
203
- with gr.Accordion("Separated Stems", open=True):
204
  with gr.Row():
205
  vocals_output = gr.Audio(label="Vocals", scale=4)
206
  slice_vocals_btn = gr.Button("Slice Vocals", scale=1)
@@ -213,26 +372,46 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="red"))
213
  with gr.Row():
214
  other_output = gr.Audio(label="Other / Instrumental", scale=4)
215
  slice_other_btn = gr.Button("Slice Other", scale=1)
 
216
  gr.Markdown("### Sliced Loops / Samples (Preview)")
217
- loop_gallery = gr.Gallery(label="Generated Loops Preview", columns=8, object_fit="contain", height="auto", preview=True)
 
 
 
218
 
219
  # --- Define Event Listeners ---
220
  def slice_and_display(stem_data, loop_choice, sensitivity, stem_name):
 
 
 
 
 
 
 
 
 
 
221
  log_history = f"Slicing {stem_name}...\n"
222
  # Make progress bar visible when starting
223
  yield {
224
  status_log: log_history,
225
- progress_bar: gr.update(value=0, visible=True, label=f"Slicing {stem_name}...")
226
  }
227
 
228
- # Define how slice_stem_real updates the progress bar using gr.Progress directly
229
  def update_single_progress(p, desc=""):
230
- gr.Progress(track_tqdm=True)(p, desc=desc) # Pass progress updates
 
 
 
231
 
232
- files, temp_dir = slice_stem_real(stem_data, loop_choice, sensitivity, stem_name, progress_fn=update_single_progress)
 
 
 
233
 
234
  if temp_dir and os.path.exists(temp_dir):
235
- shutil.rmtree(temp_dir)
236
 
237
  yield {
238
  loop_gallery: gr.update(value=files),
@@ -240,24 +419,39 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="red"))
240
  progress_bar: gr.update(visible=False) # Hide progress bar when done
241
  }
242
 
 
 
 
243
  submit_event = submit_button.click(
244
  fn=separate_stems,
245
  inputs=[audio_input, stem_options],
246
- outputs=[vocals_output, drums_output, bass_output, other_output, status_log]
 
 
 
 
247
  )
248
 
249
- stem_options.change(fn=update_output_visibility, inputs=stem_options, outputs=[vocals_output, drums_output, bass_output, other_output])
 
 
 
 
 
250
 
251
- 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, progress_bar])
252
- 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, progress_bar])
253
- 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, progress_bar])
254
- 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, progress_bar])
 
255
 
 
256
  slice_all_event = slice_all_button.click(
257
  fn=slice_all_and_zip_real,
258
  inputs=[vocals_output, drums_output, bass_output, other_output, loop_options_radio, sensitivity_slider],
259
- outputs=[download_zip_file, download_zip_file, status_log]
 
260
  )
261
 
262
- # --- Launch the UI ---
263
- demo.launch()
 
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),
 
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)
 
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)
 
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),
 
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)