songsgen / app.py
callernode's picture
Upload 2 files
96e3b68 verified
Raw History Blame Contribute Delete
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
@_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)