pnnbao-ump commited on
Commit
9b3c638
·
verified ·
1 Parent(s): ae2554a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +166 -220
app.py CHANGED
@@ -6,10 +6,17 @@ import gradio as gr
6
  import soundfile as sf
7
  import tempfile
8
  import torch
9
- from vieneu_tts import VieNeuTTS
 
 
 
 
 
 
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
- tts = VieNeuTTS(
21
- backbone_repo="pnnbao-ump/VieNeu-TTS-0.3B",
 
22
  backbone_device=device,
23
- codec_repo="neuphonic/neucodec",
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 encode_reference(self, path): return None
31
- def infer(self, text, ref, ref_text):
32
- import numpy as np
33
- # Giả lập độ trễ để test tính năng đo thời gian
34
  time.sleep(1.5)
35
- return np.random.uniform(-0.5, 0.5, 24000*3)
36
  tts = MockTTS()
37
 
38
- # --- 2. DATA ---
39
- VOICE_SAMPLES = {
40
- "Tuyên (nam miền Bắc)": {"audio": "./sample/Tuyên (nam miền Bắc).wav", "text": "./sample/Tuyên (nam miền Bắc).txt"},
41
- "Vĩnh (nam miền Nam)": {"audio": "./sample/Vĩnh (nam miền Nam).wav", "text": "./sample/Vĩnh (nam miền Nam).txt"},
42
- "Bình (nam miền Bắc)": {"audio": "./sample/Bình (nam miền Bắc).wav", "text": "./sample/Bình (nam miền Bắc).txt"},
43
- "Đoan (nữ miền Nam)": {"audio": "./sample/Đoan (nữ miền Nam).wav", "text": "./sample/Đoan (nữ miền Nam).txt"},
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 cần tổng hợp!"
69
 
70
- # --- LOGIC CHECK LIMIT 250 ---
71
- if len(text) > 250:
72
- return None, f"❌ Văn bản quá dài ({len(text)}/250 ký tự)! Vui lòng cắt ngắn lại để đảm bảo chất lượng."
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
- ref_audio_path = custom_audio
79
  ref_text_raw = custom_text
80
- print("🎨 Mode: Custom Voice")
 
 
 
 
 
 
 
81
  else: # Preset
82
- if voice_choice not in VOICE_SAMPLES:
83
- return None, "⚠️ Vui lòng chọn một giọng mẫu."
84
- ref_audio_path = VOICE_SAMPLES[voice_choice]["audio"]
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
- ref_codes = tts.encode_reference(ref_audio_path)
100
- wav = tts.infer(text, ref_codes, ref_text_raw)
 
 
 
 
 
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"✅ Thành công! (Thời gian: {process_time:.2f}s)"
111
 
112
  except Exception as e:
113
  import traceback
114
  traceback.print_exc()
115
- return None, f"❌ Lỗi hệ thống: {str(e)}"
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('Inter'), 'ui-sans-serif', 'system-ui'],
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 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
127
  )
128
 
