pnnbao-ump commited on
Commit
a168dcd
·
verified ·
1 Parent(s): 9275718

True frame-level streaming via engine.infer_stream (@spaces.GPU generator)

Browse files
Files changed (1) hide show
  1. app.py +28 -40
app.py CHANGED
@@ -19,7 +19,6 @@ import spaces
19
 
20
  from vieneu import Vieneu
21
  from vieneu_utils.core_utils import split_text_into_chunks
22
- from vieneu_utils.phonemize_text import phonemize_text_with_emotions
23
 
24
  # ── Load model once, on GPU (CUDA emulation makes this valid at startup) ───────
25
  print("⏳ Loading VieNeu-TTS v3 Turbo (PyTorch / CUDA) ...")
@@ -178,54 +177,42 @@ def synthesize_conversation(script, silence_s, temperature, max_new_frames):
178
  return (SR, full), info
179
 
180
 
181
- # ── Streaming (Kokoro pattern: CPU generator drives a streaming gr.Audio, and
182
- # each text chunk's GPU work is a short, separate @spaces.GPU call so ZeroGPU
183
- # can allocate/release per chunk). Latency to first audio ≈ first chunk time. ─
184
- @spaces.GPU(duration=30)
185
- def _encode_ref_gpu(ref_audio):
186
- """Encode a reference clip to MOSS codes on the GPU (for streamed cloning)."""
187
- return np.asarray(tts.encode_reference(ref_audio))
188
-
189
-
190
  @spaces.GPU(duration=_gpu_duration)
191
- def _infer_chunk_gpu(phonemes, ref_codes, voice_token_id, temperature, top_k,
192
- top_p, repetition_penalty, max_new_frames):
193
- wav = tts.engine.infer(
194
- text="", phonemes=phonemes, ref_codes=ref_codes,
195
- emotion="natural", voice_token_id=voice_token_id,
196
- temperature=float(temperature), top_k=int(top_k), top_p=float(top_p),
197
- repetition_penalty=float(repetition_penalty), max_new_frames=int(max_new_frames),
198
- )
199
- return np.asarray(wav, dtype=np.float32)
200
-
201
-
202
  def stream_synthesize(text, voice, ref_audio, temperature, top_k, top_p,
203
  repetition_penalty, max_new_frames, max_chars):
204
  text = (text or "").strip()
205
  if not text:
206
  raise gr.Error("Vui lòng nhập văn bản cần đọc.")
207
 
208
- # Resolve the voice once: preset → reserved token + cached codes (CPU only);
209
- # cloning → encode the reference on the GPU a single time up front.
210
  if ref_audio:
211
- ref_codes, voice_token_id = _encode_ref_gpu(ref_audio), None
212
  else:
213
  ref_codes, voice_token_id = tts._resolve_v3_ref(voice, None, None)
214
 
215
- # Smaller chunks = lower latency to first audio.
216
- chunks = split_text_into_chunks(text, max_chars=min(int(max_chars), 140))
217
-
218
  first = True
219
- for chunk in chunks:
220
- phonemes = phonemize_text_with_emotions(chunk)
221
- wav = _infer_chunk_gpu(phonemes, ref_codes, voice_token_id, temperature,
222
- top_k, top_p, repetition_penalty, max_new_frames)
223
- yield (SR, wav)
224
- if first:
225
- # Workaround for the Gradio streaming quirk where the first chunk can
226
- # be dropped; a 1-sample silence flushes the player (same as Kokoro).
227
- first = False
228
- yield (SR, np.zeros(1, dtype=np.float32))
 
 
 
 
 
229
 
230
 
231
  # ── UI ────────────────────────────────────────────────────────────────────────
@@ -294,7 +281,7 @@ with gr.Blocks(theme=theme, title="VieNeu-TTS v3 Turbo") as demo:
294
  temperature_in = gr.Slider(0.1, 1.5, value=0.8, step=0.05, label="temperature")
295
  top_p_in = gr.Slider(0.1, 1.0, value=0.95, step=0.01, label="top_p")
296
  with gr.Row():
297
- top_k_in = gr.Slider(1, 100, value=50, step=1, label="top_k")
298
  rep_pen_in = gr.Slider(1.0, 2.0, value=1.2, step=0.05, label="repetition_penalty")
299
  with gr.Row():
300
  max_frames_in = gr.Slider(50, 1200, value=300, step=10, label="max_new_frames (mỗi đoạn)")
