loopgen / app.py
Vansh Chugh
cleanup: remove dead MAGNeT/MBD loader code, drop redundant dbn=False, remove loopgen-repo reference copy
a80fd99
Raw
History Blame Contribute Delete
9.66 kB
try:
import spaces
except ImportError:
# keep @spaces.GPU usable as a no-op; ZeroGPU requires this exact name.
class spaces:
class GPU:
def __init__(self, func=None, duration=60):
self.func = func
def __call__(self, *args, **kwargs):
if self.func is not None:
return self.func(*args, **kwargs)
func = args[0]
return func
import sys
sys.stdout.reconfigure(line_buffering=True)
import contextlib
import gc
import os
import tempfile
import threading
import gradio as gr
import numpy as np
import torch
from huggingface_hub import hf_hub_download
from pyharp import ModelCard, build_endpoint
from beat_this.inference import File2Beats
from src.audiocraft.data.audio import audio_write
from src.audiocraft.models import MusicGen
from src.loopgen import LoopGen
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
MUSICGEN_MODEL = "facebook/musicgen-medium"
MAGNET_MODEL = "facebook/magnet-medium-30secs"
MAX_CFG = 5.0 # classifier-free guidance ceiling (default: 5.0, per paper)
MIN_CFG = 1.0 # classifier-free guidance floor, annealed down to this (default: 1.0, per paper)
RESCORE_WEIGHT = 0.5 # MAGNeT/MusicGen rescoring interpolation weight (default: 0.5, per paper)
HINT_DURATION = 30.0 # seconds of MusicGen hint audio generated for beat detection (default: 30.0, per repo)
MAX_RETRIES = 5 # beat-alignment retries before falling back to a fixed length (default: 5, per repo)
beat_checkpoint_path = None
beat_checkpoint_ready = False
beat_checkpoint_error = None
musicgen = None
loopgen = None
file2beats = None
model_lock = threading.Lock()
def download_beat_checkpoint():
"""Fetch the beat-tracking checkpoint in the background so the server can
start immediately. Reuses the already-deployed teamup-tech/beat-this
Space's copy instead of re-uploading the same file here."""
global beat_checkpoint_path, beat_checkpoint_ready, beat_checkpoint_error
try:
beat_checkpoint_path = hf_hub_download(
repo_id="teamup-tech/beat-this", filename="final0.ckpt", repo_type="space"
)
print("Beat-tracking checkpoint downloaded.")
except Exception as e:
beat_checkpoint_error = str(e)
print(f"Beat checkpoint download error: {e}")
finally:
beat_checkpoint_ready = True
threading.Thread(target=download_beat_checkpoint, daemon=True).start()
model_card = ModelCard(
name="LoopGen",
description="Generates a seamlessly loopable music clip from a text description, training-free, using MusicGen + MAGNeT. "
"Output audio is the loop played twice back-to-back, so you can hear it repeat.",
author="Davide Marincione, Giorgio Strano, Donato Crisostomi, Roberto Ribuoli, Emanuele Rodolà",
tags=["music generation", "loop", "text-to-music"],
)
def _write_temp_wav(wav, sample_rate):
"""Write a generated waveform to a fresh temp file and return its path.
Adapted from pipelines.py, which reused hardcoded local paths like
f"musicgen_base_{name}_{seed}.wav" across calls (reused via
os.path.exists) -- only valid for one person rerunning experiments
locally, since concurrent Space users could collide on the same
seed/name and clobber or read each other's files."""
fd, stem = tempfile.mkstemp()
os.close(fd)
os.remove(stem) # audio_write creates its own file at stem + extension
path = audio_write(stem, wav, sample_rate, loudness_compressor=True,
strategy="loudness", loudness_headroom_db=16)
return str(path)
def _beat_aligned_length(path, min_length, max_length, unit_size=0.02, length_retries=2):
"""Find a loop length, in 20ms units, aligned to a whole number of bars,
within [min_length, max_length] seconds. Adapted from pipelines.py's
get_beat_perfect_hint, unchanged except for taking an already-constructed
file2beats (construction moved to process_fn, see its docstring)."""
min_units = min_length // unit_size
max_units = max_length // unit_size
beats, downbeats = file2beats(path)
diffs = []
prev = None
beats_per_bar = []
beats_per_this_bar = 0
downbeat_idx = 0
for beat in beats:
while downbeat_idx < len(downbeats) and beat > downbeats[downbeat_idx]:
beats_per_bar.append(beats_per_this_bar)
beats_per_this_bar = 0
downbeat_idx += 1
beats_per_this_bar += 1
if prev is not None:
diffs.append(beat - prev)
prev = beat
if len(beats_per_bar) < 1:
return None
diffs = sorted(diffs)
beats_per_bar = sorted(beats_per_bar)
beat_length = diffs[len(diffs) // 2] // unit_size + 1 # quantize the median to unit_size
beats_per_bar = beats_per_bar[len(beats_per_bar) // 2] # median beats-per-bar
bar_length = beats_per_bar * beat_length
length = bar_length * 12
if length <= 0:
return None
loops_done = 0
while length < min_units or length > max_units:
if length < min_units:
length *= 2
else:
length /= 2
loops_done += 1
if loops_done > length_retries:
return None
return length
@spaces.GPU(duration=300)
@torch.inference_mode()
def process_fn(description: str, min_length: float, max_length: float, seed: int) -> str:
"""Generates a seamlessly loopable music clip: a MusicGen hint is used to
find a beat-aligned loop length (retrying up to MAX_RETRIES times), then
MAGNeT generates the final loop as a continuation of that hint. Adapted
from pipelines.py's loopgen_loop.
Model construction happens here, inside the GPU call, rather than in the
background-download thread above: LoopGen/MusicGen.get_pretrained()
bakes device="cuda" in immediately, and ZeroGPU only allows CUDA calls
made inside an @spaces.GPU-decorated call, not from a background thread."""
global musicgen, loopgen, file2beats
if not beat_checkpoint_ready:
raise gr.Error("Beat-tracking checkpoint is still downloading, please try again shortly.")
if beat_checkpoint_error is not None:
raise gr.Error(f"Beat-tracking checkpoint failed to download: {beat_checkpoint_error}")
with model_lock:
if musicgen is None:
musicgen = MusicGen.get_pretrained(MUSICGEN_MODEL, device=DEVICE)
if loopgen is None:
loopgen = LoopGen.get_pretrained(MAGNET_MODEL, device=DEVICE)
if file2beats is None:
file2beats = File2Beats(checkpoint_path=beat_checkpoint_path, device=DEVICE)
torch.manual_seed(seed)
np.random.seed(seed)
unit_length = None
hint = None
sr = None
tries = 0
while unit_length is None:
gc.collect()
if DEVICE == "cuda":
torch.cuda.empty_cache()
if tries >= MAX_RETRIES:
unit_length = 350 * 3
break
del hint
tries += 1
musicgen.set_generation_params(duration=HINT_DURATION)
hint = musicgen.generate([description])[0].to(device="cpu", dtype=torch.float32)
sr = musicgen.sample_rate
hint_path = _write_temp_wav(hint, sr)
unit_length = _beat_aligned_length(hint_path, min_length, max_length)
os.remove(hint_path)
gc.collect()
if DEVICE == "cuda":
torch.cuda.empty_cache()
left_hint = [hint[..., :int(0.02 * sr) * int(unit_length / 2)]]
valid_tokens = [int(unit_length)]
setup = {
"span_arrangement": "stride1",
"use_sampling": True,
"top_k": 0,
"top_p": 0.9,
"temperature": 3.0,
"max_cfg_coef": MAX_CFG,
"min_cfg_coef": MIN_CFG,
"decoding_steps": [100, 50, 10, 10],
"rescorer": musicgen.lm,
"rescore_weights": RESCORE_WEIGHT,
"offset": False,
}
loopgen.set_generation_params(**setup)
autocast_ctx = (torch.autocast(device_type="cuda", dtype=torch.float16)
if DEVICE == "cuda" else contextlib.nullcontext())
with autocast_ctx:
results = loopgen.generate_continuation(
text_prompt=description, negative_text_prompt=None,
left_hint=left_hint, left_hint_sr=sr, valid_tokens=valid_tokens,
)
result = results[0].to(device="cpu", dtype=torch.float32)
max_duration = int(loopgen.duration * loopgen.frame_rate)
if valid_tokens[0] == max_duration:
result = torch.cat([result, result], -1)
return _write_temp_wav(result, loopgen.sample_rate)
with gr.Blocks() as demo:
input_components = [
gr.Textbox(label="Description",
info="Text description of the music to generate."),
gr.Slider(minimum=5, maximum=30, step=1, value=15, label="Min Loop Length (s)",
info="Shortest acceptable loop length in seconds (default: 15, per repo)."),
gr.Slider(minimum=5, maximum=30, step=1, value=30, label="Max Loop Length (s)",
info="Longest acceptable loop length in seconds (default: 30, per repo)."),
gr.Number(value=42, precision=0, label="Seed",
info="Random seed for generation (default: 42, per repo)."),
]
output_components = [
gr.Audio(type="filepath", label="Output Audio").set_info("Generated loopable music clip."),
]
build_endpoint(
model_card=model_card,
input_components=input_components,
output_components=output_components,
process_fn=process_fn,
)
if __name__ == "__main__":
demo.queue().launch(pwa=True)