import sys import io if sys.platform == "win32": try: sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") except Exception: sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") import gradio as gr print("⏳ Đang khởi động Pum's Tools — TextToSpeech... Vui lòng chờ...") import soundfile as sf import tempfile from vieneu import Vieneu import os import time import numpy as np import queue import threading import yaml import gc from vieneu_utils.core_utils import split_text_into_chunks, join_audio_chunks, env_bool, get_silence_duration_v2 from vieneu_utils.phonemize_text import phonemize_to_chunks from vieneu_utils.phonemize_text import PuncNormalizer as Normalizer from tools.TextToSpeech.ui_utils import ( _format_duration, _split_estimate_status, wrap_with_estimate, cleanup_gpu_memory, get_ref_text_cached, on_codec_change, validate_audio_duration, on_custom_id_change ) from tools.TextToSpeech.ui_constants import ( theme, css, head_html, DEFAULT_TEXT_GPU, DEFAULT_TEXT_TURBO, DEFAULT_TEXT_V3, DEFAULT_CONV_SCRIPT, ) # --- CONSTANTS & CONFIG --- CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.yaml") try: with open(CONFIG_PATH, "r", encoding="utf-8") as f: _config = yaml.safe_load(f) or {} except Exception as e: raise RuntimeError(f"Không thể đọc config.yaml: {e}") BACKBONE_CONFIGS = _config.get("backbone_configs", {}) CODEC_CONFIGS = _config.get("codec_configs", {}) # Filter by GPU availability HAS_GPU = False try: import torch HAS_GPU = torch.cuda.is_available() or (sys.platform == "darwin" and torch.backends.mps.is_available()) except ImportError: pass filtered_backbones = {} filtered_backbones["VieNeu-TTS-v3-Turbo (Thử nghiệm)"] = { "repo": "pnnbao-ump/VieNeu-TTS-v3-Turbo", "supports_streaming": False, "description": "🆕 v3 Turbo (early access) — 48kHz. Giọng mặc định dùng speaker token; Voice Cloning clone từ audio mẫu. Hỗ trợ tag cảm xúc [cười]/[hắng giọng]/[thở dài] (thử nghiệm)." } if HAS_GPU: filtered_backbones["VieNeu-TTS-v2 (GPU)"] = { "repo": "pnnbao-ump/VieNeu-TTS-v2", "supports_streaming": False, "description": "VieNeu-TTS Version 2 - hỗ trợ song ngữ (Anh-Việt) và chế độ podcast" } filtered_backbones["VieNeu-TTS (GPU)"] = { "repo": "pnnbao-ump/VieNeu-TTS", "supports_streaming": False, "description": "VieNeu-TTS Version 1 - ổn định, production-ready" } BACKBONE_CONFIGS = filtered_backbones filtered_codecs = { "NeuCodec (Distill)": { "repo": "neuphonic/distill-neucodec", "description": "Codec mặc định cho model GPU", "use_preencoded": False }, "NeuCodec (ONNX)": { "repo": "neuphonic/neucodec-onnx-decoder-int8", "description": "Codec siêu nhẹ, tối ưu cho CPU (ONNX)", "use_preencoded": False }, "VieNeu-Codec": { "repo": "pnnbao-ump/VieNeu-Codec", "description": "Codec tối ưu cho Turbo v2 (ONNX)", "use_preencoded": False } } CODEC_CONFIGS = filtered_codecs _text_settings = _config.get("text_settings", {}) MAX_CHARS_PER_CHUNK = _text_settings.get("max_chars_per_chunk", 256) MAX_TOTAL_CHARS_STREAMING = _text_settings.get("max_total_chars_streaming", 3000) if not BACKBONE_CONFIGS or not CODEC_CONFIGS: raise ValueError("config.yaml thiếu backbone_configs hoặc codec_configs") # --- 1. MODEL CONFIGURATION --- tts = None current_backbone = None current_codec = None model_loaded = False model_loading = False using_lmdeploy = False PRESET_VOICES_CACHE = [] CONV_VOICES_CACHE = [] MAX_SPEAKERS = 8 _text_normalizer = Normalizer() def get_available_devices() -> list[str]: """Get list of available devices for current platform.""" devices = ["Auto", "CPU"] try: import torch if sys.platform == "darwin" and torch.backends.mps.is_available(): devices.append("MPS") elif torch.cuda.is_available(): devices.append("CUDA") except ImportError: pass return devices def _supports_cloning(backbone_choice: str) -> bool: """Voice Cloning availability by model.""" c = (backbone_choice or "").lower() return "v3" in c or c == "vieneu-tts-v2 (gpu)" def get_model_status_message() -> str: """Reconstruct status message from global state.""" global model_loaded, tts if not model_loaded or tts is None: return "⏳ Chưa tải model." return "✅ Model đã tự động tải thành công và sẵn sàng sử dụng!" def restore_ui_state(): """Update UI components based on persistence.""" global model_loaded msg = get_model_status_message() return ( msg, gr.update(interactive=model_loaded), gr.update(interactive=model_loaded), gr.update(interactive=False) ) def should_use_lmdeploy(backbone_choice: str, device_choice: str) -> bool: """Determine if we should use LMDeploy backend.""" if sys.platform == "darwin": return False bc = backbone_choice.lower() if "gguf" in bc or "v2-turbo" in bc or "v3" in bc: return False try: import torch if device_choice == "Auto": has_gpu = torch.cuda.is_available() elif device_choice == "CUDA": has_gpu = torch.cuda.is_available() else: has_gpu = False return has_gpu except ImportError: return False def load_model(backbone_choice: str, codec_choice: str, device_choice: str, force_lmdeploy: bool, custom_model_id: str = "", custom_base_model: str = "", custom_hf_token: str = ""): """Load model with optimizations.""" global tts, current_backbone, current_codec, model_loaded, using_lmdeploy, model_loading, PRESET_VOICES_CACHE, CONV_VOICES_CACHE slot_no_updates = [gr.update()] * MAX_SPEAKERS if model_loading: while model_loading: yield ( "⏳ Model đang được tải ở một phiên khác... Vui lòng đợi.", gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), *slot_no_updates ) time.sleep(1) if model_loaded: try: voices = tts.list_preset_voices() except Exception: voices = [] has_voices = len(voices) > 0 if has_voices: default_v = tts._default_voice is_tuple = (len(voices) > 0 and isinstance(voices[0], tuple)) voice_values = [v[1] for v in voices] if is_tuple else voices if not default_v and voice_values: default_v = voice_values[0] if default_v and default_v not in voice_values: if is_tuple: voices.append((default_v, default_v)) else: voices.append(default_v) if is_tuple: voices.sort(key=lambda x: str(x[0])) else: voices.sort() voice_update = gr.update(choices=voices, value=default_v, interactive=True) PRESET_VOICES_CACHE = voices def _check_podcast(v_id): val = tts._preset_voices.get(v_id, {}).get('podcast', True) if isinstance(val, str): return val.strip().lower() == "true" return bool(val) CONV_VOICES_CACHE = [v for v in voices if _check_podcast(v[1])] slot_dd_update = gr.update(choices=CONV_VOICES_CACHE) tab_p = gr.update(visible=True) tab_c = gr.update(visible=_supports_cloning(backbone_choice)) tab_sel = gr.update(selected="preset_mode") mode_state = "preset_mode" else: msg = "⚠️ Không tìm thấy file voices.json. Vui lòng dùng Tab Voice Cloning." voice_update = gr.update(choices=[msg], value=msg, interactive=False) slot_dd_update = gr.update(choices=[]) tab_p = gr.update(visible=True) tab_c = gr.update(visible=_supports_cloning(backbone_choice)) tab_sel = gr.update(selected="preset_mode") mode_state = "preset_mode" is_v2 = (backbone_choice == "VieNeu-TTS-v2 (GPU)" or backbone_choice == "VieNeu-TTS-v2 (CPU)") is_v3_conv = "v3" in (backbone_choice or "").lower() conv_tab_update = gr.update(visible=is_v2 or is_v3_conv) slot_updates = [slot_dd_update] * MAX_SPEAKERS yield ( get_model_status_message(), gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=False), voice_update, tab_p, tab_c, tab_sel, mode_state, conv_tab_update, *slot_updates ) return model_loading = True lmdeploy_error_reason = None model_loaded = False yield ( f"⏳ Đang tự động tải model {backbone_choice}... Vui lòng kiên nhẫn.", gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), *slot_no_updates ) try: if tts is not None: tts = None cleanup_gpu_memory() custom_loading = False is_merged_lora = False if backbone_choice == "Custom Model": custom_loading = True if not custom_model_id or not custom_model_id.strip(): yield ( "❌ Lỗi: Vui lòng nhập Model ID cho Custom Model.", gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=True), gr.update(interactive=False), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), *slot_no_updates ) return if "lora" in custom_model_id.lower(): print(f"🔄 Detected LoRA in name. preparing merge with base: {custom_base_model}") if custom_base_model not in BACKBONE_CONFIGS: yield ( f"❌ Lỗi: Base Model '{custom_base_model}' không hợp lệ.", gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=True), gr.update(interactive=False), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), *slot_no_updates ) return base_config = BACKBONE_CONFIGS[custom_base_model] backbone_config = { "repo": base_config["repo"], "supports_streaming": base_config["supports_streaming"], "description": f"Custom Merged: {custom_model_id} + {custom_base_model}" } is_merged_lora = True else: backbone_config = { "repo": custom_model_id.strip(), "supports_streaming": False, "description": f"Custom Model: {custom_model_id}" } else: backbone_config = BACKBONE_CONFIGS[backbone_choice] codec_config = CODEC_CONFIGS[codec_choice] use_lmdeploy = False if custom_loading: if "gguf" in backbone_config['repo'].lower() or "v2-turbo" in backbone_config['repo'].lower(): use_lmdeploy = False elif is_merged_lora: use_lmdeploy = force_lmdeploy and should_use_lmdeploy(custom_base_model, device_choice) else: use_lmdeploy = force_lmdeploy and should_use_lmdeploy("VieNeu-TTS (GPU)", device_choice) if "v2-Turbo" in backbone_choice or "v3" in backbone_choice.lower(): should_use_generic_fast = False elif custom_loading: should_use_generic_fast = False else: should_use_generic_fast = force_lmdeploy and should_use_lmdeploy(backbone_choice, device_choice) if should_use_generic_fast: use_lmdeploy = True if use_lmdeploy: lmdeploy_error_reason = None print(f"🚀 Using LMDeploy backend with optimizations") backbone_device = "cuda" if "ONNX" in codec_choice: codec_device = "cpu" else: try: import torch codec_device = "cuda" if torch.cuda.is_available() else "cpu" except ImportError: codec_device = "cpu" target_backbone_repo = backbone_config["repo"] if custom_loading and is_merged_lora: safe_name = custom_model_id.strip().replace("/", "_").replace("\\", "_").replace(":", "") cache_dir = os.path.join(os.path.dirname(__file__), "merged_models_cache", safe_name) target_backbone_repo = os.path.abspath(cache_dir) if not os.path.exists(cache_dir) or not os.path.exists(os.path.join(cache_dir, "vocab.json")): print(f"🔄 Merging LoRA for LMDeploy optimization: {cache_dir}") yield ( f"⏳ Đang merge và lưu model LoRA để tối ưu cho LMDeploy...", gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), *slot_no_updates ) try: from vieneu.standard import VieNeuTTS base_repo = BACKBONE_CONFIGS[custom_base_model]["repo"] merge_device = "cuda" if torch.cuda.is_available() else "cpu" temp_tts = VieNeuTTS( backbone_repo=base_repo, backbone_device=merge_device, codec_repo=codec_config["repo"], codec_device="cpu", hf_token=custom_hf_token ) temp_tts.load_lora_adapter(custom_model_id.strip(), hf_token=custom_hf_token) if hasattr(temp_tts.backbone, "merge_and_unload"): temp_tts.backbone = temp_tts.backbone.merge_and_unload() temp_tts.backbone.save_pretrained(cache_dir) temp_tts.tokenizer.save_pretrained(cache_dir) try: from transformers import AutoTokenizer slow_tokenizer = AutoTokenizer.from_pretrained(base_repo, use_fast=False) slow_tokenizer.save_pretrained(cache_dir) except Exception as e: print(f" ⚠️ Warning: Could not save slow tokenizer files: {e}") import json voices_json_path = os.path.join(cache_dir, "voices.json") voices_content = { "meta": {"note": "Automatically generated during LoRA merge"}, "default_voice": temp_tts._default_voice, "presets": temp_tts._preset_voices } with open(voices_json_path, 'w', encoding='utf-8') as f: json.dump(voices_content, f, ensure_ascii=False, indent=2) del temp_tts cleanup_gpu_memory() print(" ✅ Merge & Save successfully!") except Exception as e: import traceback traceback.print_exc() raise RuntimeError(f"Failed to merge & save LoRA for LMDeploy: {e}") print(f"📦 Loading optimized model...") print(f" Backbone: {target_backbone_repo} on {backbone_device}") print(f" Codec: {codec_config['repo']} on {codec_device}") try: from vieneu.fast import FastVieNeuTTS tts = FastVieNeuTTS( backbone_repo=target_backbone_repo, backbone_device=backbone_device, codec_repo=codec_config["repo"], codec_device=codec_device, memory_util=0.3, tp=1, enable_prefix_caching=False, enable_triton=True, hf_token=custom_hf_token ) using_lmdeploy = True except Exception as e: import traceback traceback.print_exc() error_str = str(e) if "$env:CUDA_PATH" in error_str: lmdeploy_error_reason = "Không tìm thấy biến môi trường CUDA_PATH." else: lmdeploy_error_reason = f"{error_str}" yield ( f"⚠️ LMDeploy Init Error: {lmdeploy_error_reason}. Đang chuyển về backend mặc định...", gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), *slot_no_updates ) time.sleep(1) use_lmdeploy = False using_lmdeploy = False if not use_lmdeploy: print(f"📦 Using original backend") if device_choice == "Auto": repo_lower = backbone_config['repo'].lower() is_gguf_backbone = "gguf" in repo_lower if is_gguf_backbone: if sys.platform == "darwin": backbone_device = "gpu" else: try: import torch backbone_device = "gpu" if torch.cuda.is_available() else "cpu" except ImportError: backbone_device = "cpu" else: try: import torch if sys.platform == "darwin": backbone_device = "mps" if torch.backends.mps.is_available() else "cpu" else: backbone_device = "cuda" if torch.cuda.is_available() else "cpu" except ImportError: backbone_device = "cpu" if "ONNX" in codec_choice: codec_device = "cpu" else: try: import torch if sys.platform == "darwin": codec_device = "mps" if torch.backends.mps.is_available() else "cpu" else: codec_device = "cuda" if torch.cuda.is_available() else "cpu" except ImportError: codec_device = "cpu" elif device_choice == "MPS": backbone_device = "mps" codec_device = "mps" if "ONNX" not in codec_choice else "cpu" else: backbone_device = device_choice.lower() codec_device = device_choice.lower() if "ONNX" in codec_choice: codec_device = "cpu" if "gguf" in backbone_config['repo'].lower() and backbone_device == "cuda": backbone_device = "gpu" print(f"📦 Loading model...") print(f" Backbone: {backbone_config['repo']} on {backbone_device}") print(f" Codec: {codec_config['repo']} on {codec_device}") if "v3-Turbo" in backbone_choice: print(" 🆕 Mode: v3 Turbo (CPU=ONNX / GPU=PyTorch)") v3_device = "cpu" if str(backbone_device).lower() == "cpu" else "auto" tts = Vieneu( mode="v3turbo", backbone_repo=backbone_config["repo"], device=v3_device, hf_token=custom_hf_token, ) elif "v2-Turbo" in backbone_choice: print(" ⚡ Mode: Turbo") mode = "turbo_gpu" if "GPU" in backbone_choice else "turbo" tts = Vieneu( mode=mode, backbone_repo=backbone_config["repo"], decoder_repo=codec_config["repo"], device=backbone_device, backend="lmdeploy" if force_lmdeploy and "GPU" in backbone_choice else "standard", hf_token=custom_hf_token ) else: from vieneu.standard import VieNeuTTS tts = VieNeuTTS( backbone_repo=backbone_config["repo"], backbone_device=backbone_device, codec_repo=codec_config["repo"], codec_device=codec_device, hf_token=custom_hf_token, gguf_filename=backbone_config.get("gguf_filename") ) if is_merged_lora and custom_loading and not using_lmdeploy: yield ( f"🔄 Đang tải và merge LoRA adapter: {custom_model_id}...", gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), *slot_no_updates ) try: tts.load_lora_adapter(custom_model_id.strip(), hf_token=custom_hf_token) if hasattr(tts, 'backbone') and hasattr(tts.backbone, 'merge_and_unload'): print(" 🔄 Merging LoRA into backbone...") tts.backbone = tts.backbone.merge_and_unload() tts._lora_loaded = False tts._current_lora_repo = None print(" ✅ Merged successfully!") else: print(" ⚠️ Warning: Model does not support merge_and_unload.") except Exception as e: raise RuntimeError(f"Failed to merge LoRA: {e}") using_lmdeploy = False current_backbone = backbone_choice current_codec = codec_choice model_loaded = True # Prepare voice update try: voices = tts.list_preset_voices() except Exception: voices = [] has_voices = len(voices) > 0 if has_voices: default_v = tts._default_voice is_tuple = (len(voices) > 0 and isinstance(voices[0], tuple)) voice_values = [v[1] for v in voices] if is_tuple else voices if not default_v and voice_values: default_v = voice_values[0] if default_v and default_v not in voice_values: if is_tuple: voices.append((default_v, default_v)) else: voices.append(default_v) if is_tuple: voices.sort(key=lambda x: str(x[0])) else: voices.sort() voice_update = gr.update(choices=voices, value=default_v, interactive=True) PRESET_VOICES_CACHE = voices def _check_podcast(v_id): val = tts._preset_voices.get(v_id, {}).get('podcast', True) if isinstance(val, str): return val.strip().lower() == "true" return bool(val) CONV_VOICES_CACHE = [v for v in voices if _check_podcast(v[1])] slot_dd_update = gr.update(choices=CONV_VOICES_CACHE) tab_p = gr.update(visible=True) tab_c = gr.update(visible=_supports_cloning(backbone_choice)) tab_sel = gr.update(selected="preset_mode") mode_state = "preset_mode" else: msg = "⚠️ Không tìm thấy file voices.json. Vui lòng dùng Tab Voice Cloning." voice_update = gr.update(choices=[msg], value=msg, interactive=False) slot_dd_update = gr.update(choices=[]) tab_p = gr.update(visible=True) tab_c = gr.update(visible=_supports_cloning(backbone_choice)) tab_sel = gr.update(selected="preset_mode") mode_state = "preset_mode" is_v2 = (backbone_choice == "VieNeu-TTS-v2 (GPU)" or backbone_choice == "VieNeu-TTS-v2 (CPU)") is_v3_conv = "v3" in (backbone_choice or "").lower() conv_tab_update = gr.update(visible=is_v2 or is_v3_conv) slot_updates = [slot_dd_update] * MAX_SPEAKERS success_msg = get_model_status_message() warning_msg = "" if lmdeploy_error_reason: warning_msg = ( f"\n\n⚠️ **Cảnh báo:** Không thể kích hoạt LMDeploy:\n" f"👉 {lmdeploy_error_reason}\n" f"💡 Đã tự động chuyển về chế độ Standard." ) if warning_msg: success_msg += warning_msg model_loading = False yield ( success_msg, gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=False), voice_update, tab_p, tab_c, tab_sel, mode_state, conv_tab_update, *slot_updates ) except Exception as e: import traceback traceback.print_exc() model_loaded = False model_loading = False using_lmdeploy = False yield ( f"❌ Lỗi khi tải model: {str(e)}", gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=True), gr.update(interactive=False), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), *slot_no_updates ) def auto_load_model_if_needed(backbone_choice: str, codec_choice: str, device_choice: str, force_lmdeploy: bool, custom_model_id: str = "", custom_base_model: str = "", custom_hf_token: str = ""): global model_loaded, tts slot_no_updates = [gr.update()] * MAX_SPEAKERS if model_loaded and tts is not None: # Model is already loaded, restore UI state success_msg = get_model_status_message() try: voices = tts.list_preset_voices() except Exception: voices = [] has_voices = len(voices) > 0 if has_voices: default_v = tts._default_voice is_tuple = (len(voices) > 0 and isinstance(voices[0], tuple)) voice_values = [v[1] for v in voices] if is_tuple else voices if not default_v and voice_values: default_v = voice_values[0] if default_v and default_v not in voice_values: if is_tuple: voices.append((default_v, default_v)) else: voices.append(default_v) if is_tuple: voices.sort(key=lambda x: str(x[0])) else: voices.sort() voice_update = gr.update(choices=voices, value=default_v, interactive=True) global PRESET_VOICES_CACHE, CONV_VOICES_CACHE PRESET_VOICES_CACHE = voices def _check_podcast(v_id): val = tts._preset_voices.get(v_id, {}).get('podcast', True) if isinstance(val, str): return val.strip().lower() == "true" return bool(val) CONV_VOICES_CACHE = [v for v in voices if _check_podcast(v[1])] slot_dd_update = gr.update(choices=CONV_VOICES_CACHE) tab_p = gr.update(visible=True) tab_c = gr.update(visible=_supports_cloning(backbone_choice)) tab_sel = gr.update(selected="preset_mode") mode_state = "preset_mode" else: msg = "⚠️ Không tìm thấy file voices.json. Vui lòng dùng Tab Voice Cloning." voice_update = gr.update(choices=[msg], value=msg, interactive=False) slot_dd_update = gr.update(choices=[]) tab_p = gr.update(visible=True) tab_c = gr.update(visible=_supports_cloning(backbone_choice)) tab_sel = gr.update(selected="preset_mode") mode_state = "preset_mode" is_v2 = (backbone_choice == "VieNeu-TTS-v2 (GPU)" or backbone_choice == "VieNeu-TTS-v2 (CPU)") is_v3_conv = "v3" in (backbone_choice or "").lower() conv_tab_update = gr.update(visible=is_v2 or is_v3_conv) slot_updates = [slot_dd_update] * MAX_SPEAKERS yield ( success_msg, gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=False), voice_update, tab_p, tab_c, tab_sel, mode_state, conv_tab_update, *slot_updates ) return # Otherwise load it yield from load_model( backbone_choice, codec_choice, device_choice, force_lmdeploy, custom_model_id, custom_base_model, custom_hf_token ) def resolve_voice_id(v_id: str) -> str: """Robustly resolve voice ID.""" if not v_id: return v_id global PRESET_VOICES_CACHE if not PRESET_VOICES_CACHE: return v_id for item in PRESET_VOICES_CACHE: if isinstance(item, (list, tuple)) and len(item) >= 2: label, value = item[0], item[1] if v_id == value or v_id == label: return value else: if v_id == item: return item return v_id # --- 2. SYNTHESIS --- # Cancellation event _STOP_EVENT = threading.Event() def synthesize_speech(text: str, voice_choice: str, custom_audio, custom_text: str, mode_tab: str, generation_mode: str, use_batch: bool, max_batch_size_run: int, temperature: float, max_chars_chunk: int, session_id: str = None): """Speech synthesis with optimization support.""" global tts, current_backbone, current_codec, model_loaded, using_lmdeploy _STOP_EVENT.clear() if not model_loaded or tts is None: yield None, "⚠️ Vui lòng tải model trước!" return if not text or text.strip() == "": yield None, "⚠️ Vui lòng nhập văn bản!" return raw_text = text.strip() codec_config = CODEC_CONFIGS[current_codec] # Setup Reference yield None, "📄 Đang xử lý Reference..." try: ref_codes = None ref_text_raw = "" v3_voice_token_id = None if mode_tab == "preset_mode": if not voice_choice: raise ValueError("Vui lòng chọn giọng mẫu.") if "⚠️" in voice_choice: raise ValueError("Không có giọng mẫu khả dụng.") v_id = resolve_voice_id(voice_choice) voice_data = tts.get_preset_voice(v_id) ref_codes = voice_data['codes'] ref_text_raw = voice_data['text'] v3_voice_token_id = voice_data.get('reserved_id') elif mode_tab == "custom_mode": if custom_audio is None: raise ValueError("Vui lòng upload file Audio mẫu!") cb_lower = (current_backbone or "").lower() needs_ref_text = "v2-turbo" not in cb_lower and "v3" not in cb_lower if needs_ref_text and (not custom_text or not custom_text.strip()): raise ValueError("Vui lòng nhập nội dung văn bản của Audio mẫu!") ref_text_raw = custom_text.strip() if custom_text else "" ref_codes = tts.encode_reference(custom_audio) if 'torch' in sys.modules: import torch if isinstance(ref_codes, torch.Tensor): ref_codes = ref_codes.cpu().numpy() except Exception as e: yield None, f"❌ Lỗi xử lý Reference Audio: {str(e)}" return # === STANDARD MODE === if generation_mode == "Standard (Một lần)": # v3 Turbo branch if "v3" in (current_backbone or "").lower(): _t0 = time.time() yield None, "⏳ Đang tổng hợp (v3 Turbo)..." sr_v3 = getattr(tts, "sample_rate", 48000) try: from vieneu_utils.phonemize_text import phonemize_text_with_emotions v3_chunks = split_text_into_chunks(raw_text, max_chars=max_chars_chunk) or [raw_text] v3_bs = max(1, int(max_batch_size_run)) if use_batch else 1 v3_engine_dev = getattr(getattr(tts, "engine", None), "device", None) v3_can_batch = ( v3_bs > 1 and len(v3_chunks) > 1 and v3_engine_dev is not None and v3_engine_dev.type == "cuda" ) if v3_can_batch: from vieneu.v3_turbo_serve import V3TurboBatchEngine if getattr(tts, "_v3_batch_engine", None) is None: tts._v3_batch_engine = V3TurboBatchEngine(tts.engine) v3_wavs = [] for i in range(0, len(v3_chunks), v3_bs): if _STOP_EVENT.is_set(): yield None, "⏹️ Đã dừng tạo giọng nói." return group = v3_chunks[i:i + v3_bs] yield None, f"⚡ v3 Turbo: lô {i // v3_bs + 1} ({len(group)} đoạn, batch size {v3_bs})..." reqs = [{"phonemes": phonemize_text_with_emotions(c), "ref_codes": ref_codes, "voice_token_id": v3_voice_token_id} for c in group] v3_wavs.extend(tts._v3_batch_engine.generate_batch( reqs, temperature=temperature, max_new_frames=300)) wav = join_audio_chunks(v3_wavs, sr=sr_v3, silence_p=0.15) else: total_v3 = len(v3_chunks) stream_kwargs = ({"voice": v_id} if mode_tab == "preset_mode" else {"ref_codes": ref_codes}) v3_wavs = [] chunk_durations = [] last_t = time.time() yield None, f"⏳ v3 Turbo: Đang xử lý đoạn 1/{total_v3}..." for i, chunk_wav in enumerate(tts.infer_stream( raw_text, temperature=temperature, max_chars=max_chars_chunk, **stream_kwargs)): if _STOP_EVENT.is_set(): yield None, "⏹️ Đã dừng tạo giọng nói." return now = time.time() chunk_durations.append(now - last_t) last_t = now if chunk_wav is not None and len(chunk_wav) > 0: v3_wavs.append(chunk_wav) done = i + 1 if done < total_v3: avg = sum(chunk_durations) / len(chunk_durations) eta = avg * (total_v3 - done) yield None, ( f"⏳ v3 Turbo: Đã xong {done}/{total_v3} đoạn " f"(ước tính còn lại: {_format_duration(eta)})... " f"đang xử lý đoạn {done + 1}/{total_v3}" ) wav = join_audio_chunks(v3_wavs, sr=sr_v3, silence_p=0.15) except Exception as e: yield None, f"❌ Lỗi tổng hợp (v3 Turbo): {str(e)}" return if wav is None or len(wav) == 0: yield None, "❌ Không sinh được audio nào." return with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp: sf.write(tmp.name, wav, sr_v3) out_path_v3 = tmp.name _dt = time.time() - _t0 _spd = f", Tốc độ: {len(wav)/sr_v3/_dt:.2f}x realtime" if _dt > 0 else "" yield out_path_v3, f"✅ Hoàn tất! (v3 Turbo, Thời gian: {_dt:.2f}s{_spd})" cleanup_gpu_memory() return # Standard/v1/v2 branch backend_name = "LMDeploy" if using_lmdeploy else "Standard" is_v2_turbo = "v2-Turbo" in (current_backbone or "") if is_v2_turbo: text_chunks = phonemize_to_chunks(raw_text, max_chars=max_chars_chunk) else: text_chunks = [] for raw_chunk in split_text_into_chunks(raw_text, max_chars=max_chars_chunk): normalized_chunk = _text_normalizer.normalize(raw_chunk) text_chunks.extend(split_text_into_chunks(normalized_chunk, max_chars=max_chars_chunk)) total_chunks = len(text_chunks) yield None, f"🚀 Bắt đầu tổng hợp {backend_name} ({total_chunks} đoạn)..." all_wavs = [] sr = 24000 start_time = time.time() try: if is_v2_turbo: for i, chunk in enumerate(text_chunks): if _STOP_EVENT.is_set(): yield None, "⏹️ Đã dừng tạo giọng nói." return yield None, f"⚡ Turbo v2: Đang xử lý đoạn {i+1}/{total_chunks}..." chunk_wav = tts.infer( chunk.text, ref_codes=ref_codes, temperature=temperature, max_chars=max_chars_chunk, skip_normalize=True, skip_phonemize=True ) if chunk_wav is not None and len(chunk_wav) > 0: all_wavs.append(chunk_wav) if i < total_chunks - 1: sil_dur = get_silence_duration_v2(chunk) sil_wav = np.zeros(int(sr * sil_dur), dtype=np.float32) all_wavs.append(sil_wav) elif use_batch and using_lmdeploy and hasattr(tts, 'infer_batch') and total_chunks > 1: num_batches = (total_chunks + max_batch_size_run - 1) // max_batch_size_run for i in range(0, total_chunks, max_batch_size_run): if _STOP_EVENT.is_set(): yield None, "⏹️ Đã dừng tạo giọng nói." return batch_idx = i // max_batch_size_run yield None, f"⚡ Đang xử lý batch {batch_idx+1}/{num_batches}..." current_batch = text_chunks[i: i + max_batch_size_run] batch_wavs = tts.infer_batch( current_batch, ref_codes=ref_codes, ref_text=ref_text_raw, max_batch_size=max_batch_size_run, temperature=temperature, skip_normalize=True ) for chunk_wav in batch_wavs: if chunk_wav is not None and len(chunk_wav) > 0: all_wavs.append(chunk_wav) else: for i, chunk in enumerate(text_chunks): if _STOP_EVENT.is_set(): yield None, "⏹️ Đã dừng tạo giọng nói." return yield None, f"⏳ Đang xử lý đoạn {i+1}/{total_chunks}..." chunk_wav = tts.infer( chunk, ref_codes=ref_codes, ref_text=ref_text_raw, temperature=temperature, max_chars=max_chars_chunk, skip_normalize=True ) if chunk_wav is not None and len(chunk_wav) > 0: all_wavs.append(chunk_wav) if not all_wavs: yield None, "❌ Không sinh được audio nào." return yield None, "💾 Đang ghép file và lưu..." silence_p = 0.15 if not is_v2_turbo else 0.0 final_wav = join_audio_chunks(all_wavs, sr=sr, silence_p=silence_p) with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp: sf.write(tmp.name, final_wav, sr) output_path = tmp.name process_time = time.time() - start_time backend_info = f" (Backend: {'LMDeploy 🚀' if using_lmdeploy else 'Standard 📦'})" speed_info = f", Tốc độ: {len(final_wav)/sr/process_time:.2f}x realtime" if process_time > 0 else "" yield output_path, f"✅ Hoàn tất! (Thời gian: {process_time:.2f}s{speed_info}){backend_info}" if using_lmdeploy and hasattr(tts, 'cleanup_memory'): tts.cleanup_memory() cleanup_gpu_memory() except Exception as e: if 'torch' in sys.modules: import torch if isinstance(e, torch.cuda.OutOfMemoryError): cleanup_gpu_memory() yield None, f"❌ GPU hết VRAM! Hãy thử giảm Batch Size hoặc độ dài văn bản." return import traceback traceback.print_exc() cleanup_gpu_memory() yield None, f"❌ Lỗi Standard Mode: {str(e)}" return # === STREAMING MODE === else: sr = 24000 crossfade_samples = int(sr * 0.03) audio_queue = queue.Queue(maxsize=100) PRE_BUFFER_SIZE = 3 end_event = threading.Event() error_event = threading.Event() error_msg = "" is_v2_turbo = "v2-Turbo" in (current_backbone or "") if is_v2_turbo: text_chunks = phonemize_to_chunks(raw_text, max_chars=max_chars_chunk) else: text_chunks = [] for raw_chunk in split_text_into_chunks(raw_text, max_chars=max_chars_chunk): normalized_chunk = _text_normalizer.normalize(raw_chunk) text_chunks.extend(split_text_into_chunks(normalized_chunk, max_chars=max_chars_chunk)) def producer_thread(): nonlocal error_msg try: previous_tail = None for i, chunk_text in enumerate(text_chunks): if _STOP_EVENT.is_set(): break if is_v2_turbo: stream_gen = tts.infer_stream( chunk_text.text, ref_codes=ref_codes, temperature=temperature, max_chars=max_chars_chunk, skip_normalize=True, skip_phonemize=True, emotion_tag="" ) else: stream_gen = tts.infer_stream( chunk_text, ref_codes=ref_codes, ref_text=ref_text_raw, temperature=temperature, max_chars=max_chars_chunk, skip_normalize=True, emotion_tag="" ) for part_idx, audio_part in enumerate(stream_gen): if _STOP_EVENT.is_set(): break if audio_part is None or len(audio_part) == 0: continue if previous_tail is not None and len(previous_tail) > 0: overlap = min(len(previous_tail), len(audio_part), crossfade_samples) if overlap > 0: fade_out = np.linspace(1.0, 0.0, overlap, dtype=np.float32) fade_in = np.linspace(0.0, 1.0, overlap, dtype=np.float32) blended = (audio_part[:overlap] * fade_in + previous_tail[-overlap:] * fade_out) processed = np.concatenate([ previous_tail[:-overlap] if len(previous_tail) > overlap else np.array([]), blended, audio_part[overlap:] ]) else: processed = np.concatenate([previous_tail, audio_part]) tail_size = min(crossfade_samples, len(processed)) previous_tail = processed[-tail_size:].copy() output_chunk = processed[:-tail_size] if len(processed) > tail_size else processed else: tail_size = min(crossfade_samples, len(audio_part)) previous_tail = audio_part[-tail_size:].copy() output_chunk = audio_part[:-tail_size] if len(audio_part) > tail_size else audio_part if len(output_chunk) > 0: audio_queue.put((sr, output_chunk)) if is_v2_turbo and i < len(text_chunks) - 1: sil_dur = get_silence_duration_v2(chunk_text) sil_wav = np.zeros(int(sr * sil_dur), dtype=np.float32) audio_queue.put((sr, sil_wav)) if previous_tail is not None and len(previous_tail) > 0: audio_queue.put((sr, previous_tail)) except Exception as e: import traceback traceback.print_exc() error_msg = str(e) error_event.set() finally: end_event.set() audio_queue.put(None) threading.Thread(target=producer_thread, daemon=True).start() yield (sr, np.zeros(int(sr * 0.05))), "📄 Đang buffering..." pre_buffer = [] while len(pre_buffer) < PRE_BUFFER_SIZE: try: item = audio_queue.get(timeout=5.0) if item is None: break pre_buffer.append(item) except queue.Empty: if error_event.is_set(): yield None, f"❌ Lỗi: {error_msg}" return break full_audio_buffer = [] backend_info = "🚀 LMDeploy" if using_lmdeploy else "📦 Standard" for sr, audio_data in pre_buffer: full_audio_buffer.append(audio_data) yield (sr, audio_data), f"🔊 Đang phát ({backend_info})..." while True: try: item = audio_queue.get(timeout=0.05) if item is None: break sr, audio_data = item full_audio_buffer.append(audio_data) yield (sr, audio_data), f"🔊 Đang phát ({backend_info})..." except queue.Empty: if error_event.is_set(): yield None, f"❌ Lỗi: {error_msg}" break if end_event.is_set() and audio_queue.empty(): break continue if full_audio_buffer: final_wav = np.concatenate(full_audio_buffer) with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp: sf.write(tmp.name, final_wav, sr) yield tmp.name, f"✅ Hoàn tất Streaming! ({backend_info})" if using_lmdeploy and hasattr(tts, 'cleanup_memory'): tts.cleanup_memory() cleanup_gpu_memory() synthesize_speech_with_estimate = wrap_with_estimate(synthesize_speech) def synthesize_conversation_with_empty_estimate(*args): for audio_path, status in synthesize_conversation(*args): yield audio_path, status, "" # --- 3. CONVERSATION LOGIC --- def _synthesize_conversation_v3(lines, mapping, temperature, max_chars_chunk, silence_duration): """v3 Turbo conversation: batch the WHOLE conversation at batch size 32.""" global tts from collections import defaultdict from vieneu_utils.core_utils import split_text_into_chunks, join_audio_chunks from vieneu_utils.phonemize_text import phonemize_text_with_emotions sr = getattr(tts, "sample_rate", 48000) t0 = time.time() def _voice_for(spk_name): cfg = mapping.get(spk_name.lower()) v_id = (cfg or {}).get('voice') or tts._default_voice try: vd = tts.get_preset_voice(v_id) except Exception: vd = tts.get_preset_voice(tts._default_voice) rc = vd['codes'] if 'torch' in sys.modules: import torch if isinstance(rc, torch.Tensor): rc = rc.cpu().numpy() return np.asarray(rc), vd.get('reserved_id') dev = getattr(getattr(tts, "engine", None), "device", None) is_cuda = dev is not None and getattr(dev, "type", None) == "cuda" if not is_cuda: all_wavs = [] for li, line in enumerate(lines): if _STOP_EVENT.is_set(): yield None, "⏹️ Đã dừng hội thoại." return cfg = mapping.get(line['speaker'].lower()) v_id = (cfg or {}).get('voice') or tts._default_voice yield None, f"⏳ [{li+1}/{len(lines)}] {line['speaker']}: {line['text'][:30]}..." try: wav = tts.infer(line['text'], voice=v_id, temperature=temperature, max_chars=max_chars_chunk) except Exception as e: print(f"❌ Lỗi câu {li+1}: {e}") continue if wav is not None and len(wav): all_wavs.append(wav) if li < len(lines) - 1 and silence_duration > 0: all_wavs.append(np.zeros(int(sr * silence_duration), dtype=np.float32)) if not all_wavs: yield None, "❌ Không thể tạo được âm thanh nào!" return yield None, "🪄 Đang ghép nối âm thanh..." final_wav = np.concatenate(all_wavs) with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp: sf.write(tmp.name, final_wav, sr) yield tmp.name, f"✅ Hoàn tất hội thoại! ({len(lines)} câu, {time.time()-t0:.1f}s, CPU tuần tự)" cleanup_gpu_memory() return voice_cache = {} reqs, req_line = [], [] for li, line in enumerate(lines): key = line['speaker'].lower() if key not in voice_cache: voice_cache[key] = _voice_for(line['speaker']) ref_codes, vtok = voice_cache[key] chunks = split_text_into_chunks(line['text'], max_chars=max_chars_chunk) or [line['text']] for c in chunks: reqs.append({"phonemes": phonemize_text_with_emotions(c), "ref_codes": ref_codes, "voice_token_id": vtok}) req_line.append(li) if not reqs: yield None, "❌ Không có lời thoại để tổng hợp." return if getattr(tts, "_v3_batch_engine", None) is None: from vieneu.v3_turbo_serve import V3TurboBatchEngine tts._v3_batch_engine = V3TurboBatchEngine(tts.engine) BS = 32 total_batches = (len(reqs) + BS - 1) // BS wavs_flat = [] for bi, i in enumerate(range(0, len(reqs), BS)): if _STOP_EVENT.is_set(): yield None, "⏹️ Đã dừng hội thoại." return group = reqs[i:i + BS] yield None, f"⚡ v3 Turbo hội thoại: lô {bi + 1}/{total_batches} ({len(group)} đoạn, batch 32)..." wavs_flat.extend(tts._v3_batch_engine.generate_batch( group, temperature=temperature, max_new_frames=300)) by_line = defaultdict(list) for w, li in zip(wavs_flat, req_line): by_line[li].append(w) all_wavs = [] for li in range(len(lines)): lw = join_audio_chunks(by_line[li], sr=sr, silence_p=0.15) if by_line[li] else None if lw is None or len(lw) == 0: continue all_wavs.append(lw) if li < len(lines) - 1 and silence_duration > 0: all_wavs.append(np.zeros(int(sr * silence_duration), dtype=np.float32)) if not all_wavs: yield None, "❌ Không thể tạo được âm thanh nào!" return yield None, "🪄 Đang ghép nối âm thanh..." final_wav = np.concatenate(all_wavs) with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp: sf.write(tmp.name, final_wav, sr) elapsed = time.time() - t0 yield tmp.name, f"✅ Hoàn tất hội thoại! ({len(lines)} câu, {len(reqs)} đoạn, {elapsed:.1f}s, batch 32)" cleanup_gpu_memory() def synthesize_conversation(script_text: str, *args): """Synthesizes multi-speaker conversation from a script.""" speaker_names = list(args[:MAX_SPEAKERS]) speaker_voices = list(args[MAX_SPEAKERS:MAX_SPEAKERS*2]) silence_duration = args[MAX_SPEAKERS * 2] temperature = args[MAX_SPEAKERS * 2 + 1] max_chars_chunk = args[MAX_SPEAKERS * 2 + 2] session_id = args[MAX_SPEAKERS * 2 + 3] if len(args) > MAX_SPEAKERS * 2 + 3 else None global tts, model_loaded, using_lmdeploy _STOP_EVENT.clear() if not model_loaded or tts is None: yield None, "⚠️ Vui lòng tải model trước!" return if not script_text or script_text.strip() == "": yield None, "⚠️ Vui lòng nhập kịch bản hội thoại!" return # Parse Script lines = [] for line in script_text.strip().split('\n'): if not line.strip(): continue if ':' in line: parts = line.split(':', 1) lines.append({'speaker': parts[0].strip(), 'text': parts[1].strip()}) else: if lines: lines[-1]['text'] += " " + line.strip() else: lines.append({'speaker': 'Narrator', 'text': line.strip()}) if not lines: yield None, "⚠️ Không tìm thấy lời thoại hợp lệ!" return # Build Speaker Mapping mapping = {} for name, voice in zip(speaker_names, speaker_voices): name = str(name).strip() if name else "" if not name: continue v_id = resolve_voice_id(str(voice)) if voice else "" mapping[name.lower()] = { 'type': 'Preset', 'voice': v_id, 'ref_text': '' } # v3 Turbo batch if "v3" in (current_backbone or "").lower(): yield from _synthesize_conversation_v3( lines, mapping, temperature, max_chars_chunk, silence_duration ) return # v1/v2 sequential all_wavs = [] sr = 24000 total_lines = len(lines) yield None, f"🎭 Đang khởi tạo hội thoại ({total_lines} câu)..." start_time = time.time() try: for i, line in enumerate(lines): if _STOP_EVENT.is_set(): yield None, "⏹️ Đã dừng hội thoại." return spk_name = line['speaker'] text = line['text'] yield None, f"⏳ [{i+1}/{total_lines}] {spk_name}: {text[:30]}..." ref_codes = None ref_text_val = None current_voice_obj = None config = mapping.get(spk_name.lower()) if not config: try: default_v_id = tts._default_voice if not default_v_id: dv_list = tts.list_preset_voices() if dv_list: first = dv_list[0] default_v_id = first[1] if isinstance(first, tuple) else first if default_v_id: current_voice_obj = tts.get_preset_voice(default_v_id) ref_codes = current_voice_obj['codes'] ref_text_val = current_voice_obj['text'] except Exception as e: print(f" ❌ Fallback failed: {e}") else: try: v_id = config['voice'] if config['type'] == "Preset": current_voice_obj = tts.get_preset_voice(v_id) if current_voice_obj and 'codes' in current_voice_obj: ref_codes = current_voice_obj['codes'] ref_text_val = current_voice_obj['text'] else: if v_id and os.path.exists(v_id): ref_codes = tts.encode_reference(v_id) ref_text_val = config.get('ref_text', '') current_voice_obj = {'codes': ref_codes, 'text': ref_text_val} except Exception as e: print(f" ❌ Lỗi nạp giọng cho {spk_name}: {e}") if 'torch' in sys.modules: import torch if isinstance(ref_codes, torch.Tensor): ref_codes = ref_codes.cpu().numpy() try: wav = tts.infer( text, voice=current_voice_obj, ref_codes=ref_codes, ref_text=ref_text_val, temperature=temperature, max_chars=max_chars_chunk, emotion_tag="<|emotion_0|>" ) all_wavs.append(wav) if i < total_lines - 1 and silence_duration > 0: silence_len = int(sr * silence_duration) silence = np.zeros(silence_len) all_wavs.append(silence) except Exception as e: print(f"❌ Lỗi tổng hợp câu {i+1}: {e}") continue if not all_wavs: yield None, "❌ Không thể tạo được âm thanh nào!" return yield None, "🪄 Đang ghép nối âm thanh..." final_wav = np.concatenate(all_wavs) with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp: sf.write(tmp.name, final_wav, sr) elapsed = time.time() - start_time yield tmp.name, f"✅ Hoàn tất hội thoại! ({total_lines} câu, xử lý trong {elapsed:.1f}s)" except Exception as e: import traceback traceback.print_exc() yield None, f"❌ Lỗi hệ thống: {str(e)}" def extract_speakers_from_script(script): """Find unique speakers and return gr.update() lists for the 8 slot components.""" global CONV_VOICES_CACHE if not script: name_updates = [gr.update(value="", visible=False)] * MAX_SPEAKERS dd_updates = [gr.update(value=None, visible=False)] * MAX_SPEAKERS row_updates = [gr.update(visible=False)] * MAX_SPEAKERS return name_updates + dd_updates + row_updates speakers = [] seen = set() for line in script.strip().split('\n'): if ':' in line: s = line.split(':', 1)[0].strip() if s and s not in seen: seen.add(s) speakers.append(s) def _best_match(name): if not CONV_VOICES_CACHE: return None name_l = name.lower() overrides = { "phương": "Trúc Ly", "dũng": "Thanh Bình", "hùng": "Thái Sơn" } if name_l in overrides: target = overrides[name_l].lower() for v in CONV_VOICES_CACHE: label, value = (v[0], v[1]) if isinstance(v, tuple) else (v, v) if target in label.lower() or target in value.lower(): return value for v in CONV_VOICES_CACHE: label, value = (v[0], v[1]) if isinstance(v, tuple) else (v, v) if name_l == label.lower() or name_l == value.lower(): return value for v in CONV_VOICES_CACHE: label, value = (v[0], v[1]) if isinstance(v, tuple) else (v, v) if name_l in label.lower() or name_l in value.lower() or label.lower() in name_l or value.lower() in name_l: return value first_voice = CONV_VOICES_CACHE[0] return first_voice[1] if isinstance(first_voice, tuple) else first_voice name_updates, dd_updates, row_updates = [], [], [] for i in range(MAX_SPEAKERS): if i < len(speakers): name_updates.append(gr.update(value=speakers[i], visible=True)) dd_updates.append(gr.update(value=_best_match(speakers[i]), choices=CONV_VOICES_CACHE, visible=True)) row_updates.append(gr.update(visible=True)) else: name_updates.append(gr.update(value="", visible=False)) dd_updates.append(gr.update(value=None, choices=CONV_VOICES_CACHE, visible=False)) row_updates.append(gr.update(visible=False)) return name_updates + dd_updates + row_updates # --- 4. GRADIO UI --- with gr.Blocks(theme=theme, css=css, title="Pum's Tools — TextToSpeech", head=head_html) as demo: session_id_state = gr.State("") with gr.Column(elem_classes="container"): # --- CONFIGURATION (Hidden from UI, but active in backend) --- with gr.Group(visible=False): with gr.Row(): default_backbone = "VieNeu-TTS-v3-Turbo (Thử nghiệm)" if default_backbone not in BACKBONE_CONFIGS: default_backbone = list(BACKBONE_CONFIGS.keys())[0] if "v3" in default_backbone.lower(): default_codec = "VieNeu-Codec" default_temp = 0.8 default_text = DEFAULT_TEXT_V3 elif "Turbo" in default_backbone: default_codec = "VieNeu-Codec" default_temp = 0.4 default_text = DEFAULT_TEXT_TURBO elif "(CPU)" in default_backbone: default_codec = "NeuCodec (ONNX)" default_temp = 0.7 default_text = DEFAULT_TEXT_GPU else: default_codec = "NeuCodec (Distill)" if "NeuCodec (Distill)" in CODEC_CONFIGS else list(CODEC_CONFIGS.keys())[0] default_temp = 0.7 default_text = DEFAULT_TEXT_GPU default_batch_size = 32 if "v3" in default_backbone.lower() else 4 backbone_select = gr.Dropdown( list(BACKBONE_CONFIGS.keys()) + ["Custom Model"], value=default_backbone, label="🦜 Backbone" ) codec_select = gr.Dropdown( list(CODEC_CONFIGS.keys()), value=default_codec, label="🎵 Codec", interactive=False ) device_choice = gr.Radio(get_available_devices(), value="Auto", label="🖥️ Device") with gr.Row(visible=False) as custom_model_group: custom_backbone_model_id = gr.Textbox( label="📦 Custom Model ID", placeholder="pnnbao-ump/VieNeu-TTS-0.3B-lora-ngoc-huyen", info="Nhập HuggingFace Repo ID hoặc đường dẫn local", scale=2 ) custom_backbone_hf_token = gr.Textbox( label="🔑 HF Token (nếu private)", placeholder="Để trống nếu repo public", type="password", scale=1 ) base_model_choices = [k for k in BACKBONE_CONFIGS.keys() if "turbo" not in k.lower() and k != "Custom Model"] custom_backbone_base_model = gr.Dropdown( base_model_choices, label="🔗 Base Model (cho LoRA)", value=base_model_choices[0] if base_model_choices else None, visible=False, scale=1 ) with gr.Row(): use_lmdeploy_cb = gr.Checkbox( value=True, label="🚀 Optimize with LMDeploy (Khuyên dùng cho NVIDIA GPU)", visible="v3" not in default_backbone.lower(), ) btn_load = gr.Button("🔄 Tải Model", variant="primary") # --- MODEL STATUS DISPLAY (Visible) --- with gr.Group(elem_classes="model-status-group"): model_status = gr.Markdown("⏳ **Trạng thái model:** Đang tự động tải model... Vui lòng kiên nhẫn.", elem_classes="model-status-markdown") with gr.Row(elem_classes="container"): # --- INPUT --- with gr.Column(scale=3): with gr.Tabs() as main_input_tabs: # --- TAB 1: SINGLE SPEAKER --- with gr.Tab("🦜 Đọc truyện", id="single_tab") as single_tab: text_input = gr.Textbox( label=f"Văn bản", lines=8, value=default_text, ) with gr.Tabs() as tabs: with gr.TabItem("👤 Preset", id="preset_mode") as tab_preset: voice_select = gr.Dropdown(choices=[], value=None, label="Giọng mẫu", allow_custom_value=True) with gr.TabItem("🦜 Voice Cloning", id="custom_mode", visible=False) as tab_custom: clone_info_md = gr.Markdown( "ℹ️ **Voice Cloning (VieNeu-TTS v3).** Chỉ cần tải lên audio mẫu " "3–5 giây; v3 clone trực tiếp từ audio, không cần nhập nội dung." ) with gr.Group(visible=True) as cloning_elements_group: custom_audio = gr.Audio(label="Audio giọng mẫu (3-5 giây) (.wav)", type="filepath") cloning_warning_msg = gr.Markdown(visible=False, elem_id="cloning-warning") custom_text = gr.Textbox(label="Nội dung audio mẫu", visible=False) gr.Markdown(""" **💡 Mẹo nhỏ:** Nếu kết quả Voice Cloning chưa như ý, hãy cân nhắc **Finetune (LoRA)** để đạt chất lượng tốt nhất. """) generation_mode = gr.Radio( ["Standard (Một lần)"], value="Standard (Một lần)", label="Chế độ sinh" ) btn_generate = gr.Button("🎵 Bắt đầu", variant="primary", scale=2, interactive=False) # --- TAB 2: MULTI-SPEAKER CONVERSATION --- with gr.Tab("🎭 Hội thoại", id="conv_tab", visible=False) as conv_tab: conv_script_input = gr.Textbox( label="Kịch bản hội thoại", placeholder="Phương: Chào mọi người, mình là Phương...", lines=10, elem_classes="script-box", value=DEFAULT_CONV_SCRIPT, ) with gr.Row(): btn_detect_speakers = gr.Button("🔍 Quét nhân vật", size="sm", variant="secondary") silence_slider = gr.Slider(minimum=0, maximum=3, value=0.1, step=0.1, label="⏱️ Khoảng lặng (giây)") gr.Markdown("### 🎭 Cấu hình giọng đọc") gr.Markdown("*Nhấn **Quét nhân vật** để tự động phát hiện và ánh xạ giọng đọc.*") speaker_name_boxes = [] speaker_voice_dds = [] speaker_slot_rows = [] for _i in range(MAX_SPEAKERS): _default_name = "" _default_voice = None _row_visible = False if _i == 0: _default_name = "Phương" _default_voice = "Ly" _row_visible = True elif _i == 1: _default_name = "Dũng" _default_voice = "Binh" _row_visible = True elif _i == 2: _default_name = "Hùng" _default_voice = "Sơn" _row_visible = True with gr.Row(visible=_row_visible) as _row: _name = gr.Textbox( value=_default_name, label="👤 Nhân vật", interactive=False, scale=1, min_width=120 ) _dd = gr.Dropdown( choices=PRESET_VOICES_CACHE, value=_default_voice, label="🎤 Giọng đọc", interactive=True, scale=3, allow_custom_value=True ) speaker_slot_rows.append(_row) speaker_name_boxes.append(_name) speaker_voice_dds.append(_dd) btn_generate_conv = gr.Button("🎭 Bắt đầu hội thoại", variant="primary", interactive=False) # Global Generation Settings with gr.Row(): use_batch = gr.Checkbox( value=True, label="⚡ Batch Processing", info="Xử lý nhiều đoạn cùng lúc (chỉ áp dụng khi sử dụng GPU)" ) max_batch_size_run = gr.Slider( minimum=1, maximum=32, value=default_batch_size, step=1, label="📊 Batch Size (Generation)", info="Số đoạn xử lý cùng lúc. Cao = nhanh hơn nhưng tốn VRAM hơn." ) with gr.Accordion("⚙️ Cài đặt nâng cao", open=False): with gr.Row(): temperature_slider = gr.Slider( minimum=0.1, maximum=1.5, value=default_temp, step=0.1, label="🌡️ Temperature", info="Cao = đa dạng cảm xúc hơn. Thấp = ổn định hơn." ) max_chars_chunk_slider = gr.Slider( minimum=128, maximum=512, value=256, step=32, label="📝 Max Chars per Chunk", info="Độ dài tối đa mỗi đoạn xử lý." ) current_mode_state = gr.State("preset_mode") with gr.Row(): btn_stop = gr.Button("⏹️ Dừng", variant="stop", scale=1, interactive=False) # --- OUTPUT --- with gr.Column(scale=2): audio_output = gr.Audio( label="Kết quả", type="filepath", autoplay=True ) with gr.Group(): status_output = gr.Textbox( label="Trạng thái", elem_classes="status-box", lines=2, max_lines=10, show_copy_button=True ) with gr.Group(): estimate_output = gr.Textbox( label="Ước tính thời gian", elem_classes="estimate-box", lines=2, max_lines=4, show_copy_button=True ) gr.Markdown("