minhahwang Copilot commited on
Commit
fa6ebf7
·
1 Parent(s): e942657

perf: reduce TTS latency - shorter chunks + sub-segment streaming

Browse files

- Split long sentences at clause boundaries (~120 char max per chunk)
- Add synthesize_custom_voice_streaming() for partial audio yields
- Shorter chunks = faster generation per chunk = smoother playback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Files changed (2) hide show
  1. tts.py +46 -7
  2. voice_clone.py +24 -0
tts.py CHANGED
@@ -20,13 +20,44 @@ import numpy as np
20
  logger = logging.getLogger(__name__)
21
 
22
  _SENTENCE_RE = re.compile(r'(?<=[.!?;])\s+')
 
23
  _SENTINEL = object()
24
 
 
 
 
25
 
26
  def split_into_chunks(text: str) -> list[str]:
27
- """Split text into sentence-level chunks suitable for TTS streaming."""
28
- parts = _SENTENCE_RE.split(text.strip())
29
- return [p.strip() for p in parts if p.strip()]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
 
32
  def generate_audio_stream(
@@ -93,13 +124,21 @@ def _start_qwen_worker(chunks, profile_id, chunk_q):
93
 
94
 
95
  def _start_custom_voice_worker(chunks, speaker, chunk_q):
96
- """Background thread: synthesize chunks with Qwen3-TTS CustomVoice (0.6B)."""
 
97
  def _worker():
98
- from voice_clone import synthesize_custom_voice
 
99
  for i, stmt in enumerate(chunks):
100
  try:
101
- wav, _sr = synthesize_custom_voice(stmt, speaker=speaker)
102
- chunk_q.put((i, wav, None))
 
 
 
 
 
 
103
  except Exception as exc:
104
  logger.exception("CustomVoice synthesis failed on chunk %d", i)
105
  chunk_q.put((i, None, str(exc)))
 
20
  logger = logging.getLogger(__name__)
21
 
22
  _SENTENCE_RE = re.compile(r'(?<=[.!?;])\s+')
23
+ _CLAUSE_RE = re.compile(r'(?<=[,;:\u2014])\s+|(?<=\.)\s+|(?<=[!?])\s+')
24
  _SENTINEL = object()
25
 
26
+ # Target chunk length — shorter chunks = lower latency per chunk
27
+ _MAX_CHUNK_CHARS = 120
28
+
29
 
30
  def split_into_chunks(text: str) -> list[str]:
31
+ """Split text into short chunks suitable for low-latency TTS streaming.
32
+
33
+ Splits by sentence first, then further splits long sentences by clause
34
+ boundaries (commas, semicolons, em-dashes) to keep each chunk under
35
+ ~120 chars for faster TTS generation.
36
+ """
37
+ sentences = _SENTENCE_RE.split(text.strip())
38
+ chunks = []
39
+ for sent in sentences:
40
+ sent = sent.strip()
41
+ if not sent:
42
+ continue
43
+ if len(sent) <= _MAX_CHUNK_CHARS:
44
+ chunks.append(sent)
45
+ else:
46
+ # Split long sentence into clauses
47
+ clauses = _CLAUSE_RE.split(sent)
48
+ current = ""
49
+ for clause in clauses:
50
+ clause = clause.strip()
51
+ if not clause:
52
+ continue
53
+ if current and len(current) + len(clause) + 1 > _MAX_CHUNK_CHARS:
54
+ chunks.append(current.strip())
55
+ current = clause
56
+ else:
57
+ current = f"{current} {clause}".strip() if current else clause
58
+ if current.strip():
59
+ chunks.append(current.strip())
60
+ return [c for c in chunks if c]
61
 
62
 
63
  def generate_audio_stream(
 
124
 
125
 
126
  def _start_custom_voice_worker(chunks, speaker, chunk_q):
127
+ """Background thread: synthesize chunks with Qwen3-TTS CustomVoice (0.6B).
128
+ Uses sub-segment streaming for lower latency — yields partial audio as generated."""
129
  def _worker():
130
+ from voice_clone import synthesize_custom_voice_streaming
131
+ import numpy as np
132
  for i, stmt in enumerate(chunks):
133
  try:
134
+ segments = []
135
+ for seg, _sr in synthesize_custom_voice_streaming(stmt, speaker=speaker):
136
+ segments.append(seg)
137
+ if segments:
138
+ wav = np.concatenate(segments)
139
+ chunk_q.put((i, wav, None))
140
+ else:
141
+ chunk_q.put((i, np.zeros(0, dtype=np.float32), None))
142
  except Exception as exc:
143
  logger.exception("CustomVoice synthesis failed on chunk %d", i)
144
  chunk_q.put((i, None, str(exc)))
voice_clone.py CHANGED
@@ -390,6 +390,30 @@ def synthesize_cloned(text: str, profile_id: str) -> tuple[np.ndarray, int]:
390
  return wav, sample_rate
391
 
392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  def synthesize_custom_voice(
394
  text: str, speaker: str = "vivian", language: str = "english"
395
  ) -> tuple[np.ndarray, int]:
 
390
  return wav, sample_rate
391
 
392
 
393
+ def synthesize_custom_voice_streaming(
394
+ text: str, speaker: str = "vivian", language: str = "english"
395
+ ):
396
+ """
397
+ Synthesize text with the 0.6B CustomVoice model, yielding partial audio segments
398
+ as they become available for lower latency.
399
+ Yields (wav_segment, sample_rate) tuples.
400
+ """
401
+ model = get_custom_voice_model()
402
+
403
+ audio_list, sample_rate = model.generate_custom_voice(
404
+ text=text,
405
+ speaker=speaker,
406
+ language=language,
407
+ non_streaming_mode=False,
408
+ **GENERATION_PARAMS,
409
+ )
410
+
411
+ # Yield each segment as it was generated
412
+ for segment in audio_list:
413
+ if segment is not None and len(segment) > 0:
414
+ yield segment, sample_rate
415
+
416
+
417
  def synthesize_custom_voice(
418
  text: str, speaker: str = "vivian", language: str = "english"
419
  ) -> tuple[np.ndarray, int]: