SaltProphet commited on
Commit
973e8b0
·
verified ·
1 Parent(s): 10bf05b

Upload 2 files

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