blackboxanalytics commited on
Commit
b05b1c1
·
1 Parent(s): d897a04

Fix HF Space build: drop audiocraft, transformers-native MusicGen, fix launch() args

Browse files
Files changed (6) hide show
  1. .gitignore +5 -0
  2. README.md +1 -1
  3. app.py +38 -10
  4. continue_music.py +94 -31
  5. packages.txt +0 -7
  6. requirements.txt +7 -9
.gitignore CHANGED
@@ -19,3 +19,8 @@ flagged/
19
  *.safetensors
20
  .DS_Store
21
  Thumbs.db
 
 
 
 
 
 
19
  *.safetensors
20
  .DS_Store
21
  Thumbs.db
22
+
23
+ # business files that do NOT belong in a public hackathon Space
24
+ *.pdf
25
+ umbra-ai-architecture.md
26
+ memory-audit-*.md
README.md CHANGED
@@ -5,7 +5,7 @@ colorFrom: yellow
5
  colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.16.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
  license: mit
 
5
  colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.16.0
8
+ python_version: '3.10'
9
  app_file: app.py
10
  pinned: false
11
  license: mit
app.py CHANGED
@@ -1,6 +1,8 @@
1
  import gradio as gr
2
  import os
3
  import tempfile
 
 
4
  from analyze import fingerprint, find_key, get_tempo, get_duration
5
 
6
  try:
@@ -157,6 +159,7 @@ def analyze_track(audio):
157
  make_stat_html("tempo", "---"),
158
  make_stat_html("duration", "---"),
159
  gr.update(visible=False),
 
160
  None
161
  )
162
 
@@ -165,14 +168,36 @@ def analyze_track(audio):
165
  key_html = make_stat_html("key", info["key"])
166
  bpm_html = make_stat_html("tempo", str(info["bpm"]) + " bpm")
167
  dur_html = make_stat_html("duration", str(info["duration"]) + "s")
168
- return (key_html, bpm_html, dur_html, gr.update(visible=True), audio)
169
  except Exception as e:
170
  err = make_stat_html("error", str(e)[:50])
171
- return (err, make_stat_html("tempo", "---"), make_stat_html("duration", "---"), gr.update(visible=False), None)
172
 
173
 
174
- def placeholder_continue(audio_path):
175
- return "continuation coming soon. MusicGen pipeline lands on Day 2."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
 
178
  HEADER_HTML = (
@@ -191,9 +216,10 @@ FOOTER_HTML = (
191
  )
192
 
193
 
194
- with gr.Blocks(title="CODA") as app:
195
 
196
  current_track = gr.State(None)
 
197
 
198
  gr.HTML(HEADER_HTML)
199
 
@@ -214,22 +240,24 @@ with gr.Blocks(title="CODA") as app:
214
  with gr.Group(elem_classes="continue-panel", visible=False) as continue_section:
215
  gr.HTML('<div class="tape-deco" style="margin-bottom:12px;">continue</div>')
216
  continue_btn = gr.Button("continue this track", variant="primary")
 
217
  continue_output = gr.Textbox(label="status", interactive=False, lines=2)
218
 
219
  audio_input.change(
220
  fn=analyze_track,
221
  inputs=[audio_input],
222
- outputs=[key_display, bpm_display, dur_display, continue_section, current_track]
223
  )
224
 
225
  continue_btn.click(
226
- fn=placeholder_continue,
227
- inputs=[current_track],
228
- outputs=[continue_output]
229
  )
230
 
231
  gr.HTML(FOOTER_HTML)
232
 
233
 
234
  if __name__ == "__main__":
235
- app.launch(css=CUSTOM_CSS, theme=gr.themes.Base())
 
 
1
  import gradio as gr
2
  import os
3
  import tempfile
4
+ import numpy as np
5
+ import soundfile as sf
6
  from analyze import fingerprint, find_key, get_tempo, get_duration
7
 
8
  try:
 
159
  make_stat_html("tempo", "---"),
160
  make_stat_html("duration", "---"),
161
  gr.update(visible=False),
162
+ None,
163
  None
164
  )
165
 
 
168
  key_html = make_stat_html("key", info["key"])
169
  bpm_html = make_stat_html("tempo", str(info["bpm"]) + " bpm")
170
  dur_html = make_stat_html("duration", str(info["duration"]) + "s")
171
+ return (key_html, bpm_html, dur_html, gr.update(visible=True), audio, info)
172
  except Exception as e:
173
  err = make_stat_html("error", str(e)[:50])
174
+ return (err, make_stat_html("tempo", "---"), make_stat_html("duration", "---"), gr.update(visible=False), None, None)
175
 
176
 
