SaltProphet commited on
Commit
5ab21d9
·
verified ·
1 Parent(s): 4c8e478

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +221 -67
app.py CHANGED
@@ -1,5 +1,4 @@
1
-
2
- # 2. Import libraries
3
  import gradio as gr
4
  import os
5
  import shutil
@@ -13,127 +12,282 @@ import zipfile
13
  import tempfile
14
  import matplotlib.pyplot as plt
15
  import matplotlib
16
- matplotlib.use('Agg')
17
 
18
  # --- Helper/Processing Functions ---
19
 
20
  def update_output_visibility(choice):
21
- # Now handles 2, 4, and 6 stem visibility
22
- is_6_stem = "6 Stems" in choice
23
- is_4_stem = "4 Stems" in choice
24
- is_2_stem = "2 Stems" in choice
25
-
26
- return {
27
- vocals_output: gr.update(visible=True),
28
- drums_output: gr.update(visible=is_4_stem or is_6_stem),
29
- bass_output: gr.update(visible=is_4_stem or is_6_stem),
30
- guitar_output: gr.update(visible=is_6_stem), # Guitar visible only for 6 stems
31
- piano_output: gr.update(visible=is_6_stem), # Piano visible only for 6 stems
32
- other_output: gr.update(visible=True, label="Instrumental (No Vocals)" if is_2_stem else "Other")
33
- }
 
 
34
 
35
  async def separate_stems(audio_file_path, stem_choice, progress=gr.Progress(track_tqdm=True)):
 
36
  if audio_file_path is None: raise gr.Error("No audio file uploaded!")
37
- progress(0, desc="Starting...")
 
 
38
  try:
39
  progress(0.05, desc="Preparing audio file...")
 
40
  original_filename_base = os.path.basename(audio_file_path).rsplit('.', 1)[0]
41
  stable_input_path = f"stable_input_{original_filename_base}.wav"
42
  shutil.copy(audio_file_path, stable_input_path)
43
 
44
- # Determine Demucs model based on choice
45
- model_name = "htdemucs" # Default to 4-stem model
46
- model_arg = ""
47
- if "2 Stems" in stem_choice:
48
- model_arg = "--two-stems=vocals"
49
- elif "6 Stems" in stem_choice:
50
- model_name = "htdemucs_6s" # Use the 6-stem model
51
-
52
  output_dir = "separated"
53
  if os.path.exists(output_dir): shutil.rmtree(output_dir)
54
 
55
- # Use -n flag to specify the model name
56
- command = f"python3 -m demucs -n {model_name} {model_arg} -o \"{output_dir}\" \"{stable_input_path}\""
57
- progress(0.2, desc=f"Running Demucs ({model_name})...")
 
 
 
 
 
58
 
59
- process = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
60
  stdout, stderr = await process.communicate()
61
 
62
  if process.returncode != 0:
63
  raise gr.Error(f"Demucs failed. Error: {stderr.decode()[:500]}")
64
 
65
- progress(0.8, desc="Locating separated stem files...")
66
  stable_filename_base = os.path.basename(stable_input_path).rsplit('.', 1)[0]
67
- # Demucs output folder now matches the model name
68
- stems_path = os.path.join(output_dir, model_name, stable_filename_base)
 
 
 
 
69
 
70
  if not os.path.exists(stems_path):
71
- raise gr.Error(f"Demucs finished, but output directory '{stems_path}' not found!")
72
 
73
- # Check for all possible stem files
74
  vocals_path = os.path.join(stems_path, "vocals.wav") if os.path.exists(os.path.join(stems_path, "vocals.wav")) else None
75
  drums_path = os.path.join(stems_path, "drums.wav") if os.path.exists(os.path.join(stems_path, "drums.wav")) else None
76
  bass_path = os.path.join(stems_path, "bass.wav") if os.path.exists(os.path.join(stems_path, "bass.wav")) else None
77
- guitar_path = os.path.join(stems_path, "guitar.wav") if os.path.exists(os.path.join(stems_path, "guitar.wav")) else None
78
- piano_path = os.path.join(stems_path, "piano.wav") if os.path.exists(os.path.join(stems_path, "piano.wav")) else None
79
-
80
- # Handle 'other' vs 'no_vocals' based on model
81
  other_filename = "no_vocals.wav" if "2 Stems" in stem_choice else "other.wav"
82
  other_path = os.path.join(stems_path, other_filename) if os.path.exists(os.path.join(stems_path, other_filename)) else None
83
 
84
  os.remove(stable_input_path)
85
- return vocals_path, drums_path, bass_path, guitar_path, piano_path, other_path
 
 
 
 
 
 
 
 
 
86
  except Exception as e:
87
- print(f"An error occurred: {e}")
88
- raise gr.Error(str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  # --- Create the full Gradio Interface ---
91
  with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="red")) as demo:
92
  gr.Markdown("# 🎵 Loop Architect")
93
-
94
  with gr.Row():
95
  with gr.Column(scale=1):
96
  gr.Markdown("### 1. Separate Stems")
97
  audio_input = gr.Audio(type="filepath", label="Upload a Track")
98
- # Added 6-stem option
99
- stem_options = gr.Radio(
100
- ["6 Stems (Vocals, Drums, Bass, Guitar, Piano, Other)",
101
- "4 Stems (Vocals, Drums, Bass, Other)",
102
- "2 Stems (Vocals + Instrumental)"],
103
- label="Separation Type",
104
- value="4 Stems (Vocals, Drums, Bass, Other)" # Default still 4 stems
105
- )
106
  submit_button = gr.Button("Separate Stems")
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  with gr.Column(scale=2):
109
  with gr.Accordion("Separated Stems", open=True):
110
- # Added players for guitar and piano, initially hidden
111
  with gr.Row():
112
  vocals_output = gr.Audio(label="Vocals", scale=4)
 
113
  with gr.Row():
114
- drums_output = gr.Audio(label="Drums", scale=4, visible=True)
115
- with gr.Row():
116
- bass_output = gr.Audio(label="Bass", scale=4, visible=True)
117
  with gr.Row():
118
- guitar_output = gr.Audio(label="Guitar", scale=4, visible=False)
 
119
  with gr.Row():
120
- piano_output = gr.Audio(label="Piano", scale=4, visible=False)
121
- with gr.Row():
122
- other_output = gr.Audio(label="Other / Instrumental", scale=4, visible=True)
 
123
 
124
  # --- Define Event Listeners ---
125
- submit_button.click(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  fn=separate_stems,
127
  inputs=[audio_input, stem_options],
128
- # Update outputs to include new players
129
- outputs=[vocals_output, drums_output, bass_output, guitar_output, piano_output, other_output]
130
- )
131
- stem_options.change(
132
- fn=update_output_visibility,
133
- inputs=stem_options,
134
- # Update outputs to include new players
135
- outputs=[vocals_output, drums_output, bass_output, guitar_output, piano_output, other_output]
136
  )
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  # --- Launch the UI ---
139
  demo.launch()
 
1
+ # 1. Import libraries
 
2
  import gradio as gr
3
  import os
4
  import shutil
 
12
  import tempfile
13
  import matplotlib.pyplot as plt
14
  import matplotlib
15
+ matplotlib.use('Agg') # Use a non-interactive backend for plotting
16
 
17
  # --- Helper/Processing Functions ---
18
 
19
  def update_output_visibility(choice):
20
+ # Shows/hides stem players based on 2 or 4 stem selection
21
+ if "2 Stems" in choice:
22
+ return {
23
+ vocals_output: gr.update(visible=True),
24
+ drums_output: gr.update(visible=False),
25
+ bass_output: gr.update(visible=False),
26
+ other_output: gr.update(visible=True, label="Instrumental (No Vocals)")
27
+ }
28
+ elif "4 Stems" in choice:
29
+ return {
30
+ vocals_output: gr.update(visible=True),
31
+ drums_output: gr.update(visible=True),
32
+ bass_output: gr.update(visible=True),
33
+ other_output: gr.update(visible=True, label="Other")
34
+ }
35
 
36
  async def separate_stems(audio_file_path, stem_choice, progress=gr.Progress(track_tqdm=True)):
37
+ # Separates the uploaded audio using Demucs
38
  if audio_file_path is None: raise gr.Error("No audio file uploaded!")
39
+ progress(0, desc="Starting separation...")
40
+ log_history = "Starting separation...\n"
41
+ yield { status_log: log_history, progress_bar: progress(0, desc="Starting...") }
42
  try:
43
  progress(0.05, desc="Preparing audio file...")
44
+ log_history += "Preparing audio file...\n"; yield { status_log: log_history, progress_bar: progress(0.05, desc="Preparing...") }
45
  original_filename_base = os.path.basename(audio_file_path).rsplit('.', 1)[0]
46
  stable_input_path = f"stable_input_{original_filename_base}.wav"
47
  shutil.copy(audio_file_path, stable_input_path)
48
 
49
+ model_arg = "--two-stems=vocals" if "2 Stems" in stem_choice else ""
 
 
 
 
 
 
 
50
  output_dir = "separated"
51
  if os.path.exists(output_dir): shutil.rmtree(output_dir)
52
 
53
+ command = f"python3 -m demucs {model_arg} -o \"{output_dir}\" \"{stable_input_path}\""
54
+ log_history += f"Running Demucs command: {command}\n(This may take a minute or more depending on track length)\n";
55
+ yield { status_log: log_history, progress_bar: progress(0.2, desc="Running Demucs...") }
56
+
57
+ process = await asyncio.create_subprocess_shell(
58
+ command,
59
+ stdout=asyncio.subprocess.PIPE,
60
+ stderr=asyncio.subprocess.PIPE)
61
 
 
62
  stdout, stderr = await process.communicate()
63
 
64
  if process.returncode != 0:
65
  raise gr.Error(f"Demucs failed. Error: {stderr.decode()[:500]}")
66
 
67
+ log_history += "Demucs finished. Locating stem files...\n"; yield { status_log: log_history, progress_bar: progress(0.8, desc="Locating stems...") }
68
  stable_filename_base = os.path.basename(stable_input_path).rsplit('.', 1)[0]
69
+
70
+ # Find the model-specific subfolder created by Demucs
71
+ subfolders = [f.name for f in os.scandir(output_dir) if f.is_dir()]
72
+ if not subfolders: raise gr.Error("Demucs output folder structure not found!")
73
+ model_folder_name = subfolders[0] # Assume the first subfolder is the correct one
74
+ stems_path = os.path.join(output_dir, model_folder_name, stable_filename_base)
75
 
76
  if not os.path.exists(stems_path):
77
+ raise gr.Error(f"Demucs output directory was not found! Looked for: {stems_path}")
78
 
 
79
  vocals_path = os.path.join(stems_path, "vocals.wav") if os.path.exists(os.path.join(stems_path, "vocals.wav")) else None
80
  drums_path = os.path.join(stems_path, "drums.wav") if os.path.exists(os.path.join(stems_path, "drums.wav")) else None
81
  bass_path = os.path.join(stems_path, "bass.wav") if os.path.exists(os.path.join(stems_path, "bass.wav")) else None
 
 
 
 
82
  other_filename = "no_vocals.wav" if "2 Stems" in stem_choice else "other.wav"
83
  other_path = os.path.join(stems_path, other_filename) if os.path.exists(os.path.join(stems_path, other_filename)) else None
84
 
85
  os.remove(stable_input_path)
