SaltProphet commited on
Commit
6f33f17
·
verified ·
1 Parent(s): aedcd4f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +53 -82
app.py CHANGED
@@ -11,26 +11,20 @@ from pathlib import Path
11
  import sys
12
 
13
  # --- Configuration ---
14
- # We use Path objects for robust cross-platform compatibility
15
  OUTPUT_DIR = Path("nightpulse_output")
16
  TEMP_DIR = Path("temp_processing")
17
 
18
  def process_track(audio_file, cover_art_image):
19
- """
20
- Main pipeline function.
21
- Returns: (zip_path, video_path)
22
- """
23
- # Initialize return variables to None to prevent 'UnboundLocalError'
24
  zip_path = None
25
  video_path = None
26
 
27
  try:
28
- # --- 0. Input Validation (Robustness Check) ---
29
  if not audio_file:
30
- raise ValueError("No audio file provided. Please upload a track.")
31
 
32
  # --- 1. Setup Directories ---
33
- # Clean previous runs to prevent file mixing
34
  if OUTPUT_DIR.exists():
35
  shutil.rmtree(OUTPUT_DIR)
36
  if TEMP_DIR.exists():
@@ -43,25 +37,24 @@ def process_track(audio_file, cover_art_image):
43
  # --- 2. Analyze BPM & Key (Librosa) ---
44
  print(f"Analyzing {filename}...")
45
  try:
46
- # Load 60s for better context, mono=True for BPM analysis
47
  y, sr = librosa.load(audio_file, duration=60, mono=True)
48
  tempo, _ = librosa.beat.beat_track(y=y, sr=sr)
49
 
50
- # Robustness: Handle different librosa versions returning array vs float
51
  if np.ndim(tempo) > 0:
52
  detected_bpm = int(round(tempo[0]))
53
  else:
54
  detected_bpm = int(round(tempo))
55
-
56
  print(f"Detected BPM: {detected_bpm}")
57
  except Exception as e:
58
- print(f"BPM Detection Warning: {e}")
59
  detected_bpm = 120 # Safe Fallback
60
 
61
  # --- 3. AI Stem Separation (Demucs) ---
62
- print("Separating stems with Demucs...")
63
  try:
64
- # We call demucs as a module using sys.executable to ensure we use the correct python environment
65
  subprocess.run([
66
  sys.executable, "-m", "demucs",
67
  "-n", "htdemucs",
@@ -69,111 +62,89 @@ def process_track(audio_file, cover_art_image):
69
  audio_file
70
  ], check=True, capture_output=True)
71
  except subprocess.CalledProcessError as e:
72
- # Capture the specific error from the subprocess
73
- raise RuntimeError(f"Demucs processing failed. Error: {e.stderr.decode()}")
74
 
75
- # Locate separated stems (Robust Path Finding)
76
  demucs_out = TEMP_DIR / "htdemucs"
77
- # Demucs might normalize filenames (spaces -> underscores), so we just find the first folder
78
  track_folder = next(demucs_out.iterdir(), None)
79
 
80
  if not track_folder:
81
- raise FileNotFoundError("Demucs output folder could not be found.")
82
 
83
  drums_path = track_folder / "drums.wav"
84
  melody_path = track_folder / "other.wav"
85
  bass_path = track_folder / "bass.wav"
86
-
87
- if not drums_path.exists():
88
- raise FileNotFoundError(f"Stems were not generated in {track_folder}")
89
 
90
- # --- 4. Loop Logic (Pydub) ---
91
- # Calculate duration of 8 bars in milliseconds
92
  if detected_bpm <= 0: detected_bpm = 120
93
  ms_per_beat = (60 / detected_bpm) * 1000
94
  eight_bars_ms = ms_per_beat * 4 * 8
95
 
96
  def create_loop(source_path, output_name):
97
- if not source_path.exists():
98
- return None, None
99
-
100
- audio = AudioSegment.from_wav(str(source_path))
101
 
102
- # Smart Chop: Grab the "middle" 8 bars to avoid intro/outro silence
103
- start_time = len(audio) // 3
104
- end_time = start_time + eight_bars_ms
 
105
 
106
- # Safety check if audio is shorter than 8 bars
107
- if len(audio) < end_time:
108
- start_time = 0
109
- end_time = min(len(audio), eight_bars_ms)
110
 
