Update app.py
Browse files
app.py
CHANGED
|
@@ -6,10 +6,17 @@ import gradio as gr
|
|
| 6 |
import soundfile as sf
|
| 7 |
import tempfile
|
| 8 |
import torch
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
import time
|
|
|
|
| 11 |
|
| 12 |
-
print("⏳ Đang khởi động VieNeu-TTS...")
|
| 13 |
|
| 14 |
# --- 1. SETUP MODEL ---
|
| 15 |
print("📦 Đang tải model...")
|
|
@@ -17,78 +24,64 @@ device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
| 17 |
print(f"🖥️ Sử dụng thiết bị: {device.upper()}")
|
| 18 |
|
| 19 |
try:
|
| 20 |
-
|
| 21 |
-
|
|
|
|
| 22 |
backbone_device=device,
|
| 23 |
-
codec_repo="
|
| 24 |
-
codec_device=device
|
|
|
|
| 25 |
)
|
| 26 |
-
print("✅ Model đã tải xong!")
|
| 27 |
except Exception as e:
|
| 28 |
print(f"⚠️ Không thể tải model (Chế độ UI Demo): {e}")
|
|
|
|
| 29 |
class MockTTS:
|
| 30 |
-
def
|
| 31 |
-
def
|
| 32 |
-
|
| 33 |
-
|
| 34 |
time.sleep(1.5)
|
| 35 |
-
return np.random.uniform(-0.
|
| 36 |
tts = MockTTS()
|
| 37 |
|
| 38 |
-
# --- 2. DATA ---
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
"Ngọc (nữ miền Bắc)": {"audio": "./sample/Ngọc (nữ miền Bắc).wav", "text": "./sample/Ngọc (nữ miền Bắc).txt"},
|
| 45 |
-
"Ly (nữ miền Bắc)": {"audio": "./sample/Ly (nữ miền Bắc).wav", "text": "./sample/Ly (nữ miền Bắc).txt"}
|
| 46 |
-
}
|
| 47 |
|
| 48 |
# --- 3. HELPER FUNCTIONS ---
|
| 49 |
-
def load_reference_info(voice_choice):
|
| 50 |
-
if voice_choice in VOICE_SAMPLES:
|
| 51 |
-
audio_path = VOICE_SAMPLES[voice_choice]["audio"]
|
| 52 |
-
text_path = VOICE_SAMPLES[voice_choice]["text"]
|
| 53 |
-
try:
|
| 54 |
-
if os.path.exists(text_path):
|
| 55 |
-
with open(text_path, "r", encoding="utf-8") as f:
|
| 56 |
-
ref_text = f.read()
|
| 57 |
-
return audio_path, ref_text
|
| 58 |
-
else:
|
| 59 |
-
return audio_path, "⚠️ Không tìm thấy file text mẫu."
|
| 60 |
-
except Exception as e:
|
| 61 |
-
return None, f"❌ Lỗi: {str(e)}"
|
| 62 |
-
return None, ""
|
| 63 |
-
|
| 64 |
@spaces.GPU(duration=120)
|
| 65 |
-
def synthesize_speech(text, voice_choice, custom_audio, custom_text, mode_tab):
|
| 66 |
try:
|
| 67 |
if not text or text.strip() == "":
|
| 68 |
-
return None, "⚠️ Vui lòng nhập văn bản
|
| 69 |
|
| 70 |
-
# --- LOGIC CHECK LIMIT
|
| 71 |
-
if len(text) >
|
| 72 |
-
return None, f"❌ Văn bản quá dài ({len(text)}/
|
| 73 |
|
| 74 |
-
# Logic chọn Reference
|
|
|
|
| 75 |
if mode_tab == "custom_mode":
|
| 76 |
if custom_audio is None or not custom_text:
|
| 77 |
-
return None, "⚠️ Vui lòng tải lên Audio và nhập nội dung Audio đó."
|
| 78 |
-
|
| 79 |
ref_text_raw = custom_text
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
else: # Preset
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
ref_text_path = VOICE_SAMPLES[voice_choice]["text"]
|
| 86 |
-
|
| 87 |
-
if not os.path.exists(ref_audio_path):
|
| 88 |
-
return None, f"❌ Không tìm thấy file audio: {ref_audio_path}"
|
| 89 |
-
|
| 90 |
-
with open(ref_text_path, "r", encoding="utf-8") as f:
|
| 91 |
-
ref_text_raw = f.read()
|
| 92 |
print(f"🎤 Mode: Preset Voice ({voice_choice})")
|
| 93 |
|
| 94 |
# Inference & Đo thời gian
|
|
@@ -96,8 +89,13 @@ def synthesize_speech(text, voice_choice, custom_audio, custom_text, mode_tab):
|
|
| 96 |
|
| 97 |
start_time = time.time()
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
end_time = time.time()
|
| 103 |
process_time = end_time - start_time
|
|
@@ -107,241 +105,189 @@ def synthesize_speech(text, voice_choice, custom_audio, custom_text, mode_tab):
|
|
| 107 |
sf.write(tmp_file.name, wav, 24000)
|
| 108 |
output_path = tmp_file.name
|
| 109 |
|
| 110 |
-
return output_path, f"✅
|
| 111 |
|
| 112 |
except Exception as e:
|
| 113 |
import traceback
|
| 114 |
traceback.print_exc()
|
| 115 |
-
return None, f"❌ Lỗi
|
| 116 |
|
| 117 |
# --- 4. UI SETUP ---
|
| 118 |
theme = gr.themes.Soft(
|
| 119 |
primary_hue="indigo",
|
| 120 |
secondary_hue="cyan",
|
| 121 |
neutral_hue="slate",
|
| 122 |
-
font=[gr.themes.GoogleFont('
|
| 123 |
).set(
|
| 124 |
button_primary_background_fill="linear-gradient(90deg, #6366f1 0%, #0ea5e9 100%)",
|
| 125 |
button_primary_background_fill_hover="linear-gradient(90deg, #4f46e5 0%, #0284c7 100%)",
|
| 126 |
-
block_shadow="0 4px
|
| 127 |
)
|
| 128 |
|
| 129 |
css = """
|
| 130 |
-
.container { max-width:
|
| 131 |
.header-box {
|
| 132 |
text-align: center;
|
| 133 |
margin-bottom: 25px;
|
| 134 |
-
padding:
|
| 135 |
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
| 136 |
-
border-radius:
|
| 137 |
border: 1px solid #334155;
|
| 138 |
-
box-shadow: 0 10px
|
| 139 |
}
|
| 140 |
.header-title {
|
| 141 |
-
font-size: 2.
|
| 142 |
font-weight: 800;
|
| 143 |
color: white;
|
| 144 |
-
background: -webkit-linear-gradient(45deg, #
|
| 145 |
-webkit-background-clip: text;
|
| 146 |
-webkit-text-fill-color: transparent;
|
| 147 |
-
margin-bottom:
|
| 148 |
}
|
| 149 |
.header-desc {
|
| 150 |
-
font-size: 1.
|
| 151 |
-
color: #
|
| 152 |
margin-bottom: 15px;
|
| 153 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
.link-group a {
|
| 155 |
text-decoration: none;
|
| 156 |
-
margin: 0
|
| 157 |
font-weight: 600;
|
| 158 |
-
color: #
|
| 159 |
-
transition:
|
| 160 |
}
|
| 161 |
-
.link-group a:hover { color: #38bdf8;
|
| 162 |
.status-box { font-weight: bold; text-align: center; border: none; background: transparent; }
|
| 163 |
-
.
|
| 164 |
-
background:
|
| 165 |
-
border:
|
| 166 |
-
border-radius:
|
| 167 |
-
padding:
|
| 168 |
-
margin:
|
| 169 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 170 |
-
}
|
| 171 |
-
.warning-banner-title {
|
| 172 |
-
font-size: 1.1rem;
|
| 173 |
-
font-weight: 700;
|
| 174 |
-
color: #92400e;
|
| 175 |
-
margin-bottom: 8px;
|
| 176 |
-
display: flex;
|
| 177 |
-
align-items: center;
|
| 178 |
-
gap: 8px;
|
| 179 |
-
}
|
| 180 |
-
.warning-banner-content {
|
| 181 |
-
color: #78350f;
|
| 182 |
-
font-size: 0.95rem;
|
| 183 |
-
line-height: 1.6;
|
| 184 |
-
}
|
| 185 |
-
.warning-banner-content strong {
|
| 186 |
-
color: #92400e;
|
| 187 |
-
font-weight: 600;
|
| 188 |
-
}
|
| 189 |
-
.warning-banner-content code {
|
| 190 |
-
background: #fef3c7;
|
| 191 |
-
padding: 2px 6px;
|
| 192 |
-
border-radius: 3px;
|
| 193 |
-
font-family: monospace;
|
| 194 |
-
color: #92400e;
|
| 195 |
-
font-weight: 500;
|
| 196 |
}
|
| 197 |
"""
|
| 198 |
|
| 199 |
EXAMPLES_LIST = [
|
| 200 |
-
["
|
| 201 |
-
["
|
| 202 |
-
["
|
| 203 |
-
["
|
| 204 |
-
["Ngày xửa ngày xưa, ở một ngôi làng nọ có cô Tấm xinh đẹp, nết na nhưng sớm mồ côi mẹ. Dù bị mẹ kế và Cám hãm hại đủ đường, Tấm vẫn giữ được tấm lòng lương thiện và cuối cùng tìm được hạnh phúc xứng đáng.", "Đoan (nữ miền Nam)"],
|
| 205 |
-
["Dạ em chào anh chị, hiện tại bên em đang có chương trình ưu đãi đặc biệt cho căn hộ hướng sông này. Với thiết kế hiện đại và không gian xanh mát, đây chắc chắn là tổ ấm lý tưởng mà gia đình mình đang tìm kiếm.", "Ly (nữ miền Bắc)"],
|
| 206 |
]
|
| 207 |
|
| 208 |
-
with gr.Blocks(theme=theme, css=css, title="VieNeu-TTS
|
| 209 |
|
| 210 |
with gr.Column(elem_classes="container"):
|
| 211 |
# Header
|
| 212 |
-
gr.HTML("""
|
| 213 |
<div class="header-box">
|
| 214 |
-
<div class="header-title">🦜 VieNeu-TTS
|
| 215 |
<div class="header-desc">
|
| 216 |
-
|
| 217 |
</div>
|
| 218 |
<div class="link-group">
|
| 219 |
-
<a href="https://huggingface.co/pnnbao-ump/VieNeu-TTS" target="_blank">🤗 Model
|
| 220 |
-
<a href="https://
|
| 221 |
-
<a href="https://
|
| 222 |
</div>
|
| 223 |
</div>
|
| 224 |
""")
|
| 225 |
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
🚀 Với LMDeploy + GPU local, tốc độ sẽ <strong>nhanh hơn 5-10 lần</strong> so với demo này!
|
| 238 |
-
</div>
|
| 239 |
-
</div>
|
| 240 |
-
""")
|
| 241 |
-
|
| 242 |
-
with gr.Row(elem_classes="container", equal_height=False):
|
| 243 |
-
|
| 244 |
-
# --- LEFT: INPUT ---
|
| 245 |
-
with gr.Column(scale=3, variant="panel"):
|
| 246 |
-
gr.Markdown("### 📝 Văn bản đầu vào")
|
| 247 |
-
text_input = gr.Textbox(
|
| 248 |
-
label="Nhập văn bản",
|
| 249 |
-
placeholder="Nhập nội dung tiếng Việt cần chuyển thành giọng nói...",
|
| 250 |
-
lines=4,
|
| 251 |
-
value="Sự bùng nổ của trí tuệ nhân tạo đang định hình lại cách chúng ta làm việc và sinh sống. Từ xe tự lái đến trợ lý ảo thông minh, công nghệ đang dần xóa nhòa ranh giới giữa thực tại và những bộ phim viễn tưởng.",
|
| 252 |
-
show_label=False
|
| 253 |
-
)
|
| 254 |
-
|
| 255 |
-
# Counter
|
| 256 |
-
with gr.Row():
|
| 257 |
-
char_count = gr.HTML("<div style='text-align: right; color: #64748B; font-size: 0.8rem;'>0 / 250 ký tự</div>")
|
| 258 |
-
|
| 259 |
-
gr.Markdown("### 🗣️ Chọn giọng đọc")
|
| 260 |
-
with gr.Tabs() as tabs:
|
| 261 |
-
with gr.TabItem("👤 Giọng có sẵn (Preset)", id="preset_mode"):
|
| 262 |
-
voice_select = gr.Dropdown(
|
| 263 |
-
choices=list(VOICE_SAMPLES.keys()),
|
| 264 |
-
value="Tuyên (nam miền Bắc)",
|
| 265 |
-
label="Danh sách giọng",
|
| 266 |
-
interactive=True
|
| 267 |
)
|
| 268 |
-
with gr.
|
| 269 |
-
|
| 270 |
-
ref_text_preview = gr.Markdown("...")
|
| 271 |
-
|
| 272 |
-
with gr.TabItem("🦜 Voice Cloning", id="custom_mode"):
|
| 273 |
-
gr.Markdown("Tải lên giọng của bạn (Zero-shot Cloning)")
|
| 274 |
-
custom_audio = gr.Audio(label="File ghi âm (.wav)", type="filepath")
|
| 275 |
-
custom_text = gr.Textbox(label="Nội dung ghi âm", placeholder="Nhập chính xác lời thoại...")
|
| 276 |
|
| 277 |
-
gr.
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
[os.path.join("examples", "audio_ref", "example_2.wav"), "Trên thực tế, các nghi ngờ đã bắt đầu xuất hiện."],
|
| 281 |
-
[os.path.join("examples", "audio_ref", "example_3.wav"), "Cậu có nhìn thấy không?"],
|
| 282 |
-
[os.path.join("examples", "audio_ref", "example_4.wav"), "Tết là dịp mọi người háo hức đón chào một năm mới với nhiều hy vọng và mong ước."]
|
| 283 |
-
],
|
| 284 |
-
inputs=[custom_audio, custom_text],
|
| 285 |
-
label="Ví dụ mẫu để thử nghiệm clone giọng"
|
| 286 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
| 290 |
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
gr.Markdown("### 🎧 Kết quả")
|
| 294 |
-
with gr.Group():
|
| 295 |
-
audio_output = gr.Audio(label="Audio đầu ra", type="filepath", autoplay=True)
|
| 296 |
-
status_output = gr.Textbox(label="Trạng thái", show_label=False, elem_classes="status-box", placeholder="Sẵn sàng...")
|
| 297 |
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
# --- LOGIC ---
|
| 305 |
def update_count(text):
|
| 306 |
l = len(text)
|
| 307 |
-
if l
|
| 308 |
-
|
| 309 |
-
msg = f"⚠️ <b>{l} / 250</b> - Quá giới hạn!"
|
| 310 |
-
elif l > 200:
|
| 311 |
-
color = "#ea580c"
|
| 312 |
-
msg = f"{l} / 250"
|
| 313 |
-
else:
|
| 314 |
-
color = "#64748B"
|
| 315 |
-
msg = f"{l} / 250 ký tự"
|
| 316 |
return f"<div style='text-align: right; color: {color}; font-size: 0.8rem; font-weight: bold'>{msg}</div>"
|
| 317 |
|
| 318 |
text_input.change(update_count, text_input, char_count)
|
| 319 |
|
| 320 |
-
def update_ref_preview(voice):
|
| 321 |
-
audio, text = load_reference_info(voice)
|
| 322 |
-
return audio, f"> *\"{text}\"*"
|
| 323 |
-
|
| 324 |
-
voice_select.change(update_ref_preview, voice_select, [ref_audio_preview, ref_text_preview])
|
| 325 |
-
demo.load(update_ref_preview, voice_select, [ref_audio_preview, ref_text_preview])
|
| 326 |
-
|
| 327 |
# Tab handling
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
def set_custom_mode():
|
| 332 |
-
return "custom_mode"
|
| 333 |
-
|
| 334 |
-
tabs.children[0].select(fn=set_preset_mode, outputs=current_mode)
|
| 335 |
-
tabs.children[1].select(fn=set_custom_mode, outputs=current_mode)
|
| 336 |
|
| 337 |
btn_generate.click(
|
| 338 |
fn=synthesize_speech,
|
| 339 |
-
inputs=[text_input, voice_select, custom_audio, custom_text, current_mode],
|
| 340 |
outputs=[audio_output, status_output]
|
| 341 |
)
|
| 342 |
|
| 343 |
if __name__ == "__main__":
|
| 344 |
-
demo.queue().launch(
|
| 345 |
-
server_name="0.0.0.0",
|
| 346 |
-
server_port=7860
|
| 347 |
-
)
|
|
|
|
| 6 |
import soundfile as sf
|
| 7 |
import tempfile
|
| 8 |
import torch
|
| 9 |
+
import sys
|
| 10 |
+
# Hỗ trợ cấu trúc thư mục trên HF Spaces: Thêm 'src' vào đường dẫn tìm kiếm module
|
| 11 |
+
src_path = os.path.join(os.path.dirname(__file__), "src")
|
| 12 |
+
if os.path.exists(src_path) and src_path not in sys.path:
|
| 13 |
+
sys.path.append(src_path)
|
| 14 |
+
|
| 15 |
+
from vieneu import Vieneu
|
| 16 |
import time
|
| 17 |
+
import numpy as np
|
| 18 |
|
| 19 |
+
print("⏳ Đang khởi động VieNeu-TTS v2-Turbo...")
|
| 20 |
|
| 21 |
# --- 1. SETUP MODEL ---
|
| 22 |
print("📦 Đang tải model...")
|
|
|
|
| 24 |
print(f"🖥️ Sử dụng thiết bị: {device.upper()}")
|
| 25 |
|
| 26 |
try:
|
| 27 |
+
# Sử dụng backend Standard (PyTorch) vì HF Space không chạy được LMDeploy dễ dàng
|
| 28 |
+
tts = Vieneu(
|
| 29 |
+
backbone_repo="pnnbao-ump/VieNeu-TTS-v2-Turbo",
|
| 30 |
backbone_device=device,
|
| 31 |
+
codec_repo="pnnbao-ump/VieNeu-Codec",
|
| 32 |
+
codec_device=device,
|
| 33 |
+
backend="standard"
|
| 34 |
)
|
| 35 |
+
print("✅ Model v2-Turbo đã tải xong!")
|
| 36 |
except Exception as e:
|
| 37 |
print(f"⚠️ Không thể tải model (Chế độ UI Demo): {e}")
|
| 38 |
+
# Mocking for UI testing if model fails
|
| 39 |
class MockTTS:
|
| 40 |
+
def list_preset_voices(self): return [("Xuân Vĩnh (Nam - Miền Nam)", "Xuân Vĩnh (Nam - Miền Nam)")]
|
| 41 |
+
def get_preset_voice(self, v): return {"codes": [3], "text": "Hello", "voice_id": 3}
|
| 42 |
+
def encode_reference(self, path): return np.zeros(10)
|
| 43 |
+
def infer(self, text, **kwargs):
|
| 44 |
time.sleep(1.5)
|
| 45 |
+
return np.random.uniform(-0.1, 0.1, 24000*3)
|
| 46 |
tts = MockTTS()
|
| 47 |
|
| 48 |
+
# --- 2. DATA (Tự động lấy từ SDK) ---
|
| 49 |
+
try:
|
| 50 |
+
PRESET_VOICES = tts.list_preset_voices()
|
| 51 |
+
VOICE_NAMES = [v[0] for v in PRESET_VOICES]
|
| 52 |
+
except:
|
| 53 |
+
VOICE_NAMES = ["Xuân Vĩnh (Nam - Miền Nam)"]
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
# --- 3. HELPER FUNCTIONS ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
@spaces.GPU(duration=120)
|
| 57 |
+
def synthesize_speech(text, voice_choice, speaker_id_val, custom_audio, custom_text, mode_tab, temperature):
|
| 58 |
try:
|
| 59 |
if not text or text.strip() == "":
|
| 60 |
+
return None, "⚠️ Vui lòng nhập văn bản hoặc chọn ví dụ bên dưới!"
|
| 61 |
|
| 62 |
+
# --- LOGIC CHECK LIMIT 500 ---
|
| 63 |
+
if len(text) > 500:
|
| 64 |
+
return None, f"❌ Văn bản quá dài ({len(text)}/500 ký tự)! Vui lòng cắt ngắn lại để đảm bảo chất lượng."
|
| 65 |
|
| 66 |
+
# Logic chọn Reference & Voice Data
|
| 67 |
+
voice_data = None
|
| 68 |
if mode_tab == "custom_mode":
|
| 69 |
if custom_audio is None or not custom_text:
|
| 70 |
+
return None, "⚠️ Vui lòng tải lên Audio và nhập nội dung Audio đó để Clone giọng."
|
| 71 |
+
ref_codes = tts.encode_reference(custom_audio)
|
| 72 |
ref_text_raw = custom_text
|
| 73 |
+
voice_data = {"codes": ref_codes, "text": ref_text_raw}
|
| 74 |
+
print("🎨 Mode: Voice Cloning")
|
| 75 |
+
elif mode_tab == "id_mode":
|
| 76 |
+
print(f"🎭 Mode: Speaker ID {speaker_id_val}")
|
| 77 |
+
# Dùng ID bất kỳ từ bộ dữ liệu
|
| 78 |
+
ref_codes = [int(speaker_id_val)]
|
| 79 |
+
ref_text_raw = "Chào mừng bạn đến với VieNeu-TTS."
|
| 80 |
+
voice_data = {"codes": ref_codes, "text": ref_text_raw, "voice_id": int(speaker_id_val)}
|
| 81 |
else: # Preset
|
| 82 |
+
voice_data = tts.get_preset_voice(voice_choice)
|
| 83 |
+
ref_codes = voice_data['codes']
|
| 84 |
+
ref_text_raw = voice_data['text']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
print(f"🎤 Mode: Preset Voice ({voice_choice})")
|
| 86 |
|
| 87 |
# Inference & Đo thời gian
|
|
|
|
| 89 |
|
| 90 |
start_time = time.time()
|
| 91 |
|
| 92 |
+
wav = tts.infer(
|
| 93 |
+
text,
|
| 94 |
+
ref_codes=ref_codes,
|
| 95 |
+
ref_text=ref_text_raw,
|
| 96 |
+
voice=voice_data,
|
| 97 |
+
temperature=temperature
|
| 98 |
+
)
|
| 99 |
|
| 100 |
end_time = time.time()
|
| 101 |
process_time = end_time - start_time
|
|
|
|
| 105 |
sf.write(tmp_file.name, wav, 24000)
|
| 106 |
output_path = tmp_file.name
|
| 107 |
|
| 108 |
+
return output_path, f"✅ Tổng hợp xong! (Thời gian: {process_time:.2f}s)"
|
| 109 |
|
| 110 |
except Exception as e:
|
| 111 |
import traceback
|
| 112 |
traceback.print_exc()
|
| 113 |
+
return None, f"❌ Lỗi: {str(e)}"
|
| 114 |
|
| 115 |
# --- 4. UI SETUP ---
|
| 116 |
theme = gr.themes.Soft(
|
| 117 |
primary_hue="indigo",
|
| 118 |
secondary_hue="cyan",
|
| 119 |
neutral_hue="slate",
|
| 120 |
+
font=[gr.themes.GoogleFont('Outfit'), 'ui-sans-serif', 'system-ui'],
|
| 121 |
).set(
|
| 122 |
button_primary_background_fill="linear-gradient(90deg, #6366f1 0%, #0ea5e9 100%)",
|
| 123 |
button_primary_background_fill_hover="linear-gradient(90deg, #4f46e5 0%, #0284c7 100%)",
|
| 124 |
+
block_shadow="0 4px 10px rgba(0, 0, 0, 0.05)",
|
| 125 |
)
|
| 126 |
|
| 127 |
css = """
|
| 128 |
+
.container { max-width: 1100px; margin: auto; }
|
| 129 |
.header-box {
|
| 130 |
text-align: center;
|
| 131 |
margin-bottom: 25px;
|
| 132 |
+
padding: 30px;
|
| 133 |
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
| 134 |
+
border-radius: 16px;
|
| 135 |
border: 1px solid #334155;
|
| 136 |
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
|
| 137 |
}
|
| 138 |
.header-title {
|
| 139 |
+
font-size: 2.8rem;
|
| 140 |
font-weight: 800;
|
| 141 |
color: white;
|
| 142 |
+
background: -webkit-linear-gradient(45deg, #818cf8, #22d3ee);
|
| 143 |
-webkit-background-clip: text;
|
| 144 |
-webkit-text-fill-color: transparent;
|
| 145 |
+
margin-bottom: 5px;
|
| 146 |
}
|
| 147 |
.header-desc {
|
| 148 |
+
font-size: 1.15rem;
|
| 149 |
+
color: #94a3b8;
|
| 150 |
margin-bottom: 15px;
|
| 151 |
}
|
| 152 |
+
.badge-v2 {
|
| 153 |
+
background: #4f46e5;
|
| 154 |
+
color: white;
|
| 155 |
+
padding: 2px 8px;
|
| 156 |
+
border-radius: 4px;
|
| 157 |
+
font-size: 0.8rem;
|
| 158 |
+
vertical-align: middle;
|
| 159 |
+
margin-left: 10px;
|
| 160 |
+
}
|
| 161 |
.link-group a {
|
| 162 |
text-decoration: none;
|
| 163 |
+
margin: 0 12px;
|
| 164 |
font-weight: 600;
|
| 165 |
+
color: #cbd5e1;
|
| 166 |
+
transition: all 0.2s;
|
| 167 |
}
|
| 168 |
+
.link-group a:hover { color: #38bdf8; transform: translateY(-1px); }
|
| 169 |
.status-box { font-weight: bold; text-align: center; border: none; background: transparent; }
|
| 170 |
+
.notice-card {
|
| 171 |
+
background: #f8fafc;
|
| 172 |
+
border: 1px solid #e2e8f0;
|
| 173 |
+
border-radius: 12px;
|
| 174 |
+
padding: 20px;
|
| 175 |
+
margin-bottom: 20px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
}
|
| 177 |
"""
|
| 178 |
|
| 179 |
EXAMPLES_LIST = [
|
| 180 |
+
["VieNeu-TTS-v2 can speak English smoothly too! You can mix both languages: Hà Nội là một thành phố beautiful với bề dày history ngàn năm văn hiến.", "Đoan Trang (Nữ - Miền Bắc)", 4, 0.4],
|
| 181 |
+
["Sự nhanh nhẹn và chính xác là ưu điểm của bản v2-Turbo. Chào mừng bạn đến với tương lai của TTS tiếng Việt!", "Xuân Vĩnh (Nam - Miền Nam)", 3, 0.4],
|
| 182 |
+
["Hà Nội vào thu thật đẹp. Đi dạo quanh Hồ Gươm sáng sớm và thưởng thức chút cốm làng Vòng là trải nghiệm khó quên.", "Phạm Tuyên (Nam - Miền Bắc)", 2, 0.5],
|
| 183 |
+
["Chào mừng bạn đến với kênh của chúng mình! Hôm nay mình sẽ giới thiệu về những địa điểm du lịch hot nhất miền Tây nhé.", "Thục Đoan (Nữ - Miền Nam)", 1, 0.4],
|
|
|
|
|
|
|
| 184 |
]
|
| 185 |
|
| 186 |
+
with gr.Blocks(theme=theme, css=css, title="VieNeu-TTS v2-Turbo") as demo:
|
| 187 |
|
| 188 |
with gr.Column(elem_classes="container"):
|
| 189 |
# Header
|
| 190 |
+
gr.HTML(f"""
|
| 191 |
<div class="header-box">
|
| 192 |
+
<div class="header-title">🦜 VieNeu-TTS v2-Turbo <span class="badge-v2">V2 RELEASE</span></div>
|
| 193 |
<div class="header-desc">
|
| 194 |
+
Mô hình TTS Anh-Việt song ngữ thế hệ mới. Khám phá kho giọng đọc khổng lồ.
|
| 195 |
</div>
|
| 196 |
<div class="link-group">
|
| 197 |
+
<a href="https://huggingface.co/pnnbao-ump/VieNeu-TTS-v2-Turbo" target="_blank">🤗 Model</a> •
|
| 198 |
+
<a href="https://github.com/pnnbao97/VieNeu-TTS" target="_blank">🦜 GitHub</a> •
|
| 199 |
+
<a href="https://pnnbao.id.vn" target="_blank">🌐 Website</a>
|
| 200 |
</div>
|
| 201 |
</div>
|
| 202 |
""")
|
| 203 |
|
| 204 |
+
with gr.Row():
|
| 205 |
+
# --- LEFT: INPUT ---
|
| 206 |
+
with gr.Column(scale=3):
|
| 207 |
+
with gr.Group():
|
| 208 |
+
gr.Markdown("### 📝 Nội dung văn bản")
|
| 209 |
+
text_input = gr.Textbox(
|
| 210 |
+
label="Nhập văn bản (hỗ trợ song ngữ)",
|
| 211 |
+
placeholder="Nhập nội dung tiếng Việt hoặc Anh-Việt mix...",
|
| 212 |
+
lines=5,
|
| 213 |
+
value="Chào mừng bạn đến với VieNeu-TTS-v2-Turbo! Đây là mô hình mới nhất hỗ trợ đọc song ngữ Anh-Việt cực kỳ mượt mà. I hope you enjoy this demo!",
|
| 214 |
+
show_label=False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
)
|
| 216 |
+
with gr.Row():
|
| 217 |
+
char_count = gr.HTML("<div style='text-align: right; color: #64748B; font-size: 0.8rem;'>0 / 500 ký tự</div>")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
+
temperature = gr.Slider(
|
| 220 |
+
minimum=0.1, maximum=1.0, value=0.4, step=0.1,
|
| 221 |
+
label="����️ Temperature", info="Cao = đa dạng cảm xúc hơn, Thấp = ổn định hơn."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
)
|
| 223 |
+
|
| 224 |
+
gr.Markdown("### 🗣️ Chọn giọng đọc")
|
| 225 |
+
with gr.Tabs() as tabs:
|
| 226 |
+
with gr.TabItem("👤 Nhân vật mẫu", id="preset_mode"):
|
| 227 |
+
voice_select = gr.Dropdown(
|
| 228 |
+
choices=VOICE_NAMES,
|
| 229 |
+
value=VOICE_NAMES[0] if VOICE_NAMES else None,
|
| 230 |
+
label="Chọn nhân vật",
|
| 231 |
+
interactive=True
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
with gr.TabItem("🎭 Giọng đa dạng (ID)", id="id_mode"):
|
| 235 |
+
gr.Markdown("Khám phá hàng ngàn giọng đọc khác nhau bằng cách chọn ID (0 - 1000).")
|
| 236 |
+
speaker_id_num = gr.Slider(
|
| 237 |
+
minimum=0, maximum=1000, value=3, step=1,
|
| 238 |
+
label="Speaker ID", info="Mỗi ID đại diện cho một giọng đọc khác nhau trong tập huấn luyện."
|
| 239 |
+
)
|
| 240 |
|
| 241 |
+
with gr.TabItem("🦜 Voice Cloning", id="custom_mode"):
|
| 242 |
+
gr.Markdown("Tải lên voice 3-5 giây để clone (Zero-shot)")
|
| 243 |
+
custom_audio = gr.Audio(label="Audio mẫu", type="filepath")
|
| 244 |
+
custom_text = gr.Textbox(label="Nội dung audio mẫu", placeholder="Nhập chính xác văn bản trong audio trên...")
|
| 245 |
|
| 246 |
+
current_mode = gr.State(value="preset_mode")
|
| 247 |
+
btn_generate = gr.Button("⚡ Bắt đầu tổng hợp", variant="primary", size="lg", elem_id="generate_btn")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
|
| 249 |
+
# --- RIGHT: OUTPUT ---
|
| 250 |
+
with gr.Column(scale=2):
|
| 251 |
+
gr.Markdown("### 🎧 Kết quả âm thanh")
|
| 252 |
+
with gr.Group():
|
| 253 |
+
audio_output = gr.Audio(label="Output Audio", type="filepath", autoplay=True)
|
| 254 |
+
status_output = gr.Textbox(label="Trạng thái", show_label=False, elem_classes="status-box", placeholder="Sẵn sàng...")
|
| 255 |
+
|
| 256 |
+
with gr.Column(elem_classes="notice-card"):
|
| 257 |
+
gr.Markdown("#### 💡 Mẹo nhỏ")
|
| 258 |
+
gr.Markdown("""
|
| 259 |
+
- **Speaker ID:** Cho phép bạn khám phá vô số giọng đọc có sẵn trong model mà không cần file mẫu.
|
| 260 |
+
- **Song ngữ:** Bạn có thể viết xen kẽ tiếng Anh và tiếng Việt.
|
| 261 |
+
- **Cloning:** Hãy sử dụng audio rõ nét, không tạp âm để có kết quả tốt nhất.
|
| 262 |
+
""")
|
| 263 |
+
|
| 264 |
+
# --- EXAMPLES ---
|
| 265 |
+
gr.Markdown("### 📚 Ví dụ thử nghiệm")
|
| 266 |
+
gr.Examples(
|
| 267 |
+
examples=EXAMPLES_LIST,
|
| 268 |
+
inputs=[text_input, voice_select, speaker_id_num, temperature],
|
| 269 |
+
label="Nhấn vào ví dụ để thử nhanh"
|
| 270 |
+
)
|
| 271 |
|
| 272 |
# --- LOGIC ---
|
| 273 |
def update_count(text):
|
| 274 |
l = len(text)
|
| 275 |
+
color = "#64748B" if l <= 500 else "#dc2626"
|
| 276 |
+
msg = f"{l} / 500 ký tự"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
return f"<div style='text-align: right; color: {color}; font-size: 0.8rem; font-weight: bold'>{msg}</div>"
|
| 278 |
|
| 279 |
text_input.change(update_count, text_input, char_count)
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
# Tab handling
|
| 282 |
+
tabs.children[0].select(fn=lambda: "preset_mode", outputs=current_mode)
|
| 283 |
+
tabs.children[1].select(fn=lambda: "id_mode", outputs=current_mode)
|
| 284 |
+
tabs.children[2].select(fn=lambda: "custom_mode", outputs=current_mode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
btn_generate.click(
|
| 287 |
fn=synthesize_speech,
|
| 288 |
+
inputs=[text_input, voice_select, speaker_id_num, custom_audio, custom_text, current_mode, temperature],
|
| 289 |
outputs=[audio_output, status_output]
|
| 290 |
)
|
| 291 |
|
| 292 |
if __name__ == "__main__":
|
| 293 |
+
demo.queue().launch(show_api=False)
|
|
|
|
|
|
|
|
|