minhahwang Copilot commited on
Commit
af03300
·
1 Parent(s): b714046

refactor: consolidate to Base 1.7B only, remove CustomVoice

Browse files

- Remove get_custom_voice_model() and _MODEL_MODE checks
- Add profile_id_override param to create_voice_profile()
- Update Profile page: 2 models, Base 1.7B as single TTS engine
- Upload vivian_reference.wav to HF Space

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

Files changed (3) hide show
  1. app.py +7 -39
  2. test_qa_audio_check.wav +3 -0
  3. voice_clone.py +40 -62
app.py CHANGED
@@ -409,9 +409,9 @@ else:
409
  print("[MomsVoice] No saved voice profile — using stock voice as default.")
410
 
411
  # Preload models at startup for fast response
412
- print("[MomsVoice] Preloading Qwen3-TTS 0.6B CustomVoice model...")
413
- from voice_clone import get_custom_voice_model
414
- get_custom_voice_model()
415
  print("[MomsVoice] TTS model ready.")
416
 
417
  print("[MomsVoice] Preloading LFM2.5-Audio-1.5B Q&A model...")
@@ -419,38 +419,6 @@ from inference_lfm import get_lfm_model
419
  get_lfm_model()
420
  print("[MomsVoice] LFM Q&A model ready. All models preloaded.")
421
 
422
- # Pre-generate first paragraph audio for all stories (stock voice) for instant start
423
- import threading
424
-
425
- def _warmup_first_paragraphs():
426
- """Background: pre-generate first 5 paragraphs of each story with stock voice to disk cache."""
427
- import logging as _log
428
- from tts import _get_cached_audio, _save_cached_audio, _synthesize_single
429
- _logger = _log.getLogger("warmup")
430
- WARMUP_PARAS = 5
431
- for book in mock_books:
432
- try:
433
- paras = load_paragraphs(book["story_path"])
434
- if not paras:
435
- continue
436
- # Pre-generate first 5 paragraphs
437
- target_paras = paras[:WARMUP_PARAS]
438
- all_chunks = split_into_chunks("\n\n".join(target_paras))
439
- generated = 0
440
- for chunk in all_chunks:
441
- if _get_cached_audio(chunk, None) is not None:
442
- continue # Already cached
443
- wav, sr = _synthesize_single(chunk, None)
444
- _save_cached_audio(chunk, None, wav, sr)
445
- generated += 1
446
- _logger.info("Warmed: %s (%d chunks, %d new)", book["title"], len(all_chunks), generated)
447
- except Exception as e:
448
- _logger.warning("Warmup failed for %s: %s", book.get("title", "?"), e)
449
- _logger.info("All stories warmed (first %d paragraphs each).", WARMUP_PARAS)
450
-
451
- threading.Thread(target=_warmup_first_paragraphs, daemon=True).start()
452
- print("[MomsVoice] Background warmup started: pre-generating first 5 paragraphs for all stories.")
453
-
454
 
455
  # Gradio Application Core setup
456
  with gr.Blocks(title="MomsVoice", css=css_code) as demo:
@@ -661,12 +629,12 @@ with gr.Blocks(title="MomsVoice", css=css_code) as demo:
661
  </div>
662
  <div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
663
  <span style="font-size: 18px; display: block;">🧠</span>
664
- <strong style="font-size: 15px; display: block; color: #1c1c19;">3</strong>
665
  <span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">Models</span>
666
  </div>
667
  <div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
668
  <span style="font-size: 18px; display: block;">🎙️</span>
669
- <strong style="font-size: 15px; display: block; color: #1c1c19;">Qwen3</strong>
670
  <span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">TTS Engine</span>
671
  </div>
672
  </div>
