File size: 11,178 Bytes
6784f71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b2e7faf
6784f71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b2e7faf
6784f71
b2e7faf
 
6784f71
 
 
 
 
38a247d
6784f71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78a2051
 
6784f71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38a247d
 
6784f71
 
38a247d
6784f71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b2e7faf
6784f71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38a247d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
"""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)