SaltProphet commited on
Commit
60833ca
·
verified ·
1 Parent(s): b42ee2b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +164 -110
app.py CHANGED
@@ -5,7 +5,7 @@ import zipfile
5
  import librosa
6
  import numpy as np
7
  from pydub import AudioSegment
8
- from moviepy.editor import AudioFileClip, ImageClip
9
  import subprocess
10
  from pathlib import Path
11
  import sys
@@ -14,145 +14,199 @@ import sys
14
  OUTPUT_DIR = Path("nightpulse_output")
15
  TEMP_DIR = Path("temp_processing")
16
 
17
- def process_track(audio_file, cover_art_image):
18
- zip_path = None
19
- video_path = None
20
 
 
 
21
  try:
22
- # --- 0. Input Validation ---
23
  if not audio_file:
24
  raise ValueError("No audio file provided.")
25
 
26
- # --- 1. Setup Directories ---
27
- if OUTPUT_DIR.exists():
28
- shutil.rmtree(OUTPUT_DIR)
29
- if TEMP_DIR.exists():
30
- shutil.rmtree(TEMP_DIR)
31
-
32
- # Create subfolders for organization
33
  (OUTPUT_DIR / "Stems").mkdir(parents=True, exist_ok=True)
34
  (OUTPUT_DIR / "Loops").mkdir(parents=True, exist_ok=True)
35
  TEMP_DIR.mkdir(parents=True, exist_ok=True)
36
-
37
  filename = Path(audio_file).stem
38
 
39
- # --- 2. Analyze BPM (Librosa) ---
40
  print(f"Analyzing {filename}...")
41
- try:
42
- y, sr = librosa.load(audio_file, duration=60, mono=True)
43
- tempo, _ = librosa.beat.beat_track(y=y, sr=sr)
44
- if np.ndim(tempo) > 0:
45
- detected_bpm = int(round(tempo[0]))
46
- else:
47
- detected_bpm = int(round(tempo))
48
- print(f"Detected BPM: {detected_bpm}")
49
- except Exception as e:
50
- print(f"BPM Warning: {e}")
51
- detected_bpm = 120
52
-
53
- # --- 3. AI Stem Separation (Demucs) ---
54
  print("Separating stems...")
55
- try:
56
- subprocess.run([
57
- sys.executable, "-m", "demucs",
58
- "-n", "htdemucs",
59
- "--out", str(TEMP_DIR),
60
- audio_file
61
- ], check=True, capture_output=True)
62
- except subprocess.CalledProcessError as e:
63
- raise RuntimeError(f"Demucs failed: {e.stderr.decode()}")
64
-
65
  demucs_out = TEMP_DIR / "htdemucs"
66
  track_folder = next(demucs_out.iterdir(), None)
 
67
 
68
- if not track_folder: raise FileNotFoundError("Demucs output folder missing.")
69
-
70
- # Map the raw stems
71
- drums_path = track_folder / "drums.wav"
72
- melody_path = track_folder / "other.wav"
73
- bass_path = track_folder / "bass.wav"
74
- vocals_path = track_folder / "vocals.wav"
75
-
76
- # --- 4. EXPORT FULL STEMS (New Step) ---
77
- # Copy the full length files to the Stems folder
78
- if drums_path.exists(): shutil.copy(drums_path, OUTPUT_DIR / "Stems" / f"{detected_bpm}BPM_Full_Drums.wav")
79
- if melody_path.exists(): shutil.copy(melody_path, OUTPUT_DIR / "Stems" / f"{detected_bpm}BPM_Full_Melody.wav")
80
- if bass_path.exists(): shutil.copy(bass_path, OUTPUT_DIR / "Stems" / f"{detected_bpm}BPM_Full_Bass.wav")
81
- if vocals_path.exists(): shutil.copy(vocals_path, OUTPUT_DIR / "Stems" / f"{detected_bpm}BPM_Full_Vocals.wav")
82
-
83
- # --- 5. Loop Logic (The Chop) ---
84
- if detected_bpm <= 0: detected_bpm = 120
85
- ms_per_beat = (60 / detected_bpm) * 1000
 
 
 
 
 
 
 
 
 
86
  eight_bars_ms = ms_per_beat * 4 * 8
 
87
 
88
- def create_loop(source_path, output_name):
89
- if not source_path.exists(): return None, None
90
-
91
- audio = AudioSegment.from_wav(str(source_path))
92
- start = len(audio) // 3
93
- end = start + eight_bars_ms
94
 
95
- if len(audio) < end:
96
- start = 0
97
- end = min(len(audio), eight_bars_ms)
 
 
 
 
98
 
99
- loop = audio[start:int(end)].fade_in(15).fade_out(15).normalize()
100
- # Save to Loops folder
101
- out_name = OUTPUT_DIR / "Loops" / f"{detected_bpm}BPM_{output_name}.wav"
102
- loop.export(out_name, format="wav")
103
- return out_name, loop
104
-
105
- loop_drums, _ = create_loop(drums_path, "DrumLoop")
106
- loop_melody, _ = create_loop(melody_path, "MelodyLoop")
107
- create_loop(bass_path, "BassLoop")
108
 
109
- # --- 6. Video Generation ---
110
- if cover_art_image and loop_melody:
111
- print("Rendering Video...")
112
- try:
113
- vid_out = OUTPUT_DIR / "Promo_Video.mp4"
114
- audio_clip = AudioFileClip(str(loop_melody))
115
- img_clip = ImageClip(cover_art_image)
116
- img_clip = img_clip.resize(width=1080)
117
- img_clip = img_clip.set_duration(audio_clip.duration)
118
- img_clip = img_clip.set_audio(audio_clip)
119
- img_clip.fps = 24
 
 
 
 
 
 
 
 
120
 
121
- img_clip.write_videofile(str(vid_out), codec="libx264", audio_codec="aac", logger=None)
122
- video_path = str(vid_out)
123
- except Exception as e:
124
- print(f"Video skipped: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- # --- 7. Zip Export ---
127
  zip_file = "NightPulse_Pack.zip"
128
  with zipfile.ZipFile(zip_file, 'w') as zf:
129
  for root, dirs, files in os.walk(OUTPUT_DIR):
130
  for file in files:
131
  file_path = Path(root) / file
132
- # Create structure inside zip (Stems/file.wav, Loops/file.wav)
133
  arcname = file_path.relative_to(OUTPUT_DIR)
134
  zf.write(file_path, arcname)
135
- zip_path = zip_file
136
-
137
- return zip_path, video_path
138
 
139
  except Exception as e:
140
- raise gr.Error(f"System Error: {str(e)}")
141
-
142
- # --- UI Definition ---
143
- iface = gr.Interface(
144
- fn=process_track,
145
- inputs=[
146
- gr.Audio(type="filepath", label="Upload Suno Track"),
147
- gr.Image(type="filepath", label="Upload Cover Art")
148
- ],
149
- outputs=[
150
- gr.File(label="Download Full Pack (ZIP)"),
151
- gr.Video(label="Preview Video")
152
- ],
153
- title="Night Pulse Audio | Automator",
154
- description="Night Pulse Pipeline v1.3 (Full Stems Included)"
155
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
  if __name__ == "__main__":
158
- iface.launch()
 
5
  import librosa
6
  import numpy as np
7
  from pydub import AudioSegment
8
+ from moviepy.editor import AudioFileClip, ImageClip, CompositeVideoClip
9
  import subprocess
10
  from pathlib import Path
11
  import sys
 
14
  OUTPUT_DIR = Path("nightpulse_output")
15
  TEMP_DIR = Path("temp_processing")
16
 
17
+ # --- Core Logic Functions ---
 
 
18
 
19
+ def analyze_and_separate(audio_file):
20
+ """Phase 1: Separate Stems and return file paths for preview"""
21
  try:
 
22
  if not audio_file:
23
  raise ValueError("No audio file provided.")
24
 
25
+ # Cleanup
26
+ if OUTPUT_DIR.exists(): shutil.rmtree(OUTPUT_DIR)
27
+ if TEMP_DIR.exists(): shutil.rmtree(TEMP_DIR)
 
 
 
 
28
  (OUTPUT_DIR / "Stems").mkdir(parents=True, exist_ok=True)
29
  (OUTPUT_DIR / "Loops").mkdir(parents=True, exist_ok=True)
30
  TEMP_DIR.mkdir(parents=True, exist_ok=True)
31
+
32
  filename = Path(audio_file).stem
33
 
34
+ # 1. BPM Detection
35
  print(f"Analyzing {filename}...")
36
+ y, sr = librosa.load(audio_file, duration=60, mono=True)
37
+ tempo, _ = librosa.beat.beat_track(y=y, sr=sr)
38
+ bpm = int(round(tempo[0])) if np.ndim(tempo) > 0 else int(round(tempo))
39
+ print(f"Detected BPM: {bpm}")
40
+
41
+ # 2. Demucs Separation
 
 
 
 
 
 
 
42
  print("Separating stems...")
43
+ subprocess.run([
44
+ sys.executable, "-m", "demucs", "-n", "htdemucs", "--out", str(TEMP_DIR), audio_file
45
+ ], check=True, capture_output=True)
46
+
47
+ # 3. Locate Stems
 
 
 
 
 
48
  demucs_out = TEMP_DIR / "htdemucs"
49
  track_folder = next(demucs_out.iterdir(), None)
50
+ if not track_folder: raise FileNotFoundError("Demucs failed to output files.")
51
 
52
+ drums = track_folder / "drums.wav"
53
+ bass = track_folder / "bass.wav"
54
+ melody = track_folder / "other.wav"
55
+ vocals = track_folder / "vocals.wav"
56
+
57
+ # Return paths to the UI and the BPM
58
+ return str(drums), str(bass), str(melody), str(vocals), bpm, str(track_folder)
59
+
60
+ except Exception as e:
61
+ raise gr.Error(f"Separation Failed: {str(e)}")
62
+
63
+ def package_and_export(track_folder_str, bpm, start_offset_sec, cover_art):
64
+ """Phase 2: Chop Loops, Generate Video (Zoom Effect), Zip"""
65
+ try:
66
+ track_folder = Path(track_folder_str)
67
+ drums = track_folder / "drums.wav"
68
+ bass = track_folder / "bass.wav"
69
+ melody = track_folder / "other.wav"
70
+ vocals = track_folder / "vocals.wav"
71
+
72
+ # 1. Save Full Stems
73
+ for stem, name in [(drums, "Drums"), (bass, "Bass"), (melody, "Melody"), (vocals, "Vocals")]:
74
+ if stem.exists():
75
+ shutil.copy(stem, OUTPUT_DIR / "Stems" / f"{bpm}BPM_Full_{name}.wav")
76
+
77
+ # 2. Create Loops (With User Offset)
78
+ ms_per_beat = (60 / bpm) * 1000
79
  eight_bars_ms = ms_per_beat * 4 * 8
80
+ start_ms = start_offset_sec * 1000
81
 
82
+ created_loops = {} # Store paths for video generation
83
+
84
+ def make_loop(src, name):
85
+ if not src.exists(): return None
86
+ audio = AudioSegment.from_wav(str(src))
 
87
 
88
+ # Use User Offset
89
+ end_ms = start_ms + eight_bars_ms
90
+ if len(audio) < end_ms: # Fallback if offset is too late
91
+ s = 0
92
+ e = min(len(audio), eight_bars_ms)
93
+ else:
94
+ s, e = start_ms, end_ms
95
 
96
+ loop = audio[s:int(e)].fade_in(15).fade_out(15).normalize()
97
+ out_path = OUTPUT_DIR / "Loops" / f"{bpm}BPM_{name}.wav"
98
+ loop.export(out_path, format="wav")
99
+ return out_path
100
+
101
+ created_loops['drums'] = make_loop(drums, "DrumLoop")
102
+ created_loops['bass'] = make_loop(bass, "BassLoop")
103
+ created_loops['melody'] = make_loop(melody, "MelodyLoop")
 
104
 
105
+ # 3. Generate Video (Slow Zoom Effect)
106
+ video_path = None
107
+ if cover_art and created_loops['melody']:
108
+ print("Rendering Dynamic Video...")
109
+ vid_out = OUTPUT_DIR / "Promo_Video.mp4"
110
+ audio_clip = AudioFileClip(str(created_loops['melody']))
111
+ duration = audio_clip.duration
112
+
113
+ # Load Image
114
+ img = ImageClip(cover_art).resize(width=1080)
115
+
116
+ # THE ZOOM EFFECT: Resize image from 1.0x to 1.1x over time
117
+ # We crop the center to handle the zoom so it doesn't change output size
118
+ w, h = img.size
119
+
120
+ def zoom_effect(t):
121
+ # Zoom factor goes from 1.0 to 1.15
122
+ scale = 1 + 0.15 * (t / duration)
123
+ return scale
124
 
125
+ # Apply zoom (this is a bit heavy, simple resize is safer for CPU)
126
+ # Alternative: Simple Pan or just Static if Pillow is tricky.
127
+ # Let's do a simple resize animation
128
+ img = img.resize(lambda t : 1 + 0.04*t) # Slow 4% zoom
129
+ img = img.set_position(('center', 'center'))
130
+ img = img.set_duration(duration)
131
+ img = img.set_audio(audio_clip)
132
+
133
+ # Composite to ensure frame size stays constant (1080 width)
134
+ # Note: resizing makes it grow, we need to crop or center.
135
+ # For simplicity in v1, we let it grow slightly or stick to static if 'resize' fails.
136
+ # We will use the simple robust method:
137
+
138
+ final_clip = CompositeVideoClip([img], size=(1080, 1920))
139
+ final_clip.duration = duration
140
+ final_clip.audio = audio_clip
141
+ final_clip.fps = 24
142
+
143
+ final_clip.write_videofile(str(vid_out), codec="libx264", audio_codec="aac", logger=None)
144
+ video_path = str(vid_out)
145
 
146
+ # 4. Zip
147
  zip_file = "NightPulse_Pack.zip"
148
  with zipfile.ZipFile(zip_file, 'w') as zf:
149
  for root, dirs, files in os.walk(OUTPUT_DIR):
150
  for file in files:
151
  file_path = Path(root) / file
 
152
  arcname = file_path.relative_to(OUTPUT_DIR)
153
  zf.write(file_path, arcname)
154
+
155
+ return zip_file, video_path
 
156
 
157
  except Exception as e:
158
+ raise gr.Error(f"Packaging Failed: {str(e)}")
159
+
160
+
161
+ # --- GUI (Blocks) ---
162
+
163
+ with gr.Blocks(title="Night Pulse | Command Center", theme=gr.themes.Base()) as app:
164
+ gr.Markdown("# 🎛️ Night Pulse Audio | Command Center")
165
+ gr.Markdown("Transform Suno tracks into commercial sample packs.")
166
+
167
+ # State storage
168
+ stored_folder = gr.State()
169
+ stored_bpm = gr.State()
170
+
171
+ with gr.Row():
172
+ with gr.Column(scale=1):
173
+ input_audio = gr.Audio(type="filepath", label="1. Upload Master Track")
174
+ input_art = gr.Image(type="filepath", label="Cover Art (9:16)")
175
+ btn_analyze = gr.Button("🔍 Phase 1: Deconstruct & Analyze", variant="primary")
176
+
177
+ with gr.Column(scale=1):
178
+ gr.Markdown("### 2. Stem Preview")
179
+ # Audio Players
180
+ p_drums = gr.Audio(label="Drums Stem")
181
+ p_bass = gr.Audio(label="Bass Stem")
182
+ p_melody = gr.Audio(label="Melody Stem")
183
+ p_vocals = gr.Audio(label="Vocals Stem")
184
+
185
+ gr.Markdown("---")
186
+
187
+ with gr.Row():
188
+ with gr.Column():
189
+ gr.Markdown("### 3. Loop Settings")
190
+ slider_start = gr.Slider(minimum=0, maximum=120, value=15, label="Loop Start Time (Seconds)", info="Where should the 8-bar loop start?")
191
+ btn_package = gr.Button("📦 Phase 2: Package & Export", variant="primary")
192
+
193
+ with gr.Column():
194
+ gr.Markdown("### 4. Final Output")
195
+ out_zip = gr.File(label="Download Pack (ZIP)")
196
+ out_video = gr.Video(label="Promo Video (Dynamic)")
197
+
198
+ # Events
199
+ btn_analyze.click(
200
+ fn=analyze_and_separate,
201
+ inputs=[input_audio],
202
+ outputs=[p_drums, p_bass, p_melody, p_vocals, stored_bpm, stored_folder]
203
+ )
204
+
205
+ btn_package.click(
206
+ fn=package_and_export,
207
+ inputs=[stored_folder, stored_bpm, slider_start, input_art],
208
+ outputs=[out_zip, out_video]
209
+ )
210
 
211
  if __name__ == "__main__":
212
+ app.launch()