Spaces:
Running
Running
| 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 | |
| def health(): | |
| return {"status": "ready" if _loaded else "loading", "error": _error} | |
| 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) |