Spaces:
Runtime error
Runtime error
Download app.py from callernode/songsgen: direct link, hf CLI and curl.
- Browser
- Download file 14.2 kB
-
https://huggingface.co/spaces/callernode/songsgen/resolve/main/app.py
- Command line
-
hf download hf://spaces/callernode/songsgen/app.py
-
curl -L -o app.py https://huggingface.co/spaces/callernode/songsgen/resolve/main/app.py
14.2 kB
| """ | |
| Hindi Song Studio — Hugging Face Space | |
| Sarvam AI writes original Hindi lyrics -> ACE-Step v1.5 sings them -> | |
| optional Sarvam Bulbul v3 spoken intro is placed before the song. | |
| Deploy by duplicating the official ACE-Step/Ace-Step-v1.5 Space (it ships the | |
| `acestep` package and all dependencies) and replacing its app.py with this file. | |
| """ | |
| import base64 | |
| import io | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import tempfile | |
| import uuid | |
| current_dir = os.path.dirname(os.path.abspath(__file__)) | |
| nano_vllm_path = os.path.join(current_dir, "acestep", "third_parts", "nano-vllm") | |
| if os.path.exists(nano_vllm_path): | |
| sys.path.insert(0, nano_vllm_path) | |
| os.environ["GRADIO_ANALYTICS_ENABLED"] = "False" | |
| # `spaces` must be imported before torch for ZeroGPU interception | |
| try: | |
| import spaces | |
| HAS_SPACES = True | |
| except ImportError: | |
| HAS_SPACES = False | |
| import gradio as gr | |
| import numpy as np | |
| import requests | |
| import soundfile as sf | |
| import torch | |
| from acestep.handler import AceStepHandler | |
| from acestep.inference import GenerationConfig, GenerationParams, generate_music | |
| from acestep.llm_inference import LLMHandler | |
| # --------------------------------------------------------------------------- | |
| # Config | |
| # --------------------------------------------------------------------------- | |
| SARVAM_API_KEY = os.environ.get("SARVAM_API_KEY", "") | |
| SARVAM_CHAT_MODEL = os.environ.get("SARVAM_CHAT_MODEL", "sarvam-105b") | |
| SARVAM_CHAT_URL = "https://api.sarvam.ai/v2/chat/completions" | |
| SARVAM_TTS_URL = "https://api.sarvam.ai/text-to-speech" | |
| DIT_MODEL = os.environ.get("DIT_MODEL", "acestep-v15-turbo") | |
| LM_MODEL = os.environ.get("LM_MODEL", "acestep-5Hz-lm-1.7B") | |
| IS_HF_SPACE = os.environ.get("SPACE_ID") is not None | |
| SAMPLE_RATE = 48000 | |
| OUT_DIR = os.path.join(tempfile.gettempdir(), "hindi_song_outputs") | |
| os.makedirs(OUT_DIR, exist_ok=True) | |
| BULBUL_SPEAKERS = [ | |
| "shubh", "aditya", "rahul", "rohan", "amit", "kabir", "varun", "manan", | |
| "ritu", "priya", "neha", "pooja", "simran", "kavya", "ishita", "shreya", | |
| "tanya", "shruti", "suhani", "roopa", | |
| ] | |
| GENRES = { | |
| "Bollywood romantic ballad": "bollywood romantic ballad, soft piano, strings, acoustic guitar, emotional male vocals, slow tempo", | |
| "Sad / heartbreak": "sad hindi song, melancholic, piano, violin, soft pads, emotional vocals, slow", | |
| "Sufi": "sufi devotional, harmonium, tabla, dholak, handclaps, soulful powerful vocals", | |
| "Punjabi bhangra pop": "punjabi pop, bhangra, dhol, tumbi, energetic, festive, upbeat", | |
| "Indie / lo-fi": "hindi indie pop, lo-fi, mellow electric guitar, soft drums, warm vocals, chill", | |
| "Bhajan / devotional": "hindi bhajan, devotional, harmonium, tabla, bells, peaceful, chorus vocals", | |
| "Dance / item number": "bollywood dance number, high energy, electronic beats, dhol, catchy hook, female vocals", | |
| "Hip-hop / desi rap": "desi hip hop, hindi rap, boom bap drums, deep bass, confident male rap vocals", | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Sarvam: lyrics | |
| # --------------------------------------------------------------------------- | |
| LYRICS_SYSTEM_PROMPT = """You are a Hindi songwriter. Write ORIGINAL song lyrics only. | |
| Rules: | |
| - Never reproduce, continue, translate or closely paraphrase any existing film song, | |
| album song or poem, even if the user names one or quotes a line. If asked for an | |
| existing song, write a new original song on the same theme instead. | |
| - Write the lyrics in Hindi (Devanagari script). Simple, singable, rhyming lines. | |
| - Use structure tags on their own lines: [Intro], [Verse 1], [Chorus], [Verse 2], | |
| [Chorus], [Bridge], [Chorus], [Outro]. Keep each section 2-6 lines. | |
| - Total length suitable for a 2-3 minute song. | |
| Return ONLY a JSON object, no markdown, with keys: | |
| "title": short Hindi song title, | |
| "lyrics": the full lyrics with tags and \\n line breaks, | |
| "spoken_intro": one or two short spoken Hindi lines (shayari style) that set the mood, | |
| "bpm": integer tempo that suits the song. | |
| """ | |
| def _extract_json(text: str) -> dict: | |
| text = text.strip() | |
| text = re.sub(r"^```(?:json)?|```$", "", text, flags=re.MULTILINE).strip() | |
| start, end = text.find("{"), text.rfind("}") | |
| if start == -1 or end == -1: | |
| raise ValueError("Model did not return JSON") | |
| return json.loads(text[start:end + 1]) | |
| def write_lyrics(idea: str, genre: str, mood: str): | |
| if not SARVAM_API_KEY: | |
| raise gr.Error("SARVAM_API_KEY secret is not set in the Space settings.") | |
| if not idea.strip(): | |
| raise gr.Error("Write a song idea first.") | |
| user_msg = f"Song idea: {idea}\nGenre: {genre}\nMood: {mood or 'as fits the idea'}" | |
| resp = requests.post( | |
| SARVAM_CHAT_URL, | |
| headers={ | |
| "api-subscription-key": SARVAM_API_KEY, | |
| "Authorization": f"Bearer {SARVAM_API_KEY}", | |
| "Content-Type": "application/json", | |
| }, | |
| json={ | |
| "model": SARVAM_CHAT_MODEL, | |
| "messages": [ | |
| {"role": "system", "content": LYRICS_SYSTEM_PROMPT}, | |
| {"role": "user", "content": user_msg}, | |
| ], | |
| "temperature": 0.8, | |
| "max_tokens": 2048, | |
| }, | |
| timeout=120, | |
| ) | |
| if resp.status_code != 200: | |
| raise gr.Error(f"Sarvam chat error {resp.status_code}: {resp.text[:300]}") | |
| content = resp.json()["choices"][0]["message"]["content"] or "" | |
| try: | |
| data = _extract_json(content) | |
| except Exception: | |
| # Fall back to treating the whole reply as lyrics | |
| data = {"title": "", "lyrics": content, "spoken_intro": "", "bpm": None} | |
| bpm = data.get("bpm") | |
| try: | |
| bpm = int(bpm) if bpm else None | |
| except (TypeError, ValueError): | |
| bpm = None | |
| return ( | |
| data.get("title", ""), | |
| data.get("lyrics", ""), | |
| data.get("spoken_intro", ""), | |
| bpm if bpm and 40 <= bpm <= 220 else None, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Sarvam: Bulbul v3 spoken intro | |
| # --------------------------------------------------------------------------- | |
| def bulbul_tts(text: str, speaker: str, pace: float) -> np.ndarray: | |
| resp = requests.post( | |
| SARVAM_TTS_URL, | |
| headers={"api-subscription-key": SARVAM_API_KEY, "Content-Type": "application/json"}, | |
| json={ | |
| "text": text[:2500], | |
| "language_code": "hi-IN", | |
| "speaker": speaker, | |
| "model": "bulbul:v3", | |
| "pace": pace, | |
| "speech_sample_rate": SAMPLE_RATE, | |
| "output_audio_codec": "wav", | |
| }, | |
| timeout=120, | |
| ) | |
| if resp.status_code != 200: | |
| raise gr.Error(f"Sarvam TTS error {resp.status_code}: {resp.text[:300]}") | |
| wav_bytes = base64.b64decode(resp.json()["audios"][0]) | |
| audio, sr = sf.read(io.BytesIO(wav_bytes), dtype="float32") | |
| if sr != SAMPLE_RATE: | |
| # Simple linear resample (Bulbul should already return 48 kHz) | |
| n = int(len(audio) * SAMPLE_RATE / sr) | |
| audio = np.interp(np.linspace(0, len(audio), n, endpoint=False), | |
| np.arange(len(audio)), audio).astype("float32") | |
| if audio.ndim == 1: | |
| audio = np.stack([audio, audio], axis=1) | |
| return audio | |
| def prepend_intro(intro: np.ndarray, song: np.ndarray, crossfade_s: float = 0.8) -> np.ndarray: | |
| """intro, song: [samples, 2]. Intro plays, then song fades in under its tail.""" | |
| peak = np.abs(intro).max() or 1.0 | |
| intro = intro / peak * 0.8 | |
| xf = min(int(crossfade_s * SAMPLE_RATE), len(intro), len(song)) | |
| fade_out = np.linspace(1.0, 0.0, xf)[:, None] | |
| fade_in = np.linspace(0.0, 1.0, xf)[:, None] | |
| overlap = intro[-xf:] * fade_out + song[:xf] * fade_in | |
| out = np.concatenate([intro[:-xf], overlap, song[xf:]], axis=0) | |
| return np.clip(out, -1.0, 1.0) | |
| # --------------------------------------------------------------------------- | |
| # ACE-Step v1.5 setup | |
| # --------------------------------------------------------------------------- | |
| def _storage_path(): | |
| for p in ("/data", os.path.join(current_dir, "data")): | |
| try: | |
| os.makedirs(p, exist_ok=True) | |
| test = os.path.join(p, ".w") | |
| open(test, "w").close() | |
| os.remove(test) | |
| return p | |
| except OSError: | |
| continue | |
| return current_dir | |
| def _make(cls, storage): | |
| try: | |
| return cls(persistent_storage_path=storage) | |
| except TypeError: # newer upstream versions take no storage argument | |
| return cls() | |
| print("Loading ACE-Step v1.5 ...") | |
| storage = _storage_path() | |
| dit_handler = _make(AceStepHandler, storage) | |
| llm_handler = _make(LLMHandler, storage) | |
| init_status, dit_ok = dit_handler.initialize_service( | |
| project_root=current_dir, | |
| config_path=DIT_MODEL, | |
| device="auto", | |
| use_flash_attention=dit_handler.is_flash_attention_available(), | |
| compile_model=False, | |
| offload_to_cpu=False, | |
| offload_dit_to_cpu=False, | |
| ) | |
| print(init_status) | |
| lm_status, lm_ok = llm_handler.initialize( | |
| checkpoint_dir=dit_handler._get_checkpoint_dir(), | |
| lm_model_path=LM_MODEL, | |
| backend="pt" if IS_HF_SPACE else os.environ.get("LM_BACKEND", "vllm"), | |
| device="auto", | |
| offload_to_cpu=False, | |
| dtype=dit_handler.dtype, | |
| ) | |
| print(lm_status) | |
| def _gpu(duration): | |
| if HAS_SPACES: | |
| return spaces.GPU(duration=duration) | |
| return lambda f: f | |
| def sing(caption: str, lyrics: str, bpm, duration: float, seed: int): | |
| if not dit_ok: | |
| raise gr.Error(f"Music model failed to load: {init_status}") | |
| params = GenerationParams( | |
| caption=caption, | |
| lyrics=lyrics, | |
| vocal_language="hi", | |
| bpm=int(bpm) if bpm else None, | |
| duration=float(duration), | |
| seed=int(seed), | |
| inference_steps=8, | |
| thinking=bool(lm_ok), | |
| ) | |
| config = GenerationConfig( | |
| batch_size=1, | |
| use_random_seed=int(seed) < 0, | |
| seeds=None if int(seed) < 0 else [int(seed)], | |
| audio_format="wav", | |
| ) | |
| result = generate_music(dit_handler, llm_handler, params, config, save_dir=OUT_DIR) | |
| if not result.success or not result.audios: | |
| raise gr.Error(f"Generation failed: {result.error or result.status_message}") | |
| audio = result.audios[0] | |
| tensor = audio["tensor"] # [channels, samples] | |
| song = tensor.detach().float().cpu().numpy().T | |
| if song.ndim == 1: | |
| song = np.stack([song, song], axis=1) | |
| elif song.shape[1] == 1: | |
| song = np.repeat(song, 2, axis=1) | |
| return song, audio.get("sample_rate", SAMPLE_RATE), audio["params"].get("seed") | |
| # --------------------------------------------------------------------------- | |
| # Full pipeline | |
| # --------------------------------------------------------------------------- | |
| def make_song(lyrics, genre, extra_style, bpm, duration, seed, | |
| add_intro, intro_text, speaker, pace): | |
| if not lyrics.strip(): | |
| raise gr.Error("Generate or paste lyrics first.") | |
| caption = GENRES.get(genre, genre) | |
| if extra_style.strip(): | |
| caption = f"{caption}, {extra_style.strip()}" | |
| caption = f"{caption}, hindi vocals"[:512] | |
| song, sr, used_seed = sing(caption, lyrics[:4096], bpm, duration, seed) | |
| global SAMPLE_RATE | |
| if sr != SAMPLE_RATE: | |
| SAMPLE_RATE = sr | |
| final = song | |
| if add_intro and intro_text.strip(): | |
| intro = bulbul_tts(intro_text.strip(), speaker, pace) | |
| final = prepend_intro(intro, song) | |
| path = os.path.join(OUT_DIR, f"hindi_song_{uuid.uuid4().hex[:8]}.wav") | |
| sf.write(path, final, SAMPLE_RATE) | |
| return path, f"Done. Seed: {used_seed} (reuse it to get a similar take)." | |
| # --------------------------------------------------------------------------- | |
| # UI | |
| # --------------------------------------------------------------------------- | |
| with gr.Blocks(title="Hindi Song Studio", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown( | |
| "# 🎵 Hindi Song Studio\n" | |
| "Sarvam AI writes **original** Hindi lyrics → ACE-Step v1.5 sings them → " | |
| "optional Bulbul v3 spoken intro." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| idea = gr.Textbox(label="Song idea", lines=3, | |
| placeholder="e.g. Do dost jo saalon baad railway station par milte hain") | |
| genre = gr.Dropdown(list(GENRES), value="Bollywood romantic ballad", label="Genre") | |
| mood = gr.Textbox(label="Mood (optional)", placeholder="nostalgic, hopeful") | |
| lyrics_btn = gr.Button("1. Write lyrics with Sarvam", variant="secondary") | |
| title = gr.Textbox(label="Title") | |
| lyrics = gr.Textbox(label="Lyrics (edit freely)", lines=16) | |
| bpm = gr.Number(label="BPM (blank = auto)", value=None, precision=0) | |
| with gr.Column(): | |
| extra_style = gr.Textbox(label="Extra style tags (optional)", | |
| placeholder="female vocals, flute solo, rain sounds") | |
| duration = gr.Slider(30, 300, value=150, step=10, label="Duration (seconds)") | |
| seed = gr.Number(label="Seed (-1 = random)", value=-1, precision=0) | |
| with gr.Accordion("Spoken intro (Sarvam Bulbul v3)", open=False): | |
| add_intro = gr.Checkbox(label="Add spoken Hindi intro", value=False) | |
| intro_text = gr.Textbox(label="Intro text", lines=2) | |
| speaker = gr.Dropdown(BULBUL_SPEAKERS, value="shubh", label="Voice") | |
| pace = gr.Slider(0.5, 1.5, value=0.9, step=0.05, label="Pace") | |
| song_btn = gr.Button("2. Generate song", variant="primary") | |
| audio_out = gr.Audio(label="Song", type="filepath") | |
| status = gr.Markdown() | |
| lyrics_btn.click(write_lyrics, [idea, genre, mood], [title, lyrics, intro_text, bpm]) | |
| song_btn.click( | |
| make_song, | |
| [lyrics, genre, extra_style, bpm, duration, seed, add_intro, intro_text, speaker, pace], | |
| [audio_out, status], | |
| ) | |
| gr.Markdown( | |
| "Use original lyrics only. Songs from ACE-Step v1.5 are MIT-licensed; " | |
| "check Sarvam's terms for API output use." | |
| ) | |
| demo.queue(max_size=20) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True) | |