@@ -313,8 +300,9 @@ with gr.Blocks(theme=theme, title="VieNeu-TTS v3 Turbo") as demo:
313
  stream_btn = gr.Button("⚡ Phát trực tiếp", variant="primary")
314
  stop_btn = gr.Button("⏹ Dừng", variant="stop")
315
  gr.Markdown(
316
- "_Phát dần theo từng câu (độ trễ thấp). Lần bấm **Phát trực tiếp** "
317
- "đầu tiên đôi khi Gradio nuốt mất chunk đầu — bấm lại là được._"
 
318
  )
319
 
320
  run_btn.click(
 
19
 
20
  from vieneu import Vieneu
21
  from vieneu_utils.core_utils import split_text_into_chunks
 
22
 
23
  # ── Load model once, on GPU (CUDA emulation makes this valid at startup) ───────
24
  print("⏳ Loading VieNeu-TTS v3 Turbo (PyTorch / CUDA) ...")
 
177
  return (SR, full), info
178
 
179
 
180
+ # ── True frame-level streaming ────────────────────────────────────────────────
181
+ # The engine's ``infer_stream`` yields the waveform in small ~chunk_frames pieces
182
+ # (with an adaptive lead-in) as the model generates — that's what makes playback
183
+ # smooth, not chunky. On ZeroGPU the whole thing must hold the GPU, so this is a
184
+ # single ``@spaces.GPU`` *generator*: it keeps the GPU for the session and pushes
185
+ # each small piece out as it is produced. Long text is split by ``max_chars`` and
186
+ # each chunk is streamed in turn (so arbitrarily long input still works).
 
 
187
  @spaces.GPU(duration=_gpu_duration)
 
 
 
 
 
 
 
 
 
 
 
188
  def stream_synthesize(text, voice, ref_audio, temperature, top_k, top_p,
189
  repetition_penalty, max_new_frames, max_chars):
190
  text = (text or "").strip()
191
  if not text:
192
  raise gr.Error("Vui lòng nhập văn bản cần đọc.")
193
 
194
+ # Preset → reserved token + cached codes; cloning → encode the reference once.
 
195
  if ref_audio:
196
+ ref_codes, voice_token_id = tts.encode_reference(ref_audio), None
197
  else:
198
  ref_codes, voice_token_id = tts._resolve_v3_ref(voice, None, None)
199
 
 
 
 
200
  first = True
201
+ for chunk in split_text_into_chunks(text, max_chars=int(max_chars)):
202
+ for wav in tts.engine.infer_stream(
203
+ text=chunk, ref_codes=ref_codes, voice_token_id=voice_token_id,
204
+ emotion="natural", temperature=float(temperature), top_k=int(top_k),
205
+ top_p=float(top_p), repetition_penalty=float(repetition_penalty),
206
+ max_new_frames=int(max_new_frames), chunk_frames=25,
207
+ ):
208
+ wav = np.asarray(wav, dtype=np.float32)
209
+ if wav.size == 0:
210
+ continue
211
+ yield (SR, wav)
212
+ if first:
213
+ # Flush the first piece (Gradio streaming sometimes drops it).
214
+ first = False
215
+ yield (SR, np.zeros(1, dtype=np.float32))
216
 
217
 
218
  # ── UI ────────────────────────────────────────────────────────────────────────
 
281
  temperature_in = gr.Slider(0.1, 1.5, value=0.8, step=0.05, label="temperature")
282
  top_p_in = gr.Slider(0.1, 1.0, value=0.95, step=0.01, label="top_p")
283
  with gr.Row():
284
+ top_k_in = gr.Slider(1, 100, value=25, step=1, label="top_k")
285
  rep_pen_in = gr.Slider(1.0, 2.0, value=1.2, step=0.05, label="repetition_penalty")
286
  with gr.Row():
287
  max_frames_in = gr.Slider(50, 1200, value=300, step=10, label="max_new_frames (mỗi đoạn)")
 
300
  stream_btn = gr.Button("⚡ Phát trực tiếp", variant="primary")
301
  stop_btn = gr.Button("⏹ Dừng", variant="stop")
302
  gr.Markdown(
303
+ "_Phát theo thời gian thực, mượt từng đoạn ngắn ngay khi model sinh. "
304
+ "Lần bấm **Phát trực tiếp** đầu tiên đôi khi Gradio nuốt mất đoạn đầu "
305
+ "— bấm lại là được._"
306
  )
307
 
308
  run_btn.click(