129
  css = """
130
- .container { max-width: 1200px; margin: auto; }
131
  .header-box {
132
  text-align: center;
133
  margin-bottom: 25px;
134
- padding: 25px;
135
  background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
136
- border-radius: 12px;
137
  border: 1px solid #334155;
138
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
139
  }
140
  .header-title {
141
- font-size: 2.5rem;
142
  font-weight: 800;
143
  color: white;
144
- background: -webkit-linear-gradient(45deg, #60A5FA, #22D3EE);
145
  -webkit-background-clip: text;
146
  -webkit-text-fill-color: transparent;
147
- margin-bottom: 10px;
148
  }
149
  .header-desc {
150
- font-size: 1.1rem;
151
- color: #cbd5e1;
152
  margin-bottom: 15px;
153
  }
 
 
 
 
 
 
 
 
 
154
  .link-group a {
155
  text-decoration: none;
156
- margin: 0 10px;
157
  font-weight: 600;
158
- color: #94a3b8;
159
- transition: color 0.2s;
160
  }
161
- .link-group a:hover { color: #38bdf8; text-shadow: 0 0 5px rgba(56, 189, 248, 0.5); }
162
  .status-box { font-weight: bold; text-align: center; border: none; background: transparent; }
163
- .warning-banner {
164
- background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
165
- border: 2px solid #f59e0b;
166
- border-radius: 8px;
167
- padding: 15px 20px;
168
- margin: 15px 0;
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
- ["Về miền Tây không chỉ để ngắm nhìn sông nước hữu tình, còn để cảm nhận tấm chân tình của người dân nơi đây. Cùng ngồi xuồng ba lá len lỏi qua rặng dừa nước, nghe câu vọng cổ ngọt ngào thì còn gì bằng.", "Vĩnh (nam miền Nam)"],
201
- [" Nội những ngày vào thu mang một vẻ đẹp trầm mặc cổ kính đến lạ thường. Đi dạo quanh Hồ Gươm vào sáng sớm, hít mùi hoa sữa nồng nàn và thưởng thức chút cốm làng Vòng là trải nghiệm khó quên.", "Bình (nam miền Bắc)"],
202
- ["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 sinh sống. Từ xe tự lái đến trợ ả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.", "Tuyên (nam miền Bắc)"],
203
- ["Sài Gòn hối hả thế, nhưng chỉ cần tấp vào một quán cà phê ven đường, gọi ly bạc xỉu đá và ngắm nhìn dòng người qua lại, bạn sẽ thấy thành phố này cũng có những khoảng lặng thật bình yên đáng yêu.", "Ngọc (nữ miền Bắc)"],
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 Studio") as demo:
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 Studio</div>
215
  <div class="header-desc">
216
- Phiên bản: VieNeu-TTS (model mới nhất, train trên 1000 giờ dữ liệu)
217
  </div>
218
  <div class="link-group">
219
- <a href="https://huggingface.co/pnnbao-ump/VieNeu-TTS" target="_blank">🤗 Model Card</a> •
220
- <a href="https://huggingface.co/datasets/pnnbao-ump/VieNeu-TTS-1000h" target="_blank">📖 Dataset 1000h</a> •
221
- <a href="https://github.com/pnnbao97/VieNeu-TTS" target="_blank">🦜 GitHub</a>
222
  </div>
223
  </div>
224
  """)
225
 
226
- # Performance Warning Banner
227
- gr.HTML("""
228
- <div class="warning-banner">
229
- <div class="warning-banner-title">
230
- ⚠️ Lưu ý về hiệu năng
231
- </div>
232
- <div class="warning-banner-content">
233
- <strong>Demo này chạy trên HF Spaces với ZeroGPU (shared GPU)</strong> nên tốc độ sẽ <strong>chậm hơn</strong> và <strong>bị giới hạn 250 ký tự</strong> vì không thể triển khai lmdeploy trên HF space.<br><br>
234
-
235
- 💡 <strong>Muốn tốc độ cực nhanh không giới hạn tự?</strong> Hãy clone nguồn từ <a href="https://github.com/pnnbao97/VieNeu-TTS" target="_blank" style="color: #92400e; text-decoration: underline;">GitHub</a> cài <code>lmdeploy</code> để chạy trên GPU của bạn:<br>
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.Accordion("Thông tin giọng mẫu", open=False):
269
- ref_audio_preview = gr.Audio(label="Audio mẫu", interactive=False, type="filepath")
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.Examples(
278
- examples=[
279
- [os.path.join("examples", "audio_ref", "example.wav"), "Ví dụ 2. Tính trung bình của dãy số."],
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
- current_mode = gr.State(value="preset_mode")
289
- btn_generate = gr.Button("Tổng hợp giọng nói", variant="primary", size="lg")
 
 
290
 
291
- # --- RIGHT: OUTPUT ---
292
- with gr.Column(scale=2):
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
- # --- EXAMPLES ---
299
- with gr.Row(elem_classes="container"):
300
- with gr.Column():
301
- gr.Markdown("### 📚 Ví dụ mẫu")
302
- gr.Examples(examples=EXAMPLES_LIST, inputs=[text_input, voice_select], label="Thử nghiệm nhanh")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
  # --- LOGIC ---
305
  def update_count(text):
306
  l = len(text)
307
- if l > 250:
308
- color = "#dc2626"
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
- def set_preset_mode():
329
- return "preset_mode"
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: Nội 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 chính xác ư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
+ [" Nội vào thu thật đẹp. Đi dạo quanh Hồ Gươm sáng sớmthưởng thức chút cốm làng Vòng 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
+ 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 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)