maya2-native / app.py
multimodalart's picture
multimodalart HF Staff
Upload app.py with huggingface_hub
38a247d verified
Raw History Blame Contribute Delete
11.2 kB
"""Maya 2 Native — multilingual streaming TTS for 12 Indic languages + English,
with zero-shot voice cloning. Apache-2.0, 2.5B params, 24 kHz output.
ZeroGPU note: the maya3 engine captures CUDA graphs and allocates pinned host
buffers at init, which requires a real CUDA context — impossible at module
scope under the ZeroGPU hijack. So the engine is built lazily inside the
@spaces.GPU worker (cached in a module global, which persists for the warm
worker process), while the ~5 GB of weights are pre-downloaded to the HF cache
at module scope so worker-side loads are disk-bound.
"""
import os
import time
import spaces # MUST precede any torch / CUDA import
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
# --- module scope: pre-warm the HF cache (pure disk/network, no CUDA) -------
from maya3 import hub
RELEASE = "maya-research/maya2-native"
_paths = hub.resolve(RELEASE) # downloads model shards + kyutai/mimi + speechbrain ECAPA
CKPT_DIR = _paths["ckpt"] # release dir; from_pretrained re-resolves offline-first
import gradio as gr
import numpy as np
LANGS = ["hi", "en", "as", "bn", "gu", "kn", "ml", "mr", "or", "pa", "ta", "te"]
LANG_NAMES = {
"hi": "Hindi", "en": "English", "as": "Assamese", "bn": "Bengali",
"gu": "Gujarati", "kn": "Kannada", "ml": "Malayalam", "mr": "Marathi",
"or": "Odia", "pa": "Punjabi", "ta": "Tamil", "te": "Telugu",
}
# 20 named voices shipped with the release (voices/voices.json).
VOICES = [
"Ahana", "Vaidehi", "Malini", "Charvi", "Jahnavi",
"Anirban", "Nakul", "Vasant", "Siddhant", "Satvik",
"Abhay", "Nivedita", "Ishwari", "Ritvik", "Vrinda",
"Sreekar", "Kayal", "Shaunak", "Akshat", "Tanushree",
]
MAX_CHARS = 1200 # ~1 minute of speech; keeps duration bounded
_tts = None
def _get_tts():
"""Build the engine once per worker process (weights load + CUDA graphs + warmup)."""
global _tts
if _tts is None:
from maya3.api import Maya3TTS
t0 = time.perf_counter()
_tts = Maya3TTS.from_pretrained(
CKPT_DIR, # release dir; re-resolves (offline-first) + registers named voices
device="cuda",
max_seats=1, # smallest graph grid; demo serves one request at a time
capacity=1024, # halves KV cache vs default; still ~80 s of audio per piece
)
print(f"[maya2] engine ready in {time.perf_counter() - t0:.1f}s", flush=True)
return _tts
@spaces.GPU(duration=90) # cold worker: ~48s engine init + a few s generation; warm: ~2s
def generate(
text: str,
lang: str,
voice: str,
normalize: bool,
seed: int,
ref_audio=None,
ref_text: str = "",
ref_lang: str = "en",
progress=gr.Progress(),
):
"""Synthesize speech from text in one of 12 Indic languages or English.
Optionally clone a voice from a 3-10 s reference clip: upload the clip,
give its exact transcript, and the same speaker will read your text —
even in a different language.
Returns 24 kHz mono audio.
"""
text = (text or "").strip()
if not text:
raise gr.Error("Please enter some text to synthesize.")
if len(text) > MAX_CHARS:
raise gr.Error(f"Text too long ({len(text)} chars); please keep it under {MAX_CHARS}.")
tts = _get_tts()
kw = {}
if ref_audio:
if not (ref_text or "").strip():
raise gr.Error("Voice cloning needs the reference clip's transcript (ref_text).")
kw = dict(ref_audio=ref_audio, ref_text=ref_text, ref_lang=ref_lang)
voice = None # engine forbids combining a named voice with ref_audio
elif voice == "(none)":
voice = None
t0 = time.perf_counter()
try:
wav, info = tts.generate(text, lang=lang, voice=voice, normalize=normalize, seed=seed,
return_info=True)
except Exception as e:
msg = str(e)
code = getattr(e, "code", None)
raise gr.Error(f"{code}: {msg}" if code else msg)
dt = time.perf_counter() - t0
dur = len(wav) / 24000
print(f"[maya2] lang={lang} voice={voice or 'clone'} chars={len(text)} "
f"audio={dur:.1f}s gpu={dt:.1f}s reason={info.get('reason')} frames={info.get('frames')}",
flush=True)
if len(wav) == 0:
raise gr.Error("The model produced no audio — try rephrasing or raising max length.")
return (24000, wav)
CSS = """
main, .gradio-container {max-width: 900px !important; margin: 0 auto !important;}
.dark .gradio-container {color: var(--body-text-color);}
"""
with gr.Blocks() as demo:
gr.HTML("""
<div style="display: flex; align-items: center; gap: 12px; justify-content: center; margin-bottom: 4px;">
<div style="font-size: 44px; line-height: 1;">🗣️</div>
<div>
<h1 style="margin: 0; font-size: 28px;">Maya 2 Native</h1>
<p style="margin: 0; opacity: 0.75;">Multilingual streaming TTS — 12 Indic languages + English, with zero-shot voice cloning</p>
</div>
</div>
<p style="text-align: center; opacity: 0.7; margin-top: 0;">
2.5B params · Apache-2.0 · 24 kHz audio · <a href="https://huggingface.co/maya-research/maya2-native" target="_blank">model card</a>
</p>
""")
with gr.Row():
with gr.Column(scale=3):
text_in = gr.Textbox(
label="Text to speak",
placeholder="Enter text in any of the 12 supported languages…",
lines=4,
)
with gr.Row():
lang_in = gr.Dropdown(
choices=[(f"{LANG_NAMES[c]} ({c})", c) for c in LANGS],
value="hi",
label="Language",
filterable=True,
)
voice_in = gr.Dropdown(
choices=["(none)"] + VOICES,
value="Ritvik",
label="Voice",
filterable=True,
info="Named voices ship with the model; or clone your own below.",
)
audio_out = gr.Audio(label="Generated speech", type="filepath")
btn = gr.Button("Generate speech", variant="primary")
with gr.Column(scale=2):
gr.Markdown("### Clone a voice (optional)")
ref_audio = gr.Audio(
label="Reference clip (3–10 s)",
type="filepath",
sources=["upload", "microphone"],
format="wav",
)
ref_text = gr.Textbox(
label="Transcript of the reference clip",
placeholder="What is said in the clip, exactly…",
lines=2,
)
ref_lang = gr.Dropdown(
choices=[(f"{LANG_NAMES[c]} ({c})", c) for c in LANGS],
value="hi",
label="Reference language",
)
gr.Markdown(
"<span style='opacity:0.7'>Upload a clip and its exact transcript; the same speaker "
"will read your text — even in a different language.</span>"
)
with gr.Accordion("Advanced options", open=False):
normalize = gr.Checkbox(
value=True,
label="Normalize text (digits, currency, dates, URLs)",
info="Recommended: the bundled normaliser expands numbers, dates and symbols into words.",
)
seed = gr.Number(
value=0,
label="Seed (0 = random)",
precision=0,
minimum=0,
maximum=2**31 - 1,
)
gr.Examples(
examples=[
["क्या आपका वर्तमान पता 45, एमजी रोड, अंधेरी ईस्ट, मुंबई 400069 है? यदि हाँ, तो 1 दबाएं।", "hi", "Ritvik"],
["The EMI for your home loan is due by 12-10-24; please ensure your account has sufficient funds.", "en", "Siddhant"],
["જી બિલકુલ, મને સિસ્ટમમાં દેખાય છે કે તમારું ₹2,500 નું પેમેન્ટ 15મી તારીખે સક્સેસફુલ થઈ ગયું છે.", "gu", "Vaidehi"],
["ಹೌದಾ... ಅದು ನಿಜಕ್ಕೂ ಕಷ್ಟ. ನಿಮ್ಮ ಅಂತಿಮ ಬಿಲ್ ₹45,000 ಆಗಿದೆ, ಅದರಲ್ಲಿ ಇನ್ಶೂರೆನ್ಸ್ ಕವರ್ ಆಗಿದೆಯಾ ಅಂತ ಚೆಕ್ ಮಾಡ್ತೀನಿ.", "kn", "Satvik"],
["റദ്ദാക്കിയ ടിക്കറ്റിന്റെ റീഫണ്ട് തുകയായ ₹1,800 മൂന്ന് ദിവസത്തിനുള്ളിൽ ലഭിക്കും.", "ml", "Anirban"],
["मला 98765 43210 या नंबरवर OTP आला नाही, कृपया पुन्हा resend करा.", "mr", "Charvi"],
["ଦଯ଼ାକରି ମନେରଖନ୍ତୁ ଯେ EMI ପ୍ରତ୍ୟେକ ମାସର 5 ତାରିଖରେ କଟିବ ଏବଂ ଏହା ଆପଣଙ୍କର 3ୟ କିସ୍ତି।", "or", "Ahana"],
["NEFT ਰਾਹੀਂ ਫੰਡ ਟਰਾਂਸਫਰ 00:30 ਤੋਂ 23:30 ਤੱਕ ਉਪਲਬਧ ਹਨ।", "pa", "Malini"],
["கடந்த நிதியாண்டில் வங்கியின் மொத்த லாபம் ₹3.5 கோடி, இதில் ₹45 லட்சம் முதலீட்டாளர்களுக்கு வழங்கப்பட்டது.", "ta", "Abhay"],
["సేవింగ్స్ తో పోలిస్తే ఫిక్స్డ్ డిపాజిట్ వడ్డీ నిష్పత్తి 3:2 గా ఉండి, 7.25% ఇస్తుంది.", "te", "Nivedita"],
["এন ই এফ টি লেনদেনটো সম্পূৰ্ণ হ'বলৈ প্ৰায় 2 ঘণ্টা 30 মিনিট সময় লাগিব পাৰে।", "as", "Nakul"],
["12/08/2023 তারিখে আপনার 1,500 টাকার refund process হয়ে গেছে, status update চেক করুন।", "bn", "Jahnavi"],
],
inputs=[text_in, lang_in, voice_in],
fn=generate,
outputs=[audio_out],
cache_examples=True,
cache_mode="lazy",
label="Try the authors' showcase examples (one per supported language)",
)
gr.Markdown(
"<span style='opacity:0.65'>Voice-cloning example: leave the examples alone, upload your own "
"3–10 s clip in <b>Clone a voice</b>, type its exact transcript, then press Generate. "
"A bundled reference clip (`samples/hi_ritvik.wav`) is in this Space's files for inspiration.</span>"
)
btn.click(
generate,
inputs=[text_in, lang_in, voice_in, normalize, seed, ref_audio, ref_text, ref_lang],
outputs=[audio_out],
api_name="generate",
concurrency_limit=1,
)
if __name__ == "__main__":
demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)