""" 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 @_gpu(180) 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)