IFMedTechdemo commited on
Commit
1d84a6c
·
verified ·
1 Parent(s): 67b7947

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +226 -0
app.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 🎙️ Multi-Engine TTS – Zero-GPU edition
3
+ Kokoro │ Veena │ pyttsx3 (fallback)
4
+ Routes every synthesis to an idle A100.
5
+ """
6
+
7
+ import os, tempfile, subprocess, numpy as np
8
+ os.environ["HF_HOME"] = "/data" # persistent cache
9
+
10
+ import gradio as gr
11
+ import soundfile as sf
12
+ import spaces # << Zero-GPU helper
13
+
14
+ # ------------------------------------------------------------------
15
+ # 1. Engine availability flags
16
+ # ------------------------------------------------------------------
17
+ KOKORO_OK = False
18
+ VEENA_OK = False
19
+ PYT_OK = False
20
+
21
+ try:
22
+ from kokoro import KPipeline
23
+ KOKORO_OK = True
24
+ except Exception as e:
25
+ print("Kokoro unavailable:", e)
26
+
27
+ try:
28
+ import torch, transformers, snac
29
+ from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
30
+ from snac import SNAC
31
+ VEENA_OK = True
32
+ except Exception as e:
33
+ print("Veena deps unavailable:", e)
34
+
35
+ try:
36
+ import pyttsx3
37
+ PYT_OK = True
38
+ except Exception as e:
39
+ print("pyttsx3 unavailable:", e)
40
+
41
+ # ------------------------------------------------------------------
42
+ # 2. Lazy model loader (runs once per GPU worker)
43
+ # ------------------------------------------------------------------
44
+ kokoro_pipe = None
45
+ veena_model = None
46
+ veena_tok = None
47
+ veena_snac = None
48
+
49
+ def load_kokoro():
50
+ global kokoro_pipe
51
+ if kokoro_pipe is None and KOKORO_OK:
52
+ kokoro_pipe = KPipeline(lang_code='a')
53
+ return kokoro_pipe
54
+
55
+ def load_veena():
56
+ global veena_model, veena_tok, veena_snac
57
+ if veena_model is None and VEENA_OK:
58
+ bnb = BitsAndBytesConfig(load_in_4bit=True,
59
+ bnb_4bit_quant_type="nf4",
60
+ bnb_4bit_compute_dtype=torch.bfloat16)
61
+ veena_model = AutoModelForCausalLM.from_pretrained(
62
+ "maya-research/veena-tts",
63
+ quantization_config=bnb,
64
+ device_map="auto",
65
+ trust_remote_code=True)
66
+ veena_tok = AutoTokenizer.from_pretrained("maya-research/veena-tts",
67
+ trust_remote_code=True)
68
+ veena_snac = SNAC.from_pretrained("hubertsiuzdak/snac_24khz").eval()
69
+ if torch.cuda.is_available():
70
+ veena_snac = veena_snac.cuda()
71
+ return veena_model
72
+
73
+ # ------------------------------------------------------------------
74
+ # 3. Generation helpers (CPU→GPU off-load)
75
+ # ------------------------------------------------------------------
76
+ AUDIO_CODE_BASE_OFFSET = 128266
77
+ START_OF_SPEECH_TOKEN = 128257
78
+ END_OF_SPEECH_TOKEN = 128258
79
+ START_OF_HUMAN_TOKEN = 128259
80
+ END_OF_HUMAN_TOKEN = 128260
81
+ START_OF_AI_TOKEN = 128261
82
+ END_OF_AI_TOKEN = 128262
83
+
84
+ def decode_snac(tokens):
85
+ if len(tokens) % 7:
86
+ return None
87
+ codes = [[] for _ in range(3)]
88
+ offsets = [AUDIO_CODE_BASE_OFFSET + i*4096 for i in range(7)]
89
+ for i in range(0, len(tokens), 7):
90
+ codes[0].append(tokens[i] - offsets[0])
91
+ codes[1].extend([tokens[i+1]-offsets[1], tokens[i+4]-offsets[4]])
92
+ codes[2].extend([tokens[i+2]-offsets[2], tokens[i+3]-offsets[3],
93
+ tokens[i+5]-offsets[5], tokens[i+6]-offsets[6]])
94
+ device = veena_snac.device
95
+ hierarchical = [torch.tensor(c, dtype=torch.int32, device=device).unsqueeze(0)
96
+ for c in codes]
97
+ with torch.no_grad():
98
+ wav = veena_snac.decode(hierarchical).squeeze().clamp(-1,1).cpu().numpy()
99
+ return wav
100
+
101
+ def tts_veena(text, speaker, temperature, top_p):
102
+ load_veena()
103
+ prompt = f"<spk_{speaker}> {text}"
104
+ tok = veena_tok.encode(prompt, add_special_tokens=False)
105
+ input_ids = [START_OF_HUMAN_TOKEN] + tok + [END_OF_HUMAN_TOKEN,
106
+ START_OF_AI_TOKEN, START_OF_SPEECH_TOKEN]
107
+ input_ids = torch.tensor([input_ids], device=veena_model.device)
108
+ max_new = min(int(len(text)*1.3)*7 + 21, 700)
109
+ out = veena_model.generate(
110
+ input_ids,
111
+ max_new_tokens=max_new,
112
+ do_sample=True,
113
+ temperature=temperature,
114
+ top_p=top_p,
115
+ repetition_penalty=1.05,
116
+ pad_token_id=veena_tok.pad_token_id,
117
+ eos_token_id=[END_OF_SPEECH_TOKEN, END_OF_AI_TOKEN])
118
+ gen = out[0, len(input_ids[0]):].tolist()
119
+ snac_toks = [t for t in gen if AUDIO_CODE_BASE_OFFSET <= t < AUDIO_CODE_BASE_OFFSET+7*4096]
120
+ if not snac_toks:
121
+ raise RuntimeError("No audio tokens produced")
122
+ return decode_snac(snac_toks)
123
+
124
+ def tts_kokoro(text, voice, speed):
125
+ pipe = load_kokoro()
126
+ generator = pipe(text, voice=voice, speed=speed)
127
+ for gs, ps, audio in generator:
128
+ return audio
129
+ raise RuntimeError("Kokoro generation failed")
130
+
131
+ def tts_pyttsx3(text, rate, volume):
132
+ engine = pyttsx3.init()
133
+ engine.setProperty('rate', rate)
134
+ engine.setProperty('volume', volume)
135
+ fd, path = tempfile.mkstemp(suffix='.wav')
136
+ os.close(fd)
137
+ engine.save_to_file(text, path)
138
+ engine.runAndWait()
139
+ wav, sr = sf.read(path)
140
+ os.remove(path)
141
+ return wav
142
+
143
+ # ------------------------------------------------------------------
144
+ # 4. ZERO-GPU ENTRY POINT (decorated)
145
+ # ------------------------------------------------------------------
146
+ @spaces.GPU
147
+ def synthesise(text, engine, voice, speed, speaker, temperature, top_p, rate, vol):
148
+ if not text.strip():
149
+ raise gr.Error("Please enter some text.")
150
+ if engine == "kokoro" and KOKORO_OK:
151
+ wav = tts_kokoro(text, voice=voice, speed=speed)
152
+ elif engine == "veena" and VEENA_OK:
153
+ wav = tts_veena(text, speaker=speaker, temperature=temperature, top_p=top_p)
154
+ elif engine == "pyttsx3" and PYT_OK:
155
+ wav = tts_pyttsx3(text, rate=rate, volume=vol)
156
+ else:
157
+ raise gr.Error(f"{engine} is not available on this Space.")
158
+ fd, tmp = tempfile.mkstemp(suffix='.wav')
159
+ os.close(fd)
160
+ sf.write(tmp, wav, 24000)
161
+ return tmp
162
+
163
+ # ------------------------------------------------------------------
164
+ # 5. Gradio UI (unchanged visuals)
165
+ # ------------------------------------------------------------------
166
+ css = """footer {visibility: hidden} #col-left {max-width: 320px}"""
167
+
168
+ with gr.Blocks(css=css, title="Multi-Engine TTS – Zero-GPU") as demo:
169
+ gr.Markdown("## 🎙️ Multi-Engine TTS Demo – Zero-GPU \n*Kokoro ‑ Veena ‑ pyttsx3*")
170
+
171
+ with gr.Row():
172
+ with gr.Column(elem_id="col-left"):
173
+ engine = gr.Radio(label="Engine",
174
+ choices=[e for e in ["kokoro","veena","pyttsx3"]
175
+ if globals()[e.upper()+"_OK"]],
176
+ value="kokoro" if KOKORO_OK else
177
+ "veena" if VEENA_OK else "pyttsx3")
178
+
179
+ with gr.Group(visible=KOKORO_OK) as kokoro_box:
180
+ voice = gr.Dropdown(label="Voice",
181
+ choices=['af_heart','af_sky','af_mist','af_dusk'],
182
+ value='af_heart')
183
+ speed = gr.Slider(0.5, 2.0, 1.0, step=0.1, label="Speed")
184
+
185
+ with gr.Group(visible=VEENA_OK) as veena_box:
186
+ speaker = gr.Dropdown(label="Speaker",
187
+ choices=['kavya','agastya','maitri','vinaya'],
188
+ value='kavya')
189
+ temperature = gr.Slider(0.1, 1.0, 0.4, step=0.05, label="Temperature")
190
+ top_p = gr.Slider(0.1, 1.0, 0.9, step=0.05, label="Top-p")
191
+
192
+ with gr.Group(visible=PYT_OK) as pyttsx3_box:
193
+ rate = gr.Slider(50, 300, 180, step=5, label="Words / min")
194
+ vol = gr.Slider(0.0, 1.0, 1.0, step=0.05, label="Volume")
195
+
196
+ with gr.Column(scale=3):
197
+ text = gr.Textbox(label="Text to speak",
198
+ placeholder="Type or paste text here …",
199
+ lines=6, max_lines=12)
200
+ btn = gr.Button("🎧 Synthesise", variant="primary")
201
+ audio_out = gr.Audio(label="Generated speech", type="filepath")
202
+
203
+ # show/hide panels
204
+ def switch_panel(e):
205
+ return (gr.update(visible=e=="kokoro"),
206
+ gr.update(visible=e=="veena"),
207
+ gr.update(visible=e=="pyttsx3"))
208
+ engine.change(switch_panel, inputs=engine,
209
+ outputs=[kokoro_box, veena_box, pyttsx3_box])
210
+
211
+ # binding
212
+ btn.click(synthesise,
213
+ inputs=[text, engine, voice, speed, speaker,
214
+ temperature, top_p, rate, vol],
215
+ outputs=audio_out)
216
+
217
+ gr.Markdown("### Tips \n"
218
+ "- **Kokoro** – fastest, good quality English \n"
219
+ "- **Veena** – multilingual, GPU-friendly (4-bit) \n"
220
+ "- **pyttsx3** – offline fallback, any language \n"
221
+ "Audio is returned as 24 kHz WAV.")
222
+
223
+ # ------------------------------------------------------------------
224
+ # 6. Launch
225
+ # ------------------------------------------------------------------
226
+ demo.launch()