@@ -681,8 +649,8 @@ with gr.Blocks(title="MomsVoice", css=css_code) as demo:
681
  gr.HTML("""
682
  <div style="padding: 12px; background: white; border-radius: 12px; border: 1px solid #ebdccb;">
683
  <div style="font-size: 11.5px; color: #1c1c19; line-height: 2;">
684
- 🎙️ <strong>Voice Cloning:</strong> Qwen3-TTS-1.7B-Base (bfloat16)<br>
685
- 🗣️ <strong>Stock TTS:</strong> Qwen3-TTS-0.6B-CustomVoice<br>
686
  🧠 <strong>Q&A (end-to-end):</strong> LFM2.5-Audio-1.5B (LiquidAI)
687
  </div>
688
  </div>
 
409
  print("[MomsVoice] No saved voice profile — using stock voice as default.")
410
 
411
  # Preload models at startup for fast response
412
+ print("[MomsVoice] Preloading Qwen3-TTS 1.7B Base model (cloning + stock voice)...")
413
+ from voice_clone import get_qwen_tts
414
+ get_qwen_tts()
415
  print("[MomsVoice] TTS model ready.")
416
 
417
  print("[MomsVoice] Preloading LFM2.5-Audio-1.5B Q&A model...")
 
419
  get_lfm_model()
420
  print("[MomsVoice] LFM Q&A model ready. All models preloaded.")
421
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
 
423
  # Gradio Application Core setup
424
  with gr.Blocks(title="MomsVoice", css=css_code) as demo:
 
629
  </div>
630
  <div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
631
  <span style="font-size: 18px; display: block;">🧠</span>
632
+ <strong style="font-size: 15px; display: block; color: #1c1c19;">2</strong>
633
  <span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">Models</span>
634
  </div>
635
  <div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
636
  <span style="font-size: 18px; display: block;">🎙️</span>
637
+ <strong style="font-size: 15px; display: block; color: #1c1c19;">Base 1.7B</strong>
638
  <span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">TTS Engine</span>
639
  </div>
640
  </div>
 
649
  gr.HTML("""
650
  <div style="padding: 12px; background: white; border-radius: 12px; border: 1px solid #ebdccb;">
651
  <div style="font-size: 11.5px; color: #1c1c19; line-height: 2;">
652
+ 🎙️ <strong>TTS + Cloning:</strong> Qwen3-TTS-1.7B-Base (single model)<br>
653
+ 🗣️ <strong>Stock Voice:</strong> Vivian (via reference audio)<br>
654
  🧠 <strong>Q&A (end-to-end):</strong> LFM2.5-Audio-1.5B (LiquidAI)
655
  </div>
656
  </div>
test_qa_audio_check.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b0f03ec6d9134fcdf7114ed7d1795d35612ff20b4618d3701f95443d15fdfe7e
3
+ size 180524
voice_clone.py CHANGED
@@ -46,13 +46,14 @@ torch.set_float32_matmul_precision("high")
46
  # ---------------------------------------------------------------------------
47
  # Model configuration
48
  # ---------------------------------------------------------------------------
49
- # Base model for zero-shot voice cloning (1.7B)
50
  BASE_MODEL_ID = "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
51
- # CustomVoice model for fast predefined speakers (0.6B, ~3x faster)
52
- CUSTOM_VOICE_MODEL_ID = "Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice"
53
 
54
- # Choose model via env var: "base" (default) or "custom_voice"
55
- _MODEL_MODE = os.environ.get("QWEN_TTS_MODE", "base").lower()
 
 
 
56
 
57
  # Optimized generation parameters (reduced from defaults: top_k=50, temp=0.9, max=2048)
58
  GENERATION_PARAMS = dict(
@@ -83,9 +84,7 @@ _PROFILE_CACHE: dict[str, list] = {}
83
  _cache_lock = threading.Lock()
84
 
85
  _qwen_tts_model = None
86
- _custom_voice_model = None
87
  _model_lock = threading.Lock()
88
- _custom_model_lock = threading.Lock()
89
 
90
 
91
  def _select_dtype() -> torch.dtype:
@@ -131,27 +130,6 @@ def get_qwen_tts():
131
  return _qwen_tts_model
132
 
133
 
134
- def get_custom_voice_model():
135
- """Lazy-load the 0.6B CustomVoice model for stock voice. Thread-safe."""
136
- global _custom_voice_model
137
- if _custom_voice_model is None:
138
- with _custom_model_lock:
139
- if _custom_voice_model is None:
140
- from qwen_tts import Qwen3TTSModel
141
-
142
- device = "cuda" if torch.cuda.is_available() else "cpu"
143
-
144
- logger.info("Loading CustomVoice 0.6B on %s...", device)
145
- attn_impl = _select_attn_impl()
146
- _custom_voice_model = Qwen3TTSModel.from_pretrained(
147
- CUSTOM_VOICE_MODEL_ID,
148
- device_map=device,
149
- attn_implementation=attn_impl,
150
- )
151
- logger.info("CustomVoice 0.6B loaded (attn=%s).", attn_impl)
152
- return _custom_voice_model
153
-
154
-
155
  def _try_torch_compile(wrapper):
156
  """Best-effort torch.compile on model submodules. Disabled for stability."""
157
  # torch.compile can cause CUDA asserts on some GPU architectures (T4/Turing)
@@ -328,21 +306,13 @@ def load_default_profile() -> str | None:
328
  # Core API
329
  # ---------------------------------------------------------------------------
330
 
331
- def create_voice_profile(ref_audio_path: str, voice_name: str = "Cloned Voice") -> str:
332
  """
333
  Extract speaker embedding from reference audio, cache it, and save to disk.
334
  Returns a profile_id string for later synthesis.
335
 
336
  Reference audio is trimmed to 3-10s for optimal latency.
337
- Only supported with the Base model (1.7B).
338
  """
339
- if _MODEL_MODE == "custom_voice":
340
- raise ValueError(
341
- "Voice cloning requires the Base model (1.7B). "
342
- "The 0.6B CustomVoice model only supports predefined speakers. "
343
- "Set QWEN_TTS_MODE=base or remove the env var."
344
- )
345
-
346
  trimmed_path = _trim_reference_audio(ref_audio_path)
347
  model = get_qwen_tts()
348
 
@@ -353,7 +323,7 @@ def create_voice_profile(ref_audio_path: str, voice_name: str = "Cloned Voice")
353
  x_vector_only_mode=True,
354
  )
355
 
356
- profile_id = uuid.uuid4().hex[:12]
357
  with _cache_lock:
358
  _PROFILE_CACHE[profile_id] = prompt_items
359
 
@@ -397,26 +367,44 @@ def synthesize_cloned(text: str, profile_id: str) -> tuple[np.ndarray, int]:
397
  return wav, sample_rate
398
 
399
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  def synthesize_custom_voice_streaming(
401
  text: str, speaker: str = "vivian", language: str = "english"
402
  ):
403
  """
404
- Synthesize text with the 0.6B CustomVoice model, yielding partial audio segments
405
- as they become available for lower latency.
406
  Yields (wav_segment, sample_rate) tuples.
407
  """
408
- model = get_custom_voice_model()
 
 
 
 
 
409
 
 
410
  with GPU_INFERENCE_LOCK:
411
- audio_list, sample_rate = model.generate_custom_voice(
412
  text=text,
413
- speaker=speaker,
414
  language=language,
 
415
  non_streaming_mode=False,
416
  **GENERATION_PARAMS,
417
  )
418
 
419
- # Yield each segment as it was generated
420
  for segment in audio_list:
421
  if segment is not None and len(segment) > 0:
422
  yield segment, sample_rate
@@ -426,23 +414,16 @@ def synthesize_custom_voice(
426
  text: str, speaker: str = "vivian", language: str = "english"
427
  ) -> tuple[np.ndarray, int]:
428
  """
429
- Synthesize text with the 0.6B CustomVoice model (predefined speakers).
430
- Much faster than cloned synthesis but no voice cloning.
431
  Returns (wav_array, sample_rate).
432
  """
433
- model = get_custom_voice_model()
434
-
435
- with GPU_INFERENCE_LOCK:
436
- audio_list, sample_rate = model.generate_custom_voice(
437
- text=text,
438
- speaker=speaker,
439
- language=language,
440
- non_streaming_mode=False,
441
- **GENERATION_PARAMS,
442
- )
443
-
444
- wav = np.concatenate(audio_list) if audio_list else np.zeros(0, dtype=np.float32)
445
- return wav, sample_rate
446
 
447
 
448
  def synthesize_cloned_preview(profile_id: str) -> tuple[np.ndarray, int]:
@@ -465,6 +446,3 @@ def has_profile(profile_id: str | None) -> bool:
465
  return pt_path.exists()
466
 
467
 
468
- def get_model_mode() -> str:
469
- """Return the current model mode ('base' or 'custom_voice')."""
470
- return _MODEL_MODE
 
46
  # ---------------------------------------------------------------------------
47
  # Model configuration
48
  # ---------------------------------------------------------------------------
49
+ # Base model for zero-shot voice cloning (1.7B) — used for BOTH cloned and stock voice
50
  BASE_MODEL_ID = "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
 
 
51
 
52
+ # Stock voice reference audio (pre-generated "vivian" sample)
53
+ VIVIAN_REF_PATH = Path(__file__).parent / "assets" / "vivian_reference.wav"
54
+
55
+ # Profile ID for the built-in stock voice
56
+ STOCK_VOICE_PROFILE_ID = "__stock_vivian__"
57
 
58
  # Optimized generation parameters (reduced from defaults: top_k=50, temp=0.9, max=2048)
59
  GENERATION_PARAMS = dict(
 
84
  _cache_lock = threading.Lock()
85
 
86
  _qwen_tts_model = None
 
87
  _model_lock = threading.Lock()
 
88
 
89
 
90
  def _select_dtype() -> torch.dtype:
 
130
  return _qwen_tts_model
131
 
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  def _try_torch_compile(wrapper):
134
  """Best-effort torch.compile on model submodules. Disabled for stability."""
135
  # torch.compile can cause CUDA asserts on some GPU architectures (T4/Turing)
 
306
  # Core API
307
  # ---------------------------------------------------------------------------
308
 
309
+ def create_voice_profile(ref_audio_path: str, voice_name: str = "Cloned Voice", profile_id_override: str | None = None) -> str:
310
  """
311
  Extract speaker embedding from reference audio, cache it, and save to disk.
312
  Returns a profile_id string for later synthesis.
313
 
314
  Reference audio is trimmed to 3-10s for optimal latency.
 
315
  """
 
 
 
 
 
 
 
316
  trimmed_path = _trim_reference_audio(ref_audio_path)
317
  model = get_qwen_tts()
318
 
 
323
  x_vector_only_mode=True,
324
  )
325
 
326
+ profile_id = profile_id_override or uuid.uuid4().hex[:12]
327
  with _cache_lock:
328
  _PROFILE_CACHE[profile_id] = prompt_items
329
 
 
367
  return wav, sample_rate
368
 
369
 
370
+ def _ensure_stock_voice_profile():
371
+ """Ensure the stock vivian voice profile is loaded (uses Base 1.7B with reference audio)."""
372
+ with _cache_lock:
373
+ if STOCK_VOICE_PROFILE_ID in _PROFILE_CACHE:
374
+ return
375
+ # Create profile from vivian reference audio
376
+ if VIVIAN_REF_PATH.exists():
377
+ try:
378
+ create_voice_profile(str(VIVIAN_REF_PATH), voice_name="Vivian (Stock)", profile_id_override=STOCK_VOICE_PROFILE_ID)
379
+ logger.info("Stock vivian voice profile created from reference audio.")
380
+ except Exception as e:
381
+ logger.warning("Failed to create stock voice profile: %s", e)
382
+
383
+
384
  def synthesize_custom_voice_streaming(
385
  text: str, speaker: str = "vivian", language: str = "english"
386
  ):
387
  """
388
+ Synthesize text with the Base 1.7B model using stock vivian reference.
 
389
  Yields (wav_segment, sample_rate) tuples.
390
  """
391
+ _ensure_stock_voice_profile()
392
+ with _cache_lock:
393
+ prompt_items = _PROFILE_CACHE.get(STOCK_VOICE_PROFILE_ID)
394
+ if prompt_items is None:
395
+ logger.warning("Stock voice profile not available, synthesizing without clone.")
396
+ return
397
 
398
+ model = get_qwen_tts()
399
  with GPU_INFERENCE_LOCK:
400
+ audio_list, sample_rate = model.generate_voice_clone(
401
  text=text,
 
402
  language=language,
403
+ voice_clone_prompt=prompt_items,
404
  non_streaming_mode=False,
405
  **GENERATION_PARAMS,
406
  )
407
 
 
408
  for segment in audio_list:
409
  if segment is not None and len(segment) > 0:
410
  yield segment, sample_rate
 
414
  text: str, speaker: str = "vivian", language: str = "english"
415
  ) -> tuple[np.ndarray, int]:
416
  """
417
+ Synthesize text with the Base 1.7B model using stock vivian reference.
 
418
  Returns (wav_array, sample_rate).
419
  """
420
+ segments = list(synthesize_custom_voice_streaming(text, speaker, language))
421
+ if segments:
422
+ wav = np.concatenate([s for s, _ in segments])
423
+ sr = segments[0][1]
424
+ return wav, sr
425
+ # Fallback: empty audio
426
+ return np.zeros(0, dtype=np.float32), 24000
 
 
 
 
 
 
427
 
428
 
429
  def synthesize_cloned_preview(profile_id: str) -> tuple[np.ndarray, int]:
 
446
  return pt_path.exists()
447
 
448