111
- loop = audio[start_time:end_time]
112
- # 15ms fade to prevent clicks
113
- loop = loop.fade_in(15).fade_out(15).normalize()
114
-
115
- out_filename = f"{detected_bpm}BPM_{output_name}.wav"
116
- out_file = OUTPUT_DIR / out_filename
117
- loop.export(out_file, format="wav")
118
- return out_file, loop
119
 
120
- # Generate Loops
121
- loop_drums_path, _ = create_loop(drums_path, "DrumLoop")
122
- loop_melody_path, melody_audio = create_loop(melody_path, "MelodyLoop")
123
  create_loop(bass_path, "BassLoop")
124
 
125
- # --- 5. Video Generation (MoviePy) ---
126
- # Logic: Only generate video if User uploaded Art AND we successfully made a melody loop
127
- if cover_art_image is not None and loop_melody_path is not None:
128
- print("Rendering Promo Video...")
129
  try:
130
- video_out_path = OUTPUT_DIR / "Promo_Video_Reel.mp4"
131
-
132
- audio_clip = AudioFileClip(str(loop_melody_path))
133
- image_clip = ImageClip(cover_art_image)
134
-
135
- # Resize logic: Fit to width 1080 (standard), maintain aspect ratio
136
- image_clip = image_clip.resize(width=1080)
137
 
138
- # Set duration to match audio loop
139
- image_clip = image_clip.set_duration(audio_clip.duration)
140
- image_clip = image_clip.set_audio(audio_clip)
141
- image_clip.fps = 24
 
142
 
143
- image_clip.write_videofile(str(video_out_path), codec="libx264", audio_codec="aac", logger=None)
144
- video_path = str(video_out_path)
145
  except Exception as e:
146
- print(f"Video generation skipped due to error: {e}")
147
- # We don't fail the whole pipeline here, we just skip the video part
148
- video_path = None
149
-
150
- # --- 6. Zip It Up ---
151
- zip_file_path = "NightPulse_Pack.zip"
152
- with zipfile.ZipFile(zip_file_path, 'w') as zipf:
153
- for file in OUTPUT_DIR.iterdir():
154
- zipf.write(file, file.name)
155
- zip_path = zip_file_path
156
 
157
  return zip_path, video_path
158
 
159
  except Exception as e:
160
- # This catches ANY crash and shows it in the UI as a red Error box
161
  raise gr.Error(f"System Error: {str(e)}")
162
 
163
- # --- UI Definition ---
164
  iface = gr.Interface(
165
  fn=process_track,
166
  inputs=[
167
- gr.Audio(type="filepath", label="Upload Suno Track (MP3/WAV)"),
168
- gr.Image(type="filepath", label="Upload Cover Art (Optional)")
169
  ],
170
  outputs=[
171
- gr.File(label="Download Completed Pack (ZIP)"),
172
- gr.Video(label="Preview Promo Video")
173
  ],
174
  title="Night Pulse Audio | Automator",
175
- description="<b>Night Pulse Pipeline v1.0</b><br>Upload a Suno track to automatically separate stems, normalize, chop loops, and generate a promo video.",
176
- theme="default"
177
  )
178
 
179
  if __name__ == "__main__":
 
11
  import sys
12
 
13
  # --- Configuration ---
 
14
  OUTPUT_DIR = Path("nightpulse_output")
15
  TEMP_DIR = Path("temp_processing")
16
 
17
  def process_track(audio_file, cover_art_image):
18
+ # Initialize return variables
 
 
 
 
19
  zip_path = None
20
  video_path = None
21
 
22
  try:
23
+ # --- 0. Input Validation ---
24
  if not audio_file:
25
+ raise ValueError("No audio file provided.")
26
 
27
  # --- 1. Setup Directories ---
 
28
  if OUTPUT_DIR.exists():
29
  shutil.rmtree(OUTPUT_DIR)
30
  if TEMP_DIR.exists():
 
37
  # --- 2. Analyze BPM & Key (Librosa) ---
38
  print(f"Analyzing {filename}...")
39
  try:
40
+ # Mono load for robust BPM detection
41
  y, sr = librosa.load(audio_file, duration=60, mono=True)
42
  tempo, _ = librosa.beat.beat_track(y=y, sr=sr)
43
 
44
+ # Handle different librosa return types (float vs array)
45
  if np.ndim(tempo) > 0:
46
  detected_bpm = int(round(tempo[0]))
47
  else:
48
  detected_bpm = int(round(tempo))
 
49
  print(f"Detected BPM: {detected_bpm}")