177
+ @spaces.GPU(duration=180)
178
+ def run_continuation(audio_path, info):
179
+ if not audio_path:
180
+ return None, "upload a track first."
181
+
182
+ try:
183
+ # lazy import so the UI boots fast and torch only loads on demand
184
+ import librosa
185
+ from continue_music import continue_track, stitch_with_crossfade
186
+
187
+ key = info.get("key") if info else None
188
+ bpm = info.get("bpm") if info else None
189
+
190
+ continuation, sr = continue_track(audio_path, key=key, bpm=bpm)
191
+ original, _ = librosa.load(audio_path, sr=sr, mono=True)
192
+ full = stitch_with_crossfade(original, continuation, sr)
193
+
194
+ out_path = os.path.join(tempfile.mkdtemp(), "coda_continuation.wav")
195
+ sf.write(out_path, full, sr)
196
+
197
+ added = len(continuation) / sr
198
+ return out_path, f"added {added:.1f}s. seam crossfaded at {len(original)/sr:.1f}s."
199
+ except Exception as e:
200
+ return None, f"continuation failed: {str(e)[:200]}"
201
 
202
 
203
  HEADER_HTML = (
 
216
  )
217
 
218
 
219
+ with gr.Blocks(title="CODA", css=CUSTOM_CSS, theme=gr.themes.Base()) as app:
220
 
221
  current_track = gr.State(None)
222
+ track_info = gr.State(None)
223
 
224
  gr.HTML(HEADER_HTML)
225
 
 
240
  with gr.Group(elem_classes="continue-panel", visible=False) as continue_section:
241
  gr.HTML('<div class="tape-deco" style="margin-bottom:12px;">continue</div>')
242
  continue_btn = gr.Button("continue this track", variant="primary")
243
+ result_audio = gr.Audio(label="finished track", type="filepath", interactive=False)
244
  continue_output = gr.Textbox(label="status", interactive=False, lines=2)
245
 
246
  audio_input.change(
247
  fn=analyze_track,
248
  inputs=[audio_input],
249
+ outputs=[key_display, bpm_display, dur_display, continue_section, current_track, track_info]
250
  )
251
 
252
  continue_btn.click(
253
+ fn=run_continuation,
254
+ inputs=[current_track, track_info],
255
+ outputs=[result_audio, continue_output]
256
  )
257
 
258
  gr.HTML(FOOTER_HTML)
259
 
260
 
261
  if __name__ == "__main__":
262
+ app.launch()
263
+
continue_music.py CHANGED
@@ -1,56 +1,119 @@
 
1
  import torch
2
  import torchaudio
3
- from audiocraft.models import MusicGen
4
 
5
 
 
 
 
 
 
 
 
 
 
6
  _model = None
 
7
 
8
 
9
  def _load_model():
10
- global _model
11
  if _model is None:
12
- _model = MusicGen.get_pretrained('facebook/musicgen-large')
13
- return _model
14
-
 
 
 
 
 
15
 
16
- def continue_track(path, prompt_duration=10, gen_duration=15, key=None, bpm=None):
17
- """
18
- takes the last `prompt_duration` seconds of the input track
19
- and generates `gen_duration` seconds of continuation.
20
- key and bpm are hints for the text prompt.
21
- """
22
- model = _load_model()
23
- model.set_generation_params(duration=gen_duration)
24
 
 
 
25
  track, sr = torchaudio.load(path)
26
 
27
- # grab the tail end as context
28
- tail_samples = int(prompt_duration * sr)
 
 
 
 
 
29
  if track.shape[1] > tail_samples:
30
- tail = track[:, -tail_samples:]
31
- else:
32
- tail = track
33
 
34
- # resample to 32kHz if needed (musicgen expects this)
35
- if sr != 32000:
36
- resampler = torchaudio.transforms.Resample(sr, 32000)
37
- tail = resampler(tail)
38
 
39
- # mono
40
- if tail.shape[0] > 1:
41
- tail = tail.mean(dim=0, keepdim=True)
 
 
 
 
42
 
43
- tail = tail.unsqueeze(0) # batch dim
44
 
45
- # build a natural description
46
  desc = "continue this song"
47
  if key and bpm:
48
- desc = f"continue this song in {key} at {bpm} bpm"
49
  elif key:
50
  desc = f"continue this song in {key}"
51
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  with torch.no_grad():
53
- output = model.generate_continuation(tail, 32000, [desc])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- result = output[0].cpu()
56
- return result, 32000
 
1
+ import numpy as np
2
  import torch
3
  import torchaudio
4
+ from transformers import AutoProcessor, MusicgenForConditionalGeneration
5
 
6
 
7
+ # transformers-native MusicGen. the audiocraft package is abandoned and
8
+ # hard-pins torch==2.1.0 / xformers<0.0.23, which breaks the HF Space build
9
+ # against gradio 6.x. transformers supports audio-prompted continuation
10
+ # directly, so we don't need audiocraft at all.
11
+
12
+ MODEL_ID = "facebook/musicgen-large"
13
+ MUSICGEN_SR = 32000
14
+ FRAME_RATE = 50 # musicgen decoder tokens per second
15
+
16
  _model = None
17
+ _processor = None
18
 
19
 
20
  def _load_model():
21
+ global _model, _processor
22
  if _model is None:
