ndmhung6's picture
feat: add local offline document translation tool with multiple styles
6cb4903
Raw
History Blame Contribute Delete
82.2 kB
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("<div style='text-align: center; color: #64748b; font-size: 0.8rem;'>🔒 Audio được đóng dấu bản quyền ẩn (Watermarker).</div>")
# --- EVENT HANDLERS ---
codec_select.change(
on_codec_change,
inputs=[codec_select, current_mode_state],
outputs=[tab_custom, tabs, current_mode_state]
)
tab_preset.select(lambda: "preset_mode", outputs=current_mode_state)
tab_custom.select(lambda: "custom_mode", outputs=current_mode_state)
custom_audio.change(validate_audio_duration, inputs=[custom_audio], outputs=[cloning_warning_msg])
def on_backbone_change(choice):
is_custom = (choice == "Custom Model")
is_v3 = "v3" in (choice or "").lower()
is_v2_gpu = (choice == "VieNeu-TTS-v2 (GPU)")
clone_ok = is_v3 or is_v2_gpu
is_hw_accel_supported = "(GPU)" in choice or "v2-Turbo" in choice or "v3" in choice.lower() or is_custom
if is_hw_accel_supported:
dev_choices = get_available_devices()
initial_dev = "Auto"
else:
dev_choices = ["CPU"]
initial_dev = "CPU"
if is_v3:
codec_update = gr.update(value="VieNeu-Codec", interactive=False)
text_update = gr.update(value=DEFAULT_TEXT_V3)
temp_update = gr.update(value=0.8)
elif "Turbo" in choice:
codec_update = gr.update(value="VieNeu-Codec", interactive=False)
text_update = gr.update(value=DEFAULT_TEXT_TURBO)
temp_update = gr.update(value=0.4)
elif "(CPU)" in choice:
codec_update = gr.update(value="NeuCodec (ONNX)", interactive=False)
text_update = gr.update(value=DEFAULT_TEXT_GPU)
temp_update = gr.update(value=0.7)
else:
codec_update = gr.update(value="NeuCodec (Distill)", interactive=False)
text_update = gr.update(value=DEFAULT_TEXT_GPU)
temp_update = gr.update(value=0.7)
if is_v2_gpu:
clone_info_update = gr.update(value=(
"ℹ️ **Voice Cloning (VieNeu-TTS v2).** Tải lên audio mẫu 3–5 giây "
"và **nhập đúng nội dung** của audio đó."
))
else:
clone_info_update = gr.update(value=(
"ℹ️ **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."
))
return (
gr.update(visible=is_custom),
codec_update,
text_update,
temp_update,
gr.update(choices=dev_choices, value=initial_dev),
gr.update(visible=clone_ok),
gr.update(visible=clone_ok),
gr.update(value=32 if is_v3 else 4),
gr.update(visible=not is_v3),
gr.update(visible=is_v2_gpu),
clone_info_update,
)
backbone_select.change(
on_backbone_change,
inputs=[backbone_select],
outputs=[
custom_model_group,
codec_select,
text_input,
temperature_slider,
device_choice,
cloning_elements_group,
tab_custom,
max_batch_size_run,
use_lmdeploy_cb,
custom_text,
clone_info_md,
]
)
custom_backbone_model_id.change(
on_custom_id_change,
inputs=[custom_backbone_model_id],
outputs=[custom_backbone_base_model, custom_audio, custom_text]
)
btn_load.click(
fn=load_model,
inputs=[backbone_select, codec_select, device_choice, use_lmdeploy_cb,
custom_backbone_model_id, custom_backbone_base_model, custom_backbone_hf_token],
outputs=[model_status, btn_generate, btn_generate_conv, btn_load, btn_stop, voice_select,
tab_preset, tab_custom, tabs, current_mode_state,
conv_tab,
*speaker_voice_dds]
)
# Conversation handlers
btn_detect_speakers.click(
fn=extract_speakers_from_script,
inputs=[conv_script_input],
outputs=speaker_name_boxes + speaker_voice_dds + speaker_slot_rows
)
conv_gen_event = btn_generate_conv.click(
fn=synthesize_conversation_with_empty_estimate,
inputs=[conv_script_input,
*speaker_name_boxes,
*speaker_voice_dds,
silence_slider, temperature_slider, max_chars_chunk_slider,
session_id_state],
outputs=[audio_output, status_output, estimate_output]
)
btn_generate_conv.click(lambda: gr.update(interactive=True), outputs=btn_stop)
conv_gen_event.then(lambda: gr.update(interactive=False), outputs=btn_stop)
# Auto-adjust Temperature on Tab Switch
conv_tab.select(
fn=lambda bb: gr.update(value=0.8 if "v3" in (bb or "").lower() else 1.0),
inputs=backbone_select,
outputs=temperature_slider
)
single_tab.select(
fn=lambda bb: gr.update(value=0.8 if "v3" in (bb or "").lower() else default_temp),
inputs=backbone_select,
outputs=temperature_slider
)
# Standard Generation Handlers
gen_event = btn_generate.click(
fn=synthesize_speech_with_estimate,
inputs=[text_input, voice_select, custom_audio, custom_text, current_mode_state,
generation_mode, use_batch, max_batch_size_run,
temperature_slider, max_chars_chunk_slider, session_id_state],
outputs=[audio_output, status_output, estimate_output]
)
btn_generate.click(lambda: gr.update(interactive=True), outputs=btn_stop)
gen_event.then(lambda: gr.update(interactive=False), outputs=btn_stop)
# Stop Button
def request_stop():
print("🛑 STOP REQUESTED via button click.")
_STOP_EVENT.set()
return None, "⏹️ Đã dừng tạo giọng nói.", "", gr.update(interactive=False)
btn_stop.click(fn=request_stop, outputs=[audio_output, status_output, estimate_output, btn_stop])
# Automatic Model Load on Start
demo.load(
fn=auto_load_model_if_needed,
inputs=[backbone_select, codec_select, device_choice, use_lmdeploy_cb,
custom_backbone_model_id, custom_backbone_base_model, custom_backbone_hf_token],
outputs=[model_status, btn_generate, btn_generate_conv, btn_load, btn_stop, voice_select,
tab_preset, tab_custom, tabs, current_mode_state,
conv_tab,
*speaker_voice_dds]
)
def main():
server_name = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0" if ("SPACE_ID" in os.environ or "PORT" in os.environ) else "127.0.0.1")
default_port = "7860" if "SPACE_ID" in os.environ else ("7861" if "PORT" not in os.environ else os.getenv("PORT"))
server_port = int(os.getenv("GRADIO_SERVER_PORT", default_port))
is_on_colab = os.getenv("COLAB_RELEASE_TAG") is not None
share = env_bool("GRADIO_SHARE", default=is_on_colab)
if server_name == "0.0.0.0" and os.getenv("GRADIO_SHARE") is None:
share = False
demo.queue().launch(server_name=server_name, server_port=server_port, share=share)
if __name__ == "__main__":
main()