50
  except Exception as e:
51
+ print(f"BPM Warning: {e}")
52
  detected_bpm = 120 # Safe Fallback
53
 
54
  # --- 3. AI Stem Separation (Demucs) ---
55
+ print("Separating stems...")
56
  try:
57
+ # Call Demucs via subprocess to ensure clean execution
58
  subprocess.run([
59
  sys.executable, "-m", "demucs",
60
  "-n", "htdemucs",
 
62
  audio_file
63
  ], check=True, capture_output=True)
64
  except subprocess.CalledProcessError as e:
65
+ raise RuntimeError(f"Demucs failed: {e.stderr.decode()}")
 
66
 
67
+ # Locate Stems (Robust Search)
68
  demucs_out = TEMP_DIR / "htdemucs"
 
69
  track_folder = next(demucs_out.iterdir(), None)
70
 
71
  if not track_folder:
72
+ raise FileNotFoundError("Demucs output folder missing.")
73
 
74
  drums_path = track_folder / "drums.wav"
75
  melody_path = track_folder / "other.wav"
76
  bass_path = track_folder / "bass.wav"
 
 
 
77
 
78
+ # --- 4. Loop Logic ---
 
79
  if detected_bpm <= 0: detected_bpm = 120
80
  ms_per_beat = (60 / detected_bpm) * 1000
81
  eight_bars_ms = ms_per_beat * 4 * 8
82
 
83
  def create_loop(source_path, output_name):
84
+ if not source_path.exists(): return None, None
 
 
 
85
 
86
+ audio = AudioSegment.from_wav(str(source_path))
87
+ # Grab middle 8 bars
88
+ start = len(audio) // 3
89
+ end = start + eight_bars_ms
90
 
91
+ if len(audio) < end:
92
+ start = 0
93
+ end = min(len(audio), eight_bars_ms)
 
94
 
95
+ loop = audio[start:int(end)].fade_in(15).fade_out(15).normalize()
96
+ out_name = OUTPUT_DIR / f"{detected_bpm}BPM_{output_name}.wav"
97
+ loop.export(out_name, format="wav")
98
+ return out_name, loop
 
 
 
 
99
 
100
+ loop_drums, _ = create_loop(drums_path, "DrumLoop")
101
+ loop_melody, _ = create_loop(melody_path, "MelodyLoop")
 
102
  create_loop(bass_path, "BassLoop")
103
 
104
+ # --- 5. Video Generation ---
105
+ if cover_art_image and loop_melody:
106
+ print("Rendering Video...")
 
107
  try:
108
+ vid_out = OUTPUT_DIR / "Promo_Video.mp4"
109
+ audio_clip = AudioFileClip(str(loop_melody))
110
+ img_clip = ImageClip(cover_art_image)
 
 
 
 
111
 
112
+ # Resize to 1080w (maintain aspect ratio)
113
+ img_clip = img_clip.resize(width=1080)
114
+ img_clip = img_clip.set_duration(audio_clip.duration)
115
+ img_clip = img_clip.set_audio(audio_clip)
116
+ img_clip.fps = 24
117
 
118
+ img_clip.write_videofile(str(vid_out), codec="libx264", audio_codec="aac", logger=None)
119
+ video_path = str(vid_out)
120
  except Exception as e:
121
+ print(f"Video skipped: {e}")
122
+
123
+ # --- 6. Zip Export ---
124
+ zip_file = "NightPulse_Pack.zip"
125
+ with zipfile.ZipFile(zip_file, 'w') as zf:
126
+ for f in OUTPUT_DIR.iterdir():
127
+ zf.write(f, f.name)
128
+ zip_path = zip_file
 
 
129
 
130
  return zip_path, video_path
131
 
132
  except Exception as e:
 
133
  raise gr.Error(f"System Error: {str(e)}")
134
 
135
+ # --- UI Definition (Corrected) ---
136
  iface = gr.Interface(
137
  fn=process_track,
138
  inputs=[
139
+ gr.Audio(type="filepath", label="Upload Suno Track"),
140
+ gr.Image(type="filepath", label="Upload Cover Art")
141
  ],
142
  outputs=[
143
+ gr.File(label="Download ZIP"),
144
+ gr.Video(label="Preview Video")
145
  ],
146
  title="Night Pulse Audio | Automator",
147
+ description="Night Pulse Pipeline v1.1 (Stable)"
 
148
  )
149
 
150
  if __name__ == "__main__":