crazydev919 commited on
Commit
520d894
Β·
verified Β·
1 Parent(s): 165c75f

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +175 -155
app.py CHANGED
@@ -12,7 +12,7 @@ import nltk
12
  nltk.download("punkt_tab", quiet=True)
13
 
14
  # ══════════════════════════════════════════════════════════
15
- # KIKUYU β€” lightweight VITS model
16
  # ══════════════════════════════════════════════════════════
17
  from transformers import VitsModel, AutoTokenizer
18
 
@@ -32,129 +32,160 @@ def synthesize_kikuyu(text):
32
  return "/tmp/kikuyu_output.wav"
33
 
34
  # ══════════════════════════════════════════════════════════
35
- # LUHYA β€” StyleTTS2 fine-tuned model
36
  # ══════════════════════════════════════════════════════════
37
- os.system("git clone https://github.com/yl4579/StyleTTS2 /app/StyleTTS2 2>/dev/null || true")
38
-
39
- for fpath in ["/app/StyleTTS2/models.py", "/app/StyleTTS2/utils.py"]:
40
- with open(fpath) as f:
41
- code = f.read()
42
- patched = re.sub(
43
- r'torch\.load\(([^)]+)\)',
44
- lambda m: m.group(0) if 'weights_only' in m.group(1)
45
- else f'torch.load({m.group(1)}, weights_only=False)',
46
- code
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  )
48
- with open(fpath, "w") as f:
49
- f.write(patched)
50
- print("βœ… Patched torch.load calls")
51
-
52
- sys.path.insert(0, "/app/StyleTTS2")
53
- os.chdir("/app/StyleTTS2")
54
-
55
- LUHYA_REPO = "crazydev919/luhya-tts"
56
- model_dir = snapshot_download(LUHYA_REPO)
57
-
58
- from models import *
59
- from utils import *
60
- from text_utils import TextCleaner
61
- from Modules.diffusion.sampler import DiffusionSampler, ADPM2Sampler, KarrasSchedule
62
-
63
- device = "cuda" if torch.cuda.is_available() else "cpu"
64
- print(f"Running on: {device}")
65
- textcleaner = TextCleaner()
66
-
67
- config = yaml.safe_load(open(f"{model_dir}/config.yml"))
68
- config["ASR_path"] = f"{model_dir}/Utils/ASR/epoch_00080.pth"
69
- config["ASR_config"] = f"{model_dir}/Utils/ASR/config.yml"
70
- config["F0_path"] = f"{model_dir}/Utils/JDC/bst.t7"
71
- config["PLBERT_dir"] = f"{model_dir}/Utils/PLBERT/"
72
-
73
- text_aligner = load_ASR_models(config["ASR_path"], config["ASR_config"])
74
- pitch_extractor = load_F0_models(config["F0_path"])
75
- from Utils.PLBERT.util import load_plbert
76
- plbert = load_plbert(config["PLBERT_dir"])
77
-
78
- model_params = recursive_munch(config["model_params"])
79
- luhya_model = build_model(model_params, text_aligner, pitch_extractor, plbert)
80
- _ = [luhya_model[key].eval() for key in luhya_model]
81
- _ = [luhya_model[key].to(device) for key in luhya_model]
82
-
83
- params = torch.load(
84
- f"{model_dir}/model.pth", map_location="cpu", weights_only=False
85
- )["net"]
86
- for key in luhya_model:
87
- if key in params:
88
- try:
89
- luhya_model[key].load_state_dict(params[key])
90
- except:
91
- from collections import OrderedDict
92
- sd = OrderedDict()
93
- for k, v in params[key].items():
94
- sd[k[7:] if k.startswith("module.") else k] = v
95
- luhya_model[key].load_state_dict(sd, strict=False)
96
- _ = [luhya_model[key].eval() for key in luhya_model]
97
- print("βœ… Luhya model loaded")
98
-
99
- sampler = DiffusionSampler(
100
- luhya_model.diffusion.diffusion,
101
- sampler=ADPM2Sampler(),
102
- sigma_schedule=KarrasSchedule(sigma_min=0.0001, sigma_max=3.0, rho=9.0),
103
- clamp=False
104
- )
105
-
106
- to_mel = torchaudio.transforms.MelSpectrogram(
107
- n_mels=80, n_fft=2048, win_length=1200, hop_length=300)
108
- mean, std = -4, 4
109
 
