Arrcttacsrks commited on
Commit
86fdb38
·
verified ·
1 Parent(s): 9f84367

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +363 -86
app.py CHANGED
@@ -95,28 +95,36 @@ VOICE_SAMPLES = {
95
  }
96
 
97
  # --- HISTORY MANAGEMENT ---
 
 
98
  def load_history():
99
- """Tải lịch sử từ file JSON"""
100
- if os.path.exists(HISTORY_JSON):
101
- try:
102
- with open(HISTORY_JSON, 'r', encoding='utf-8') as f:
103
- return json.load(f)
104
- except:
105
- return []
106
- return []
 
 
107
 
108
  def save_history(history):
109
- """Lưu lịch sử vào file JSON"""
110
- with open(HISTORY_JSON, 'w', encoding='utf-8') as f:
111
- json.dump(history, f, ensure_ascii=False, indent=2)
 
 
 
 
112
 
113
  def add_to_history(text, voice, audio_path, duration, status):
114
- """Thêm một bản ghi vào lịch sử"""
115
  try:
116
  history = load_history()
117
 
118
  # Tạo tên file duy nhất
119
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
120
  filename = f"tts_{timestamp}.wav"
121
  permanent_path = os.path.join(HISTORY_DIR, filename)
122
 
@@ -127,6 +135,8 @@ def add_to_history(text, voice, audio_path, duration, status):
127
  except Exception as e:
128
  print(f"⚠️ Không thể copy file audio: {e}")
129
  permanent_path = audio_path # Dùng file tạm nếu không copy được
 
 
130
 
131
  record = {
132
  "id": timestamp,
@@ -146,45 +156,129 @@ def add_to_history(text, voice, audio_path, duration, status):
146
  # Xóa file audio cũ
147
  old_record = history.pop()
148
  try:
149
- if os.path.exists(old_record['audio_path']):
150
  os.remove(old_record['audio_path'])
151
- except:
152
- pass
153
 
154
  save_history(history)
155
  return permanent_path
156
  except Exception as e:
157
  print(f"⚠️ Lỗi khi lưu lịch sử: {e}")
 
 
158
  return audio_path if audio_path else None
159
 
160
  def get_history_list():
161
- """Lấy danh sách lịch sử để hiển thị"""
162
  history = load_history()
163
  if not history:
164
- return "Chưa có lịch sử tổng hợp nào."
 
 
 
 
 
 
 
165
 
166
- output = []
167
  for i, record in enumerate(history[:50], 1): # Hiển thị 50 bản ghi gần nhất
168
- output.append(f"**{i}. [{record['timestamp']}]** - {record['voice']}")
169
- output.append(f" 📝 {record['text']}")
170
- output.append(f" ⏱️ Thời lượng: {record['duration']:.2f}s | {record['status']}")
171
- output.append("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
- return "\n".join(output)
 
174
 
175
  def delete_history_item(item_id):
176
  """Xóa một item khỏi lịch sử"""
177
  history = load_history()
178
- history = [h for h in history if h['id'] != item_id]
179
- save_history(history)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
  # --- BACKGROUND PROCESSING QUEUE ---
182
  processing_queue = Queue()
183
  is_processing = False
 
184
 
185
  def background_processor():
186
  """Xử lý queue tổng hợp trong background"""
187
- global is_processing
 
188
  while True:
189
  task = processing_queue.get()
190
  if task is None:
@@ -196,9 +290,22 @@ def background_processor():
196
  try:
197
  print(f"[Background] Bắt đầu tổng hợp: {text[:50]}...")
198
  result = synthesize_speech_internal(text, voice)
199
- print(f"[Background] Hoàn thành: {result}")
 
 
 
 
 
 
 
 
 
200
  except Exception as e:
201
- print(f"[Background] Lỗi: {e}")
 
 
 
 
202
 
203
  is_processing = False
204
  processing_queue.task_done()
@@ -207,6 +314,10 @@ def background_processor():
207
  bg_thread = threading.Thread(target=background_processor, daemon=True)
208
  bg_thread.start()
209
 
 
 
 
 
210
  def split_text_into_chunks(text, max_chars=256):
211
  """Chia text thành chunks"""
212
  sentences = re.split(r'([.!?,;])', text)
@@ -275,12 +386,15 @@ def synthesize_speech_internal(text, voice_choice):
275
  global backbone, codec, model_loaded
276
 
277
  if not model_loaded:
 
278
  return None
279
 
280
  if not text or text.strip() == "":
 
281
  return None
282
 
283
  if voice_choice not in VOICE_SAMPLES:
 
284
  return None
285
 
286
  raw_text = text.strip()
@@ -309,6 +423,7 @@ def synthesize_speech_internal(text, voice_choice):
309
  return None
310
 
311
  if ref_codes is None or len(ref_codes) == 0:
 
312
  return None
313
 
314
  # Split text
@@ -321,6 +436,8 @@ def synthesize_speech_internal(text, voice_choice):
321
 
322
  try:
323
  for i, chunk in enumerate(text_chunks):
 
 
324
  # Phonemize
325
  ref_text_phoneme = phonemize_with_dict(ref_text_raw)
326
  input_text_phoneme = phonemize_with_dict(chunk)
@@ -343,7 +460,7 @@ def synthesize_speech_internal(text, voice_choice):
343
  )
344
  output_str = output["choices"][0]["text"]
345
 
346
- # Decode - CHỈ DECODE PHẦN OUTPUT (không bao gồm reference codes)
347
  chunk_wav = decode_audio(output_str, codec)
348
 
349
  if chunk_wav is not None and len(chunk_wav) > 0:
@@ -352,6 +469,8 @@ def synthesize_speech_internal(text, voice_choice):
352
  all_audio_segments.append(silence_pad)
353
 
354
  if not all_audio_segments:
 
 
355
  return None
356
 
357
  final_wav = np.concatenate(all_audio_segments)
@@ -364,11 +483,13 @@ def synthesize_speech_internal(text, voice_choice):
364
  # Lưu vào lịch sử
365
  permanent_path = add_to_history(raw_text, voice_choice, output_path, process_time, "Thành công")
366
 
 
367
  return permanent_path
368
 
369
  except Exception as e:
370
  import traceback
371
  traceback.print_exc()
 
372
  return None
373
 
374
  # --- SYNTHESIS FUNCTION (UI) ---
@@ -432,7 +553,7 @@ def synthesize_speech(text, voice_choice):
432
 
433
  try:
434
  for i, chunk in enumerate(text_chunks):
435
- yield None, f"⏳ Đang xử lý đoạn {i+1}/{total_chunks}..."
436
 
437
  # Phonemize
438
  ref_text_phoneme = phonemize_with_dict(ref_text_raw)
@@ -456,7 +577,7 @@ def synthesize_speech(text, voice_choice):
456
  )
457
  output_str = output["choices"][0]["text"]
458
 
459
- # Decode - CHỈ DECODE PHẦN OUTPUT (không bao gồm reference codes)
460
  chunk_wav = decode_audio(output_str, codec)
461
 
462
  if chunk_wav is not None and len(chunk_wav) > 0:
@@ -466,6 +587,7 @@ def synthesize_speech(text, voice_choice):
466
 
467
  if not all_audio_segments:
468
  yield None, "❌ Không sinh được audio nào."
 
469
  return
470
 
471
  yield None, "💾 Đang ghép file và lưu..."
@@ -480,7 +602,7 @@ def synthesize_speech(text, voice_choice):
480
  # Lưu vào lịch sử
481
  permanent_path = add_to_history(raw_text, voice_choice, output_path, process_time, "Thành công")
482
 
483
- yield permanent_path, f"✅ Hoàn tất! (Tổng thời gian: {process_time:.2f}s)"
484
 
485
  except Exception as e:
486
  import traceback
@@ -492,26 +614,38 @@ def refresh_history():
492
  """Làm mới danh sách lịch sử"""
493
  return get_history_list()
494
 
495
- def load_history_item(history_index):
496
- """Tải một item từ lịch sử"""
497
- if not history_index:
498
- return None, "", "", ""
499
 
500
  try:
501
- index = int(history_index.split(".")[0]) - 1
502
  history = load_history()
503
- if 0 <= index < len(history):
504
- record = history[index]
505
- return (
506
- record['audio_path'] if os.path.exists(record['audio_path']) else None,
507
- record['full_text'],
508
- record['voice'],
509
- f"📅 {record['timestamp']} | ⏱️ {record['duration']:.2f}s"
510
- )
511
- except:
512
- pass
513
-
514
- return None, "", "", ""
 
 
 
 
 
 
 
 
 
 
 
 
515
 
516
  # --- UI SETUP ---