86
+
87
+ log_history += "✅ Stem separation complete!\n";
88
+ yield {
89
+ status_log: log_history,
90
+ progress_bar: progress(1, desc="Complete!"),
91
+ vocals_output: gr.update(value=vocals_path),
92
+ drums_output: gr.update(value=drums_path),
93
+ bass_output: gr.update(value=bass_path),
94
+ other_output: gr.update(value=other_path)
95
+ }
96
  except Exception as e:
97
+ print(f"An error occurred during separation: {e}")
98
+ yield { status_log: log_history + f"❌ ERROR: {e}" }
99
+
100
+ def slice_stem_real(stem_audio_data, loop_choice, sensitivity, stem_name, progress_fn=None):
101
+ # Slices a single stem into loops or one-shots
102
+ if stem_audio_data is None:
103
+ return None, None
104
+
105
+ sample_rate, y_int = stem_audio_data
106
+ y = librosa.util.buf_to_float(y_int)
107
+ y_mono = librosa.to_mono(y.T) if y.ndim > 1 else y
108
+
109
+ if progress_fn: progress_fn(0.1, desc="Detecting BPM...")
110
+ tempo, beats = librosa.beat.beat_track(y=y_mono, sr=sample_rate)
111
+ bpm = 120 if tempo is None else np.round(tempo)
112
+ bpm_int = int(bpm.item())
113
+ if bpm_int == 0: bpm_int = 120; print("BPM detection failed, defaulting to 120 BPM.")
114
+ print(f"Detected BPM for {stem_name}: {bpm_int}")
115
+
116
+ output_files = []
117
+ loops_dir = tempfile.mkdtemp() # Use a unique temp dir for each run
118
+
119
+ if "One-Shots" in loop_choice:
120
+ if progress_fn: progress_fn(0.3, desc="Finding transients...")
121
+ 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)
122
+ onset_samples = librosa.frames_to_samples(onset_frames)
123
+
124
+ if progress_fn: progress_fn(0.5, desc="Slicing one-shots...")
125
+ if len(onset_samples) > 0:
126
+ num_onsets = len(onset_samples)
127
+ for i, start_sample in enumerate(onset_samples):
128
+ end_sample = onset_samples[i+1] if i+1 < num_onsets else len(y)
129
+ slice_data = y[start_sample:end_sample]
130
+ filename = os.path.join(loops_dir, f"{stem_name}_one_shot_{i+1:03d}.wav")
131
+ sf.write(filename, slice_data, sample_rate, subtype='PCM_16')
132
+ output_files.append(filename)
133
+ if progress_fn and num_onsets > 1:
134
+ progress_fn(0.5 + (i / (num_onsets - 1) * 0.5), desc=f"Exporting slice {i+1}/{num_onsets}...")
135
+ else: # Handle bar loops
136
+ bars = int(loop_choice.split(" ")[0])
137
+ seconds_per_beat = 60.0 / bpm_int
138
+ seconds_per_bar = seconds_per_beat * 4
139
+ loop_duration_seconds = seconds_per_bar * bars
140
+ loop_duration_samples = int(loop_duration_seconds * sample_rate)
141
+
142
+ if progress_fn: progress_fn(0.4, desc=f"Slicing into {bars}-bar loops...")
143
+ num_loops = len(y) // loop_duration_samples
144
+ if num_loops == 0:
145
+ print(f"Audio for {stem_name} is too short for {bars}-bar loops at {bpm_int} BPM.")
146
+ return None, None
147
+ for i in range(num_loops):
148
+ start_sample = i * loop_duration_samples
149
+ end_sample = start_sample + loop_duration_samples
150
+ slice_data = y[start_sample:end_sample]
151
+ filename = os.path.join(loops_dir, f"{stem_name}_{bars}bar_loop_{i+1:03d}_{bpm_int}bpm.wav")
152
+ sf.write(filename, slice_data, sample_rate, subtype='PCM_16')
153
+ output_files.append(filename)
154
+ if progress_fn and num_loops > 1:
155
+ progress_fn(0.4 + (i / (num_loops - 1) * 0.6), desc=f"Exporting loop {i+1}/{num_loops}...")
156
+
157
+ if not output_files:
158
+ return None, None
159
+
160
+ return output_files, loops_dir
161
+
162
+ async def slice_all_and_zip_real(vocals, drums, bass, other, loop_choice, sensitivity, progress=gr.Progress(track_tqdm=True)):
163
+ # Slices all available stems and creates a zip pack
164
+ log_history = "Starting batch slice...\n"
165
+ yield { status_log: log_history, progress_bar: progress(0, desc="Starting...") }
166
+ await asyncio.sleep(0.1) # Give Gradio time to update UI
167
+
168
+ stems_to_process = {"vocals": vocals, "drums": drums, "bass": bass, "other": other}
169
+ zip_path = "Loop_Architect_Pack.zip"
170
+
171
+ num_stems = sum(1 for data in stems_to_process.values() if data is not None)
172
+ if num_stems == 0: raise gr.Error("No stems to process! Please separate stems first.")
173
+
174
+ all_temp_dirs = []
175
+
176
+ with zipfile.ZipFile(zip_path, 'w') as zf:
177
+ processed_count = 0
178
+ for name, data in stems_to_process.items():
179
+ if data is not None:
180
+ log_history += f"--- Slicing {name} stem ---\n"; yield { status_log: log_history }
181
+
182
+ # Define a function to update the main progress bar from within slice_stem_real
183
+ def update_main_progress(p, desc=""):
184
+ overall_progress = (processed_count + p) / num_stems
185
+ progress(overall_progress, desc=f"Slicing {name}: {desc}")
186
+
187
+ sliced_files, temp_dir = slice_stem_real((data[0], data[1]), loop_choice, sensitivity, name, progress_fn=update_main_progress)
188
+
189
+ if sliced_files:
190
+ log_history += f"Generated {len(sliced_files)} slices for {name}.\n"; yield { status_log: log_history }
191
+ all_temp_dirs.append(temp_dir) # Keep track of temp dirs for cleanup
192
+ for loop_file in sliced_files:
193
+ arcname = os.path.join(name, os.path.basename(loop_file)) # Add to subfolder in zip
194
+ zf.write(loop_file, arcname)
195
+ else:
196
+ log_history += f"No slices generated for {name}.\n"; yield { status_log: log_history }
197
+
198
+ processed_count += 1
199
+ # Yield intermediate progress after each stem is fully processed
200
+ yield { status_log: log_history, progress_bar: progress(processed_count / num_stems, desc=f"Finished {name}") }
201
+
202
+ log_history += "Packaging complete!\n"; yield { status_log: log_history, progress_bar: progress(1, desc="Pack Ready!") }
203
+
204
+ # Clean up all temporary directories used for slicing
205
+ for d in all_temp_dirs:
206
+ if d and os.path.exists(d):
207
+ shutil.rmtree(d)
208
+
209
+ yield { download_zip_file: gr.update(value=zip_path, visible=True), status_log: log_history + "✅ Pack ready for download!"}
210
+
211
 
