MusicGen-CPU / app.py
Jacky2305's picture
Update app.py
6122789 verified
Raw
History Blame Contribute Delete
7.55 kB
import io
import time
import queue
import threading
import torch
import scipy.io.wavfile as wavfile
import uvicorn
from transformers import AutoProcessor, MusicgenForConditionalGeneration, LogitsProcessor, LogitsProcessorList
import gradio as gr
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
print("[musicgen] 🚀 App starting...")
torch.set_num_threads(2)
torch.set_num_interop_threads(1)
_model = None
_processor = None
_lock = threading.Lock()
_loaded = False
_error = None
def _load():
global _model, _processor, _loaded, _error
if _loaded:
return
try:
print("[musicgen] ⏳ Loading facebook/musicgen-medium ...")
t0 = time.time()
_processor = AutoProcessor.from_pretrained("facebook/musicgen-medium")
_model = MusicgenForConditionalGeneration.from_pretrained("facebook/musicgen-medium")
_model.eval()
_loaded = True
print(f"[musicgen] ✅ Model loaded in {time.time()-t0:.1f}s")
except Exception as e:
_error = str(e)
print(f"[musicgen] ❌ Load failed: {e}")
raise
threading.Thread(target=_load, daemon=True).start()
# ── 进度追踪:每生成 10 个 token 回调一次 ──────────────────
class ProgressTracker(LogitsProcessor):
def __init__(self, max_tokens: int, log_fn):
self.max_tokens = max_tokens
self.generated = 0
self.log_fn = log_fn
self.start_time = time.time()
def __call__(self, input_ids, scores):
self.generated += 1
if self.generated % 10 == 0 or self.generated == self.max_tokens:
elapsed = time.time() - self.start_time
pct = self.generated / self.max_tokens * 100
eta = (elapsed / self.generated) * (self.max_tokens - self.generated) if self.generated > 0 else 0
self.log_fn(
f"[{pct:5.1f}%] token {self.generated:>4}/{self.max_tokens} | "
f"elapsed {elapsed:>5.0f}s | ETA ~{eta:.0f}s"
)
return scores
def _generate(prompt: str, duration: int = 8, guidance: float = 3.0, log_fn=None):
if not _loaded:
_load()
max_new_tokens = int(duration * 50)
processors = LogitsProcessorList([ProgressTracker(max_new_tokens, log_fn)]) if log_fn else None
with _lock:
inputs = _processor(text=[prompt], padding=True, return_tensors="pt")
with torch.no_grad():
audio_values = _model.generate(
**inputs,
max_new_tokens=max_new_tokens,
guidance_scale=guidance,
logits_processor=processors,
)
sr = _model.config.audio_encoder.sampling_rate
audio_np = audio_values[0, 0].numpy()
return sr, audio_np
# ── Gradio UI ────────────────────────────────────────
def get_status():
if _error:
return f"**Status:** ❌ Load failed: {_error}"
if _loaded:
return "**Status:** ✅ Model ready — you can generate now!"
return "**Status:** ⏳ Model loading, please wait..."
def ui_generate(prompt, duration, guidance):
if not prompt.strip():
raise gr.Error("Please enter a prompt.")
if not _loaded:
raise gr.Error("Model is still loading. Please retry.")
log_q = queue.Queue()
result = {}
def log_fn(msg):
log_q.put(msg)
def run():
try:
sr, audio_np = _generate(prompt, int(duration), float(guidance), log_fn=log_fn)
result["sr"] = sr
result["audio"] = audio_np
except Exception as e:
result["error"] = str(e)
finally:
log_q.put(None) # 结束信号
threading.Thread(target=run, daemon=True).start()
t0 = time.time()
max_tokens = int(duration * 50)
log_lines = [f"[ 0.0%] Starting generation — {max_tokens} tokens total ({duration}s audio)..."]
yield None, "⏳ Generating...", "\n".join(log_lines)
while True:
try:
msg = log_q.get(timeout=30)
except queue.Empty:
log_lines.append("[WARN] No response for 30s, still waiting...")
yield None, "⏳ Generating...", "\n".join(log_lines)
continue
if msg is None:
break
log_lines.append(msg)
yield None, "⏳ Generating...", "\n".join(log_lines)
if "error" in result:
raise gr.Error(result["error"])
elapsed = time.time() - t0
log_lines.append(f"[100.0%] ✅ Done in {elapsed:.1f}s")
yield (result["sr"], result["audio"]), f"✅ Done in {elapsed:.1f}s", "\n".join(log_lines)
with gr.Blocks(title="MusicGen") as demo:
gr.Markdown("# 🎵 MusicGen — Text to Music")
status = gr.Markdown(get_status())
prompt = gr.Textbox(label="Prompt", placeholder="upbeat pop song with electric guitar")
with gr.Row():
dur = gr.Slider(2, 30, value=8, step=1, label="Duration (s)")
guide = gr.Slider(1.0, 5.0, value=3.0, step=0.1, label="Guidance")
btn = gr.Button("🎵 Generate", variant="primary")
audio = gr.Audio(label="Result", type="numpy")
msg = gr.Markdown("")
log_box = gr.Textbox(label="📋 Generation Log", lines=12, interactive=False, max_lines=20)
btn.click(ui_generate, [prompt, dur, guide], [audio, msg, log_box])
timer = gr.Timer(5)
timer.tick(get_status, outputs=status)
# ── 关键修复:自己建 FastAPI app,把自定义路由和 Gradio 挂到同一个 app 上 ──
# 之前直接用 demo.app 注册路由 + demo.launch() 起服务,
# 两者不是同一个 FastAPI 实例,导致 /health /generate 全部 404。
# 现在改为:自建 app → 注册自定义路由 → mount_gradio_app 挂 Gradio UI →
# uvicorn.run(app) 统一对外提供服务。
app = FastAPI()
class GenerateRequest(BaseModel):
prompt: str
duration: int = 8
guidance: float = 3.0
@app.get("/health")
def health():
return {"status": "ready" if _loaded else "loading", "error": _error}
@app.post("/generate")
def generate(req: GenerateRequest):
if not req.prompt.strip():
raise HTTPException(status_code=400, detail="prompt is required")
if not _loaded:
raise HTTPException(status_code=503, detail="Model still loading, retry later")
try:
sr, audio_np = _generate(req.prompt, req.duration, req.guidance)
buf = io.BytesIO()
wavfile.write(buf, sr, audio_np)
buf.seek(0)
return StreamingResponse(buf, media_type="audio/wav",
headers={"Content-Disposition": "attachment; filename=output.wav"})
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# CORS:允许你的网站域名跨域调用 /generate /health
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 如需限制改成你的网站域名,如 ["https://yoursite.com"]
allow_methods=["GET", "POST"],
allow_headers=["Content-Type"],
)
# 把 Gradio UI 挂载到根路径 "/",和上面的自定义路由共用同一个 app 实例
app = gr.mount_gradio_app(app, demo, path="/")
if __name__ == "__main__":
# 不再用 demo.launch(),改为直接用 uvicorn 运行合并后的 app,
# 确保 /health /generate 和 Gradio UI 是同一个进程里的同一个 app。
uvicorn.run(app, host="0.0.0.0", port=7860)