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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +225 -144
app.py CHANGED
@@ -3,18 +3,19 @@
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)
@@ -74,13 +75,13 @@ sampling_rate = int(XTTS_MODEL.config.audio["sample_rate"])
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:
@@ -94,8 +95,7 @@ def _crossfade_concat(a: np.ndarray, b: np.ndarray, sr: int, fade_ms: float) ->
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)
@@ -105,143 +105,221 @@ def _crossfade_concat(a: np.ndarray, b: np.ndarray, sr: int, fade_ms: float) ->
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("Увядзі хоць нейкі тэкст 🙂")
@@ -264,44 +342,46 @@ def text_to_speech(belarusian_story, speaker_audio_file=None):
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
  [
@@ -341,7 +421,7 @@ analytics_script = """
341
  """
342
 
343
  # ---------------------------------------------------------
344
- # 6) Gradio UI (аўтапрайграванне, мінімальная затрымка)
345
  # ---------------------------------------------------------
346
  with gr.Blocks() as demo:
347
  gr.HTML(analytics_script)
@@ -356,13 +436,14 @@ with gr.Blocks() as demo:
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,
 
3
 
4
  import os
5
  import sys
6
+ import re
7
  import time
8
  import tempfile
9
  import subprocess
10
  import inspect
11
+ from typing import Iterator, Iterable, Optional, Tuple, Any
12
 
13
  import spaces
14
  import gradio as gr
15
  import torch
16
+ import numpy as np
17
  from huggingface_hub import hf_hub_download
18
  from scipy.io.wavfile import write
 
19
 
20
  # ---------------------------------------------------------
21
  # 1) Клануем і падключаем coqui-ai-TTS (fork з падтрымкай BE)
 
75
  tokenizer = VoiceBpeTokenizer(vocab_file=vocab_file)
76
  XTTS_MODEL.tokenizer = tokenizer
77
 
78
+ # =========================================================
79
+ # 4) «Як у прыкладзе»: патч Xtts.generate / sample_stream
80
+ # =========================================================
 
81
 
82
+ # Канстанты латэнтнасці/буферу
83
+ MIN_BUFFER_S = 0.050 # ~50 ms цэлявы буфер для аўдыя
84
+ FADE_MS = 8e-3 # кароткі cross-fade паміж чанкамі
85
  TOKENS_PER_STEP = 4 # памер кроку «токенаў» у fallback (BPE/субсловы)
86
 
87
  def _seconds_to_samples(sec: float, sr: int) -> int:
 
95
  return a.astype(np.float32, copy=False)
96
  a = a.astype(np.float32, copy=False)
97
  b = b.astype(np.float32, copy=False)
98
+ fade_n = min(_seconds_to_samples(fade_ms, sr), 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)
 
105
  rest = b[fade_n:]
106
  return np.concatenate([head, tail, rest], axis=0)
107
 
108
+ def _bpe_prefixes(text: str, lang: str, step_tokens: int) -> Iterable[str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  """
110
+ Вяртае прэфіксы па BPE/субсловах; калі encode/decode недаступны — псэўда-токены (словы+прабелы).
 
111
  """
112
+ # 1) BPE праз VoiceBpeTokenizer, калі падтрымліваецца
113
  try:
 
114
  ids = tokenizer.encode(text, lang=lang)
115
  n = len(ids)
116
  for k in range(step_tokens, n + 1, step_tokens):
117
+ yield tokenizer.decode(ids[:k], lang=lang)
 
118
  if n % step_tokens != 0:
119
  yield tokenizer.decode(ids, lang=lang)
120
  return
121
  except Exception:
122
  pass
123
+ # 2) Падстрахоўка: «словы+раздзяляльнікі»
 
124
  pseudo_tokens = re.findall(r"\S+|\s+", text)
125
+ acc = ""
126
  for i in range(0, len(pseudo_tokens), step_tokens):
127
+ acc = "".join(pseudo_tokens[: i + step_tokens])
128
+ yield acc
129
+ if acc.strip() != text.strip():
130
  yield text
131
 
132
+ def _native_stream(
133
+ model: Xtts,
134
+ text: str,
135
+ language: str,
136
+ gpt_cond_latent: Any,
137
+ speaker_embedding: Any,
138
+ **gen_kwargs,
139
+ ) -> Iterator[np.ndarray]:
140
  """
141
+ Натыўны паток, калі ў форку ёсць model.inference_stream(...)-> iterator of PCM/ndarray.
 
142
  """
143
+ sig = inspect.signature(model.inference_stream)
144
+ call_kwargs = dict(
145
  text=text,
146
+ language=language,
147
  gpt_cond_latent=gpt_cond_latent,
148
  speaker_embedding=speaker_embedding,
 
 
 
 
 
149
  )
150
+ # Перадаём тыповыя параметры генерацыі, калі яны ёсць у подпісе
151
+ for k in ("temperature", "length_penalty", "repetition_penalty", "top_k", "top_p"):
152
+ if k in gen_kwargs and k in sig.parameters:
153
+ call_kwargs[k] = gen_kwargs[k]
154
+ # Памер стрим-чанка (секунды), калі ёсць у подпісе
155
  if "stream_chunk_size_s" in sig.parameters:
156
+ call_kwargs["stream_chunk_size_s"] = float(gen_kwargs.get("min_buffer_s", MIN_BUFFER_S))
157
+
158
+ generator = model.inference_stream(**call_kwargs)
159
+ for out in generator:
160
+ arr = out["wav"] if isinstance(out, dict) and "wav" in out else np.asarray(out, dtype=np.float32)
161
+ yield arr.astype(np.float32, copy=False)
162
+
163
+ def _fallback_incremental(
164
+ model: Xtts,
165
+ text: str,
166
+ language: str,
167
+ gpt_cond_latent: Any,
168
+ speaker_embedding: Any,
169
+ tokens_per_step: int,
170
+ **gen_kwargs,
171
+ ) -> Iterator[np.ndarray]:
 
172
  """
173
+ Fallback: павялічваем прэфікс па токенах і вяртаем ТОЛЬКІ «новую» частку гуку.
 
174
  """
175
  emitted = 0
176
+ for prefix in _bpe_prefixes(text, language, tokens_per_step):
 
 
177
  with torch.no_grad():
178
+ wav = model.inference(
179
  text=prefix,
180
+ language=language,
181
  gpt_cond_latent=gpt_cond_latent,
182
  speaker_embedding=speaker_embedding,
183
+ temperature=gen_kwargs.get("temperature", 0.1),
184
+ length_penalty=gen_kwargs.get("length_penalty", 1.0),
185
+ repetition_penalty=gen_kwargs.get("repetition_penalty", 10.0),
186
+ top_k=gen_kwargs.get("top_k", 10),
187
+ top_p=gen_kwargs.get("top_p", 0.3),
188
  )["wav"].astype(np.float32)
 
 
189
  new_part = wav[emitted:]
190
+ emitted = wav.size
191
+ if new_part.size:
192
+ yield new_part
193
+
194
+ class NewTTSGenerationMixin:
195
+ """
196
+ «Як у transformers-stream-generator»: дадаём generate() і sample_stream()
197
+ у мадэль Xtts. return: або поўны wav (ndarray), або ітэратар чанкаў (ndarray).
198
+ """
199
+
200
+ @torch.inference_mode()
201
+ def generate(
202
+ self: Xtts,
203
+ text: Optional[str] = None,
204
+ *,
205
+ do_stream: bool = False,
206
+ language: str = "be",
207
+ gpt_cond_latent: Any = None,
208
+ speaker_embedding: Any = None,
209
+ min_buffer_s: float = MIN_BUFFER_S,
210
+ tokens_per_step: int = TOKENS_PER_STEP,
211
+ **gen_kwargs,
212
+ ):
213
+ """
214
+ Калі do_stream=False -> вяртае поўны wav (ndarray).
215
+ Калі do_stream=True -> вяртае генератар чанкаў wav (Iterator[np.ndarray]).
216
+ """
217
+ assert isinstance(text, str) and text.strip(), "text is required"
218
+ # Блакіруючы рэжым — адным махам
219
+ if not do_stream:
220
+ out = self.inference(
221
+ text=text,
222
+ language=language,
223
+ gpt_cond_latent=gpt_cond_latent,
224
+ speaker_embedding=speaker_embedding,
225
+ temperature=gen_kwargs.get("temperature", 0.1),
226
+ length_penalty=gen_kwargs.get("length_penalty", 1.0),
227
+ repetition_penalty=gen_kwargs.get("repetition_penalty", 10.0),
228
+ top_k=gen_kwargs.get("top_k", 10),
229
+ top_p=gen_kwargs.get("top_p", 0.3),
230
+ )
231
+ return out["wav"].astype(np.float32)
232
+
233
+ # Стрымінгавы рэжым — як у прыкладзе: асобны генератар
234
+ return self.sample_stream(
235
+ text=text,
236
+ language=language,
237
+ gpt_cond_latent=gpt_cond_latent,
238
+ speaker_embedding=speaker_embedding,
239
+ min_buffer_s=min_buffer_s,
240
+ tokens_per_step=tokens_per_step,
241
+ **gen_kwargs,
242
+ )
243
+
244
+ @torch.inference_mode()
245
+ def sample_stream(
246
+ self: Xtts,
247
+ *,
248
+ text: str,
249
+ language: str,
250
+ gpt_cond_latent: Any,
251
+ speaker_embedding: Any,
252
+ min_buffer_s: float = MIN_BUFFER_S,
253
+ tokens_per_step: int = TOKENS_PER_STEP,
254
+ **gen_kwargs,
255
+ ) -> Iterator[np.ndarray]:
256
+ """
257
+ Вяртае генератар чанкаў wav. Стараемся даваць маленькія кавалкі як мага часцей.
258
+ """
259
+ # 1) Калі ёсць натыўны паток — проста перасылаем яго
260
+ if hasattr(self, "inference_stream"):
261
+ for chunk in _native_stream(
262
+ self, text, language, gpt_cond_latent, speaker_embedding, min_buffer_s=min_buffer_s, **gen_kwargs
263
+ ):
264
+ # тут мы не чакаем — верхні слой сам злімітуе плынь буферам
265
+ yield chunk
266
+ return
267
+
268
+ # 2) Інакш — інкрементальны fallback па токенах
269
+ for chunk in _fallback_incremental(
270
+ self, text, language, gpt_cond_latent, speaker_embedding, tokens_per_step, **gen_kwargs
271
+ ):
272
+ yield chunk
273
+
274
 
275
+ def init_stream_support():
276
+ """Прапатчыць Xtts, дадаўшы generate/sample_stream (як у прыкладзе)."""
277
+ Xtts.generate = NewTTSGenerationMixin.generate
278
+ Xtts.sample_stream = NewTTSGenerationMixin.sample_stream
279
+
280
+ # Актывуем стрим-падтрымку
281
+ init_stream_support()
282
+
283
+ # ---------------------------------------------------------
284
+ # 5) Службовыя функцыі для Gradio (буферы, cross-fade, затрымкі)
285
+ # ---------------------------------------------------------
286
+ def _yield_buffered_chunks_for_gradio(
287
+ chunks: Iterable[np.ndarray],
288
+ sr: int,
289
+ target_s: float = MIN_BUFFER_S,
290
+ ) -> Iterator[Tuple[int, np.ndarray]]:
291
+ """
292
+ Назапашваем невялікі буфер (~50 ms), каб плэер Gradio паспеў «дагуляць»
293
+ і не накладваў наступны чанк.
294
+ """
295
+ target_samples = _seconds_to_samples(target_s, sr)
296
+ buf = np.zeros((0,), dtype=np.float32)
297
+ for c in chunks:
298
+ c = np.asarray(c, dtype=np.float32)
299
+ if c.size == 0:
300
+ continue
301
+ if buf.size == 0:
302
+ buf = c
303
+ else:
304
+ buf = _crossfade_concat(buf, c, sr, FADE_MS)
305
+ if buf.size >= target_samples:
306
+ yield (sr, buf)
307
+ time.sleep(buf.size / float(sr))
308
+ buf = np.zeros((0,), dtype=np.float32)
309
+ if buf.size:
310
+ yield (sr, buf)
311
+ time.sleep(buf.size / float(sr))
312
 
313
+ # ---------------------------------------------------------
314
+ # 6) Асноўная функцыя TTS для Gradio (як у цябе, але праз model.generate do_stream)
315
+ # ---------------------------------------------------------
316
  @spaces.GPU(duration=60)
317
  def text_to_speech(belarusian_story, speaker_audio_file=None):
318
  """
319
+ Streaming для gr.Audio:
320
+ - падобна да прыкладу з transformers-stream-generator: model.generate(..., do_stream=True)
321
+ - аддаём невялікія чанкі (sr, chunk) з мінімальнай затрымкай;
322
+ - у фінале шлях да поўнага WAV.
 
323
  """
324
  if not belarusian_story or str(belarusian_story).strip() == "":
325
  raise gr.Error("Увядзі хоць нейкі тэкст 🙂")
 
342
  except Exception as e:
343
  raise gr.Error(f"Памылка пры атрыманні латэнтаў голасу: {e}")
344
 
345
+ # --- Генератар па аналагіі з .generate(... do_stream=True) ---
346
+ generator = XTTS_MODEL.generate(
347
+ text=str(belarusian_story).strip(),
348
+ do_stream=True,
349
+ language="be",
350
+ gpt_cond_latent=gpt_cond_latent,
351
+ speaker_embedding=speaker_embedding,
352
+ min_buffer_s=MIN_BUFFER_S,
353
+ tokens_per_step=TOKENS_PER_STEP,
354
+ temperature=0.1,
355
+ length_penalty=1.0,
356
+ repetition_penalty=10.0,
357
+ top_k=10,
358
+ top_p=0.3,
359
+ )
360
+
361
+ # Будзем назапашваць увесь аўдыё для фінальнага WAV
362
+ full_audio_chunks: list[np.ndarray] = []
363
 
364
+ # Аддаём у Gradio дробныя порцыі з невялікім буферам і рэальным «сном»
365
+ for sr, chunk in _yield_buffered_chunks_for_gradio(generator, sampling_rate, MIN_BUFFER_S):
366
+ full_audio_chunks.append(chunk)
367
+ yield (sr, chunk)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
 
369
+ # Гатовы поўны WAV
370
+ if not full_audio_chunks:
371
  raise gr.Error("Нічога не згенеравана. Праверце ўваходныя даныя або лагі.")
372
+ full_audio = full_audio_chunks[0]
373
+ for i in range(1, len(full_audio_chunks)):
374
+ full_audio = _crossfade_concat(full_audio, full_audio_chunks[i], sampling_rate, FADE_MS)
375
 
 
376
  try:
377
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
378
+ write(tmp.name, sampling_rate, full_audio.astype(np.float32))
379
+ yield tmp.name
380
  except Exception as e:
381
  raise gr.Error(f"Памылка пры запісе фінальнага WAV: {e}")
382
 
383
  # ---------------------------------------------------------
384
+ # 7) Прыклады (тэкст + файл голасу)
385
  # ---------------------------------------------------------
386
  examples = [
387
  [
 
421
  """
422
 
423
  # ---------------------------------------------------------
424
+ # 8) Gradio UI (аўтапрайграванне)
425
  # ---------------------------------------------------------
426
  with gr.Blocks() as demo:
427
  gr.HTML(analytics_script)
 
436
  ),
437
  ],
438
  outputs=gr.Audio(
439
+ type="filepath", # падчас стриму (sr, ndarray); у фінале — шлях
440
  label="Згенераванае аўдыя (па токенах, мінімальная затрымка)",
441
  autoplay=True,
442
  ),
443
+ title="Belarusian TTS — Token Streaming (як у transformers-stream-generator)",
444
  description="""
445
+ <p>Мадэль <code>Xtts</code> мае метады <code>generate()</code> і <code>sample_stream()</code>, як у прыкладзе.
446
+ Калі даступны <code>inference_stream</code>, выкарыстоўваем яго; інакш — інкрементальна па «токенах» з ~50&nbsp;мс буферам.</p>
447
  """,
448
  examples=examples,
449
  cache_examples=False,