archivartaunik commited on
Commit
396a344
·
verified ·
1 Parent(s): a4c6f74

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +373 -0
app.py ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Калі запускаеце ў чыстым асяроддзі (раскаментуйце):
2
+ # !pip install -q gradio spaces huggingface_hub torch scipy tqdm gitpython
3
+
4
+ import os
5
+ import sys
6
+ import time
7
+ import tempfile
8
+ import subprocess
9
+ import inspect
10
+ import re
11
+
12
+ import spaces
13
+ import gradio as gr
14
+ import torch
15
+ from huggingface_hub import hf_hub_download
16
+ from scipy.io.wavfile import write
17
+ import numpy as np
18
+
19
+ # ---------------------------------------------------------
20
+ # 1) Клануем і падключаем coqui-ai-TTS (fork з падтрымкай BE)
21
+ # ---------------------------------------------------------
22
+ REPO_URL = "https://github.com/tuteishygpt/coqui-ai-TTS.git"
23
+ REPO_DIR = "coqui-ai-TTS"
24
+
25
+ if not os.path.exists(REPO_DIR):
26
+ subprocess.run(["git", "clone", REPO_URL, REPO_DIR], check=True)
27
+
28
+ repo_root = os.path.abspath(REPO_DIR)
29
+ if repo_root not in sys.path:
30
+ sys.path.insert(0, repo_root)
31
+
32
+ from TTS.tts.configs.xtts_config import XttsConfig
33
+ from TTS.tts.models.xtts import Xtts
34
+ from TTS.tts.layers.xtts.tokenizer import VoiceBpeTokenizer
35
+
36
+ # ---------------------------------------------------------
37
+ # 2) Файлы мадэлі
38
+ # ---------------------------------------------------------
39
+ repo_id = "archivartaunik/BE_XTTS_V2_10ep250k"
40
+ model_dir = "./model"
41
+ os.makedirs(model_dir, exist_ok=True)
42
+
43
+ checkpoint_file = os.path.join(model_dir, "model.pth")
44
+ config_file = os.path.join(model_dir, "config.json")
45
+ vocab_file = os.path.join(model_dir, "vocab.json")
46
+ default_voice_file = os.path.join(model_dir, "voice.wav")
47
+
48
+ if not os.path.exists(checkpoint_file):
49
+ hf_hub_download(repo_id, filename="model.pth", local_dir=model_dir)
50
+ if not os.path.exists(config_file):
51
+ hf_hub_download(repo_id, filename="config.json", local_dir=model_dir)
52
+ if not os.path.exists(vocab_file):
53
+ hf_hub_download(repo_id, filename="vocab.json", local_dir=model_dir)
54
+ if not os.path.exists(default_voice_file):
55
+ hf_hub_download(repo_id, filename="voice.wav", local_dir=model_dir)
56
+
57
+ # ---------------------------------------------------------
58
+ # 3) Загрузка мадэлі і токенайзера
59
+ # ---------------------------------------------------------
60
+ config = XttsConfig()
61
+ config.load_json(config_file)
62
+ XTTS_MODEL = Xtts.init_from_config(config)
63
+ XTTS_MODEL.load_checkpoint(
64
+ config,
65
+ checkpoint_path=checkpoint_file,
66
+ vocab_path=vocab_file,
67
+ use_deepspeed=False,
68
+ )
69
+
70
+ device = "cuda:0" if torch.cuda.is_available() else "cpu"
71
+ XTTS_MODEL.to(device).eval()
72
+ sampling_rate = int(XTTS_MODEL.config.audio["sample_rate"])
73
+
74
+ tokenizer = VoiceBpeTokenizer(vocab_file=vocab_file)
75
+ XTTS_MODEL.tokenizer = tokenizer
76
+
77
+ # ---------------------------------------------------------
78
+ # 4) Патокавая TTS па "токенах" з мінімальнай затрымкай
79
+ # (натыйўны стримінг -> fallback інкрементальны прэфікс)
80
+ # ---------------------------------------------------------
81
+
82
+ MIN_BUFFER_MS = 0.05 # мэтавы мінімальны буфер ~50 ms
83
+ FADE_MS = 0.008 # кароткі cross-fade паміж чанкамі
84
+ TOKENS_PER_STEP = 4 # памер кроку «токенаў» у fallback (BPE/субсловы)
85
+
86
+ def _seconds_to_samples(sec: float, sr: int) -> int:
87
+ return max(1, int(sec * sr))
88
+
89
+ def _crossfade_concat(a: np.ndarray, b: np.ndarray, sr: int, fade_ms: float) -> np.ndarray:
90
+ """Плыўна зліць два кавалкі без клікаў."""
91
+ if a.size == 0:
92
+ return b.astype(np.float32, copy=False)
93
+ if b.size == 0:
94
+ return a.astype(np.float32, copy=False)
95
+ a = a.astype(np.float32, copy=False)
96
+ b = b.astype(np.float32, copy=False)
97
+ fade_n = _seconds_to_samples(fade_ms, sr)
98
+ fade_n = min(fade_n, a.size, b.size)
99
+ if fade_n <= 1:
100
+ return np.concatenate([a, b], axis=0)
101
+ fade_out = np.linspace(1.0, 0.0, fade_n, endpoint=True, dtype=np.float32)
102
+ fade_in = 1.0 - fade_out
103
+ head = a[:-fade_n]
104
+ tail = (a[-fade_n:] * fade_out) + (b[:fade_n] * fade_in)
105
+ rest = b[fade_n:]
106
+ return np.concatenate([head, tail, rest], axis=0)
107
+
108
+ def _merge_chunks_with_crossfade(chunks: list[np.ndarray], sr: int) -> np.ndarray:
109
+ merged = np.zeros((0,), dtype=np.float32)
110
+ for c in chunks:
111
+ if c is None or np.asarray(c).size == 0:
112
+ continue
113
+ merged = _crossfade_concat(merged, np.asarray(c, dtype=np.float32), sr, FADE_MS)
114
+ return merged
115
+
116
+ def _yield_buffered_chunks(chunks: list[np.ndarray], sr: int, target_ms: float):
117
+ """
118
+ Збіраем маленькія кавалкі пакуль не назапасім ~target_ms,
119
+ пасля чаго yield (sr, buffer) і спім роўна на працягласць buffer.
120
+ """
121
+ target_samples = _seconds_to_samples(target_ms, sr)
122
+ buf = np.zeros((0,), dtype=np.float32)
123
+ for c in chunks:
124
+ if c is None:
125
+ continue
126
+ c = np.asarray(c, dtype=np.float32)
127
+ if c.size == 0:
128
+ continue
129
+ if buf.size == 0:
130
+ buf = c
131
+ else:
132
+ buf = _crossfade_concat(buf, c, sr, FADE_MS)
133
+ if buf.size >= target_samples:
134
+ yield (sr, buf)
135
+ # даём плэеру «дагуляць» без накладання
136
+ time.sleep(buf.size / float(sr))
137
+ buf = np.zeros((0,), dtype=np.float32)
138
+ if buf.size:
139
+ yield (sr, buf)
140
+ time.sleep(buf.size / float(sr))
141
+
142
+ def _bpe_prefixes(text: str, lang: str, step_tokens: int):
143
+ """
144
+ Вяртае паступовыя прэфіксы тэксту па BPE/субсловах, калі атрымліваецца.
145
+ Інакш — fallback па «псэўда-токенах» (словы+прабелы/пунктуацыя).
146
+ """
147
+ # 1) Спроба праз VoiceBpeTokenizer (калі ёсць encode/decode)
148
+ try:
149
+ # у вашым форку можа быць encode(text, lang=...), decode(ids, lang=...)
150
+ ids = tokenizer.encode(text, lang=lang)
151
+ n = len(ids)
152
+ for k in range(step_tokens, n + 1, step_tokens):
153
+ prefix = tokenizer.decode(ids[:k], lang=lang)
154
+ yield prefix
155
+ if n % step_tokens != 0:
156
+ yield tokenizer.decode(ids, lang=lang)
157
+ return
158
+ except Exception:
159
+ pass
160
+
161
+ # 2) Падстрахоўка: разбіць на «словы+знакі»
162
+ pseudo_tokens = re.findall(r"\S+|\s+", text)
163
+ buff = ""
164
+ for i in range(0, len(pseudo_tokens), step_tokens):
165
+ buff = "".join(pseudo_tokens[: i + step_tokens])
166
+ yield buff
167
+ if buff.strip() != text.strip():
168
+ yield text
169
+
170
+ def _stream_from_model_native(text: str, gpt_cond_latent, speaker_embedding, sr: int, lang: str):
171
+ """
172
+ Калі ў форку ёсць натыўны струмень (inference_stream) — карыстаемся ім.
173
+ Павінен yield'іць PCM фрагменты па меры дэкавання.
174
+ """
175
+ # Сфармуем kwargs у залежнасці ад подпісу функцыі
176
+ common_kwargs = dict(
177
+ text=text,
178
+ language=lang,
179
+ gpt_cond_latent=gpt_cond_latent,
180
+ speaker_embedding=speaker_embedding,
181
+ temperature=0.1,
182
+ length_penalty=1.0,
183
+ repetition_penalty=10.0,
184
+ top_k=10,
185
+ top_p=0.3,
186
+ )
187
+ sig = inspect.signature(XTTS_MODEL.inference_stream)
188
+ if "stream_chunk_size_s" in sig.parameters:
189
+ common_kwargs["stream_chunk_size_s"] = MIN_BUFFER_MS
190
+
191
+ gen = XTTS_MODEL.inference_stream(**common_kwargs)
192
+ raw_chunks = []
193
+ for out in gen:
194
+ cur = out["wav"] if isinstance(out, dict) and "wav" in out else np.asarray(out, dtype=np.float32)
195
+ cur = cur.astype(np.float32, copy=False)
196
+ raw_chunks.append(cur)
197
+ # выдаём дробнымі порцыямі з невялікім буферам
198
+ yield from _yield_buffered_chunks([cur], sr, MIN_BUFFER_MS)
199
+
200
+ # Сабраць «хвост» у адзін WAV з лёгкім cross-fade
201
+ if raw_chunks:
202
+ final_full = _merge_chunks_with_crossfade(raw_chunks, sr)
203
+ yield ("__FINAL__", final_full)
204
+
205
+ def _stream_fallback_incremental(text: str, gpt_cond_latent, speaker_embedding, sr: int, lang: str):
206
+ """
207
+ Fallback: павялічваем прэфікс тэксту па токенах і кожны раз
208
+ генеруем гукавыя дадаткі (толькі «хвост» новай версіі).
209
+ """
210
+ emitted = 0
211
+ last_full = np.zeros((0,), dtype=np.float32)
212
+
213
+ for prefix in _bpe_prefixes(text, lang, TOKENS_PER_STEP):
214
+ with torch.no_grad():
215
+ wav = XTTS_MODEL.inference(
216
+ text=prefix,
217
+ language=lang,
218
+ gpt_cond_latent=gpt_cond_latent,
219
+ speaker_embedding=speaker_embedding,
220
+ temperature=0.1,
221
+ length_penalty=1.0,
222
+ repetition_penalty=10.0,
223
+ top_k=10,
224
+ top_p=0.3,
225
+ )["wav"].astype(np.float32)
226
+
227
+ # бярэм толькі новую частку адносна ўжо аддадзенага
228
+ new_part = wav[emitted:]
229
+ if new_part.size > 0:
230
+ yield from _yield_buffered_chunks([new_part], sr, MIN_BUFFER_MS)
231
+ emitted = wav.size
232
+ last_full = wav
233
+
234
+ if last_full.size:
235
+ yield ("__FINAL__", last_full)
236
+
237
+ @spaces.GPU(duration=60)
238
+ def text_to_speech(belarusian_story, speaker_audio_file=None):
239
+ """
240
+ Патокавы вывад з мінімальнай затрымкай.
241
+ - Крок 1: латэнты голасу.
242
+ - Крок 2: спроба натыўнага streaming з мадэлі.
243
+ - Крок 3: fallback — інкрементальны прэфікс (токен-крокі).
244
+ Выхад для gr.Audio: шмат (sr, chunk) + у фінале шлях да поўнага WAV.
245
+ """
246
+ if not belarusian_story or str(belarusian_story).strip() == "":
247
+ raise gr.Error("Увядзі хоць нейкі тэкст 🙂")
248
+
249
+ # Голас па змаўчанні
250
+ if not speaker_audio_file or (
251
+ not isinstance(speaker_audio_file, str)
252
+ and getattr(speaker_audio_file, "name", "") == ""
253
+ ):
254
+ speaker_audio_file = default_voice_file
255
+
256
+ # Conditioning latents
257
+ try:
258
+ gpt_cond_latent, speaker_embedding = XTTS_MODEL.get_conditioning_latents(
259
+ audio_path=speaker_audio_file,
260
+ gpt_cond_len=XTTS_MODEL.config.gpt_cond_len,
261
+ max_ref_length=XTTS_MODEL.config.max_ref_len,
262
+ sound_norm_refs=XTTS_MODEL.config.sound_norm_refs,
263
+ )
264
+ except Exception as e:
265
+ raise gr.Error(f"Памылка пры атрыманні латэнтаў голасу: {e}")
266
+
267
+ lang = "be"
268
+ full_audio = None
269
+
270
+ # 1) Спачатку — натыўны паток, калі ёсць
271
+ try:
272
+ if hasattr(XTTS_MODEL, "inference_stream"):
273
+ for out in _stream_from_model_native(
274
+ belarusian_story, gpt_cond_latent, speaker_embedding, sampling_rate, lang
275
+ ):
276
+ if isinstance(out, tuple) and out and out[0] == "__FINAL__":
277
+ full_audio = out[1]
278
+ else:
279
+ yield out
280
+ else:
281
+ raise AttributeError("No native inference_stream in this build.")
282
+ except Exception:
283
+ # 2) fallback — інкрементальны прэфікс (токен-крокі)
284
+ for out in _stream_fallback_incremental(
285
+ belarusian_story, gpt_cond_latent, speaker_embedding, sampling_rate, lang
286
+ ):
287
+ if isinstance(out, tuple) and out and out[0] == "__FINAL__":
288
+ full_audio = out[1]
289
+ else:
290
+ yield out
291
+
292
+ if full_audio is None:
293
+ raise gr.Error("Нічога не згенеравана. Праверце ўваходныя даныя або лагі.")
294
+
295
+ # Фінальны WAV у temp-файл
296
+ try:
297
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
298
+ write(temp_file.name, sampling_rate, full_audio.astype(np.float32))
299
+ yield temp_file.name
300
+ except Exception as e:
301
+ raise gr.Error(f"Памылка пры запісе фінальнага WAV: {e}")
302
+
303
+ # ---------------------------------------------------------
304
+ # 5) Прыклады (тэкст + файл голасу)
305
+ # ---------------------------------------------------------
306
+ examples = [
307
+ [
308
+ "Такім чынам, клуб стаў уладальнікам усіх існых на сёння міжнародных трафеяў паўднёваамерыканскага футболу.",
309
+ "Nestarka.wav",
310
+ ],
311
+ [
312
+ "Яму не ўдалося палепшыць фінансавае становішча каралеўства, а, наадварот, прыйшлося распрадаваць каштоўнасці чэшскай кароны.",
313
+ "muzh.wav",
314
+ ],
315
+ [
316
+ "Кампілятарамі называюць праграмы, якія пераўтвараюць код вышэйшага ўзроўню ў код ніжэйшага ўзроўню.",
317
+ "chunk_100.wav",
318
+ ],
319
+ [
320
+ "Акрамя таго, ліхачы аддаюць перавагу рэгі, хіп-хопу і класічнай музыцы.",
321
+ "d1015.mp3",
322
+ ],
323
+ [
324
+ "Позірк можа быць уважлівым, зацікаўленым, захопленым, але бывае і нахабным, задзірлівым, пагардлівым, напышлівым.",
325
+ "donarka_ench.wav",
326
+ ],
327
+ [
328
+ "Такі нават шчыры, ці што: родная мова народу – трасянка, а беларуская яму чужая!",
329
+ "muzhcynski.wav",
330
+ ],
331
+ ]
332
+
333
+ analytics_script = """
334
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-TKDCRCQ7FK"></script>
335
+ <script>
336
+ window.dataLayer = window.dataLayer || [];
337
+ function gtag(){dataLayer.push(arguments);}
338
+ gtag('js', new Date());
339
+ gtag('config', 'G-TKDCRCQ7FK');
340
+ </script>
341
+ """
342
+
343
+ # ---------------------------------------------------------
344
+ # 6) Gradio UI (аўтапрайграванне, мінімальная затрымка)
345
+ # ---------------------------------------------------------
346
+ with gr.Blocks() as demo:
347
+ gr.HTML(analytics_script)
348
+ gr.Interface(
349
+ fn=text_to_speech, # генератар
350
+ inputs=[
351
+ gr.Textbox(lines=5, label="Тэкст на беларускай мове"),
352
+ gr.Audio(
353
+ type="filepath",
354
+ label="Прыклад голасу (без іншых гукаў) не карацей 7 секунд",
355
+ interactive=True,
356
+ ),
357
+ ],
358
+ outputs=gr.Audio(
359
+ type="filepath", # прымае (sr, ndarray) падчас стриму і фінальны шлях у фінале
360
+ label="Згенераванае аўдыя (па токенах, мінімальная затрымка)",
361
+ autoplay=True,
362
+ ),
363
+ title="Belarusian TTS — Token Streaming (мінімальная затрымка)",
364
+ description="""
365
+ <p>Вывод гуку <b>па токенах</b> з буферам ~50&nbsp;мс. Калі мадэль падтрымлівае <code>inference_stream</code> — выкарыстоўваецца ён; інакш працуе інкрементальны fallback па BPE.</p>
366
+ """,
367
+ examples=examples,
368
+ cache_examples=False,
369
+ allow_flagging="never",
370
+ )
371
+
372
+ if __name__ == "__main__":
373
+ demo.launch()