MuseForge / app.py
mukesh8252's picture
Fix ZeroGPU SDK duration multiplier
5f10469
Raw History Blame Contribute Delete
9.66 kB
"""MuseForge HeartMuLa ZeroGPU Test — standalone real model check.
Hugging Face ZeroGPU -> HeartMuLa 3B -> HeartCodec -> real Hindi MP3.
Flow:
1. Space starts on CPU: checks disk, downloads the 3 public model repos
only when their marker files are missing, then builds the heartlib
pipeline with lazy_load=True (tokenizer + config only). NO GPU
allocation and NO model weights in VRAM at startup.
2. Clicking "Generate MP3" calls the ZeroGPU-decorated function -> ZeroGPU
allocates a GPU -> heartlib lazily loads HeartMuLa weights onto it,
generates up to the requested audio length, decodes with HeartCodec
and saves an MP3. heartlib's own _unload() (del + gc + empty_cache)
frees VRAM after the forward pass and after codec decoding, before
the function returns.
Model repos (all public, Apache-2.0):
- HeartMuLa/HeartMuLaGen -> ckpt/gen_config.json + ckpt/tokenizer.json
- HeartMuLa/HeartMuLa-oss-3B -> ckpt/HeartMuLa-oss-3B/
- HeartMuLa/HeartCodec-oss-20260123 -> ckpt/HeartCodec-oss/
(the bare "HeartCodec-oss" repo is gated/not downloadable — 401 on the
Hub API; the official heartlib README recommends the -20260123 codec,
which is public. Swap HEARTMULA_REPO to the happy-new-year variant by
changing one constant if desired.)
"""
import gc
import os
import shutil
import threading
import time
import traceback
from pathlib import Path
import spaces
import gradio as gr
import torch
try:
from heartlib.pipelines.music_generation import HeartMuLaGenPipeline
except ModuleNotFoundError:
# heartlib pins transformers==4.57.0 (needs huggingface-hub<1.0), which
# conflicts with the Space runtime's gradio 6.24.0 (needs huggingface-hub
# >=1.16). Install heartlib with --no-deps so pip does not enforce its
# incompatible pins; all real runtime deps are pinned in requirements.txt.
import subprocess
import sys
print("[startup] installing heartlib (--no-deps, pinned commit)...", flush=True)
subprocess.check_call(
[
sys.executable,
"-m",
"pip",
"install",
"--no-deps",
"heartlib @ git+https://github.com/HeartMuLa/heartlib.git@3783bdb8441f2c298b1e64c8651173aac200361c",
]
)
from heartlib.pipelines.music_generation import HeartMuLaGenPipeline
APP_DIR = Path("/home/user/app")
CKPT_DIR = APP_DIR / "ckpt"
VERIFY_DIR = APP_DIR / "verify"
HEARTMULA_REPO = "HeartMuLa/HeartMuLa-oss-3B"
CODEC_REPO = "HeartMuLa/HeartCodec-oss-20260123"
GEN_CONFIG_REPO = "HeartMuLa/HeartMuLaGen"
MODEL_VERSION = "3B"
DEFAULT_LYRICS = """[verse]
रात के सन्नाटे में
एक नई सुबह की आहट है
गिरकर भी मैं उठता हूँ
दिल में अब भी चाहत है
[chorus]
मैं फिर उठूँगा
मैं फिर चलूँगा
हज़ार बार गिरकर
फिर संभलूँगा
[verse]
रास्ते चाहे मुश्किल हों
आँधियाँ चाहे तेज़ चलें
मेरे कदम नहीं रुकेंगे
जब तक सपने साथ चलें
[chorus]
मैं फिर उठूँगा
मैं फिर चलूँगा
हज़ार बार गिरकर
फिर संभलूँगा"""
DEFAULT_TAGS = "hindi, cinematic, emotional, powerful male vocal, warm vocal, piano, strings, atmospheric, melodic"
GEN_LOCK = threading.Lock()
PIPE = None
# ---------------------------------------------------------------- startup (CPU)
def _download_if_missing(repo_id: str, local_dir: Path, marker: Path) -> None:
if marker.exists():
print(f"[startup] {repo_id}: already present ({marker.name})", flush=True)
return
from huggingface_hub import snapshot_download
print(f"[startup] downloading {repo_id} -> {local_dir} ...", flush=True)
snapshot_download(repo_id=repo_id, local_dir=str(local_dir))
if not marker.exists():
raise RuntimeError(f"{repo_id} download finished but {marker} is missing")
def _ensure_models() -> None:
total, used, free = shutil.disk_usage(APP_DIR)
print(f"[startup] disk: {free / 1024**3:.1f} GB free (models need ~22.4 GB)", flush=True)
if free < 24 * 1024**3:
raise RuntimeError(
f"Insufficient disk: {free / 1024**3:.1f} GB free, need >= 24 GB"
)
_download_if_missing(GEN_CONFIG_REPO, CKPT_DIR, CKPT_DIR / "gen_config.json")
_download_if_missing(
HEARTMULA_REPO,
CKPT_DIR / "HeartMuLa-oss-3B",
CKPT_DIR / "HeartMuLa-oss-3B" / "model.safetensors.index.json",
)
_download_if_missing(
CODEC_REPO,
CKPT_DIR / "HeartCodec-oss",
CKPT_DIR / "HeartCodec-oss" / "model.safetensors.index.json",
)
print(f"[startup] PyTorch {torch.__version__} (CUDA emulation outside the ZeroGPU decorator)", flush=True)
_ensure_models()
print("[startup] creating heartlib pipeline with lazy_load=True (no GPU used yet)...", flush=True)
PIPE = HeartMuLaGenPipeline.from_pretrained(
str(CKPT_DIR),
device=torch.device("cuda"),
dtype={"mula": torch.bfloat16, "codec": torch.float32},
version=MODEL_VERSION,
lazy_load=True,
)
print("[startup] pipeline ready. HeartMuLa/HeartCodec weights load on first generation.", flush=True)
# ---------------------------------------------------------------- GPU function
def _allocation_duration(lyrics: str, tags: str, duration_sec: float) -> int:
"""Declared ZeroGPU runtime ceiling (quota is billed on actual runtime).
The spaces SDK multiplies this value by its duration_factor (1.5 on
zero-a10g -> RTX PRO 6000 Blackwell) before requesting ZeroGPU, so cap
at 196 so the SDK-equivalent request is 196 * 1.5 = 294s, below the
300s free quota (300s requests are rejected).
"""
return min(196, 120 + int(duration_sec) * 2)
@spaces.GPU(duration=_allocation_duration)
def generate_music(lyrics: str, tags: str, duration_sec: float):
"""Runs inside a real ZeroGPU allocation. Returns (mp3_path, info_text)."""
started = time.time()
current_stage = "unknown"
def stage(name: str) -> None:
print(f"[generation] {name} (+{time.time() - started:.1f}s)", flush=True)
try:
props = torch.cuda.get_device_properties(0)
info = [
f"GPU: {props.name}",
f"VRAM total: {props.total_memory / 1024**3:.2f} GB",
f"CUDA: {torch.version.cuda}",
f"PyTorch: {torch.__version__}",
]
VERIFY_DIR.mkdir(parents=True, exist_ok=True)
save_path = str(VERIFY_DIR / "hindi_heartmula_test.mp3")
if os.path.exists(save_path):
os.remove(save_path)
max_ms = int(max(10, min(int(duration_sec), 240)) * 1000)
current_stage = "loading model"
stage("Loading model (HeartMuLa weights -> VRAM)")
t0 = time.time()
_ = PIPE.mula # heartlib lazy weight load
info.append(f"model load: {time.time() - t0:.1f}s")
current_stage = "generating"
stage("Generating (HeartMuLa forward)")
t0 = time.time()
PIPE(
{"lyrics": lyrics, "tags": tags},
max_audio_length_ms=max_ms,
topk=50,
temperature=1.0,
cfg_scale=1.5,
save_path=save_path,
)
info.append(f"generation + codec decode: {time.time() - t0:.1f}s")
current_stage = "processing audio"
stage("Processing audio (MP3 saved)")
if not os.path.isfile(save_path) or os.path.getsize(save_path) < 10_000:
raise RuntimeError("MP3 output missing or too small (silence/corrupt audio?)")
info.append(f"peak VRAM used: {torch.cuda.max_memory_allocated() / 1024**3:.2f} GB")
info.append(f"output: {save_path}")
info.append(f"total time: {time.time() - started:.1f}s")
info.append("STATUS: SUCCESS")
stage("Completed")
return save_path, "\n".join(info)
except Exception as exc:
traceback.print_exc()
try:
mem = (
f"\nCUDA allocated: {torch.cuda.memory_allocated() / 1024**3:.2f} GB, "
f"reserved: {torch.cuda.memory_reserved() / 1024**3:.2f} GB"
)
except Exception:
mem = ""
return "", (
"STATUS: FAILED\n"
f"stage: {current_stage}\n"
f"error type: {type(exc).__name__}\n"
f"error: {exc}{mem}"
)
finally:
gc.collect()
torch.cuda.empty_cache()
def generate_clicked(lyrics: str, tags: str, duration_sec: float):
with GEN_LOCK: # one generation at a time, ever
return generate_music(lyrics or DEFAULT_LYRICS, tags or DEFAULT_TAGS, int(duration_sec or 40))
# ---------------------------------------------------------------- Gradio UI
with gr.Blocks(title="MuseForge HeartMuLa ZeroGPU Test") as demo:
gr.Markdown("# MuseForge HeartMuLa ZeroGPU Test")
lyrics = gr.Textbox(label="Lyrics", value=DEFAULT_LYRICS, lines=14)
tags = gr.Textbox(label="Music tags", value=DEFAULT_TAGS, lines=2)
duration = gr.Slider(label="Duration (seconds)", minimum=10, maximum=120, value=40, step=5)
generate_btn = gr.Button("Generate MP3", variant="primary")
audio = gr.Audio(label="Generated MP3", type="filepath")
info = gr.Textbox(label="GPU status / result", lines=14)
generate_btn.click(generate_clicked, inputs=[lyrics, tags, duration], outputs=[audio, info])
demo.launch()