517
  theme = gr.themes.Ocean(
@@ -575,13 +709,20 @@ css = """
575
  color: #2563eb;
576
  text-decoration: underline;
577
  }
578
- .history-list {
579
- max-height: 600px;
580
  overflow-y: auto;
581
- padding: 15px;
582
- background: #f8fafc;
583
  border-radius: 8px;
584
  }
 
 
 
 
 
 
 
585
  """
586
 
587
  EXAMPLES_LIST = [
@@ -600,7 +741,7 @@ with gr.Blocks(title="VieNeu-TTS", theme=theme, css=css) as demo:
600
  <span class="header-icon">🦜</span>
601
  <span class="gradient-text">VieNeu-TTS Studio</span>
602
  </h1>
603
- <p>Chế độ: {DEVICE_INFO} | Background Processing ✅ | History ✅</p>
604
  <p style="font-size: 0.85rem; color: #94a3b8;">📁 Lịch sử lưu tại: {HISTORY_DIR}</p>
605
  <div class="model-card-content">
606
  <div class="model-card-item">
@@ -615,7 +756,7 @@ with gr.Blocks(title="VieNeu-TTS", theme=theme, css=css) as demo:
615
  </div>
616
  """)
617
 
618
- gr.Markdown(initial_status)
619
 
620
  # --- TABS ---
621
  with gr.Tabs():
@@ -624,8 +765,9 @@ with gr.Blocks(title="VieNeu-TTS", theme=theme, css=css) as demo:
624
  with gr.Row(elem_classes="container"):
625
  with gr.Column(scale=3):
626
  text_input = gr.Textbox(
627
- label=f"Văn bản (Chia chunk: {MAX_CHARS_PER_CHUNK} ký tự)",
628
- lines=6,
 
629
  value="Hà Nội, trái tim của Việt Nam, là một thành phố ngàn năm văn hiến với bề dày lịch sử và văn hóa độc đáo. Bước chân trên những con phố cổ kính quanh Hồ Hoàn Kiếm, du khách như được du hành ngược thời gian.",
630
  )
631
 
@@ -635,15 +777,22 @@ with gr.Blocks(title="VieNeu-TTS", theme=theme, css=css) as demo:
635
  label="👤 Chọn giọng mẫu",
636
  )
637
 
638
- btn_generate = gr.Button("🎵 Bắt đầu tổng hợp", variant="primary", size="lg", interactive=model_loaded)
 
 
639
 
640
  with gr.Column(scale=2):
641
  audio_output = gr.Audio(
642
- label="Kết quả",
643
  type="filepath",
644
  autoplay=True
645
  )
646
- status_output = gr.Textbox(label="Trạng thái", elem_classes="status-box", value="Chờ nhập văn bản...")
 
 
 
 
 
647
 
648
  gr.Examples(
649
  examples=EXAMPLES_LIST,
@@ -651,51 +800,179 @@ with gr.Blocks(title="VieNeu-TTS", theme=theme, css=css) as demo:
651
  outputs=[audio_output, status_output],
652
  fn=synthesize_speech,
653
  cache_examples=False,
654
- label="Các ví dụ nhanh"
655
  )
656
 
657
  # TAB 2: Lịch sử
658
  with gr.Tab("📜 Lịch sử"):
659
  with gr.Row():
660
- with gr.Column(scale=1):
661
- btn_refresh = gr.Button("🔄 Làm mới", size="sm")
662
- history_display = gr.Markdown(
 
 
 
 
 
 
 
 
 
 
 
 
663
  value=get_history_list(),
664
- elem_classes="history-list"
665
  )
666
 
667
- with gr.Column(scale=1):
668
- gr.Markdown("### Chi tiết")
669
- history_audio = gr.Audio(label="Audio", type="filepath")
670
- history_text = gr.Textbox(label="Văn bản đầy đủ", lines=5)
671
- history_voice = gr.Textbox(label="Giọng đã dùng")
672
- history_info = gr.Textbox(label="Thông tin")
673
- history_select = gr.Textbox(
674
- label="Nhập số thứ tự để xem (vd: 1)",
675
- placeholder="Nhập số..."
 
 
 
 
 
 
 
 
 
 
 
676
  )
677
- btn_load_history = gr.Button("📂 Tải item", variant="secondary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
 
679
- btn_refresh.click(
680
- fn=refresh_history,
681
- outputs=history_display
682
- )
683
 
684
- btn_load_history.click(
685
- fn=load_history_item,
686
- inputs=history_select,
687
- outputs=[history_audio, history_text, history_voice, history_info]
688
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
 
690
- # Event handlers
691
  btn_generate.click(
692
  fn=synthesize_speech,
693
  inputs=[text_input, voice_select],
694
  outputs=[audio_output, status_output]
695
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
 
697
  if __name__ == "__main__":
 
 
 
 
 
 
 
698
  demo.queue().launch(
699
  server_name="0.0.0.0",
700
  server_port=7860,
 
701
  )
 
95
  }
96
 
97
  # --- HISTORY MANAGEMENT ---
98
+ history_lock = threading.Lock()
99
+
100
  def load_history():
101
+ """Tải lịch sử từ file JSON - Thread-safe"""
102
+ with history_lock:
103
+ if os.path.exists(HISTORY_JSON):
104
+ try:
105
+ with open(HISTORY_JSON, 'r', encoding='utf-8') as f:
106
+ return json.load(f)
107
+ except Exception as e:
108
+ print(f"⚠️ Lỗi đọc history.json: {e}")
109
+ return []
110
+ return []
111
 
112
  def save_history(history):
113
+ """Lưu lịch sử vào file JSON - Thread-safe"""
114
+ with history_lock:
115
+ try:
116
+ with open(HISTORY_JSON, 'w', encoding='utf-8') as f:
117
+ json.dump(history, f, ensure_ascii=False, indent=2)
118
+ except Exception as e:
119
+ print(f"⚠️ Lỗi ghi history.json: {e}")
120
 
121
  def add_to_history(text, voice, audio_path, duration, status):
122
+ """Thêm một bản ghi vào lịch sử - Thread-safe"""
123
  try:
124
  history = load_history()
125
 
126
  # Tạo tên file duy nhất
127
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
128
  filename = f"tts_{timestamp}.wav"
129
  permanent_path = os.path.join(HISTORY_DIR, filename)
130
 
 
135
  except Exception as e:
136
  print(f"⚠️ Không thể copy file audio: {e}")
137
  permanent_path = audio_path # Dùng file tạm nếu không copy được
138
+ else:
139
+ permanent_path = None
140
 
141
  record = {
142
  "id": timestamp,
 
156
  # Xóa file audio cũ
157
  old_record = history.pop()
158
  try:
159
+ if old_record.get('audio_path') and os.path.exists(old_record['audio_path']):
160
  os.remove(old_record['audio_path'])
161
+ except Exception as e:
162
+ print(f"⚠️ Không thể xóa file cũ: {e}")
163
 
164
  save_history(history)
165
  return permanent_path
166
  except Exception as e:
167
  print(f"⚠️ Lỗi khi lưu lịch sử: {e}")
168
+ import traceback
169
+ traceback.print_exc()
170
  return audio_path if audio_path else None
171
 
172
  def get_history_list():
173
+ """Lấy danh sách lịch sử để hiển thị dạng HTML"""
174
  history = load_history()
175
  if not history:
176
+ return """
177
+ <div style='padding: 20px; text-align: center; color: #64748b;'>
178
+ <p style='font-size: 1.1em;'>📭 Chưa có lịch sử tổng hợp nào</p>
179
+ <p style='font-size: 0.9em;'>Các bản ghi sẽ xuất hiện ở đây sau khi bạn tổng hợp giọng nói</p>
180
+ </div>
181
+ """
182
+
183
+ html_parts = ["<div style='font-family: system-ui; line-height: 1.6;'>"]
184
 
 
185
  for i, record in enumerate(history[:50], 1): # Hiển thị 50 bản ghi gần nhất
186
+ status_color = "#10b981" if record['status'] == "Thành công" else "#ef4444"
187
+ status_icon = "✅" if record['status'] == "Thành công" else "❌"
188
+
189
+ html_parts.append(f"""
190
+ <div style='
191
+ background: white;
192
+ border: 1px solid #e2e8f0;
193
+ border-radius: 8px;
194
+ padding: 15px;
195
+ margin-bottom: 12px;
196
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
197
+ '>
198
+ <div style='display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;'>
199
+ <div style='font-weight: 600; color: #1e293b; font-size: 0.95em;'>
200
+ <span style='color: #64748b;'>#{i}</span> {record['voice']}
201
+ </div>
202
+ <div style='font-size: 0.85em; color: #64748b;'>
203
+ {record['timestamp']}
204
+ </div>
205
+ </div>
206
+
207
+ <div style='
208
+ background: #f8fafc;
209
+ padding: 10px;
210
+ border-radius: 6px;
211
+ margin-bottom: 8px;
212
+ color: #334155;
213
+ font-size: 0.9em;
214
+ border-left: 3px solid #3b82f6;
215
+ '>
216
+ {record['text']}
217
+ </div>
218
+
219
+ <div style='display: flex; gap: 20px; font-size: 0.85em; color: #64748b;'>
220
+ <div>⏱️ {record['duration']:.2f}s</div>
221
+ <div style='color: {status_color}; font-weight: 500;'>
222
+ {status_icon} {record['status']}
223
+ </div>
224
+ <div style='margin-left: auto; color: #3b82f6; cursor: pointer;'>
225
+ ID: {record['id'][:13]}...
226
+ </div>
227
+ </div>
228
+ </div>
229
+ """)
230
 
231
+ html_parts.append("</div>")
232
+ return "".join(html_parts)
233
 
234
  def delete_history_item(item_id):
235
  """Xóa một item khỏi lịch sử"""
236
  history = load_history()
237
+ new_history = []
238
+ deleted = False
239
+
240
+ for record in history:
241
+ if record['id'] == item_id:
242
+ # Xóa file audio
243
+ try:
244
+ if record.get('audio_path') and os.path.exists(record['audio_path']):
245
+ os.remove(record['audio_path'])
246
+ except Exception as e:
247
+ print(f"⚠️ Không thể xóa file: {e}")
248
+ deleted = True
249
+ else:
250
+ new_history.append(record)
251
+
252
+ if deleted:
253
+ save_history(new_history)
254
+ return True, "✅ Đã xóa bản ghi"
255
+ return False, "❌ Không tìm thấy bản ghi"
256
+
257
+ def clear_all_history():
258
+ """Xóa toàn bộ lịch sử"""
259
+ history = load_history()
260
+
261
+ # Xóa tất cả file audio
262
+ for record in history:
263
+ try:
264
+ if record.get('audio_path') and os.path.exists(record['audio_path']):
265
+ os.remove(record['audio_path'])
266
+ except Exception as e:
267
+ print(f"⚠️ Không thể xóa file: {e}")
268
+
269
+ # Xóa file JSON
270
+ save_history([])
271
+ return "✅ Đã xóa toàn bộ lịch sử"
272
 
273
  # --- BACKGROUND PROCESSING QUEUE ---
274
  processing_queue = Queue()
275
  is_processing = False
276
+ processing_stats = {"total": 0, "success": 0, "failed": 0}
277
 
278
  def background_processor():
279
  """Xử lý queue tổng hợp trong background"""
280
+ global is_processing, processing_stats
281
+
282
  while True:
283
  task = processing_queue.get()
284
  if task is None:
 
290
  try:
291
  print(f"[Background] Bắt đầu tổng hợp: {text[:50]}...")
292
  result = synthesize_speech_internal(text, voice)
293
+
294
+ if result:
295
+ processing_stats["success"] += 1
296
+ print(f"[Background] ✅ Hoàn thành: {result}")
297
+ else:
298
+ processing_stats["failed"] += 1
299
+ print(f"[Background] ❌ Thất bại")
300
+
301
+ processing_stats["total"] += 1
302
+
303
  except Exception as e:
304
+ processing_stats["failed"] += 1
305
+ processing_stats["total"] += 1
306
+ print(f"[Background] ❌ Lỗi: {e}")
307
+ import traceback
308
+ traceback.print_exc()
309
 
310
  is_processing = False
311
  processing_queue.task_done()
 
314
  bg_thread = threading.Thread(target=background_processor, daemon=True)
315
  bg_thread.start()
316
 
317
+ def get_processing_stats():
318
+ """Lấy thống kê xử lý"""
319
+ return f"📊 Tổng: {processing_stats['total']} | ✅ Thành công: {processing_stats['success']} | ❌ Thất bại: {processing_stats['failed']}"
320
+
321
  def split_text_into_chunks(text, max_chars=256):
322
  """Chia text thành chunks"""
323
  sentences = re.split(r'([.!?,;])', text)
 
386
  global backbone, codec, model_loaded
387
 
388
  if not model_loaded:
389
+ print("❌ Model chưa được tải")
390
  return None
391
 
392
  if not text or text.strip() == "":
393
+ print("❌ Text rỗng")
394
  return None
395
 
396
  if voice_choice not in VOICE_SAMPLES:
397
+ print(f"❌ Giọng không hợp lệ: {voice_choice}")
398
  return None
399
 
400
  raw_text = text.strip()
 
423
  return None
424
 
425
  if ref_codes is None or len(ref_codes) == 0:
426
+ print("❌ Codes rỗng")
427
  return None
428
 
429
  # Split text
 
436
 
437
  try:
438
  for i, chunk in enumerate(text_chunks):
439
+ print(f"[Internal] Xử lý chunk {i+1}/{len(text_chunks)}")
440
+
441
  # Phonemize
442
  ref_text_phoneme = phonemize_with_dict(ref_text_raw)
443
  input_text_phoneme = phonemize_with_dict(chunk)
 
460
  )
461
  output_str = output["choices"][0]["text"]
462
 
463
+ # Decode
464
  chunk_wav = decode_audio(output_str, codec)
465
 
466
  if chunk_wav is not None and len(chunk_wav) > 0:
 
469
  all_audio_segments.append(silence_pad)
470
 
471
  if not all_audio_segments:
472
+ print("❌ Không có audio segment nào")
473
+ add_to_history(raw_text, voice_choice, None, 0, "Lỗi: Không sinh được audio")
474
  return None
475
 
476
  final_wav = np.concatenate(all_audio_segments)
 
483
  # Lưu vào lịch sử
484
  permanent_path = add_to_history(raw_text, voice_choice, output_path, process_time, "Thành công")
485
 
486
+ print(f"✅ Hoàn thành: {permanent_path}")
487
  return permanent_path
488
 
489
  except Exception as e:
490
  import traceback
491
  traceback.print_exc()
492
+ add_to_history(raw_text, voice_choice, None, 0, f"Lỗi: {str(e)}")
493
  return None
494
 
495
  # --- SYNTHESIS FUNCTION (UI) ---
 
553
 
554
  try:
555
  for i, chunk in enumerate(text_chunks):
556
+ yield None, f"⏳ Đang xử lý đoạn {i+1}/{total_chunks}... ({int((i/total_chunks)*100)}%)"
557
 
558
  # Phonemize
559
  ref_text_phoneme = phonemize_with_dict(ref_text_raw)
 
577
  )
578
  output_str = output["choices"][0]["text"]
579
 
580
+ # Decode
581
  chunk_wav = decode_audio(output_str, codec)
582
 
583
  if chunk_wav is not None and len(chunk_wav) > 0:
 
587
 
588
  if not all_audio_segments:
589
  yield None, "❌ Không sinh được audio nào."
590
+ add_to_history(raw_text, voice_choice, None, 0, "Lỗi: Không sinh được audio")
591
  return
592
 
593
  yield None, "💾 Đang ghép file và lưu..."
 
602
  # Lưu vào lịch sử
603
  permanent_path = add_to_history(raw_text, voice_choice, output_path, process_time, "Thành công")
604
 
605
+ yield permanent_path, f"✅ Hoàn tất! (Tổng thời gian: {process_time:.2f}s | RTF: {process_time/(len(final_wav)/SAMPLE_RATE):.3f})"
606
 
607
  except Exception as e:
608
  import traceback
 
614
  """Làm mới danh sách lịch sử"""
615
  return get_history_list()
616
 
617
+ def load_history_item(item_index):
618
+ """Tải một item từ lịch sử theo số thứ tự"""
619
+ if not item_index or item_index.strip() == "":
620
+ return None, "", "", "⚠️ Vui lòng nhập số thứ tự"
621
 
622
  try:
623
+ index = int(item_index.strip()) - 1
624
  history = load_history()
625
+
626
+ if index < 0 or index >= len(history):
627
+ return None, "", "", f"❌ Số thứ tự không hợp lệ (1-{len(history)})"
628
+
629
+ record = history[index]
630
+
631
+ audio_path = None
632
+ if record.get('audio_path') and os.path.exists(record['audio_path']):
633
+ audio_path = record['audio_path']
634
+
635
+ info = f"""
636
+ 📅 Thời gian: {record['timestamp']}
637
+ ⏱️ Thời lượng: {record['duration']:.2f}s
638
+ 🎭 Giọng: {record['voice']}
639
+ 📊 Trạng thái: {record['status']}
640
+ 🆔 ID: {record['id']}
641
+ """.strip()
642
+
643
+ return audio_path, record['full_text'], record['voice'], info
644
+
645
+ except ValueError:
646
+ return None, "", "", "❌ Vui lòng nhập số hợp lệ"
647
+ except Exception as e:
648
+ return None, "", "", f"❌ Lỗi: {str(e)}"
649
 
650
  # --- UI SETUP ---
651
  theme = gr.themes.Ocean(
 
709
  color: #2563eb;
710
  text-decoration: underline;
711
  }
712
+ .history-container {
713
+ max-height: 650px;
714
  overflow-y: auto;
715
+ padding: 10px;
716
+ background: #f1f5f9;
717
  border-radius: 8px;
718
  }
719
+ .info-box {
720
+ background: #f8fafc;
721
+ padding: 12px;
722
+ border-radius: 6px;
723
+ border-left: 4px solid #3b82f6;
724
+ margin: 10px 0;
725
+ }
726
  """
727
 
728
  EXAMPLES_LIST = [
 
741
  <span class="header-icon">🦜</span>
742
  <span class="gradient-text">VieNeu-TTS Studio</span>
743
  </h1>
744
+ <p style="margin: 10px 0;">Chế độ: {DEVICE_INFO} | Background Processing ✅ | History ✅</p>
745
  <p style="font-size: 0.85rem; color: #94a3b8;">📁 Lịch sử lưu tại: {HISTORY_DIR}</p>
746
  <div class="model-card-content">
747
  <div class="model-card-item">
 
756
  </div>
757
  """)
758
 
759
+ status_banner = gr.Markdown(initial_status)
760
 
761
  # --- TABS ---
762
  with gr.Tabs():
 
765
  with gr.Row(elem_classes="container"):
766
  with gr.Column(scale=3):
767
  text_input = gr.Textbox(
768
+ label=f"📝 Văn bản (Chia chunk: {MAX_CHARS_PER_CHUNK} ký tự)",
769
+ lines=7,
770
+ placeholder="Nhập văn bản tiếng Việt cần tổng hợp...",
771
  value="Hà Nội, trái tim của Việt Nam, là một thành phố ngàn năm văn hiến với bề dày lịch sử và văn hóa độc đáo. Bước chân trên những con phố cổ kính quanh Hồ Hoàn Kiếm, du khách như được du hành ngược thời gian.",
772
  )
773
 
 
777
  label="👤 Chọn giọng mẫu",
778
  )
779
 
780
+ with gr.Row():
781
+ btn_generate = gr.Button("🎵 Bắt đầu tổng hợp", variant="primary", size="lg", interactive=model_loaded)
782
+ btn_clear = gr.Button("🗑️ Xóa", variant="secondary", size="lg")
783
 
784
  with gr.Column(scale=2):
785
  audio_output = gr.Audio(
786
+ label="🔊 Kết quả",
787
  type="filepath",
788
  autoplay=True
789
  )
790
+ status_output = gr.Textbox(
791
+ label="📊 Trạng thái",
792
+ elem_classes="status-box",
793
+ value="Chờ nhập văn bản...",
794
+ lines=2
795
+ )
796
 
797
  gr.Examples(
798
  examples=EXAMPLES_LIST,
 
800
  outputs=[audio_output, status_output],
801
  fn=synthesize_speech,
802
  cache_examples=False,
803
+ label="💡 Các ví dụ nhanh"
804
  )
805
 
806
  # TAB 2: Lịch sử
807
  with gr.Tab("📜 Lịch sử"):
808
  with gr.Row():
809
+ with gr.Column(scale=3):
810
+ gr.Markdown("### 📋 Danh sách lịch sử")
811
+
812
+ with gr.Row():
813
+ btn_refresh = gr.Button("🔄 Làm mới", size="sm", variant="secondary")
814
+ btn_clear_all = gr.Button("🗑️ Xóa toàn bộ", size="sm", variant="stop")
815
+ stats_display = gr.Textbox(
816
+ value=get_processing_stats(),
817
+ label="",
818
+ show_label=False,
819
+ interactive=False,
820
+ container=False
821
+ )
822
+
823
+ history_display = gr.HTML(
824
  value=get_history_list(),
825
+ elem_classes="history-container"
826
  )
827
 
828
+ with gr.Column(scale=2):
829
+ gr.Markdown("### 🔍 Chi tiết bản ghi")
830
+
831
+ with gr.Row():
832
+ history_select = gr.Textbox(
833
+ label="Nhập số thứ tự (vd: 1, 2, 3...)",
834
+ placeholder="Nhập số...",
835
+ scale=3
836
+ )
837
+ btn_load_history = gr.Button("📂 Tải", variant="primary", scale=1)
838
+
839
+ history_info = gr.Textbox(
840
+ label="ℹ️ Thông tin",
841
+ lines=6,
842
+ elem_classes="info-box"
843
+ )
844
+
845
+ history_audio = gr.Audio(
846
+ label="🔊 Audio",
847
+ type="filepath"
848
  )
849
+
850
+ history_voice = gr.Textbox(
851
+ label="🎭 Giọng đã dùng",
852
+ interactive=False
853
+ )
854
+
855
+ history_text = gr.Textbox(
856
+ label="📄 Văn bản đầy đủ",
857
+ lines=5,
858
+ interactive=False
859
+ )
860
+
861
+ with gr.Row():
862
+ btn_reuse = gr.Button("♻️ Tái sử dụng văn bản", variant="secondary")
863
+
864
+ # TAB 3: Thông tin
865
+ with gr.Tab("ℹ️ Thông tin"):
866
+ gr.Markdown(f"""
867
+ ## 🎯 Về VieNeu-TTS
868
 
869
+ **VieNeu-TTS** là hệ thống tổng hợp giọng nói tiếng Việt sử dụng công nghệ AI tiên tiến.
 
 
 
870
 
871
+ ### ⚙️ Cấu hình hiện tại
872
+ - **Model Backbone**: Q4 GGUF (llama-cpp)
873
+ - **Codec**: ONNX Decoder
874
+ - **Sample Rate**: {SAMPLE_RATE} Hz
875
+ - **Max Chunk Size**: {MAX_CHARS_PER_CHUNK} ký tự
876
+ - **Thư mục lịch sử**: `{HISTORY_DIR}`
877
+
878
+ ### 🎭 Giọng mẫu có sẵn
879
+ Hệ thống hỗ trợ **{len(VOICE_SAMPLES)}** giọng mẫu:
880
+ - **Nam miền Bắc**: Tuyên, Bình
881
+ - **Nam miền Nam**: Vĩnh, Nguyên, Sơn
882
+ - **Nữ miền Bắc**: Ngọc, Ly
883
+ - **Nữ miền Nam**: Đoan, Dung
884
+
885
+ ### 📌 Hướng dẫn sử dụng
886
+
887
+ 1. **Tổng hợp giọng nói**:
888
+ - Nhập văn bản vào ô "Văn bản"
889
+ - Chọn giọng mẫu phù hợp
890
+ - Nhấn "Bắt đầu tổng hợp"
891
+ - Đợi hệ thống xử lý và nghe kết quả
892
+
893
+ 2. **Xem lịch sử**:
894
+ - Vào tab "Lịch sử"
895
+ - Nhấn "Làm mới" để cập nhật danh sách
896
+ - Nhập số thứ tự và nhấn "Tải" để xem chi tiết
897
+
898
+ 3. **Tái sử dụng**:
899
+ - Trong tab Lịch sử, tải bản ghi cũ
900
+ - Nhấn "Tái sử dụng văn bản" để copy sang tab Tổng hợp
901
+
902
+ ### 🔧 Tính năng nâng cao
903
+ - ✅ Xử lý background (không phụ thuộc UI)
904
+ - ✅ Lưu lịch sử tự động
905
+ - ✅ Chia chunk thông minh
906
+ - ✅ Thread-safe operations
907
+ - ✅ Tự động xóa file cũ khi vượt quá 100 bản ghi
908
+
909
+ ### 📊 Thống kê
910
+ {get_processing_stats()}
911
+
912
+ ### 🔗 Liên kết
913
+ - [GitHub Repository](https://github.com/pnnbao97/VieNeu-TTS)
914
+ - [Model Hub](https://huggingface.co/pnnbao-ump)
915
+
916
+ ---
917
+ **Phiên bản**: 2.0 Enhanced | **Tác giả**: Phạm Nguyễn Ngọc Bảo
918
+ """)
919
 
920
+ # Event handlers - Tab Tổng hợp
921
  btn_generate.click(
922
  fn=synthesize_speech,
923
  inputs=[text_input, voice_select],
924
  outputs=[audio_output, status_output]
925
  )
926
+
927
+ btn_clear.click(
928
+ fn=lambda: ("", None, "Đã xóa"),
929
+ outputs=[text_input, audio_output, status_output]
930
+ )
931
+
932
+ # Event handlers - Tab Lịch sử
933
+ btn_refresh.click(
934
+ fn=refresh_history,
935
+ outputs=history_display
936
+ ).then(
937
+ fn=get_processing_stats,
938
+ outputs=stats_display
939
+ )
940
+
941
+ btn_load_history.click(
942
+ fn=load_history_item,
943
+ inputs=history_select,
944
+ outputs=[history_audio, history_text, history_voice, history_info]
945
+ )
946
+
947
+ def clear_all():
948
+ msg = clear_all_history()
949
+ return get_history_list(), msg
950
+
951
+ btn_clear_all.click(
952
+ fn=clear_all,
953
+ outputs=[history_display, stats_display]
954
+ )
955
+
956
+ # Nút tái sử dụng văn bản
957
+ def reuse_text(text, voice):
958
+ return text, voice, "✅ Đã tải văn bản vào tab Tổng hợp"
959
+
960
+ btn_reuse.click(
961
+ fn=reuse_text,
962
+ inputs=[history_text, history_voice],
963
+ outputs=[text_input, voice_select, status_output]
964
+ )
965
 
966
  if __name__ == "__main__":
967
+ print(f"\n{'='*60}")
968
+ print(f"🚀 VieNeu-TTS Studio đã sẵn sàng!")
969
+ print(f"📂 Lịch sử lưu tại: {HISTORY_DIR}")
970
+ print(f"🎭 Số giọng mẫu: {len(VOICE_SAMPLES)}")
971
+ print(f"⚙️ Chế độ: {DEVICE_INFO}")
972
+ print(f"{'='*60}\n")
973
+
974
  demo.queue().launch(
975
  server_name="0.0.0.0",
976
  server_port=7860,
977
+ show_error=True
978
  )