23
+ _processor = AutoProcessor.from_pretrained(MODEL_ID)
24
+ device = "cuda" if torch.cuda.is_available() else "cpu"
25
+ dtype = torch.float16 if device == "cuda" else torch.float32
26
+ _model = MusicgenForConditionalGeneration.from_pretrained(
27
+ MODEL_ID, torch_dtype=dtype
28
+ ).to(device)
29
+ _model.eval()
30
+ return _model, _processor
31
 
 
 
 
 
 
 
 
 
32
 
33
+ def _load_tail(path, prompt_duration):
34
+ """load audio, return mono 32kHz numpy tail of `prompt_duration` seconds."""
35
  track, sr = torchaudio.load(path)
36
 
37
+ if sr != MUSICGEN_SR:
38
+ track = torchaudio.transforms.Resample(sr, MUSICGEN_SR)(track)
39
+
40
+ if track.shape[0] > 1:
41
+ track = track.mean(dim=0, keepdim=True)
42
+
43
+ tail_samples = int(prompt_duration * MUSICGEN_SR)
44
  if track.shape[1] > tail_samples:
45
+ track = track[:, -tail_samples:]
46
+
47
+ return track.squeeze(0).numpy()
48
 
 
 
 
 
49
 
50
+ def continue_track(path, prompt_duration=10, gen_duration=15, key=None, bpm=None):
51
+ """
52
+ takes the last `prompt_duration` seconds of the input track and
53
+ generates `gen_duration` seconds of continuation.
54
+ returns (continuation_only as 1-D float32 numpy, sample_rate).
55
+ """
56
+ model, processor = _load_model()
57
 
58
+ tail = _load_tail(path, prompt_duration)
59
 
 
60
  desc = "continue this song"
61
  if key and bpm:
62
+ desc = f"continue this song in {key} at {round(bpm)} bpm"
63
  elif key:
64
  desc = f"continue this song in {key}"
65
 
66
+ inputs = processor(
67
+ audio=tail,
68
+ sampling_rate=MUSICGEN_SR,
69
+ text=[desc],
70
+ padding=True,
71
+ return_tensors="pt",
72
+ ).to(model.device)
73
+
74
+ # cast audio prompt to model dtype (fp16 on gpu)
75
+ if "input_values" in inputs:
76
+ inputs["input_values"] = inputs["input_values"].to(model.dtype)
77
+
78
  with torch.no_grad():
79
+ output = model.generate(
80
+ **inputs,
81
+ do_sample=True,
82
+ guidance_scale=3.0,
83
+ max_new_tokens=int(gen_duration * FRAME_RATE),
84
+ )
85
+
86
+ audio = output[0, 0].float().cpu().numpy()
87
+
88
+ # generate_continuation-style output contains the prompt audio at the
89
+ # start; trim it so we return only the new material.
90
+ if audio.shape[0] > tail.shape[0]:
91
+ audio = audio[tail.shape[0]:]
92
+
93
+ return audio, MUSICGEN_SR
94
+
95
+
96
+ def stitch_with_crossfade(original, continuation, sr, fade_seconds=0.5):
97
+ """
98
+ join original track and continuation with an equal-power crossfade
99
+ so the seam doesn't click. both inputs 1-D numpy at the same sr.
100
+ """
101
+ fade = int(fade_seconds * sr)
102
+ fade = min(fade, len(original), len(continuation))
103
+ if fade <= 0:
104
+ return np.concatenate([original, continuation])
105
+
106
+ t = np.linspace(0.0, np.pi / 2, fade, dtype=np.float32)
107
+ fade_out = np.cos(t)
108
+ fade_in = np.sin(t)
109
+
110
+ head = original[:-fade]
111
+ seam = original[-fade:] * fade_out + continuation[:fade] * fade_in
112
+ rest = continuation[fade:]
113
+
114
+ out = np.concatenate([head, seam, rest]).astype(np.float32)
115
+ peak = np.abs(out).max()
116
+ if peak > 1.0:
117
+ out = out / peak
118
+ return out
119
 
 
 
packages.txt CHANGED
@@ -1,8 +1 @@
1
  ffmpeg
2
- libavformat-dev
3
- libavcodec-dev
4
- libavdevice-dev
5
- libavutil-dev
6
- libavfilter-dev
7
- libswscale-dev
8
- libswresample-dev
 
1
  ffmpeg
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,13 +1,11 @@
1
- gradio>=5.0.0
2
- torch>=2.1.0
3
- torchaudio>=2.1.0
 
 
4
  librosa>=0.10.2
5
- numpy>=1.24.0
6
  soundfile>=0.12.1
7
  Pillow>=10.0.0
8
- transformers>=4.51.0
9
- accelerate>=0.26.0
10
  spaces
11
- av
12
- audiocraft
13
- demucs
 
1
+ torch>=2.4.0
2
+ torchaudio>=2.4.0
3
+ transformers>=4.51.0,<5
4
+ accelerate>=0.26.0
5
+ sentencepiece
6
  librosa>=0.10.2
7
+ numpy>=1.24.0,<2.0
8
  soundfile>=0.12.1
9
  Pillow>=10.0.0
10
+ demucs>=4.0.1
 
11
  spaces