212
  # --- Create the full Gradio Interface ---
213
  with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="red")) as demo:
214
  gr.Markdown("# 🎵 Loop Architect")
 
215
  with gr.Row():
216
  with gr.Column(scale=1):
217
  gr.Markdown("### 1. Separate Stems")
218
  audio_input = gr.Audio(type="filepath", label="Upload a Track")
219
+ stem_options = gr.Radio(["4 Stems (Vocals, Drums, Bass, Other)", "2 Stems (Vocals + Instrumental)"], label="Separation Type", value="4 Stems (Vocals, Drums, Bass, Other)")
 
 
 
 
 
 
 
220
  submit_button = gr.Button("Separate Stems")
221
 
222
+ # --- SLICING OPTIONS ARE BACK ---
223
+ with gr.Accordion("Slicing Options", open=True):
224
+ loop_options_radio = gr.Radio(["One-Shots (All Transients)", "4 Bar Loops", "8 Bar Loops"], label="Slice Type", value="One-Shots (All Transients)")
225
+ 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")
226
+
227
+ gr.Markdown("### 3. Create Pack")
228
+ slice_all_button = gr.Button("Slice All Stems & Create Pack", variant="primary")
229
+ download_zip_file = gr.File(label="Download Your Loop Pack", visible=False)
230
+
231
+ gr.Markdown("### Status")
232
+ progress_bar = gr.Progress(label="Task Progress")
233
+ status_log = gr.Textbox(label="Status Log", lines=10, interactive=False)
234
+
235
  with gr.Column(scale=2):