110
- def length_to_mask(lengths):
111
- mask = torch.arange(lengths.max()).unsqueeze(0).expand(
112
- lengths.shape[0], -1).type_as(lengths)
113
- mask = torch.gt(mask + 1, lengths.unsqueeze(1))
114
- return mask
 
115
 
116
- def preprocess(wave):
117
- wave_tensor = torch.from_numpy(wave).float()
118
- mel_tensor = to_mel(wave_tensor)
119
- mel_tensor = (torch.log(1e-5 + mel_tensor.unsqueeze(0)) - mean) / std
120
- return mel_tensor
121
 
122
- def compute_style(path):
123
- wave, sr = librosa.load(path, sr=24000)
124
  audio, _ = librosa.effects.trim(wave, top_db=30)
125
- mel = preprocess(audio).to(device)
 
 
 
126
  with torch.no_grad():
127
- ref_s = luhya_model.style_encoder(mel.unsqueeze(1))
128
- ref_p = luhya_model.predictor_encoder(mel.unsqueeze(1))
129
- return torch.cat([ref_s, ref_p], dim=1)
130
 
131
- ref_candidates = (
132
- glob.glob(f"{model_dir}/ref_wavs/*.wav") +
133
- glob.glob(f"{model_dir}/*.wav")
134
- )
135
- DEFAULT_REF = sorted(ref_candidates)[0]
136
- DEFAULT_STYLE = compute_style(DEFAULT_REF)
137
- print(f"βœ… Default reference: {DEFAULT_REF}")
138
 
139
  def synthesize_luhya(text, ref_audio=None, alpha=0.3, beta=0.7, steps=5):
 
 
140
  import phonemizer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  pb = phonemizer.backend.EspeakBackend(
142
  language="sw", preserve_punctuation=True, with_stress=True)
143
- ref_s = compute_style(ref_audio) if ref_audio else DEFAULT_STYLE
144
 
145
  ps = " ".join(word_tokenize(pb.phonemize([text.strip()])[0]))
146
- tokens = textcleaner(ps)
147
  tokens.insert(0, 0)
148
  tokens = torch.LongTensor(tokens).to(device).unsqueeze(0)
149
 
150
  with torch.no_grad():
151
  il = torch.LongTensor([tokens.shape[-1]]).to(device)
152
  tm = length_to_mask(il).to(device)
153
- t_en = luhya_model.text_encoder(tokens, il, tm)
154
- bd = luhya_model.bert(tokens, attention_mask=(~tm).int())
155
- d_en = luhya_model.bert_encoder(bd).transpose(-1, -2)
156
 
