SaltProphet commited on
Commit
ae02c77
·
verified ·
1 Parent(s): ed25ca2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +137 -43
app.py CHANGED
@@ -21,7 +21,6 @@ matplotlib.use('Agg') # Use a non-interactive backend for plotting
21
  # --- Helper/Processing Functions ---
22
 
23
  def update_output_visibility(choice):
24
- # This function remains the same
25
  if "2 Stems" in choice:
26
  return {
27
  vocals_output: gr.update(visible=True),
@@ -38,7 +37,6 @@ def update_output_visibility(choice):
38
  }
39
 
40
  async def separate_stems(audio_file_path, stem_choice, progress=gr.Progress(track_tqdm=True)):
41
- # This function now returns to both the visible UI and the hidden state
42
  if audio_file_path is None: raise gr.Error("No audio file uploaded!")
43
  progress(0, desc="Starting...")
44
  try:
@@ -52,17 +50,25 @@ async def separate_stems(audio_file_path, stem_choice, progress=gr.Progress(trac
52
  if os.path.exists(output_dir): shutil.rmtree(output_dir)
53
 
54
  command = f"python3 -m demucs {model_arg} -o \"{output_dir}\" \"{stable_input_path}\""
55
- progress(0.2, desc="Running Demucs...");
56
- process = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
57
- await process.communicate()
58
- if process.returncode != 0: raise gr.Error(f"Demucs failed to run.")
 
 
 
 
 
 
 
59
 
60
  progress(0.8, desc="Locating separated stem files...")
61
  stable_filename_base = os.path.basename(stable_input_path).rsplit('.', 1)[0]
62
  model_folder_name = next(os.walk(output_dir))[1][0]
63
  stems_path = os.path.join(output_dir, model_folder_name, stable_filename_base)
64
 
65
- if not os.path.exists(stems_path): raise gr.Error(f"Demucs finished, but the output directory was not found!")
 
66
 
67
  vocals_path = os.path.join(stems_path, "vocals.wav") if os.path.exists(os.path.join(stems_path, "vocals.wav")) else None
68
  drums_path = os.path.join(stems_path, "drums.wav") if os.path.exists(os.path.join(stems_path, "drums.wav")) else None
@@ -71,65 +77,153 @@ async def separate_stems(audio_file_path, stem_choice, progress=gr.Progress(trac
71
  other_path = os.path.join(stems_path, other_filename) if os.path.exists(os.path.join(stems_path, other_filename)) else None
72
 
73
  os.remove(stable_input_path)
74
- # Return values for the visible UI and the hidden state variables
75
- return vocals_path, drums_path, bass_path, other_path, vocals_path, drums_path, bass_path, other_path
76
  except Exception as e:
77
- print(f"An error occurred: {e}"); raise gr.Error(str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
- # --- NEW FUNCTION TO REPOPULATE UI ON PAGE LOAD ---
80
- def repopulate_ui_from_state(vocals_path, drums_path, bass_path, other_path):
81
- # This function takes the saved paths from the state and updates the audio players
82
- print("Page reloaded. Repopulating UI from session state.")
83
- return vocals_path, drums_path, bass_path, other_path
84
 
85
  # --- Create the full Gradio Interface ---
86
  with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="red")) as demo:
87
  gr.Markdown("# 🎵 Loop Architect")
88
-
89
- # --- NEW STATE COMPONENTS FOR PERSISTENCE ---
90
- vocals_path_state = gr.State(None)
91
- drums_path_state = gr.State(None)
92
- bass_path_state = gr.State(None)
93
- other_path_state = gr.State(None)
94
-
95
  with gr.Row():
96
  with gr.Column(scale=1):
97
  gr.Markdown("### 1. Separate Stems")
98
  audio_input = gr.Audio(type="filepath", label="Upload a Track")
99
  stem_options = gr.Radio(["4 Stems (Vocals, Drums, Bass, Other)", "2 Stems (Vocals + Instrumental)"], label="Separation Type", value="4 Stems (Vocals, Drums, Bass, Other)")
100
  submit_button = gr.Button("Separate Stems")
101
-
 
 
 
 
 
102
  with gr.Column(scale=2):
103
  with gr.Accordion("Separated Stems", open=True):
104
  with gr.Row():
105
  vocals_output = gr.Audio(label="Vocals", scale=4)
 
106
  with gr.Row():
107
  drums_output = gr.Audio(label="Drums", scale=4)
 
108
  with gr.Row():
109
  bass_output = gr.Audio(label="Bass", scale=4)
 
110
  with gr.Row():
111
  other_output = gr.Audio(label="Other / Instrumental", scale=4)
 
 
 
112
 
113
  # --- Define Event Listeners ---
114
-
115
- # The submit button now updates both the visible audio players AND the hidden state variables
116
- submit_button.click(
117
- fn=separate_stems,
118
- inputs=[audio_input, stem_options],
119
- outputs=[
120
- vocals_output, drums_output, bass_output, other_output,
121
- vocals_path_state, drums_path_state, bass_path_state, other_path_state
122
- ]
123
- )
124
-
125
- stem_options.change(fn=update_output_visibility, inputs=stem_options, outputs=[vocals_output, drums_output, bass_output, other_output])
126
-
127
- # --- NEW EVENT LISTENER FOR PAGE LOAD ---
128
- demo.load(
129
- fn=repopulate_ui_from_state,
130
- inputs=[vocals_path_state, drums_path_state, bass_path_state, other_path_state],
131
- outputs=[vocals_output, drums_output, bass_output, other_output]
132
- )
133
-
134
  # --- Launch the UI ---
135
  demo.launch()
 
21
  # --- Helper/Processing Functions ---
22
 
23
  def update_output_visibility(choice):
 
24
  if "2 Stems" in choice:
25
  return {
26
  vocals_output: gr.update(visible=True),
 
37
  }
38
 
39
  async def separate_stems(audio_file_path, stem_choice, progress=gr.Progress(track_tqdm=True)):
 
40
  if audio_file_path is None: raise gr.Error("No audio file uploaded!")
41
  progress(0, desc="Starting...")
42
  try:
 
50
  if os.path.exists(output_dir): shutil.rmtree(output_dir)
51
 
52
  command = f"python3 -m demucs {model_arg} -o \"{output_dir}\" \"{stable_input_path}\""
53
+ progress(0.2, desc="Running Demucs (this may take a minute)...")
54
+
55
+ process = await asyncio.create_subprocess_shell(
56
+ command,
57
+ stdout=asyncio.subprocess.PIPE,
58
+ stderr=asyncio.subprocess.PIPE)
59
+
60
+ stdout, stderr = await process.communicate()
61
+
62
+ if process.returncode != 0:
63
+ raise gr.Error(f"Demucs failed to run. 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
  model_folder_name = next(os.walk(output_dir))[1][0]
68
  stems_path = os.path.join(output_dir, model_folder_name, stable_filename_base)
69
 
70
+ if not os.path.exists(stems_path):
71
+ raise gr.Error(f"Demucs finished, but the output directory was not found!")
72
 
73
  vocals_path = os.path.join(stems_path, "vocals.wav") if os.path.exists(os.path.join(stems_path, "vocals.wav")) else None
74
  drums_path = os.path.join(stems_path, "drums.wav") if os.path.exists(os.path.join(stems_path, "drums.wav")) else None
 
77
  other_path = os.path.join(stems_path, other_filename) if os.path.exists(os.path.join(stems_path, other_filename)) else None
78
 
79
  os.remove(stable_input_path)
80
+ return vocals_path, drums_path, bass_path, other_path
 
81
  except Exception as e:
82
+ print(f"An error occurred: {e}")
83
+ raise gr.Error(str(e))
84
+
85
+ def slice_stem_real(stem_audio_data, loop_choice, sensitivity, progress=gr.Progress(track_tqdm=True)):
86
+ if stem_audio_data is None:
87
+ gr.Warning("This stem is empty. Cannot slice.")
88
+ return None, None
89
+
90
+ sample_rate, y_int = stem_audio_data
91
+ y = librosa.util.buf_to_float(y_int)
92
+ y_mono = librosa.to_mono(y.T) if y.ndim > 1 else y
93
+
94
+ progress(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)
97
+ bpm_int = int(bpm.item())
98
+
99
+ if bpm_int == 0:
100
+ bpm_int = 120
101
+ gr.Warning("BPM detection failed, defaulting to 120 BPM.")
102
+ print(f"Detected BPM: {bpm_int}")
103
+
104
+ output_files = []
105
+ loops_dir = tempfile.mkdtemp()
106
+
107
+ if "One-Shots" in loop_choice:
108
+ progress(0.3, desc="Finding transients...")
109
+ 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)
110
+ onset_samples = librosa.frames_to_samples(onset_frames)
111
+
112
+ progress(0.5, desc="Slicing one-shots...")
113
+ if len(onset_samples) > 0:
114
+ for i, start_sample in enumerate(onset_samples):
115
+ end_sample = onset_samples[i+1] if i+1 < len(onset_samples) else len(y)
116
+ slice_data = y[start_sample:end_sample]
117
+ filename = os.path.join(loops_dir, f"one_shot_{i+1:03d}.wav")
118
+ sf.write(filename, slice_data, sample_rate, subtype='PCM_16')
119
+ output_files.append(filename)
120
+ if len(onset_samples) > 1:
121
+ progress(0.5 + (i / (len(onset_samples) - 1) * 0.5), desc=f"Exporting slice {i+1}...")
122
+ else: # Handle bar loops
123
+ bars = int(loop_choice.split(" ")[0])
124
+ seconds_per_beat = 60.0 / bpm_int
125
+ seconds_per_bar = seconds_per_beat * 4
126
+ loop_duration_seconds = seconds_per_bar * bars
127
+ loop_duration_samples = int(loop_duration_seconds * sample_rate)
128
+
129
+ progress(0.4, desc=f"Slicing into {bars}-bar loops...")
130
+ num_loops = len(y) // loop_duration_samples
131
+ if num_loops == 0:
132
+ gr.Warning(f"Audio is too short to create a {bars}-bar loop at {bpm_int} BPM.")
133
+ return None, None
134
+ for i in range(num_loops):
135
+ start_sample = i * loop_duration_samples
136
+ end_sample = start_sample + loop_duration_samples
137
+ slice_data = y[start_sample:end_sample]
138
+ filename = os.path.join(loops_dir, f"{bars}bar_loop_{i+1:03d}_{bpm_int}bpm.wav")
139
+ sf.write(filename, slice_data, sample_rate, subtype='PCM_16')
140
+ output_files.append(filename)
141
+ if num_loops > 1:
142
+ progress(0.4 + (i / (num_loops - 1) * 0.6), desc=f"Exporting loop {i+1}...")
143
+
144
+ if not output_files:
145
+ gr.Warning("No loops or one-shots could be generated.")
146
+ return None, None
147
+
148
+ return output_files, loops_dir
149
+
150
+ async def slice_all_and_zip_real(vocals, drums, bass, other, loop_choice, sensitivity, progress=gr.Progress(track_tqdm=True)):
151
+ progress(0, desc="Starting batch slice...")
152
+ await asyncio.sleep(0.1)
153
+ stems_to_process = {"vocals": vocals, "drums": drums, "bass": bass, "other": other}
154
+
155
+ zip_path = "Loop_Architect_Pack.zip"
156
+
157
+ num_stems = sum(1 for data in stems_to_process.values() if data is not None)
158
+ if num_stems == 0: raise gr.Error("No stems to process! Please separate stems first.")
159
+
160
+ with zipfile.ZipFile(zip_path, 'w') as zf:
161
+ processed_count = 0
162
+ for name, data in stems_to_process.items():
163
+ if data is not None:
164
+ def update_main_progress(p, desc=""):
165
+ overall_progress = (processed_count + p) / num_stems
166
+ progress(overall_progress, desc=f"Slicing {name}: {desc}")
167
+
168
+ sliced_files, temp_dir = slice_stem_real((data[0], data[1]), loop_choice, sensitivity, progress=update_main_progress)
169
+
170
+ if sliced_files:
171
+ for loop_file in sliced_files:
172
+ arcname = os.path.join(name, os.path.basename(loop_file))
173
+ zf.write(loop_file, arcname)
174
+
175
+ if temp_dir and os.path.exists(temp_dir):
176
+ shutil.rmtree(temp_dir)
177
+ processed_count += 1
178
+
179
+ progress(1, desc="Pack Ready!")
180
+ return zip_path, gr.update(visible=True)
181
 
 
 
 
 
 
182
 
183
  # --- Create the full Gradio Interface ---
184
  with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="red")) as demo:
185
  gr.Markdown("# 🎵 Loop Architect")
 
 
 
 
 
 
 
186
  with gr.Row():
187
  with gr.Column(scale=1):
188
  gr.Markdown("### 1. Separate Stems")
189
  audio_input = gr.Audio(type="filepath", label="Upload a Track")
190
  stem_options = gr.Radio(["4 Stems (Vocals, Drums, Bass, Other)", "2 Stems (Vocals + Instrumental)"], label="Separation Type", value="4 Stems (Vocals, Drums, Bass, Other)")
191
  submit_button = gr.Button("Separate Stems")
192
+ with gr.Accordion("Slicing Options", open=True):
193
+ loop_options_radio = gr.Radio(["One-Shots (All Transients)", "4 Bar Loops", "8 Bar Loops"], label="Loop Length", value="One-Shots (All Transients)")
194
+ sensitivity_slider = gr.Slider(minimum=0.01, maximum=0.5, value=0.05, step=0.01, label="One-Shot Sensitivity")
195
+ gr.Markdown("### 3. Create Pack")
196
+ slice_all_button = gr.Button("Slice All Stems & Create Pack", variant="primary")
197
+ download_zip_file = gr.File(label="Download Your Loop Pack", visible=False)
198
  with gr.Column(scale=2):
199
  with gr.Accordion("Separated Stems", open=True):
200
  with gr.Row():
201
  vocals_output = gr.Audio(label="Vocals", scale=4)
202
+ slice_vocals_btn = gr.Button("Slice", scale=1)
203
  with gr.Row():
204
  drums_output = gr.Audio(label="Drums", scale=4)
205
+ slice_drums_btn = gr.Button("Slice", scale=1)
206
  with gr.Row():
207
  bass_output = gr.Audio(label="Bass", scale=4)
208
+ slice_bass_btn = gr.Button("Slice", scale=1)
209
  with gr.Row():
210
  other_output = gr.Audio(label="Other / Instrumental", scale=4)
211
+ slice_other_btn = gr.Button("Slice", scale=1)
212
+ gr.Markdown("### Sliced Loops / Samples")
213
+ loop_gallery = gr.Gallery(label="Generated Loops", columns=8, object_fit="contain", height="auto")
214
 
215
  # --- Define Event Listeners ---
216
+ def slice_and_display(stem_data, loop_choice, sensitivity):
217
+ files, _ = slice_stem_real(stem_data, loop_choice, sensitivity)
218
+ return files
219
+
220
+ submit_button.click(fn=separate_stems, inputs=[audio_input, stem_options], outputs=[vocals_output, drums_output, bass_output, other_output])
221
+ stem_options.change(fn=update_output_visibility, inputs=[stem_options], outputs=[vocals_output, drums_output, bass_output, other_output])
222
+ slice_vocals_btn.click(fn=slice_and_display, inputs=[vocals_output, loop_options_radio, sensitivity_slider], outputs=loop_gallery)
223
+ slice_drums_btn.click(fn=slice_and_display, inputs=[drums_output, loop_options_radio, sensitivity_slider], outputs=loop_gallery)
224
+ slice_bass_btn.click(fn=slice_and_display, inputs=[bass_output, loop_options_radio, sensitivity_slider], outputs=loop_gallery)
225
+ slice_other_btn.click(fn=slice_and_display, inputs=[other_output, loop_options_radio, sensitivity_slider], outputs=loop_gallery)
226
+ slice_all_button.click(fn=slice_all_and_zip_real, inputs=[vocals_output, drums_output, bass_output, other_output, loop_options_radio, sensitivity_slider], outputs=[download_zip_file, download_zip_file])
227
+
 
 
 
 
 
 
 
 
228
  # --- Launch the UI ---
229
  demo.launch()