236
  with gr.Accordion("Separated Stems", open=True):
 
237
  with gr.Row():
238
  vocals_output = gr.Audio(label="Vocals", scale=4)
239
+ slice_vocals_btn = gr.Button("Slice Vocals", scale=1) # Renamed for clarity
240
  with gr.Row():
241
+ drums_output = gr.Audio(label="Drums", scale=4)
242
+ slice_drums_btn = gr.Button("Slice Drums", scale=1)
 
243
  with gr.Row():
244
+ bass_output = gr.Audio(label="Bass", scale=4)
245
+ slice_bass_btn = gr.Button("Slice Bass", scale=1)
246
  with gr.Row():
247
+ other_output = gr.Audio(label="Other / Instrumental", scale=4)
248
+ slice_other_btn = gr.Button("Slice Other", scale=1)
249
+ gr.Markdown("### Sliced Loops / Samples (Preview)")
250
+ loop_gallery = gr.Gallery(label="Generated Loops Preview", columns=8, object_fit="contain", height="auto", preview=True)
251
 
252
  # --- Define Event Listeners ---
253
+ def slice_and_display(stem_data, loop_choice, sensitivity, stem_name):
254
+ # Wrapper to handle progress display for single slice buttons
255
+ log_history = f"Slicing {stem_name}...\n"
256
+ yield { status_log: log_history, progress_bar: gr.update(visible=True) }
257
+
258
+ def update_single_progress(p, desc=""):
259
+ progress_bar.update(value=p, label=desc, visible=True) # Cannot yield from nested func
260
+
261
+ files, temp_dir = slice_stem_real(stem_data, loop_choice, sensitivity, stem_name, progress_fn=update_single_progress)
262
+
263
+ if temp_dir and os.path.exists(temp_dir):
264
+ shutil.rmtree(temp_dir) # Clean up temp dir after preview
265
+
266
+ yield {
267
+ loop_gallery: gr.update(value=files),
268
+ status_log: log_history + f"✅ Sliced {stem_name} into {len(files) if files else 0} pieces.",
269
+ progress_bar: gr.update(visible=False)
270
+ }
271
+
272
+
273
+ submit_event = submit_button.click(
274
  fn=separate_stems,
275
  inputs=[audio_input, stem_options],
276
+ outputs=[vocals_output, drums_output, bass_output, other_output, status_log, progress_bar]
 
 
 
 
 
 
 
277
  )
278
 
279
+ stem_options.change(fn=update_output_visibility, inputs=stem_options, outputs=[vocals_output, drums_output, bass_output, other_output])
280
+
281
+ 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])
282
+ 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])
283
+ 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])
284
+ 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])
285
+
286
+ slice_all_event = slice_all_button.click(
287
+ fn=slice_all_and_zip_real,
288
+ inputs=[vocals_output, drums_output, bass_output, other_output, loop_options_radio, sensitivity_slider],
289
+ outputs=[download_zip_file, download_zip_file, status_log, progress_bar] # Added status log and progress bar outputs
290
+ )
291
+
292
  # --- Launch the UI ---
293
  demo.launch()