157
- sp = sampler(
158
  noise=torch.randn((1, 256)).unsqueeze(1).to(device),
159
  embedding=bd, embedding_scale=1,
160
  features=ref_s, num_steps=int(steps)
@@ -163,9 +194,9 @@ def synthesize_luhya(text, ref_audio=None, alpha=0.3, beta=0.7, steps=5):
163
  s = beta * sp[:, 128:] + (1 - beta) * ref_s[:, 128:]
164
  ref = alpha * sp[:, :128] + (1 - alpha) * ref_s[:, :128]
165
 
166
- d = luhya_model.predictor.text_encoder(d_en, s, il, tm)
167
- x, _ = luhya_model.predictor.lstm(d)
168
- dur = torch.sigmoid(luhya_model.predictor.duration_proj(x)).sum(axis=-1)
169
  pd = torch.round(dur.squeeze()).clamp(min=1)
170
 
171
  at = torch.zeros(il, int(pd.sum().data))
@@ -177,7 +208,7 @@ def synthesize_luhya(text, ref_audio=None, alpha=0.3, beta=0.7, steps=5):
177
  en = d.transpose(-1, -2) @ at.unsqueeze(0).to(device)
178
  asr = t_en @ at.unsqueeze(0).to(device)
179
 
180
- if model_params.decoder.type == "hifigan":
181
  en_new = torch.zeros_like(en)
182
  asr_new = torch.zeros_like(asr)
183
  en_new[:, :, 0] = en[:, :, 0]
@@ -186,15 +217,15 @@ def synthesize_luhya(text, ref_audio=None, alpha=0.3, beta=0.7, steps=5):
186
  asr_new[:, :, 1:] = asr[:, :, :-1]
187
  en, asr = en_new, asr_new
188
 
189
- F0, N = luhya_model.predictor.F0Ntrain(en, s)
190
- out = luhya_model.decoder(asr, F0, N, ref.squeeze().unsqueeze(0))
191
 
192
  wav = out.squeeze().cpu().numpy()[..., :-50]
193
  sf.write("/tmp/luhya_output.wav", wav, 24000)
194
  return "/tmp/luhya_output.wav"
195
 
196
  # ══════════════════════════════════════════════════════════
197
- # UNIFIED FUNCTION β€” language param routes to correct model
198
  # ══════════════════════════════════════════════════════════
199
  def synthesize(language, text, ref_audio=None, alpha=0.3, beta=0.7, steps=5):
200
  if language == "Kikuyu":
@@ -206,66 +237,55 @@ def synthesize(language, text, ref_audio=None, alpha=0.3, beta=0.7, steps=5):
206
  # GRADIO UI
207
  # ══════════════════════════════════════════════════════════
208
  with gr.Blocks(title="Kenyan Languages TTS") as demo:
209
- gr.Markdown("# πŸ—£οΈ Kenyan Languages TTS\nText-to-speech for Luhya (Lunyore) and Kikuyu.")
210
-
211
- with gr.Row():
212
- language = gr.Radio(
213
- choices = ["Luhya", "Kikuyu"],
214
- value = "Luhya",
215
- label = "Language"
216
- )
217
 
218
- with gr.Row():
219
- text = gr.Textbox(
220
- label = "Text",
221
- value = "mirembe. obulani lwa bwana nyasaye.",
222
- lines = 4
223
- )
 
 
 
 
224
 
225
- # Luhya-only controls
226
- with gr.Group(visible=True) as luhya_controls:
227
  gr.Markdown("**Luhya voice controls**")
 
228
  with gr.Row():
229
- ref_audio = gr.Audio(
230
- label = "Reference Voice (optional)",
231
- type = "filepath",
232
- value = None
233
- )
234
- with gr.Row():
235
- alpha = gr.Slider(0.0, 1.0, value=0.3, step=0.1, label="Alpha (style)")
236
- beta = gr.Slider(0.0, 1.0, value=0.7, step=0.1, label="Beta (prosody)")
237
- steps = gr.Slider(1, 10, value=5, step=1, label="Diffusion steps")
238
 
239
  output_audio = gr.Audio(label="Generated Speech", type="filepath")
240
- generate_btn = gr.Button("Generate", variant="primary")
241
 
242
- # Show/hide luhya controls based on language
243
- def toggle_controls(lang):
244
- return gr.update(visible=(lang == "Luhya"))
245
-
246
- language.change(fn=toggle_controls, inputs=language, outputs=luhya_controls)
247
-
248
- # Update placeholder text based on language
249
- def update_placeholder(lang):
250
- examples = {
251
- "Luhya" : "mirembe. obulani lwa bwana nyasaye.",
252
- "Kikuyu" : "MΕ©tΕ©Ε©rΔ©re wa ndΕ©ire nΔ© kΔ©heo kΔ©a mwanya mΕ©no.",
253
  }
254
- return gr.update(value=examples[lang])
255
 
256
- language.change(fn=update_placeholder, inputs=language, outputs=text)
257
 
258
- generate_btn.click(
259
  fn = synthesize,
260
  inputs = [language, text, ref_audio, alpha, beta, steps],
261
  outputs = output_audio
262
  )
263
 
264
  gr.Markdown("""
265
- **API usage:**
266
  POST /api/predict
267
- {"data": ["Luhya", "mirembe.", null, 0.3, 0.7, 5]}
268
- {"data": ["Kikuyu", "MΕ©tΕ©Ε©rΔ©re wa ndΕ©ire nΔ© kΔ©heo.", null, 0.3, 0.7, 5]}
269
- """)
270
-
271
- demo.launch(prevent_thread_lock=True)
 
 
 
 
 
 
12
  nltk.download("punkt_tab", quiet=True)
13
 
14
  # ══════════════════════════════════════════════════════════
15
+ # KIKUYU β€” load at startup (small, 145MB)
16
  # ══════════════════════════════════════════════════════════
17
  from transformers import VitsModel, AutoTokenizer
18
 
 
32
  return "/tmp/kikuyu_output.wav"
33
 
34
  # ══════════════════════════════════════════════════════════
35
+ # LUHYA β€” lazy load on first request (heavy, 2.18GB)
36
  # ══════════════════════════════════════════════════════════
37
+ _luhya_loaded = False
38
+ _luhya_model = None
39
+ _luhya_sampler = None
40
+ _default_style = None
41
+ _model_params = None
42
+ _textcleaner = None
43
+ _model_dir = None
44
+
45
+ def load_luhya():
46
+ global _luhya_loaded, _luhya_model, _luhya_sampler
47
+ global _default_style, _model_params, _textcleaner, _model_dir
48
+
49
+ if _luhya_loaded:
50
+ return
51
+
52
+ print("Loading Luhya TTS (first request)...")
53
+
54
+ # Clone + patch StyleTTS2
55
+ os.system("git clone https://github.com/yl4579/StyleTTS2 /app/StyleTTS2 2>/dev/null || true")
56
+ for fpath in ["/app/StyleTTS2/models.py", "/app/StyleTTS2/utils.py"]:
57
+ with open(fpath) as f:
58
+ code = f.read()
59
+ patched = re.sub(
60
+ r'torch\.load\(([^)]+)\)',
61
+ lambda m: m.group(0) if 'weights_only' in m.group(1)
62
+ else f'torch.load({m.group(1)}, weights_only=False)',
63
+ code
64
+ )
65
+ with open(fpath, "w") as f:
66
+ f.write(patched)
67
+
68
+ if "/app/StyleTTS2" not in sys.path:
69
+ sys.path.insert(0, "/app/StyleTTS2")
70
+ os.chdir("/app/StyleTTS2")
71
+
72
+ _model_dir = snapshot_download("crazydev919/luhya-tts")
73
+
74
+ from models import build_model, load_ASR_models, load_F0_models
75
+ from utils import recursive_munch
76
+ from text_utils import TextCleaner
77
+ from Modules.diffusion.sampler import DiffusionSampler, ADPM2Sampler, KarrasSchedule
78
+
79
+ _textcleaner = TextCleaner()
80
+ device = "cpu"
81
+
82
+ config = yaml.safe_load(open(f"{_model_dir}/config.yml"))
83
+ config["ASR_path"] = f"{_model_dir}/Utils/ASR/epoch_00080.pth"
84
+ config["ASR_config"] = f"{_model_dir}/Utils/ASR/config.yml"
85
+ config["F0_path"] = f"{_model_dir}/Utils/JDC/bst.t7"
86
+ config["PLBERT_dir"] = f"{_model_dir}/Utils/PLBERT/"
87
+
88
+ text_aligner = load_ASR_models(config["ASR_path"], config["ASR_config"])
89
+ pitch_extractor = load_F0_models(config["F0_path"])
90
+
91
+ from Utils.PLBERT.util import load_plbert
92
+ plbert = load_plbert(config["PLBERT_dir"])
93
+
94
+ _model_params = recursive_munch(config["model_params"])
95
+ _luhya_model = build_model(_model_params, text_aligner, pitch_extractor, plbert)
96
+ _ = [_luhya_model[k].eval() for k in _luhya_model]
97
+ _ = [_luhya_model[k].to(device) for k in _luhya_model]
98
+
99
+ params = torch.load(
100
+ f"{_model_dir}/model.pth", map_location="cpu", weights_only=False
101
+ )["net"]
102
+ for key in _luhya_model:
103
+ if key in params:
104
+ try:
105
+ _luhya_model[key].load_state_dict(params[key])
106
+ except:
107
+ from collections import OrderedDict
108
+ sd = OrderedDict()
109
+ for k, v in params[key].items():
110
+ sd[k[7:] if k.startswith("module.") else k] = v
111
+ _luhya_model[key].load_state_dict(sd, strict=False)
112
+ _ = [_luhya_model[k].eval() for k in _luhya_model]
113
+
114
+ _luhya_sampler = DiffusionSampler(
115
+ _luhya_model.diffusion.diffusion,
116
+ sampler=ADPM2Sampler(),
117
+ sigma_schedule=KarrasSchedule(sigma_min=0.0001, sigma_max=3.0, rho=9.0),
118
+ clamp=False
119
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
+ # Precompute default style
122
+ ref_candidates = (
123
+ glob.glob(f"{_model_dir}/ref_wavs/*.wav") +
124
+ glob.glob(f"{_model_dir}/*.wav")
125
+ )
126
+ DEFAULT_REF = sorted(ref_candidates)[0]
127
 
128
+ to_mel = torchaudio.transforms.MelSpectrogram(
129
+ n_mels=80, n_fft=2048, win_length=1200, hop_length=300)
130
+ mean, std = -4, 4
 
 
131
 
132
+ wave, sr = librosa.load(DEFAULT_REF, sr=24000)
 
133
  audio, _ = librosa.effects.trim(wave, top_db=30)
134
+ wave_t = torch.from_numpy(audio).float()
135
+ mel = (torch.log(1e-5 + to_mel(wave_t).unsqueeze(0)) - mean) / std
136
+ mel = mel.to(device)
137
+
138
  with torch.no_grad():
139
+ ref_s = _luhya_model.style_encoder(mel.unsqueeze(1))
140
+ ref_p = _luhya_model.predictor_encoder(mel.unsqueeze(1))
141
+ _default_style = torch.cat([ref_s, ref_p], dim=1)
142
 
143
+ _luhya_loaded = True
144
+ print("βœ… Luhya model loaded and cached")
 
 
 
 
 
145
 
146
  def synthesize_luhya(text, ref_audio=None, alpha=0.3, beta=0.7, steps=5):
147
+ load_luhya() # lazy load
148
+
149
  import phonemizer
150
+ device = "cpu"
151
+
152
+ to_mel = torchaudio.transforms.MelSpectrogram(
153
+ n_mels=80, n_fft=2048, win_length=1200, hop_length=300)
154
+ mean, std = -4, 4
155
+
156
+ def length_to_mask(lengths):
157
+ mask = torch.arange(lengths.max()).unsqueeze(0).expand(
158
+ lengths.shape[0], -1).type_as(lengths)
159
+ mask = torch.gt(mask + 1, lengths.unsqueeze(1))
160
+ return mask
161
+
162
+ def compute_style(path):
163
+ wave, sr = librosa.load(path, sr=24000)
164
+ audio, _ = librosa.effects.trim(wave, top_db=30)
165
+ wave_t = torch.from_numpy(audio).float()
166
+ mel = (torch.log(1e-5 + to_mel(wave_t).unsqueeze(0)) - mean) / std
167
+ with torch.no_grad():
168
+ ref_s = _luhya_model.style_encoder(mel.unsqueeze(1))
169
+ ref_p = _luhya_model.predictor_encoder(mel.unsqueeze(1))
170
+ return torch.cat([ref_s, ref_p], dim=1)
171
+
172
  pb = phonemizer.backend.EspeakBackend(
173
  language="sw", preserve_punctuation=True, with_stress=True)
174
+ ref_s = compute_style(ref_audio) if ref_audio else _default_style
175
 
176
  ps = " ".join(word_tokenize(pb.phonemize([text.strip()])[0]))
177
+ tokens = _textcleaner(ps)
178
  tokens.insert(0, 0)
179
  tokens = torch.LongTensor(tokens).to(device).unsqueeze(0)
180
 
181
  with torch.no_grad():
182
  il = torch.LongTensor([tokens.shape[-1]]).to(device)
183
  tm = length_to_mask(il).to(device)
184
+ t_en = _luhya_model.text_encoder(tokens, il, tm)
185
+ bd = _luhya_model.bert(tokens, attention_mask=(~tm).int())
186
+ d_en = _luhya_model.bert_encoder(bd).transpose(-1, -2)
187
 
188
+ sp = _luhya_sampler(
189
  noise=torch.randn((1, 256)).unsqueeze(1).to(device),
190
  embedding=bd, embedding_scale=1,
191
  features=ref_s, num_steps=int(steps)
 
194
  s = beta * sp[:, 128:] + (1 - beta) * ref_s[:, 128:]
195
  ref = alpha * sp[:, :128] + (1 - alpha) * ref_s[:, :128]
196
 
197
+ d = _luhya_model.predictor.text_encoder(d_en, s, il, tm)
198
+ x, _ = _luhya_model.predictor.lstm(d)
199
+ dur = torch.sigmoid(_luhya_model.predictor.duration_proj(x)).sum(axis=-1)
200
  pd = torch.round(dur.squeeze()).clamp(min=1)
201
 
202
  at = torch.zeros(il, int(pd.sum().data))
 
208
  en = d.transpose(-1, -2) @ at.unsqueeze(0).to(device)
209
  asr = t_en @ at.unsqueeze(0).to(device)
210
 
211
+ if _model_params.decoder.type == "hifigan":
212
  en_new = torch.zeros_like(en)
213
  asr_new = torch.zeros_like(asr)
214
  en_new[:, :, 0] = en[:, :, 0]
 
217
  asr_new[:, :, 1:] = asr[:, :, :-1]
218
  en, asr = en_new, asr_new
219
 
220
+ F0, N = _luhya_model.predictor.F0Ntrain(en, s)
221
+ out = _luhya_model.decoder(asr, F0, N, ref.squeeze().unsqueeze(0))
222
 
223
  wav = out.squeeze().cpu().numpy()[..., :-50]
224
  sf.write("/tmp/luhya_output.wav", wav, 24000)
225
  return "/tmp/luhya_output.wav"
226
 
227
  # ══════════════════════════════════════════════════════════
228
+ # UNIFIED ENTRY POINT
229
  # ══════════════════════════════════════════════════════════
230
  def synthesize(language, text, ref_audio=None, alpha=0.3, beta=0.7, steps=5):
231
  if language == "Kikuyu":
 
237
  # GRADIO UI
238
  # ══════════════════════════════════════════════════════════
239
  with gr.Blocks(title="Kenyan Languages TTS") as demo:
240
+ gr.Markdown("# πŸ—£οΈ Kenyan Languages TTS\nLuhya (Lunyore) and Kikuyu text-to-speech.")
 
 
 
 
 
 
 
241
 
242
+ language = gr.Radio(
243
+ choices=["Kikuyu", "Luhya"],
244
+ value="Kikuyu",
245
+ label="Language"
246
+ )
247
+ text = gr.Textbox(
248
+ label="Text",
249
+ value="MΕ©tΕ©Ε©rΔ©re wa ndΕ©ire nΔ© kΔ©heo kΔ©a mwanya.",
250
+ lines=4
251
+ )
252
 
253
+ with gr.Group(visible=False) as luhya_controls:
 
254
  gr.Markdown("**Luhya voice controls**")
255
+ ref_audio = gr.Audio(label="Reference Voice (optional)", type="filepath")
256
  with gr.Row():
257
+ alpha = gr.Slider(0.0, 1.0, value=0.3, step=0.1, label="Alpha")
258
+ beta = gr.Slider(0.0, 1.0, value=0.7, step=0.1, label="Beta")
259
+ steps = gr.Slider(1, 10, value=5, step=1, label="Steps")
 
 
 
 
 
 
260
 
261
  output_audio = gr.Audio(label="Generated Speech", type="filepath")
262
+ btn = gr.Button("Generate", variant="primary")
263
 
264
+ def toggle(lang):
265
+ texts = {
266
+ "Kikuyu": "MΕ©tΕ©Ε©rΔ©re wa ndΕ©ire nΔ© kΔ©heo kΔ©a mwanya.",
267
+ "Luhya" : "mirembe. obulani lwa bwana nyasaye.",
 
 
 
 
 
 
 
268
  }
269
+ return gr.update(visible=(lang=="Luhya")), gr.update(value=texts[lang])
270
 
271
+ language.change(fn=toggle, inputs=language, outputs=[luhya_controls, text])
272
 
273
+ btn.click(
274
  fn = synthesize,
275
  inputs = [language, text, ref_audio, alpha, beta, steps],
276
  outputs = output_audio
277
  )
278
 
279
  gr.Markdown("""
280
+ **API:**
281
  POST /api/predict
282
+ {"data": ["Kikuyu", "your kikuyu text", null, 0.3, 0.7, 5]}
283
+ {"data": ["Luhya", "your luhya text", null, 0.3, 0.7, 5]}
284
+ Note: First Luhya request takes ~30s to load the model.
285
+ """)
286
+
287
+ demo.queue(max_size=3).launch(
288
+ prevent_thread_lock = True,
289
+ max_threads = 1,
290
+ show_error = True,
291
+ )