Spaces:
Running
Running
| 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 | |
| import subprocess | |
| from pathlib import Path | |
| import sys | |
| # --- Configuration --- | |
| OUTPUT_DIR = Path("nightpulse_output") | |
| TEMP_DIR = Path("temp_processing") | |
| def process_track(audio_file, cover_art_image): | |
| # Initialize return variables | |
| zip_path = None | |
| video_path = None | |
| try: | |
| # --- 0. Input Validation --- | |
| if not audio_file: | |
| raise ValueError("No audio file provided.") | |
| # --- 1. Setup Directories --- | |
| if OUTPUT_DIR.exists(): | |
| shutil.rmtree(OUTPUT_DIR) | |
| if TEMP_DIR.exists(): | |
| shutil.rmtree(TEMP_DIR) | |
| OUTPUT_DIR.mkdir(parents=True, exist_ok=True) | |
| TEMP_DIR.mkdir(parents=True, exist_ok=True) | |
| filename = Path(audio_file).stem | |
| # --- 2. Analyze BPM & Key (Librosa) --- | |
| print(f"Analyzing {filename}...") | |
| try: | |
| # Mono load for robust BPM detection | |
| y, sr = librosa.load(audio_file, duration=60, mono=True) | |
| tempo, _ = librosa.beat.beat_track(y=y, sr=sr) | |
| # Handle different librosa return types (float vs array) | |
| if np.ndim(tempo) > 0: | |
| detected_bpm = int(round(tempo[0])) | |
| else: | |
| detected_bpm = int(round(tempo)) | |
| print(f"Detected BPM: {detected_bpm}") | |
| except Exception as e: | |
| print(f"BPM Warning: {e}") | |
| detected_bpm = 120 # Safe Fallback | |
| # --- 3. AI Stem Separation (Demucs) --- | |
| print("Separating stems...") | |
| try: | |
| # Call Demucs via subprocess to ensure clean execution | |
| subprocess.run([ | |
| sys.executable, "-m", "demucs", | |
| "-n", "htdemucs", | |
| "--out", str(TEMP_DIR), | |
| audio_file | |
| ], check=True, capture_output=True) | |
| except subprocess.CalledProcessError as e: | |
| raise RuntimeError(f"Demucs failed: {e.stderr.decode()}") | |
| # Locate Stems (Robust Search) | |
| demucs_out = TEMP_DIR / "htdemucs" | |
| track_folder = next(demucs_out.iterdir(), None) | |
| if not track_folder: | |
| raise FileNotFoundError("Demucs output folder missing.") | |
| drums_path = track_folder / "drums.wav" | |
| melody_path = track_folder / "other.wav" | |
| bass_path = track_folder / "bass.wav" | |
| # --- 4. Loop Logic --- | |
| if detected_bpm <= 0: detected_bpm = 120 | |
| ms_per_beat = (60 / detected_bpm) * 1000 | |
| eight_bars_ms = ms_per_beat * 4 * 8 | |
| def create_loop(source_path, output_name): | |
| if not source_path.exists(): return None, None | |
| audio = AudioSegment.from_wav(str(source_path)) | |
| # Grab middle 8 bars | |
| start = len(audio) // 3 | |
| end = start + eight_bars_ms | |
| if len(audio) < end: | |
| start = 0 | |
| end = min(len(audio), eight_bars_ms) | |
| loop = audio[start:int(end)].fade_in(15).fade_out(15).normalize() | |
| out_name = OUTPUT_DIR / f"{detected_bpm}BPM_{output_name}.wav" | |
| loop.export(out_name, format="wav") | |
| return out_name, loop | |
| loop_drums, _ = create_loop(drums_path, "DrumLoop") | |
| loop_melody, _ = create_loop(melody_path, "MelodyLoop") | |
| create_loop(bass_path, "BassLoop") | |
| # --- 5. Video Generation --- | |
| if cover_art_image and loop_melody: | |
| print("Rendering Video...") | |
| try: | |
| vid_out = OUTPUT_DIR / "Promo_Video.mp4" | |
| audio_clip = AudioFileClip(str(loop_melody)) | |
| img_clip = ImageClip(cover_art_image) | |
| # Resize to 1080w (maintain aspect ratio) | |
| img_clip = img_clip.resize(width=1080) | |
| img_clip = img_clip.set_duration(audio_clip.duration) | |
| img_clip = img_clip.set_audio(audio_clip) | |
| img_clip.fps = 24 | |
| img_clip.write_videofile(str(vid_out), codec="libx264", audio_codec="aac", logger=None) | |
| video_path = str(vid_out) | |
| except Exception as e: | |
| print(f"Video skipped: {e}") | |
| # --- 6. Zip Export --- | |
| zip_file = "NightPulse_Pack.zip" | |
| with zipfile.ZipFile(zip_file, 'w') as zf: | |
| for f in OUTPUT_DIR.iterdir(): | |
| zf.write(f, f.name) | |
| zip_path = zip_file | |
| return zip_path, video_path | |
| except Exception as e: | |
| raise gr.Error(f"System Error: {str(e)}") | |
| # --- UI Definition (Corrected) --- | |
| iface = gr.Interface( | |
| fn=process_track, | |
| inputs=[ | |
| gr.Audio(type="filepath", label="Upload Suno Track"), | |
| gr.Image(type="filepath", label="Upload Cover Art") | |
| ], | |
| outputs=[ | |
| gr.File(label="Download ZIP"), | |
| gr.Video(label="Preview Video") | |
| ], | |
| title="Night Pulse Audio | Automator", | |
| description="Night Pulse Pipeline v1.1 (Stable)" | |
| ) | |
| if __name__ == "__main__": | |
| iface.launch() |