Spaces:
Paused
Paused
| """ | |
| MusePlayer Backend Server | |
| ========================= | |
| Self-hosted ACE-Step 1.5 music generation API on Hugging Face Spaces. | |
| Deploy this as a Gradio Space. It serves both a Gradio UI and a REST API. | |
| Requirements (requirements.txt): | |
| ------------------------------ | |
| torch>=2.5.0 | |
| diffusers>=0.38.0 | |
| transformers>=4.45.0 | |
| accelerate>=0.34.0 | |
| soundfile>=0.12.1 | |
| numpy>=1.26.0 | |
| gradio>=5.0.0 | |
| fastapi>=0.115.0 | |
| uvicorn>=0.30.0 | |
| pydantic>=2.9.0 | |
| requests>=2.32.0 | |
| pillow>=10.0.0 | |
| """ | |
| import os | |
| import io | |
| import uuid | |
| import json | |
| import base64 | |
| import time | |
| import threading | |
| from datetime import datetime | |
| from typing import Optional, List, Dict, Any | |
| from dataclasses import dataclass, field, asdict | |
| from queue import Queue | |
| import torch | |
| import soundfile as sf | |
| import numpy as np | |
| from pydantic import BaseModel, Field | |
| from fastapi import FastAPI, HTTPException, BackgroundTasks, Query | |
| from fastapi.responses import StreamingResponse, JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| import gradio as gr | |
| # ------------------------------------------------------------------ | |
| # Configuration | |
| # ------------------------------------------------------------------ | |
| MODEL_ID = os.getenv("ACESTEP_MODEL", "ACE-Step/Ace-Step1.5") | |
| DEVICE = os.getenv("ACESTEP_DEVICE", "cuda" if torch.cuda.is_available() else "cpu") | |
| TORCH_DTYPE = torch.bfloat16 if DEVICE == "cuda" else torch.float32 | |
| MAX_QUEUE = int(os.getenv("MAX_QUEUE", 5)) | |
| OUTPUT_DIR = os.getenv("OUTPUT_DIR", "/tmp/museplayer_outputs") | |
| os.makedirs(OUTPUT_DIR, exist_ok=True) | |
| # ------------------------------------------------------------------ | |
| # Global state | |
| # ------------------------------------------------------------------ | |
| pipe = None | |
| generation_queue = Queue(maxsize=MAX_QUEUE) | |
| results_store: Dict[str, Dict[str, Any]] = {} | |
| user_prefs: Dict[str, Dict[str, Any]] = {} | |
| # ------------------------------------------------------------------ | |
| # Pydantic models | |
| # ------------------------------------------------------------------ | |
| class GenerateRequest(BaseModel): | |
| prompt: str = Field(..., description="Music style description") | |
| lyrics: str = Field("", description="Lyrics with [verse]/[chorus] tags") | |
| duration: float = Field(30.0, ge=5.0, le=600.0, description="Duration in seconds") | |
| bpm: Optional[int] = Field(None, ge=40, le=300, description="Beats per minute") | |
| keyscale: Optional[str] = Field(None, description="e.g. C major, A minor") | |
| timesignature: Optional[str] = Field(None, description="e.g. 4 for 4/4") | |
| vocal_language: str = Field("en", description="Lyrics language code") | |
| num_inference_steps: int = Field(8, ge=1, le=100) | |
| guidance_scale: float = Field(7.0, ge=1.0, le=20.0) | |
| seed: Optional[int] = Field(None, description="Random seed for reproducibility") | |
| user_id: str = Field("anonymous", description="User identifier for personalization") | |
| station_type: str = Field("custom", description="Station genre/mood tag") | |
| class FeedbackRequest(BaseModel): | |
| track_id: str | |
| user_id: str | |
| action: str = Field(..., pattern="^(like|dislike|skip|complete|favorite)$") | |
| station_type: Optional[str] = None | |
| class UserProfile(BaseModel): | |
| user_id: str | |
| liked_prompts: List[str] = [] | |
| disliked_prompts: List[str] = [] | |
| favorite_genres: List[str] = [] | |
| preferred_duration: float = 60.0 | |
| preferred_bpm_range: List[int] = [80, 140] | |
| preferred_keys: List[str] = [] | |
| # ------------------------------------------------------------------ | |
| # Model loading | |
| # ------------------------------------------------------------------ | |
| def load_model(): | |
| """Lazy-load the ACE-Step 1.5 pipeline.""" | |
| global pipe | |
| if pipe is not None: | |
| return pipe | |
| from diffusers import AceStepPipeline | |
| print(f"[MusePlayer] Loading ACE-Step model: {MODEL_ID} on {DEVICE} ...") | |
| pipe = AceStepPipeline.from_pretrained( | |
| MODEL_ID, | |
| torch_dtype=TORCH_DTYPE, | |
| ) | |
| pipe = pipe.to(DEVICE) | |
| if hasattr(pipe, "vae") and hasattr(pipe.vae, "enable_slicing"): | |
| pipe.vae.enable_slicing() | |
| if hasattr(pipe, "vae") and hasattr(pipe.vae, "enable_tiling"): | |
| pipe.vae.enable_tiling() | |
| print("[MusePlayer] Model loaded successfully.") | |
| return pipe | |
| # ------------------------------------------------------------------ | |
| # Music generation | |
| # ------------------------------------------------------------------ | |
| def generate_music( | |
| prompt: str, | |
| lyrics: str = "", | |
| duration: float = 30.0, | |
| bpm: Optional[int] = None, | |
| keyscale: Optional[str] = None, | |
| timesignature: Optional[str] = None, | |
| vocal_language: str = "en", | |
| num_inference_steps: int = 8, | |
| guidance_scale: float = 7.0, | |
| seed: Optional[int] = None, | |
| track_id: Optional[str] = None, | |
| ) -> str: | |
| """Generate music and return the path to the output file.""" | |
| model = load_model() | |
| generator = None | |
| if seed is not None: | |
| generator = torch.Generator(device=DEVICE).manual_seed(seed) | |
| kwargs = dict( | |
| prompt=prompt, | |
| lyrics=lyrics, | |
| audio_duration=duration, | |
| vocal_language=vocal_language, | |
| num_inference_steps=num_inference_steps, | |
| guidance_scale=guidance_scale, | |
| shift=3.0, | |
| generator=generator, | |
| ) | |
| if bpm is not None: | |
| kwargs["bpm"] = bpm | |
| if keyscale is not None: | |
| kwargs["keyscale"] = keyscale | |
| if timesignature is not None: | |
| kwargs["timesignature"] = timesignature | |
| audio = model(**kwargs).audios | |
| # audio shape: [batch, channels, samples] or [batch, samples] | |
| # Save to file | |
| tid = track_id or str(uuid.uuid4())[:8] | |
| out_path = os.path.join(OUTPUT_DIR, f"{tid}.wav") | |
| audio_np = audio[0] | |
| if hasattr(audio_np, "cpu"): | |
| audio_np = audio_np.cpu() | |
| if hasattr(audio_np, "numpy"): | |
| audio_np = audio_np.numpy() | |
| if audio_np.ndim == 1: | |
| audio_np = audio_np[np.newaxis, :] | |
| sf.write(out_path, audio_np.T, model.sample_rate) | |
| return out_path | |
| def worker_loop(): | |
| """Background thread processing generation queue.""" | |
| while True: | |
| try: | |
| job = generation_queue.get(timeout=1) | |
| except Exception: | |
| continue | |
| track_id = job["track_id"] | |
| try: | |
| start = time.time() | |
| path = generate_music(track_id=track_id, **job["params"]) | |
| elapsed = time.time() - start | |
| results_store[track_id] = { | |
| "status": "completed", | |
| "track_id": track_id, | |
| "file_path": path, | |
| "created_at": datetime.utcnow().isoformat(), | |
| "generation_time_sec": round(elapsed, 2), | |
| "params": job["params"], | |
| "station_type": job.get("station_type", "custom"), | |
| "user_id": job.get("user_id", "anonymous"), | |
| } | |
| except Exception as e: | |
| results_store[track_id] = { | |
| "status": "failed", | |
| "track_id": track_id, | |
| "error": str(e), | |
| "params": job["params"], | |
| } | |
| finally: | |
| generation_queue.task_done() | |
| # Start background worker | |
| threading.Thread(target=worker_loop, daemon=True).start() | |
| # ------------------------------------------------------------------ | |
| # Preference / Personalization engine | |
| # ------------------------------------------------------------------ | |
| PROMPT_TEMPLATES = { | |
| "lofi": "lo-fi hip hop, warm vinyl crackle, soft piano chords, dusty drums, relaxed, nostalgic, bedroom studio", | |
| "energetic": "upbeat electronic dance, driving four-on-the-floor kick, bright synth stabs, energetic, festival anthem", | |
| "chill": "ambient downtempo, airy pads, gentle acoustic guitar, warm bass, relaxed evening vibes, soft rain", | |
| "focus": "minimal instrumental, steady soft beat, warm synth pads, no vocals, productive focus, clean mix", | |
| "sleep": "slow ambient drone, deep soft pads, no percussion, gentle piano, sleep meditation, 432 Hz", | |
| "workout": "high-energy trap, aggressive 808s, fast hi-hats, powerful build-ups, gym motivation, intense", | |
| "acoustic": "intimate acoustic folk, fingerstyle guitar, warm vocals, gentle harmonica, campfire storytelling", | |
| "jazz": "smooth jazz, brushed drums, walking bass, muted trumpet, late night lounge, sophisticated", | |
| "classical": "cinematic orchestral, soaring strings, grand piano, emotional film score, epic crescendo", | |
| "synthwave": "retro synthwave, analog synth arpeggios, driving electronic beat, neon lights, 80s nostalgia", | |
| } | |
| BPM_RANGES = { | |
| "lofi": (60, 90), | |
| "energetic": (120, 140), | |
| "chill": (70, 100), | |
| "focus": (60, 90), | |
| "sleep": (40, 70), | |
| "workout": (130, 160), | |
| "acoustic": (80, 110), | |
| "jazz": (80, 120), | |
| "classical": (60, 120), | |
| "synthwave": (100, 130), | |
| } | |
| def get_user_profile(user_id: str) -> Dict[str, Any]: | |
| """Retrieve or initialize user preference profile.""" | |
| if user_id not in user_prefs: | |
| user_prefs[user_id] = { | |
| "user_id": user_id, | |
| "liked_prompts": [], | |
| "disliked_prompts": [], | |
| "favorite_genres": [], | |
| "preferred_duration": 60.0, | |
| "preferred_bpm_range": [80, 140], | |
| "preferred_keys": [], | |
| "liked_tracks": [], | |
| "listening_history": [], | |
| "created_at": datetime.utcnow().isoformat(), | |
| } | |
| return user_prefs[user_id] | |
| def update_preferences_from_feedback(user_id: str, action: str, track_meta: Dict[str, Any]): | |
| """Update user profile based on feedback.""" | |
| profile = get_user_profile(user_id) | |
| prompt = track_meta.get("params", {}).get("prompt", "") | |
| station = track_meta.get("station_type", "custom") | |
| if action in ("like", "favorite"): | |
| if prompt and prompt not in profile["liked_prompts"]: | |
| profile["liked_prompts"].append(prompt) | |
| if station not in profile["favorite_genres"]: | |
| profile["favorite_genres"].append(station) | |
| profile["liked_tracks"].append(track_meta["track_id"]) | |
| elif action == "dislike": | |
| if prompt and prompt not in profile["disliked_prompts"]: | |
| profile["disliked_prompts"].append(prompt) | |
| elif action == "complete": | |
| # Implicit positive signal | |
| pass | |
| # Keep history bounded | |
| profile["listening_history"].append({ | |
| "track_id": track_meta.get("track_id"), | |
| "action": action, | |
| "station": station, | |
| "prompt": prompt, | |
| "timestamp": datetime.utcnow().isoformat(), | |
| }) | |
| if len(profile["listening_history"]) > 200: | |
| profile["listening_history"] = profile["listening_history"][-200:] | |
| def build_personalized_prompt(user_id: str, station_type: str = "custom") -> Dict[str, Any]: | |
| """Build a generation prompt tailored to the user's taste.""" | |
| profile = get_user_profile(user_id) | |
| # Start from station template | |
| if station_type in PROMPT_TEMPLATES: | |
| base = PROMPT_TEMPLATES[station_type] | |
| bpm_low, bpm_high = BPM_RANGES[station_type] | |
| else: | |
| # Custom / blend from liked prompts | |
| if profile["liked_prompts"]: | |
| base = " ".join(profile["liked_prompts"][-3:]) | |
| bpm_low, bpm_high = 80, 140 | |
| else: | |
| base = "unique instrumental track, blended genre fusion, modern production" | |
| bpm_low, bpm_high = 80, 140 | |
| # Inject positive style keywords from liked prompts | |
| liked_keywords = [] | |
| for p in profile["liked_prompts"][-5:]: | |
| liked_keywords.extend(p.split(", ")[:3]) | |
| if liked_keywords: | |
| liked_str = ", ".join(set(liked_keywords[-6:])) | |
| base = f"{base}. Also incorporate: {liked_str}" | |
| # Avoid disliked styles | |
| for dp in profile["disliked_prompts"][-3:]: | |
| if dp: | |
| base += f". Avoid: {dp.split(',')[0]} style" | |
| # Pick BPM in preferred range | |
| bpm = int((bpm_low + bpm_high) / 2) | |
| if profile["preferred_bpm_range"]: | |
| low, high = profile["preferred_bpm_range"] | |
| bpm = int(max(low, min(high, bpm))) | |
| duration = profile.get("preferred_duration", 60.0) | |
| return { | |
| "prompt": base, | |
| "duration": duration, | |
| "bpm": bpm, | |
| "station_type": station_type, | |
| } | |
| # ------------------------------------------------------------------ | |
| # FastAPI app | |
| # ------------------------------------------------------------------ | |
| app = FastAPI(title="MusePlayer API", version="1.0.0") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| async def health(): | |
| return {"status": "ok", "model_loaded": pipe is not None, "queue_size": generation_queue.qsize()} | |
| async def list_stations(): | |
| """Return available station types with descriptions.""" | |
| return { | |
| "stations": [ | |
| {"id": k, "name": k.replace("_", " ").title(), "description": v[:80] + "..."} | |
| for k, v in PROMPT_TEMPLATES.items() | |
| ] | |
| } | |
| async def api_generate(req: GenerateRequest, background_tasks: BackgroundTasks): | |
| """Queue a music generation job.""" | |
| if generation_queue.full(): | |
| raise HTTPException(status_code=503, detail="Generation queue is full. Try again shortly.") | |
| track_id = str(uuid.uuid4())[:12] | |
| params = { | |
| "prompt": req.prompt, | |
| "lyrics": req.lyrics, | |
| "duration": req.duration, | |
| "bpm": req.bpm, | |
| "keyscale": req.keyscale, | |
| "timesignature": req.timesignature, | |
| "vocal_language": req.vocal_language, | |
| "num_inference_steps": req.num_inference_steps, | |
| "guidance_scale": req.guidance_scale, | |
| "seed": req.seed, | |
| } | |
| job = { | |
| "track_id": track_id, | |
| "params": params, | |
| "user_id": req.user_id, | |
| "station_type": req.station_type, | |
| } | |
| # Pre-register result | |
| results_store[track_id] = { | |
| "status": "queued", | |
| "track_id": track_id, | |
| "queued_at": datetime.utcnow().isoformat(), | |
| "params": params, | |
| } | |
| generation_queue.put(job) | |
| return {"track_id": track_id, "status": "queued", "estimated_wait_sec": generation_queue.qsize() * 30} | |
| async def api_generate_personalized(user_id: str = "anonymous", station_type: str = "lofi"): | |
| """Generate a track tailored to the user's taste profile.""" | |
| if generation_queue.full(): | |
| raise HTTPException(status_code=503, detail="Generation queue is full.") | |
| profile = get_user_profile(user_id) | |
| built = build_personalized_prompt(user_id, station_type) | |
| track_id = str(uuid.uuid4())[:12] | |
| params = { | |
| "prompt": built["prompt"], | |
| "lyrics": "", | |
| "duration": built["duration"], | |
| "bpm": built["bpm"], | |
| "keyscale": None, | |
| "timesignature": None, | |
| "vocal_language": "en", | |
| "num_inference_steps": 8, | |
| "guidance_scale": 7.0, | |
| "seed": None, | |
| } | |
| job = { | |
| "track_id": track_id, | |
| "params": params, | |
| "user_id": user_id, | |
| "station_type": station_type, | |
| } | |
| results_store[track_id] = { | |
| "status": "queued", | |
| "track_id": track_id, | |
| "queued_at": datetime.utcnow().isoformat(), | |
| "params": params, | |
| "personalized": True, | |
| } | |
| generation_queue.put(job) | |
| return { | |
| "track_id": track_id, | |
| "status": "queued", | |
| "prompt_used": built["prompt"], | |
| "estimated_wait_sec": generation_queue.qsize() * 30, | |
| } | |
| async def get_track(track_id: str): | |
| """Get track status or stream audio if complete.""" | |
| if track_id not in results_store: | |
| raise HTTPException(status_code=404, detail="Track not found") | |
| meta = results_store[track_id] | |
| if meta["status"] == "completed": | |
| file_path = meta["file_path"] | |
| if not os.path.exists(file_path): | |
| raise HTTPException(status_code=404, detail="Audio file missing") | |
| def iterfile(): | |
| with open(file_path, "rb") as f: | |
| yield from f | |
| return StreamingResponse( | |
| iterfile(), | |
| media_type="audio/wav", | |
| headers={"Content-Disposition": f'attachment; filename="{track_id}.wav"'}, | |
| ) | |
| return JSONResponse(content={"status": meta["status"], "track_id": track_id}) | |
| async def track_status(track_id: str): | |
| if track_id not in results_store: | |
| raise HTTPException(status_code=404, detail="Track not found") | |
| meta = results_store[track_id] | |
| return { | |
| "status": meta["status"], | |
| "track_id": track_id, | |
| "params": meta.get("params"), | |
| "generation_time_sec": meta.get("generation_time_sec"), | |
| "error": meta.get("error"), | |
| } | |
| async def track_download(track_id: str, format: str = "wav"): | |
| """Download track in requested format (wav or mp3).""" | |
| if track_id not in results_store or results_store[track_id]["status"] != "completed": | |
| raise HTTPException(status_code=404, detail="Track not available") | |
| file_path = results_store[track_id]["file_path"] | |
| if format == "mp3": | |
| # Simple wav->mp3 conversion stub (requires ffmpeg in real deploy) | |
| mp3_path = file_path.replace(".wav", ".mp3") | |
| if not os.path.exists(mp3_path): | |
| # In production, use ffmpeg: os.system(f"ffmpeg -i {file_path} -q:a 2 {mp3_path}") | |
| # For HF Space, we'll return wav as fallback | |
| format = "wav" | |
| file_path = results_store[track_id]["file_path"] | |
| else: | |
| file_path = mp3_path | |
| def iterfile(): | |
| with open(file_path, "rb") as f: | |
| yield from f | |
| return StreamingResponse( | |
| iterfile(), | |
| media_type="audio/wav" if format == "wav" else "audio/mpeg", | |
| headers={"Content-Disposition": f'attachment; filename="{track_id}.{format}"'}, | |
| ) | |
| async def api_feedback(req: FeedbackRequest): | |
| """Record user feedback for personalization.""" | |
| track_id = req.track_id | |
| if track_id not in results_store: | |
| raise HTTPException(status_code=404, detail="Track not found") | |
| track_meta = results_store[track_id] | |
| update_preferences_from_feedback(req.user_id, req.action, track_meta) | |
| # Update station type if provided | |
| if req.station_type: | |
| track_meta["station_type"] = req.station_type | |
| return {"status": "ok", "user_id": req.user_id, "action": req.action} | |
| async def get_profile(user_id: str): | |
| return get_user_profile(user_id) | |
| async def update_profile(user_id: str, updates: Dict[str, Any]): | |
| profile = get_user_profile(user_id) | |
| allowed = {"preferred_duration", "preferred_bpm_range", "preferred_keys", "favorite_genres"} | |
| for k, v in updates.items(): | |
| if k in allowed: | |
| profile[k] = v | |
| return profile | |
| async def get_history(user_id: str, limit: int = Query(20, ge=1, le=100)): | |
| profile = get_user_profile(user_id) | |
| history = profile.get("listening_history", []) | |
| return {"history": history[-limit:]} | |
| async def list_tracks(): | |
| """List all generated tracks (for admin/demo).""" | |
| tracks = [] | |
| for tid, meta in results_store.items(): | |
| tracks.append({ | |
| "track_id": tid, | |
| "status": meta["status"], | |
| "station_type": meta.get("station_type", "custom"), | |
| "user_id": meta.get("user_id", "anonymous"), | |
| "created_at": meta.get("created_at", meta.get("queued_at")), | |
| "prompt": meta.get("params", {}).get("prompt", "")[:60], | |
| }) | |
| return {"tracks": sorted(tracks, key=lambda x: x.get("created_at", ""), reverse=True)} | |
| # ------------------------------------------------------------------ | |
| # Gradio UI (for HF Space demo + manual testing) | |
| # ------------------------------------------------------------------ | |
| def gradio_generate(prompt, lyrics, duration, bpm, steps, guidance, seed): | |
| track_id = str(uuid.uuid4())[:8] | |
| out_path = generate_music( | |
| prompt=prompt, | |
| lyrics=lyrics, | |
| duration=duration, | |
| bpm=int(bpm) if bpm else None, | |
| num_inference_steps=int(steps), | |
| guidance_scale=float(guidance), | |
| seed=int(seed) if seed else None, | |
| track_id=track_id, | |
| ) | |
| return out_path, f"Track ID: {track_id}" | |
| def create_gradio_ui(): | |
| with gr.Blocks(title="MusePlayer ACE-Step Server") as demo: | |
| gr.Markdown("# π΅ MusePlayer Backend β ACE-Step 1.5") | |
| gr.Markdown("Self-hosted AI music generation. Use the REST API or test below.") | |
| with gr.Row(): | |
| with gr.Column(): | |
| prompt = gr.Textbox( | |
| label="Prompt", | |
| value="A beautiful piano piece with soft melodies and gentle rhythm", | |
| lines=2, | |
| ) | |
| lyrics = gr.Textbox( | |
| label="Lyrics (optional)", | |
| placeholder="[verse]\nSoft notes in the morning light\n[chorus]\nMusic fills the air tonight", | |
| lines=4, | |
| ) | |
| duration = gr.Slider(5, 120, value=30, step=5, label="Duration (seconds)") | |
| bpm = gr.Number(value=120, label="BPM (optional)") | |
| steps = gr.Slider(1, 50, value=8, step=1, label="Inference Steps") | |
| guidance = gr.Slider(1.0, 20.0, value=7.0, step=0.5, label="Guidance Scale") | |
| seed = gr.Number(value=None, label="Seed (optional)") | |
| btn = gr.Button("Generate Music", variant="primary") | |
| with gr.Column(): | |
| audio_out = gr.Audio(label="Generated Music", type="filepath") | |
| info_out = gr.Textbox(label="Info", interactive=False) | |
| btn.click( | |
| gradio_generate, | |
| inputs=[prompt, lyrics, duration, bpm, steps, guidance, seed], | |
| outputs=[audio_out, info_out], | |
| ) | |
| gr.Markdown(""" | |
| ### API Endpoints | |
| - `POST /generate` β Queue a track | |
| - `POST /generate-personalized` β Auto-tailored track | |
| - `GET /track/{id}` β Stream audio | |
| - `GET /track/{id}/status` β Check status | |
| - `POST /feedback` β Like / dislike / skip | |
| - `GET /profile/{user_id}` β Get user taste profile | |
| - `GET /stations` β List stations | |
| """) | |
| return demo | |
| # Mount Gradio into FastAPI so both run on the same port | |
| demo = create_gradio_ui() | |
| app = gr.mount_gradio_app(app, demo, path="/") | |
| # ------------------------------------------------------------------ | |
| # Entry point | |
| # ------------------------------------------------------------------ | |
| if __name__ == "__main__": | |
| import uvicorn | |
| # Preload model on startup if env says so | |
| if os.getenv("PRELOAD_MODEL", "1") == "1": | |
| threading.Thread(target=load_model, daemon=True).start() | |
| port = int(os.getenv("PORT", 7860)) | |
| uvicorn.run(app, host="0.0.0.0", port=port) | |