Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import os | |
| import shutil | |
| import zipfile | |
| import librosa | |
| import numpy as np | |
| from pydub import AudioSegment | |
| from moviepy.editor import AudioFileClip, ImageClip, CompositeVideoClip | |
| import subprocess | |
| from pathlib import Path | |
| import sys | |
| # --- Configuration --- | |
| OUTPUT_DIR = Path("nightpulse_output") | |
| TEMP_DIR = Path("temp_processing") | |
| # --- Core Logic Functions --- | |
| def analyze_and_separate(audio_file): | |
| """Phase 1: Separate 6 Stems (Drums, Bass, Guitar, Piano, Vocals, Other)""" | |
| try: | |
| if not audio_file: | |
| raise ValueError("No audio file provided.") | |
| # Cleanup | |
| if OUTPUT_DIR.exists(): shutil.rmtree(OUTPUT_DIR) | |
| if TEMP_DIR.exists(): shutil.rmtree(TEMP_DIR) | |
| (OUTPUT_DIR / "Stems").mkdir(parents=True, exist_ok=True) | |
| (OUTPUT_DIR / "Loops").mkdir(parents=True, exist_ok=True) | |
| TEMP_DIR.mkdir(parents=True, exist_ok=True) | |
| filename = Path(audio_file).stem | |
| # 1. BPM Detection | |
| print(f"Analyzing {filename}...") | |
| y, sr = librosa.load(audio_file, duration=60, mono=True) | |
| tempo, _ = librosa.beat.beat_track(y=y, sr=sr) | |
| if np.ndim(tempo) > 0: | |
| bpm = int(round(tempo[0])) | |
| else: | |
| bpm = int(round(tempo)) | |
| print(f"Detected BPM: {bpm}") | |
| # 2. Demucs Separation (Using 6-Stem Model) | |
| print("Separating stems (6-Stem Model)...") | |
| subprocess.run([ | |
| sys.executable, "-m", "demucs", "-n", "htdemucs_6s", "--out", str(TEMP_DIR), audio_file | |
| ], check=True, capture_output=True) | |
| # 3. Locate Stems | |
| demucs_out = TEMP_DIR / "htdemucs_6s" | |
| track_folder = next(demucs_out.iterdir(), None) | |
| if not track_folder: raise FileNotFoundError("Demucs failed to output files.") | |
| # Map all 6 stems | |
| drums = track_folder / "drums.wav" | |
| bass = track_folder / "bass.wav" | |
| guitar = track_folder / "guitar.wav" | |
| piano = track_folder / "piano.wav" | |
| vocals = track_folder / "vocals.wav" | |
| other = track_folder / "other.wav" | |
| # Return paths to the UI and the BPM | |
| return str(drums), str(bass), str(guitar), str(piano), str(other), str(vocals), bpm, str(track_folder) | |
| except Exception as e: | |
| raise gr.Error(f"Separation Failed: {str(e)}") | |
| def package_and_export(track_folder_str, bpm, start_offset_sec, cover_art): | |
| """Phase 2: Chop Loops, Generate Video, Zip""" | |
| try: | |
| track_folder = Path(track_folder_str) | |
| # Re-map paths | |
| stems = { | |
| "Drums": track_folder / "drums.wav", | |
| "Bass": track_folder / "bass.wav", | |
| "Guitar": track_folder / "guitar.wav", | |
| "Piano": track_folder / "piano.wav", | |
| "Synths": track_folder / "other.wav", | |
| "Vocals": track_folder / "vocals.wav" | |
| } | |
| # 1. Save Full Stems (Copy to Stems folder) | |
| for name, path in stems.items(): | |
| if path.exists(): | |
| shutil.copy(path, OUTPUT_DIR / "Stems" / f"{bpm}BPM_Full_{name}.wav") | |
| # 2. Create Loops (With User Offset) | |
| ms_per_beat = (60 / bpm) * 1000 | |
| eight_bars_ms = ms_per_beat * 4 * 8 | |
| start_ms = start_offset_sec * 1000 | |
| created_loops = {} | |
| def make_loop(src, name): | |
| if not src.exists(): return None | |
| audio = AudioSegment.from_wav(str(src)) | |
| end_ms = start_ms + eight_bars_ms | |
| if len(audio) < end_ms: | |
| s = 0 | |
| e = min(len(audio), eight_bars_ms) | |
| else: | |
| s, e = start_ms, end_ms | |
| loop = audio[s:int(e)].fade_in(15).fade_out(15).normalize() | |
| out_path = OUTPUT_DIR / "Loops" / f"{bpm}BPM_{name}.wav" | |
| loop.export(out_path, format="wav") | |
| return out_path | |
| # Generate loops for all 6 stems | |
| created_loops['melody'] = make_loop(stems['Synths'], "SynthLoop") | |
| make_loop(stems['Drums'], "DrumLoop") | |
| make_loop(stems['Bass'], "BassLoop") | |
| make_loop(stems['Guitar'], "GuitarLoop") | |
| make_loop(stems['Piano'], "PianoLoop") | |
| make_loop(stems['Vocals'], "VocalChop") | |
| # 3. Generate Video | |
| video_path = None | |
| if cover_art and created_loops['melody']: | |
| print("Rendering Dynamic Video...") | |
| vid_out = OUTPUT_DIR / "Promo_Video.mp4" | |
| audio_clip = AudioFileClip(str(created_loops['melody'])) | |
| duration = audio_clip.duration | |
| # Load and Resize Image | |
| img = ImageClip(cover_art).resize(width=1080) | |
| # Simple Zoom Animation | |
| img = img.resize(lambda t : 1 + 0.02*t) | |
| img = img.set_position(('center', 'center')) | |
| img = img.set_duration(duration) | |
| img = img.set_audio(audio_clip) | |
| final_clip = CompositeVideoClip([img], size=(1080, 1920)) | |
| final_clip.duration = duration | |
| final_clip.audio = audio_clip | |
| final_clip.fps = 24 | |
| final_clip.write_videofile(str(vid_out), codec="libx264", audio_codec="aac", logger=None) | |
| video_path = str(vid_out) | |
| # 4. Zip | |
| zip_file = "NightPulse_Pack.zip" | |
| with zipfile.ZipFile(zip_file, 'w') as zf: | |
| for root, dirs, files in os.walk(OUTPUT_DIR): | |
| for file in files: | |
| file_path = Path(root) / file | |
| arcname = file_path.relative_to(OUTPUT_DIR) | |
| zf.write(file_path, arcname) | |
| return zip_file, video_path | |
| except Exception as e: | |
| raise gr.Error(f"Packaging Failed: {str(e)}") | |
| # --- GUI (Blocks) --- | |
| with gr.Blocks(title="Night Pulse | Command Center (6-Stem)") as app: | |
| gr.Markdown("# 🎛️ Night Pulse | 6-Stem Command Center") | |
| gr.Markdown("Deconstruct audio into 6 stems: Drums, Bass, Guitar, Piano, Vocals, Synths.") | |
| # State storage | |
| stored_folder = gr.State() | |
| stored_bpm = gr.State() | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| input_audio = gr.Audio(type="filepath", label="1. Upload Master Track") | |
| input_art = gr.Image(type="filepath", label="Cover Art (9:16)") | |
| btn_analyze = gr.Button("🔍 Phase 1: Separate (6 Stems)", variant="primary") | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 2. Stem Preview") | |
| with gr.Row(): | |
| p_drums = gr.Audio(label="Drums") | |
| p_bass = gr.Audio(label="Bass") | |
| with gr.Row(): | |
| p_guitar = gr.Audio(label="Guitar") | |
| p_piano = gr.Audio(label="Piano") | |
| with gr.Row(): | |
| p_other = gr.Audio(label="Synths/Other") | |
| p_vocals = gr.Audio(label="Vocals") | |
| gr.Markdown("---") | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### 3. Loop Logic") | |
| slider_start = gr.Slider(minimum=0, maximum=120, value=15, label="Loop Start Time (Seconds)", info="Select the start point for the 8-bar loop cut.") | |
| btn_package = gr.Button("📦 Phase 2: Package & Export", variant="primary") | |
| with gr.Column(): | |
| gr.Markdown("### 4. Final Output") | |
| out_zip = gr.File(label="Download Pack (ZIP)") | |
| out_video = gr.Video(label="Promo Video") | |
| # Events | |
| btn_analyze.click( | |
| fn=analyze_and_separate, | |
| inputs=[input_audio], | |
| outputs=[p_drums, p_bass, p_guitar, p_piano, p_other, p_vocals, stored_bpm, stored_folder] | |
| ) | |
| btn_package.click( | |
| fn=package_and_export, | |
| inputs=[stored_folder, stored_bpm, slider_start, input_art], | |
| outputs=[out_zip, out_video] | |
| ) | |
| if __name__ == "__main__": | |
| app.launch() |