Arrcttacsrks commited on
Commit
4bfe6a6
·
verified ·
1 Parent(s): 168f397

Upload app-16.py

Browse files
Files changed (1) hide show
  1. app-16.py +1508 -0
app-16.py ADDED
@@ -0,0 +1,1508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import soundfile as sf
3
+ import tempfile
4
+ import os
5
+ import time
6
+ import numpy as np
7
+ import librosa
8
+ import re
9
+ import json
10
+ import shutil
11
+ from pathlib import Path
12
+ from datetime import datetime
13
+ from llama_cpp import Llama
14
+ from neucodec import NeuCodecOnnxDecoder
15
+ import torch
16
+ from utils.phonemize_text import phonemize_with_dict
17
+ import threading
18
+ from queue import Queue
19
+ from dataclasses import dataclass, asdict
20
+ from typing import Optional, Dict, List, Tuple
21
+ import hashlib
22
+
23
+ print("⏳ Đang khởi động VieNeu-TTS Enhanced v3.0...")
24
+
25
+ # --- CONSTANTS ---
26
+ MAX_CHARS_PER_CHUNK = 256
27
+ SAMPLE_RATE = 24000
28
+ DEVICE_INFO = "Q4 GGUF (llama-cpp) + ONNX Codec"
29
+ VERSION = "3.0 Enhanced"
30
+
31
+ # --- DATA CLASSES ---
32
+ @dataclass
33
+ class VoiceSettings:
34
+ """Cài đặt tùy chỉnh giọng nói"""
35
+ temperature: float = 1.0
36
+ top_k: int = 50
37
+ top_p: float = 0.95
38
+ speed_ratio: float = 1.0 # 0.5-2.0
39
+ pitch_shift: int = 0 # -12 to +12 semitones
40
+ volume_gain: float = 1.0 # 0.5-2.0
41
+ silence_duration: float = 0.15 # seconds between chunks
42
+
43
+ def to_dict(self):
44
+ return asdict(self)
45
+
46
+ @dataclass
47
+ class HistoryRecord:
48
+ """Bản ghi lịch sử"""
49
+ id: str
50
+ timestamp: str
51
+ text: str
52
+ full_text: str
53
+ voice: str
54
+ audio_path: Optional[str]
55
+ duration: float
56
+ status: str
57
+ settings: Dict
58
+ text_hash: str
59
+
60
+ def to_dict(self):
61
+ return asdict(self)
62
+
63
+ # Thư mục lưu lịch sử
64
+ try:
65
+ HISTORY_DIR = "./tts_history"
66
+ os.makedirs(HISTORY_DIR, exist_ok=True)
67
+ test_file = os.path.join(HISTORY_DIR, ".test")
68
+ with open(test_file, 'w') as f:
69
+ f.write("test")
70
+ os.remove(test_file)
71
+ except (PermissionError, OSError):
72
+ HISTORY_DIR = os.path.join(tempfile.gettempdir(), "vieneu_tts_history")
73
+ os.makedirs(HISTORY_DIR, exist_ok=True)
74
+ print(f"⚠️ Không có quyền ghi, sử dụng thư mục tạm: {HISTORY_DIR}")
75
+
76
+ HISTORY_JSON = os.path.join(HISTORY_DIR, "history.json")
77
+ SETTINGS_DIR = os.path.join(HISTORY_DIR, "presets")
78
+ os.makedirs(SETTINGS_DIR, exist_ok=True)
79
+
80
+ # Đường dẫn model
81
+ BACKBONE_REPO = "pnnbao-ump/VieNeu-TTS-q8-gguf"
82
+ CODEC_REPO = "neuphonic/neucodec-onnx-decoder"
83
+
84
+ # Giọng mẫu
85
+ VOICE_SAMPLES = {
86
+ "Tuyên (nam miền Bắc)": {
87
+ "audio": "./sample/Tuyên (nam miền Bắc).wav",
88
+ "text": "./sample/Tuyên (nam miền Bắc).txt",
89
+ "codes": "./sample/Tuyên (nam miền Bắc).pt"
90
+ },
91
+ "Vĩnh (nam miền Nam)": {
92
+ "audio": "./sample/Vĩnh (nam miền Nam).wav",
93
+ "text": "./sample/Vĩnh (nam miền Nam).txt",
94
+ "codes": "./sample/Vĩnh (nam miền Nam).pt"
95
+ },
96
+ "Bình (nam miền Bắc)": {
97
+ "audio": "./sample/Bình (nam miền Bắc).wav",
98
+ "text": "./sample/Bình (nam miền Bắc).txt",
99
+ "codes": "./sample/Bình (nam miền Bắc).pt"
100
+ },
101
+ "Nguyên (nam miền Nam)": {
102
+ "audio": "./sample/Nguyên (nam miền Nam).wav",
103
+ "text": "./sample/Nguyên (nam miền Nam).txt",
104
+ "codes": "./sample/Nguyên (nam miền Nam).pt"
105
+ },
106
+ "Sơn (nam miền Nam)": {
107
+ "audio": "./sample/Sơn (nam miền Nam).wav",
108
+ "text": "./sample/Sơn (nam miền Nam).txt",
109
+ "codes": "./sample/Sơn (nam miền Nam).pt"
110
+ },
111
+ "Đoan (nữ miền Nam)": {
112
+ "audio": "./sample/Đoan (nữ miền Nam).wav",
113
+ "text": "./sample/Đoan (nữ miền Nam).txt",
114
+ "codes": "./sample/Đoan (nữ miền Nam).pt"
115
+ },
116
+ "Ngọc (nữ miền Bắc)": {
117
+ "audio": "./sample/Ngọc (nữ miền Bắc).wav",
118
+ "text": "./sample/Ngọc (nữ miền Bắc).txt",
119
+ "codes": "./sample/Ngọc (nữ miền Bắc).pt"
120
+ },
121
+ "Ly (nữ miền Bắc)": {
122
+ "audio": "./sample/Ly (nữ miền Bắc).wav",
123
+ "text": "./sample/Ly (nữ miền Bắc).txt",
124
+ "codes": "./sample/Ly (nữ miền Bắc).pt"
125
+ },
126
+ "Dung (nữ miền Nam)": {
127
+ "audio": "./sample/Dung (nữ miền Nam).wav",
128
+ "text": "./sample/Dung (nữ miền Nam).txt",
129
+ "codes": "./sample/Dung (nữ miền Nam).pt"
130
+ }
131
+ }
132
+
133
+ # --- PRESET MANAGEMENT ---
134
+ DEFAULT_PRESETS = {
135
+ "Mặc định": VoiceSettings(),
136
+ "Giọng nhanh": VoiceSettings(speed_ratio=1.3, silence_duration=0.1),
137
+ "Giọng chậm": VoiceSettings(speed_ratio=0.8, silence_duration=0.2),
138
+ "Giọng trầm": VoiceSettings(pitch_shift=-3),
139
+ "Giọng cao": VoiceSettings(pitch_shift=3),
140
+ "Nhiệt tình": VoiceSettings(temperature=1.2, volume_gain=1.2),
141
+ "Bình tĩnh": VoiceSettings(temperature=0.8, volume_gain=0.9, speed_ratio=0.9),
142
+ }
143
+
144
+ history_lock = threading.Lock()
145
+ settings_lock = threading.Lock()
146
+
147
+ def load_presets() -> Dict[str, VoiceSettings]:
148
+ """Tải các preset đã lưu"""
149
+ presets = DEFAULT_PRESETS.copy()
150
+ try:
151
+ preset_files = Path(SETTINGS_DIR).glob("*.json")
152
+ for file in preset_files:
153
+ with open(file, 'r', encoding='utf-8') as f:
154
+ data = json.load(f)
155
+ name = file.stem
156
+ presets[name] = VoiceSettings(**data)
157
+ except Exception as e:
158
+ print(f"⚠️ Lỗi tải preset: {e}")
159
+ return presets
160
+
161
+ def save_preset(name: str, settings: VoiceSettings):
162
+ """Lưu preset"""
163
+ with settings_lock:
164
+ try:
165
+ preset_path = os.path.join(SETTINGS_DIR, f"{name}.json")
166
+ with open(preset_path, 'w', encoding='utf-8') as f:
167
+ json.dump(settings.to_dict(), f, indent=2)
168
+ return True, f"✅ Đã lưu preset '{name}'"
169
+ except Exception as e:
170
+ return False, f"❌ Lỗi lưu preset: {e}"
171
+
172
+ def delete_preset(name: str):
173
+ """Xóa preset"""
174
+ if name in DEFAULT_PRESETS:
175
+ return False, "❌ Không thể xóa preset mặc định"
176
+
177
+ with settings_lock:
178
+ try:
179
+ preset_path = os.path.join(SETTINGS_DIR, f"{name}.json")
180
+ if os.path.exists(preset_path):
181
+ os.remove(preset_path)
182
+ return True, f"✅ Đã xóa preset '{name}'"
183
+ return False, "❌ Preset không tồn tại"
184
+ except Exception as e:
185
+ return False, f"❌ Lỗi xóa preset: {e}"
186
+
187
+ # --- HISTORY MANAGEMENT ---
188
+ def load_history() -> List[HistoryRecord]:
189
+ """Tải lịch sử từ file JSON"""
190
+ with history_lock:
191
+ if os.path.exists(HISTORY_JSON):
192
+ try:
193
+ with open(HISTORY_JSON, 'r', encoding='utf-8') as f:
194
+ data = json.load(f)
195
+ return [HistoryRecord(**item) for item in data]
196
+ except Exception as e:
197
+ print(f"⚠️ Lỗi đọc history.json: {e}")
198
+ return []
199
+ return []
200
+
201
+ def save_history(history: List[HistoryRecord]):
202
+ """Lưu lịch sử vào file JSON"""
203
+ with history_lock:
204
+ try:
205
+ data = [record.to_dict() for record in history]
206
+ with open(HISTORY_JSON, 'w', encoding='utf-8') as f:
207
+ json.dump(data, f, ensure_ascii=False, indent=2)
208
+ except Exception as e:
209
+ print(f"⚠️ Lỗi ghi history.json: {e}")
210
+
211
+ def get_text_hash(text: str) -> str:
212
+ """Tạo hash cho text để tránh trùng lặp"""
213
+ return hashlib.md5(text.encode('utf-8')).hexdigest()[:8]
214
+
215
+ def add_to_history(text: str, voice: str, audio_path: Optional[str],
216
+ duration: float, status: str, settings: VoiceSettings) -> Optional[str]:
217
+ """Thêm bản ghi vào lịch sử"""
218
+ try:
219
+ history = load_history()
220
+
221
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
222
+ filename = f"tts_{timestamp}.wav"
223
+ permanent_path = os.path.join(HISTORY_DIR, filename)
224
+
225
+ if audio_path and os.path.exists(audio_path):
226
+ try:
227
+ shutil.copy2(audio_path, permanent_path)
228
+ except Exception as e:
229
+ print(f"⚠️ Không thể copy file audio: {e}")
230
+ permanent_path = audio_path
231
+ else:
232
+ permanent_path = None
233
+
234
+ record = HistoryRecord(
235
+ id=timestamp,
236
+ timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
237
+ text=text[:100] + "..." if len(text) > 100 else text,
238
+ full_text=text,
239
+ voice=voice,
240
+ audio_path=permanent_path,
241
+ duration=duration,
242
+ status=status,
243
+ settings=settings.to_dict(),
244
+ text_hash=get_text_hash(text)
245
+ )
246
+
247
+ history.insert(0, record)
248
+
249
+ # Giới hạn 100 bản ghi
250
+ if len(history) > 100:
251
+ old_record = history.pop()
252
+ try:
253
+ if old_record.audio_path and os.path.exists(old_record.audio_path):
254
+ os.remove(old_record.audio_path)
255
+ except Exception as e:
256
+ print(f"⚠️ Không thể xóa file cũ: {e}")
257
+
258
+ save_history(history)
259
+ return permanent_path
260
+ except Exception as e:
261
+ print(f"⚠️ Lỗi khi lưu lịch sử: {e}")
262
+ import traceback
263
+ traceback.print_exc()
264
+ return audio_path if audio_path else None
265
+
266
+ def get_history_list(filter_voice: str = "Tất cả", search_text: str = "") -> str:
267
+ """Tạo HTML hiển thị lịch sử với filter"""
268
+ history = load_history()
269
+
270
+ # Filter
271
+ if filter_voice != "Tất cả":
272
+ history = [r for r in history if r.voice == filter_voice]
273
+ if search_text:
274
+ history = [r for r in history if search_text.lower() in r.full_text.lower()]
275
+
276
+ if not history:
277
+ return """
278
+ <div style='padding: 20px; text-align: center; color: #64748b;'>
279
+ <p style='font-size: 1.1em;'>📭 Không tìm thấy bản ghi nào</p>
280
+ </div>
281
+ """
282
+
283
+ html_parts = ["<div style='font-family: system-ui; line-height: 1.6;'>"]
284
+
285
+ for i, record in enumerate(history[:50], 1):
286
+ status_color = "#10b981" if record.status == "Thành công" else "#ef4444"
287
+ status_icon = "✅" if record.status == "Thành công" else "❌"
288
+
289
+ # Settings summary
290
+ settings_html = f"""
291
+ <div style='font-size: 0.75em; color: #94a3b8; margin-top: 4px;'>
292
+ 🎛️ Temp:{record.settings.get('temperature', 1.0):.1f} |
293
+ Speed:{record.settings.get('speed_ratio', 1.0):.1f}x |
294
+ Pitch:{record.settings.get('pitch_shift', 0):+d}
295
+ </div>
296
+ """
297
+
298
+ html_parts.append(f"""
299
+ <div style='
300
+ background: white;
301
+ border: 1px solid #e2e8f0;
302
+ border-radius: 8px;
303
+ padding: 15px;
304
+ margin-bottom: 12px;
305
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
306
+ transition: all 0.2s;
307
+ ' onmouseover="this.style.boxShadow='0 4px 6px rgba(0,0,0,0.15)'"
308
+ onmouseout="this.style.boxShadow='0 1px 3px rgba(0,0,0,0.1)'">
309
+ <div style='display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;'>
310
+ <div style='font-weight: 600; color: #1e293b; font-size: 0.95em;'>
311
+ <span style='color: #64748b;'>#{i}</span> {record.voice}
312
+ {settings_html}
313
+ </div>
314
+ <div style='font-size: 0.85em; color: #64748b;'>
315
+ {record.timestamp}
316
+ </div>
317
+ </div>
318
+
319
+ <div style='
320
+ background: #f8fafc;
321
+ padding: 10px;
322
+ border-radius: 6px;
323
+ margin-bottom: 8px;
324
+ color: #334155;
325
+ font-size: 0.9em;
326
+ border-left: 3px solid #3b82f6;
327
+ '>
328
+ {record.text}
329
+ </div>
330
+
331
+ <div style='display: flex; gap: 20px; font-size: 0.85em; color: #64748b;'>
332
+ <div>⏱️ {record.duration:.2f}s</div>
333
+ <div style='color: {status_color}; font-weight: 500;'>
334
+ {status_icon} {record.status}
335
+ </div>
336
+ <div style='margin-left: auto; color: #3b82f6; cursor: pointer;' title='{record.id}'>
337
+ ID: {record.id[:13]}...
338
+ </div>
339
+ </div>
340
+ </div>
341
+ """)
342
+
343
+ html_parts.append("</div>")
344
+ return "".join(html_parts)
345
+
346
+ def clear_all_history():
347
+ """Xóa toàn bộ lịch sử"""
348
+ history = load_history()
349
+ for record in history:
350
+ try:
351
+ if record.audio_path and os.path.exists(record.audio_path):
352
+ os.remove(record.audio_path)
353
+ except Exception as e:
354
+ print(f"⚠️ Không thể xóa file: {e}")
355
+ save_history([])
356
+ return "✅ Đã xóa toàn bộ lịch sử"
357
+
358
+ # --- BACKGROUND PROCESSING ---
359
+ processing_queue = Queue()
360
+ is_processing = False
361
+ processing_stats = {"total": 0, "success": 0, "failed": 0}
362
+ stats_lock = threading.Lock()
363
+
364
+ def update_stats(success: bool):
365
+ """Cập nhật thống kê thread-safe"""
366
+ with stats_lock:
367
+ processing_stats["total"] += 1
368
+ if success:
369
+ processing_stats["success"] += 1
370
+ else:
371
+ processing_stats["failed"] += 1
372
+
373
+ def get_processing_stats() -> str:
374
+ """Lấy thống kê xử lý"""
375
+ with stats_lock:
376
+ return (f"📊 Tổng: {processing_stats['total']} | "
377
+ f"✅ Thành công: {processing_stats['success']} | "
378
+ f"❌ Thất bại: {processing_stats['failed']}")
379
+
380
+ # --- AUDIO PROCESSING ---
381
+ def apply_audio_effects(audio: np.ndarray, settings: VoiceSettings) -> np.ndarray:
382
+ """Áp dụng hiệu ứng audio"""
383
+ try:
384
+ # Speed change
385
+ if settings.speed_ratio != 1.0:
386
+ audio = librosa.effects.time_stretch(audio, rate=settings.speed_ratio)
387
+
388
+ # Pitch shift
389
+ if settings.pitch_shift != 0:
390
+ audio = librosa.effects.pitch_shift(
391
+ audio,
392
+ sr=SAMPLE_RATE,
393
+ n_steps=settings.pitch_shift
394
+ )
395
+
396
+ # Volume
397
+ if settings.volume_gain != 1.0:
398
+ audio = audio * settings.volume_gain
399
+ audio = np.clip(audio, -1.0, 1.0)
400
+
401
+ return audio
402
+ except Exception as e:
403
+ print(f"⚠️ Lỗi áp dụng hiệu ứng: {e}")
404
+ return audio
405
+
406
+ def split_text_into_chunks(text: str, max_chars: int = 256) -> List[str]:
407
+ """Chia text thành chunks thông minh"""
408
+ sentences = re.split(r'([.!?,;])', text)
409
+ chunks = []
410
+ current = ""
411
+
412
+ for i in range(0, len(sentences), 2):
413
+ sentence = sentences[i]
414
+ punct = sentences[i+1] if i+1 < len(sentences) else ""
415
+ segment = sentence + punct
416
+
417
+ if len(current) + len(segment) <= max_chars:
418
+ current += segment
419
+ else:
420
+ if current:
421
+ chunks.append(current.strip())
422
+ current = segment
423
+
424
+ if current:
425
+ chunks.append(current.strip())
426
+
427
+ return chunks if chunks else [text]
428
+
429
+ def decode_audio(codes_str: str, codec) -> np.ndarray:
430
+ """Decode speech tokens to audio"""
431
+ speech_ids = [int(num) for num in re.findall(r"<\|speech_(\d+)\|>", codes_str)]
432
+
433
+ if len(speech_ids) == 0:
434
+ print("Không tìm thấy mã giọng nói hợp lệ.")
435
+ return np.array([], dtype=np.float32)
436
+
437
+ codes = np.array(speech_ids, dtype=np.int32)[np.newaxis, np.newaxis, :]
438
+ recon = codec.decode_code(codes)
439
+ return recon[0, 0, :]
440
+
441
+ # --- MODEL LOADING ---
442
+ print("📦 Đang tải model Q4 GGUF và Codec ONNX...")
443
+ model_loaded = False
444
+ backbone = None
445
+ codec = None
446
+
447
+ try:
448
+ backbone = Llama.from_pretrained(
449
+ repo_id=BACKBONE_REPO,
450
+ filename="*.gguf",
451
+ verbose=False,
452
+ n_gpu_layers=-1,
453
+ n_ctx=2048,
454
+ mlock=True,
455
+ flash_attn=True,
456
+ )
457
+
458
+ codec = NeuCodecOnnxDecoder.from_pretrained(CODEC_REPO)
459
+
460
+ print("✅ Model đã tải thành công!")
461
+ model_loaded = True
462
+ except Exception as e:
463
+ import traceback
464
+ traceback.print_exc()
465
+ print(f"❌ Lỗi khi tải model: {e}")
466
+ model_loaded = False
467
+
468
+ # --- SYNTHESIS FUNCTIONS ---
469
+ def synthesize_speech_internal(text: str, voice_choice: str,
470
+ settings: VoiceSettings) -> Optional[str]:
471
+ """Internal synthesis function"""
472
+ global backbone, codec, model_loaded
473
+
474
+ if not model_loaded or not text.strip() or voice_choice not in VOICE_SAMPLES:
475
+ return None
476
+
477
+ raw_text = text.strip()
478
+
479
+ # Load reference
480
+ ref_text_path = VOICE_SAMPLES[voice_choice]["text"]
481
+ try:
482
+ with open(ref_text_path, "r", encoding="utf-8") as f:
483
+ ref_text_raw = f.read()
484
+ except Exception as e:
485
+ print(f"❌ Lỗi đọc file text mẫu: {e}")
486
+ return None
487
+
488
+ # Load codes
489
+ codes_path = VOICE_SAMPLES[voice_choice]["codes"]
490
+ try:
491
+ ref_codes_tensor = torch.load(codes_path, map_location="cpu")
492
+ if isinstance(ref_codes_tensor, torch.Tensor):
493
+ ref_codes = ref_codes_tensor.cpu().numpy()
494
+ else:
495
+ ref_codes = np.array(ref_codes_tensor)
496
+ except Exception as e:
497
+ print(f"❌ Lỗi khi tải codes: {e}")
498
+ return None
499
+
500
+ if ref_codes is None or len(ref_codes) == 0:
501
+ return None
502
+
503
+ # Split text
504
+ text_chunks = split_text_into_chunks(raw_text, max_chars=MAX_CHARS_PER_CHUNK)
505
+ all_audio_segments = []
506
+ silence_pad = np.zeros(int(SAMPLE_RATE * settings.silence_duration), dtype=np.float32)
507
+
508
+ start_time = time.time()
509
+
510
+ try:
511
+ for i, chunk in enumerate(text_chunks):
512
+ print(f"[Internal] Xử lý chunk {i+1}/{len(text_chunks)}")
513
+
514
+ # Phonemize
515
+ ref_text_phoneme = phonemize_with_dict(ref_text_raw)
516
+ input_text_phoneme = phonemize_with_dict(chunk)
517
+
518
+ # Create prompt
519
+ codes_str = "".join([f"<|speech_{idx}|>" for idx in ref_codes])
520
+ prompt = (
521
+ f"user: Convert the text to speech:<|TEXT_PROMPT_START|>{ref_text_phoneme} {input_text_phoneme}"
522
+ f"<|TEXT_PROMPT_END|>\nassistant:<|SPEECH_GENERATION_START|>{codes_str}"
523
+ )
524
+
525
+ # Generate
526
+ output = backbone(
527
+ prompt,
528
+ max_tokens=2048,
529
+ temperature=settings.temperature,
530
+ top_k=settings.top_k,
531
+ top_p=settings.top_p,
532
+ stop=["<|SPEECH_GENERATION_END|>"],
533
+ )
534
+ output_str = output["choices"][0]["text"]
535
+
536
+ # Decode
537
+ chunk_wav = decode_audio(output_str, codec)
538
+
539
+ # Apply effects
540
+ chunk_wav = apply_audio_effects(chunk_wav, settings)
541
+
542
+ if chunk_wav is not None and len(chunk_wav) > 0:
543
+ all_audio_segments.append(chunk_wav)
544
+ if i < len(text_chunks) - 1:
545
+ all_audio_segments.append(silence_pad)
546
+
547
+ if not all_audio_segments:
548
+ add_to_history(raw_text, voice_choice, None, 0, "Lỗi: Không sinh được audio", settings)
549
+ return None
550
+
551
+ final_wav = np.concatenate(all_audio_segments)
552
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
553
+ sf.write(tmp.name, final_wav, SAMPLE_RATE)
554
+ output_path = tmp.name
555
+
556
+ process_time = time.time() - start_time
557
+ permanent_path = add_to_history(raw_text, voice_choice, output_path, process_time, "Thành công", settings)
558
+
559
+ update_stats(True)
560
+ print(f"✅ Hoàn thành: {permanent_path}")
561
+ return permanent_path
562
+
563
+ except Exception as e:
564
+ import traceback
565
+ traceback.print_exc()
566
+ add_to_history(raw_text, voice_choice, None, 0, f"Lỗi: {str(e)}", settings)
567
+ update_stats(False)
568
+ return None
569
+
570
+ def synthesize_speech(text: str, voice_choice: str,
571
+ temperature: float, top_k: int, top_p: float,
572
+ speed_ratio: float, pitch_shift: int, volume_gain: float,
573
+ silence_duration: float):
574
+ """Main synthesis function với UI feedback"""
575
+ global backbone, codec, model_loaded
576
+
577
+ if not model_loaded:
578
+ yield None, "⚠️ Model chưa tải. Vui lòng kiểm tra lỗi console!"
579
+ return
580
+
581
+ if not text or text.strip() == "":
582
+ yield None, "⚠️ Vui lòng nhập văn bản!"
583
+ return
584
+
585
+ if voice_choice not in VOICE_SAMPLES:
586
+ yield None, "⚠️ Vui lòng chọn giọng mẫu."
587
+ return
588
+
589
+ # Create settings
590
+ settings = VoiceSettings(
591
+ temperature=temperature,
592
+ top_k=top_k,
593
+ top_p=top_p,
594
+ speed_ratio=speed_ratio,
595
+ pitch_shift=pitch_shift,
596
+ volume_gain=volume_gain,
597
+ silence_duration=silence_duration
598
+ )
599
+
600
+ raw_text = text.strip()
601
+
602
+ # Load reference
603
+ ref_text_path = VOICE_SAMPLES[voice_choice]["text"]
604
+ try:
605
+ with open(ref_text_path, "r", encoding="utf-8") as f:
606
+ ref_text_raw = f.read()
607
+ except Exception as e:
608
+ yield None, f"❌ Lỗi đọc file text mẫu: {e}"
609
+ return
610
+
611
+ yield None, "📄 Đang xử lý Reference..."
612
+
613
+ # Load codes
614
+ codes_path = VOICE_SAMPLES[voice_choice]["codes"]
615
+ try:
616
+ ref_codes_tensor = torch.load(codes_path, map_location="cpu")
617
+ if isinstance(ref_codes_tensor, torch.Tensor):
618
+ ref_codes = ref_codes_tensor.cpu().numpy()
619
+ else:
620
+ ref_codes = np.array(ref_codes_tensor)
621
+ except Exception as e:
622
+ yield None, f"❌ Lỗi khi tải codes: {e}"
623
+ return
624
+
625
+ if ref_codes is None or len(ref_codes) == 0:
626
+ yield None, "❌ Codes tham chiếu không hợp lệ."
627
+ return
628
+
629
+ # Split text
630
+ text_chunks = split_text_into_chunks(raw_text, max_chars=MAX_CHARS_PER_CHUNK)
631
+ total_chunks = len(text_chunks)
632
+
633
+ yield None, f"🚀 Bắt đầu tổng hợp ({total_chunks} đoạn)..."
634
+
635
+ all_audio_segments = []
636
+ silence_pad = np.zeros(int(SAMPLE_RATE * settings.silence_duration), dtype=np.float32)
637
+
638
+ start_time = time.time()
639
+
640
+ try:
641
+ for i, chunk in enumerate(text_chunks):
642
+ yield None, f"⏳ Đang xử lý đoạn {i+1}/{total_chunks}... ({int((i/total_chunks)*100)}%)"
643
+
644
+ # Phonemize
645
+ ref_text_phoneme = phonemize_with_dict(ref_text_raw)
646
+ input_text_phoneme = phonemize_with_dict(chunk)
647
+
648
+ # Create prompt
649
+ codes_str = "".join([f"<|speech_{idx}|>" for idx in ref_codes])
650
+ prompt = (
651
+ f"user: Convert the text to speech:<|TEXT_PROMPT_START|>{ref_text_phoneme} {input_text_phoneme}"
652
+ f"<|TEXT_PROMPT_END|>\nassistant:<|SPEECH_GENERATION_START|>{codes_str}"
653
+ )
654
+
655
+ # Generate
656
+ output = backbone(
657
+ prompt,
658
+ max_tokens=2048,
659
+ temperature=settings.temperature,
660
+ top_k=settings.top_k,
661
+ top_p=settings.top_p,
662
+ stop=["<|SPEECH_GENERATION_END|>"],
663
+ )
664
+ output_str = output["choices"][0]["text"]
665
+
666
+ # Decode
667
+ chunk_wav = decode_audio(output_str, codec)
668
+
669
+ # Apply effects
670
+ yield None, f"🎨 Áp dụng hiệu ứng cho đoạn {i+1}/{total_chunks}..."
671
+ chunk_wav = apply_audio_effects(chunk_wav, settings)
672
+
673
+ if chunk_wav is not None and len(chunk_wav) > 0:
674
+ all_audio_segments.append(chunk_wav)
675
+ if i < total_chunks - 1:
676
+ all_audio_segments.append(silence_pad)
677
+
678
+ if not all_audio_segments:
679
+ yield None, "❌ Không sinh được audio nào."
680
+ add_to_history(raw_text, voice_choice, None, 0, "Lỗi: Không sinh được audio", settings)
681
+ update_stats(False)
682
+ return
683
+
684
+ yield None, "💾 Đang ghép file và lưu..."
685
+
686
+ final_wav = np.concatenate(all_audio_segments)
687
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
688
+ sf.write(tmp.name, final_wav, SAMPLE_RATE)
689
+ output_path = tmp.name
690
+
691
+ process_time = time.time() - start_time
692
+ audio_duration = len(final_wav) / SAMPLE_RATE
693
+ rtf = process_time / audio_duration
694
+
695
+ # Lưu vào lịch sử
696
+ permanent_path = add_to_history(raw_text, voice_choice, output_path, process_time, "Thành công", settings)
697
+ update_stats(True)
698
+
699
+ yield permanent_path, (f"✅ Hoàn tất! | Thời gian: {process_time:.2f}s | "
700
+ f"RTF: {rtf:.3f} | Audio: {audio_duration:.2f}s")
701
+
702
+ except Exception as e:
703
+ import traceback
704
+ traceback.print_exc()
705
+ add_to_history(raw_text, voice_choice, None, 0, f"Lỗi: {str(e)}", settings)
706
+ update_stats(False)
707
+ yield None, f"❌ Lỗi tổng hợp: {str(e)}"
708
+
709
+ def load_history_item(item_index: str):
710
+ """Tải một item từ lịch sử"""
711
+ if not item_index or item_index.strip() == "":
712
+ return None, "", "", "", "⚠️ Vui lòng nhập số thứ tự"
713
+
714
+ try:
715
+ index = int(item_index.strip()) - 1
716
+ history = load_history()
717
+
718
+ if index < 0 or index >= len(history):
719
+ return None, "", "", "", f"❌ Số thứ tự không hợp lệ (1-{len(history)})"
720
+
721
+ record = history[index]
722
+
723
+ audio_path = None
724
+ if record.audio_path and os.path.exists(record.audio_path):
725
+ audio_path = record.audio_path
726
+
727
+ info = f"""
728
+ 📅 Thời gian: {record.timestamp}
729
+ ⏱️ Thời lượng: {record.duration:.2f}s
730
+ 🎭 Giọng: {record.voice}
731
+ 📊 Trạng thái: {record.status}
732
+ 🆔 ID: {record.id}
733
+
734
+ 🎛️ Cài đặt đã dùng:
735
+ • Temperature: {record.settings.get('temperature', 1.0):.2f}
736
+ • Top-K: {record.settings.get('top_k', 50)}
737
+ • Top-P: {record.settings.get('top_p', 0.95):.2f}
738
+ • Tốc độ: {record.settings.get('speed_ratio', 1.0):.1f}x
739
+ • Cao độ: {record.settings.get('pitch_shift', 0):+d} semitones
740
+ • Âm lượng: {record.settings.get('volume_gain', 1.0):.1f}x
741
+ • Khoảng lặng: {record.settings.get('silence_duration', 0.15):.2f}s
742
+ """.strip()
743
+
744
+ return audio_path, record.full_text, record.voice, record.id, info
745
+
746
+ except ValueError:
747
+ return None, "", "", "", "❌ Vui lòng nhập số hợp lệ"
748
+ except Exception as e:
749
+ return None, "", "", "", f"❌ Lỗi: {str(e)}"
750
+
751
+ def load_preset_to_ui(preset_name: str):
752
+ """Tải preset vào UI"""
753
+ presets = load_presets()
754
+ if preset_name not in presets:
755
+ return [1.0, 50, 0.95, 1.0, 0, 1.0, 0.15, f"❌ Preset '{preset_name}' không tồn tại"]
756
+
757
+ settings = presets[preset_name]
758
+ return [
759
+ settings.temperature,
760
+ settings.top_k,
761
+ settings.top_p,
762
+ settings.speed_ratio,
763
+ settings.pitch_shift,
764
+ settings.volume_gain,
765
+ settings.silence_duration,
766
+ f"✅ Đã tải preset '{preset_name}'"
767
+ ]
768
+
769
+ def save_current_preset(name: str, temp: float, top_k: int, top_p: float,
770
+ speed: float, pitch: int, volume: float, silence: float):
771
+ """Lưu cài đặt hiện tại thành preset"""
772
+ if not name or name.strip() == "":
773
+ return "❌ Vui lòng nhập tên preset"
774
+
775
+ name = name.strip()
776
+ if name in DEFAULT_PRESETS:
777
+ return f"❌ Không thể ghi đè preset mặc định '{name}'"
778
+
779
+ settings = VoiceSettings(
780
+ temperature=temp,
781
+ top_k=top_k,
782
+ top_p=top_p,
783
+ speed_ratio=speed,
784
+ pitch_shift=pitch,
785
+ volume_gain=volume,
786
+ silence_duration=silence
787
+ )
788
+
789
+ success, msg = save_preset(name, settings)
790
+ return msg
791
+
792
+ def get_preset_list():
793
+ """Lấy danh sách preset"""
794
+ presets = load_presets()
795
+ return list(presets.keys())
796
+
797
+ # --- UI SETUP ---
798
+ theme = gr.themes.Ocean(
799
+ primary_hue="indigo",
800
+ secondary_hue="cyan",
801
+ neutral_hue="slate",
802
+ font=[gr.themes.GoogleFont('Inter'), 'ui-sans-serif', 'system-ui'],
803
+ ).set(
804
+ button_primary_background_fill="linear-gradient(90deg, #6366f1 0%, #0ea5e9 100%)",
805
+ button_primary_background_fill_hover="linear-gradient(90deg, #4f46e5 0%, #0284c7 100%)",
806
+ )
807
+
808
+ css = """
809
+ .container { max-width: 1600px; margin: auto; }
810
+ .header-box {
811
+ text-align: center;
812
+ margin-bottom: 25px;
813
+ padding: 25px;
814
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
815
+ border-radius: 12px;
816
+ color: white;
817
+ box-shadow: 0 10px 40px rgba(0,0,0,0.2);
818
+ }
819
+ .header-title {
820
+ font-size: 2.8rem;
821
+ font-weight: 800;
822
+ margin-bottom: 10px;
823
+ }
824
+ .gradient-text {
825
+ background: linear-gradient(45deg, #60A5FA, #22D3EE, #A78BFA);
826
+ -webkit-background-clip: text;
827
+ -webkit-text-fill-color: transparent;
828
+ background-clip: text;
829
+ }
830
+ .status-box {
831
+ font-weight: bold;
832
+ text-align: center;
833
+ border: none;
834
+ background: transparent;
835
+ }
836
+ .settings-panel {
837
+ background: #f8fafc;
838
+ padding: 20px;
839
+ border-radius: 10px;
840
+ border: 2px solid #e2e8f0;
841
+ }
842
+ .history-container {
843
+ max-height: 650px;
844
+ overflow-y: auto;
845
+ padding: 10px;
846
+ background: #f1f5f9;
847
+ border-radius: 8px;
848
+ }
849
+ .info-box {
850
+ background: #f8fafc;
851
+ padding: 15px;
852
+ border-radius: 8px;
853
+ border-left: 4px solid #3b82f6;
854
+ margin: 10px 0;
855
+ font-family: 'Courier New', monospace;
856
+ font-size: 0.9em;
857
+ }
858
+ .preset-badge {
859
+ display: inline-block;
860
+ background: #3b82f6;
861
+ color: white;
862
+ padding: 4px 12px;
863
+ border-radius: 12px;
864
+ font-size: 0.85em;
865
+ font-weight: 600;
866
+ margin: 2px;
867
+ }
868
+ """
869
+
870
+ EXAMPLES_LIST = [
871
+ ["Về miền Tây không chỉ để ngắm nhìn sông nước hữu tình, mà còn để cảm nhận tấm chân tình của người dân nơi đây.", "Vĩnh (nam miền Nam)"],
872
+ ["Hà Nội những ngày vào thu mang một vẻ đẹp trầm mặc và cổ kính đến lạ thường.", "Bình (nam miền Bắc)"],
873
+ ["Thành phố Hồ Chí Minh luôn chuyển mình không ngừng với nhịp sống hối hả, năng động.", "Dung (nữ miền Nam)"],
874
+ ]
875
+
876
+ initial_status = (f"✅ Model đã tải thành công! (Chạy trên **{DEVICE_INFO}**). "
877
+ f"Full features enabled: Custom Voice Settings ✅ | Presets ✅ | History ✅") if model_loaded else "❌ Lỗi khi tải model."
878
+
879
+ with gr.Blocks(title=f"VieNeu-TTS v{VERSION}", theme=theme, css=css) as demo:
880
+ with gr.Column(elem_classes="container"):
881
+ gr.HTML(f"""
882
+ <div class="header-box">
883
+ <h1 class="header-title">
884
+ <span style="font-size: 2.5rem;">🦜</span>
885
+ <span class="gradient-text">VieNeu-TTS Studio Pro</span>
886
+ </h1>
887
+ <p style="margin: 10px 0; font-size: 1.1em;">
888
+ <strong>v{VERSION}</strong> | {DEVICE_INFO} | Advanced Voice Customization
889
+ </p>
890
+ <p style="font-size: 0.85rem; color: #94a3b8;">
891
+ 📁 History: {HISTORY_DIR} | 🎛️ Presets: {SETTINGS_DIR}
892
+ </p>
893
+ <div style="margin-top: 15px; display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">
894
+ <span class="preset-badge">🎯 Custom Settings</span>
895
+ <span class="preset-badge">💾 Presets Manager</span>
896
+ <span class="preset-badge">🎨 Audio Effects</span>
897
+ <span class="preset-badge">📊 Advanced Stats</span>
898
+ <span class="preset-badge">🔍 Smart Search</span>
899
+ </div>
900
+ </div>
901
+ """)
902
+
903
+ status_banner = gr.Markdown(initial_status)
904
+
905
+ # --- TABS ---
906
+ with gr.Tabs():
907
+ # TAB 1: Tổng hợp với Custom Settings
908
+ with gr.Tab("🎙️ Tổng hợp nâng cao"):
909
+ with gr.Row():
910
+ with gr.Column(scale=2):
911
+ text_input = gr.Textbox(
912
+ label=f"📝 Văn bản (Chunk: {MAX_CHARS_PER_CHUNK} ký tự)",
913
+ lines=6,
914
+ placeholder="Nhập văn bản tiếng Việt cần tổng hợp...",
915
+ 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.",
916
+ )
917
+
918
+ voice_select = gr.Dropdown(
919
+ choices=list(VOICE_SAMPLES.keys()),
920
+ value=list(VOICE_SAMPLES.keys())[0],
921
+ label="👤 Chọn giọng mẫu",
922
+ )
923
+
924
+ # Preset Manager
925
+ with gr.Accordion("🎛️ Preset Manager", open=False):
926
+ with gr.Row():
927
+ preset_dropdown = gr.Dropdown(
928
+ choices=get_preset_list(),
929
+ value="Mặc định",
930
+ label="Chọn preset",
931
+ scale=2
932
+ )
933
+ btn_load_preset = gr.Button("📂 Tải", size="sm", scale=1)
934
+ btn_delete_preset = gr.Button("🗑️ Xóa", size="sm", variant="stop", scale=1)
935
+
936
+ with gr.Row():
937
+ preset_name_input = gr.Textbox(
938
+ label="Tên preset mới",
939
+ placeholder="Nhập tên preset...",
940
+ scale=3
941
+ )
942
+ btn_save_preset = gr.Button("💾 Lưu preset", variant="primary", scale=1)
943
+
944
+ preset_status = gr.Textbox(
945
+ label="Trạng thái preset",
946
+ interactive=False,
947
+ show_label=False
948
+ )
949
+
950
+ # Custom Voice Settings
951
+ with gr.Accordion("⚙️ Cài đặt giọng nói tùy chỉnh", open=True, elem_classes="settings-panel"):
952
+ gr.Markdown("### 🎯 Model Parameters")
953
+ with gr.Row():
954
+ temperature_slider = gr.Slider(
955
+ minimum=0.1, maximum=2.0, value=1.0, step=0.1,
956
+ label="🌡️ Temperature (Độ sáng tạo)",
957
+ info="Cao = đa dạng, Thấp = ổn định"
958
+ )
959
+ top_k_slider = gr.Slider(
960
+ minimum=1, maximum=100, value=50, step=1,
961
+ label="🔝 Top-K",
962
+ info="Số lượng token được xem xét"
963
+ )
964
+
965
+ top_p_slider = gr.Slider(
966
+ minimum=0.1, maximum=1.0, value=0.95, step=0.05,
967
+ label="🎲 Top-P (Nucleus Sampling)",
968
+ info="Xác suất tích lũy"
969
+ )
970
+
971
+ gr.Markdown("### 🎨 Audio Effects")
972
+ with gr.Row():
973
+ speed_slider = gr.Slider(
974
+ minimum=0.5, maximum=2.0, value=1.0, step=0.1,
975
+ label="⚡ Tốc độ (Speed)",
976
+ info="0.5x = chậm, 2.0x = nhanh"
977
+ )
978
+ pitch_slider = gr.Slider(
979
+ minimum=-12, maximum=12, value=0, step=1,
980
+ label="🎵 Cao độ (Pitch Shift)",
981
+ info="Semitones: -12 (thấp) đến +12 (cao)"
982
+ )
983
+
984
+ with gr.Row():
985
+ volume_slider = gr.Slider(
986
+ minimum=0.5, maximum=2.0, value=1.0, step=0.1,
987
+ label="🔊 Âm lượng (Volume)",
988
+ info="0.5x = nhỏ, 2.0x = to"
989
+ )
990
+ silence_slider = gr.Slider(
991
+ minimum=0.05, maximum=1.0, value=0.15, step=0.05,
992
+ label="⏸️ Khoảng lặng (Pause)",
993
+ info="Giây giữa các chunk"
994
+ )
995
+
996
+ with gr.Row():
997
+ btn_reset_settings = gr.Button("🔄 Reset về mặc định", size="sm")
998
+
999
+ with gr.Row():
1000
+ btn_generate = gr.Button(
1001
+ "🎵 Bắt đầu tổng hợp",
1002
+ variant="primary",
1003
+ size="lg",
1004
+ interactive=model_loaded,
1005
+ scale=3
1006
+ )
1007
+ btn_clear = gr.Button("🗑️ Xóa", variant="secondary", size="lg", scale=1)
1008
+
1009
+ with gr.Column(scale=1):
1010
+ audio_output = gr.Audio(
1011
+ label="🔊 Kết quả",
1012
+ type="filepath",
1013
+ autoplay=True
1014
+ )
1015
+ status_output = gr.Textbox(
1016
+ label="📊 Trạng thái",
1017
+ elem_classes="status-box",
1018
+ value="Chờ nhập văn bản...",
1019
+ lines=3
1020
+ )
1021
+
1022
+ gr.Markdown("### 💡 Quick Tips")
1023
+ gr.Markdown("""
1024
+ - **Temperature ↑** → Giọng đa dạng hơn
1025
+ - **Speed < 1.0** → Rõ ràng, dễ nghe
1026
+ - **Pitch +3** → Giọng trẻ/nữ tính
1027
+ - **Pitch -3** → Giọng trầm/nam tính
1028
+ - **Volume 1.2** → Tăng âm lượng 20%
1029
+ """)
1030
+
1031
+ gr.Examples(
1032
+ examples=EXAMPLES_LIST,
1033
+ inputs=[text_input, voice_select],
1034
+ label="💡 Các ví dụ nhanh"
1035
+ )
1036
+
1037
+ # TAB 2: Lịch sử nâng cao
1038
+ with gr.Tab("📜 Lịch sử & Phân tích"):
1039
+ with gr.Row():
1040
+ with gr.Column(scale=3):
1041
+ gr.Markdown("### 📋 Danh sách lịch sử")
1042
+
1043
+ with gr.Row():
1044
+ filter_voice = gr.Dropdown(
1045
+ choices=["Tất cả"] + list(VOICE_SAMPLES.keys()),
1046
+ value="Tất cả",
1047
+ label="Lọc theo giọng",
1048
+ scale=2
1049
+ )
1050
+ search_text = gr.Textbox(
1051
+ label="Tìm kiếm văn bản",
1052
+ placeholder="Nhập từ khóa...",
1053
+ scale=3
1054
+ )
1055
+ btn_search = gr.Button("🔍 Tìm", size="sm", variant="primary", scale=1)
1056
+
1057
+ with gr.Row():
1058
+ btn_refresh = gr.Button("🔄 Làm mới", size="sm", variant="secondary")
1059
+ btn_clear_all = gr.Button("🗑️ Xóa toàn bộ", size="sm", variant="stop")
1060
+ stats_display = gr.Textbox(
1061
+ value=get_processing_stats(),
1062
+ label="",
1063
+ show_label=False,
1064
+ interactive=False,
1065
+ container=False
1066
+ )
1067
+
1068
+ history_display = gr.HTML(
1069
+ value=get_history_list(),
1070
+ elem_classes="history-container"
1071
+ )
1072
+
1073
+ with gr.Column(scale=2):
1074
+ gr.Markdown("### 🔍 Chi tiết bản ghi")
1075
+
1076
+ with gr.Row():
1077
+ history_select = gr.Textbox(
1078
+ label="Nhập số thứ tự",
1079
+ placeholder="Số...",
1080
+ scale=3
1081
+ )
1082
+ btn_load_history = gr.Button("📂 Tải", variant="primary", scale=1)
1083
+
1084
+ history_id = gr.Textbox(
1085
+ label="🆔 ID bản ghi",
1086
+ interactive=False,
1087
+ visible=False
1088
+ )
1089
+
1090
+ history_info = gr.Textbox(
1091
+ label="ℹ️ Thông tin chi tiết",
1092
+ lines=10,
1093
+ elem_classes="info-box"
1094
+ )
1095
+
1096
+ history_audio = gr.Audio(
1097
+ label="🔊 Audio",
1098
+ type="filepath"
1099
+ )
1100
+
1101
+ history_voice = gr.Textbox(
1102
+ label="🎭 Giọng đã dùng",
1103
+ interactive=False
1104
+ )
1105
+
1106
+ history_text = gr.Textbox(
1107
+ label="📄 Văn bản đầy đủ",
1108
+ lines=5,
1109
+ interactive=False
1110
+ )
1111
+
1112
+ with gr.Row():
1113
+ btn_reuse = gr.Button("♻️ Tái sử dụng", variant="secondary", scale=1)
1114
+ btn_export = gr.Button("📥 Export", variant="primary", scale=1)
1115
+
1116
+ # TAB 3: Thông tin & Hướng dẫn
1117
+ with gr.Tab("ℹ️ Thông tin & Hướng dẫn"):
1118
+ with gr.Row():
1119
+ with gr.Column():
1120
+ gr.Markdown(f"""
1121
+ ## 🎯 Về VieNeu-TTS Studio Pro
1122
+
1123
+ **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
1124
+ với khả năng tùy chỉnh giọng nói toàn diện.
1125
+
1126
+ ### ⚙️ Cấu hình hiện tại
1127
+ ```
1128
+ Version: {VERSION}
1129
+ Model Backbone: Q4 GGUF (llama-cpp)
1130
+ Codec: ONNX Decoder
1131
+ Sample Rate: {SAMPLE_RATE} Hz
1132
+ Max Chunk Size: {MAX_CHARS_PER_CHUNK} ký tự
1133
+ History Dir: {HISTORY_DIR}
1134
+ Presets Dir: {SETTINGS_DIR}
1135
+ ```
1136
+
1137
+ ### 🎭 Giọng mẫu ({len(VOICE_SAMPLES)} giọng)
1138
+
1139
+ | Khu vực | Nam | Nữ |
1140
+ |---------|-----|-----|
1141
+ | **Miền Bắc** | Tuyên, Bình | Ngọc, Ly |
1142
+ | **Miền Nam** | Vĩnh, Nguyên, Sơn | Đoan, Dung |
1143
+
1144
+ ### 🎛️ Tính năng nâng cao
1145
+
1146
+ #### Model Parameters
1147
+ - **Temperature (0.1-2.0)**: Kiểm soát độ sáng tạo
1148
+ - `0.5-0.8`: Ổn định, nhất quán
1149
+ - `1.0`: Cân bằng (mặc định)
1150
+ - `1.2-2.0`: Đa dạng, biểu cảm
1151
+
1152
+ - **Top-K (1-100)**: Giới hạn lựa chọn token
1153
+ - `20-30`: Bảo thủ
1154
+ - `50`: Cân bằng (mặc định)
1155
+ - `80-100`: Sáng tạo
1156
+
1157
+ - **Top-P (0.1-1.0)**: Nucleus sampling
1158
+ - `0.9`: An toàn
1159
+ - `0.95`: Cân bằng (mặc định)
1160
+ - `1.0`: Tự do hoàn toàn
1161
+
1162
+ #### Audio Effects
1163
+ - **Speed (0.5-2.0x)**: Thay đổi tốc độ không ảnh hưởng cao độ
1164
+ - **Pitch (-12 đến +12 semitones)**: Thay đổi cao độ giọng nói
1165
+ - **Volume (0.5-2.0x)**: Điều chỉnh âm lượng
1166
+ - **Silence (0.05-1.0s)**: Khoảng dừng giữa các câu
1167
+
1168
+ ### 📚 Preset System
1169
+
1170
+ Hệ thống preset giúp lưu và tái sử dụng cài đặt yêu thích:
1171
+
1172
+ **Preset mặc định:**
1173
+ - 🎯 **Mặc định**: Cài đặt chuẩn
1174
+ - ⚡ **Giọng nhanh**: Speed 1.3x, pause ngắn
1175
+ - 🐢 **Giọng chậm**: Speed 0.8x, pause dài
1176
+ - 🎵 **Giọng trầm**: Pitch -3
1177
+ - 🎶 **Giọng cao**: Pitch +3
1178
+ - 🔥 **Nhiệt tình**: Temp 1.2, volume cao
1179
+ - 😌 **Bình tĩnh**: Temp 0.8, speed chậm
1180
+
1181
+ **Tạo preset mới:**
1182
+ 1. Điều chỉnh các thông số theo ý muốn
1183
+ 2. Nhập tên preset
1184
+ 3. Nhấn "Lưu preset"
1185
+
1186
+ ### 📊 History & Analytics
1187
+
1188
+ - ✅ Lưu tự động mọi lần tổng hợp
1189
+ - 🔍 Tìm kiếm và lọc theo giọng
1190
+ - 📈 Thống kê chi tiết (thời gian, RTF, settings)
1191
+ - ♻️ Tái sử dụng cài đặt từ lịch sử
1192
+ - 🗑️ Tự động xóa khi > 100 bản ghi
1193
+
1194
+ ### 🚀 Workflow gợi ý
1195
+
1196
+ 1. **Thử nghiệm nhanh**:
1197
+ - Chọn giọng → Nhập text → Tổng hợp
1198
+ - Dùng preset mặc định
1199
+
1200
+ 2. **Tùy chỉnh chi tiết**:
1201
+ - Thử các preset có sẵn
1202
+ - Điều chỉnh từng thông số
1203
+ - Lưu thành preset mới
1204
+
1205
+ 3. **Production**:
1206
+ - Dùng preset đã tối ưu
1207
+ - Kiểm tra lịch sử để đảm bảo chất lượng
1208
+ - Export audio khi hài lòng
1209
+
1210
+ ### 🎓 Tips & Tricks
1211
+
1212
+ **Giọng nói tự nhiên**:
1213
+ ```
1214
+ Temperature: 0.9-1.1
1215
+ Speed: 1.0
1216
+ Pitch: 0
1217
+ ```
1218
+
1219
+ **Podcast/Audiobook**:
1220
+ ```
1221
+ Temperature: 0.8
1222
+ Speed: 0.9
1223
+ Silence: 0.2s
1224
+ Volume: 1.1
1225
+ ```
1226
+
1227
+ **Quảng cáo/Promotional**:
1228
+ ```
1229
+ Temperature: 1.2
1230
+ Speed: 1.1
1231
+ Volume: 1.3
1232
+ Pitch: +2
1233
+ ```
1234
+
1235
+ **Tin tức/News**:
1236
+ ```
1237
+ Temperature: 0.85
1238
+ Speed: 1.0
1239
+ Silence: 0.15s
1240
+ ```
1241
+
1242
+ ### 🔧 Troubleshooting
1243
+
1244
+ **Giọng không tự nhiên?**
1245
+ - Giảm Temperature xuống 0.8-0.9
1246
+ - Kiểm tra Speed (nên = 1.0)
1247
+
1248
+ **Âm thanh vỡ/méo?**
1249
+ - Giảm Volume về 1.0
1250
+ - Kiểm tra Pitch (tránh quá ±6)
1251
+
1252
+ **Xử lý chậm?**
1253
+ - Chia nhỏ văn bản
1254
+ - Đóng các tab khác
1255
+
1256
+ ### 📊 Thống kê hệ thống
1257
+ {get_processing_stats()}
1258
+
1259
+ ### 🔗 Liên kết
1260
+ - 🌐 [GitHub Repository](https://github.com/pnnbao97/VieNeu-TTS)
1261
+ - 🤗 [Model Hub](https://huggingface.co/pnnbao-ump)
1262
+ - 📖 [Documentation](https://github.com/pnnbao97/VieNeu-TTS/wiki)
1263
+
1264
+ ---
1265
+ **Phiên bản**: {VERSION} | **Tác giả**: Phạm Nguyễn Ngọc Bảo
1266
+ **License**: MIT | **Last Updated**: December 2024
1267
+ """)
1268
+
1269
+ # ==================== EVENT HANDLERS ====================
1270
+
1271
+ # Tab 1: Tổng hợp
1272
+ btn_generate.click(
1273
+ fn=synthesize_speech,
1274
+ inputs=[
1275
+ text_input, voice_select,
1276
+ temperature_slider, top_k_slider, top_p_slider,
1277
+ speed_slider, pitch_slider, volume_slider, silence_slider
1278
+ ],
1279
+ outputs=[audio_output, status_output]
1280
+ )
1281
+
1282
+ btn_clear.click(
1283
+ fn=lambda: ("", None, "Đã xóa"),
1284
+ outputs=[text_input, audio_output, status_output]
1285
+ )
1286
+
1287
+ # Reset settings
1288
+ def reset_settings():
1289
+ return [1.0, 50, 0.95, 1.0, 0, 1.0, 0.15, "✅ Đã reset về mặc định"]
1290
+
1291
+ btn_reset_settings.click(
1292
+ fn=reset_settings,
1293
+ outputs=[
1294
+ temperature_slider, top_k_slider, top_p_slider,
1295
+ speed_slider, pitch_slider, volume_slider, silence_slider,
1296
+ preset_status
1297
+ ]
1298
+ )
1299
+
1300
+ # Preset management
1301
+ btn_load_preset.click(
1302
+ fn=load_preset_to_ui,
1303
+ inputs=preset_dropdown,
1304
+ outputs=[
1305
+ temperature_slider, top_k_slider, top_p_slider,
1306
+ speed_slider, pitch_slider, volume_slider, silence_slider,
1307
+ preset_status
1308
+ ]
1309
+ )
1310
+
1311
+ btn_save_preset.click(
1312
+ fn=save_current_preset,
1313
+ inputs=[
1314
+ preset_name_input,
1315
+ temperature_slider, top_k_slider, top_p_slider,
1316
+ speed_slider, pitch_slider, volume_slider, silence_slider
1317
+ ],
1318
+ outputs=preset_status
1319
+ ).then(
1320
+ fn=lambda: (get_preset_list(), ""),
1321
+ outputs=[preset_dropdown, preset_name_input]
1322
+ )
1323
+
1324
+ def delete_preset_handler(name):
1325
+ success, msg = delete_preset(name)
1326
+ return get_preset_list(), msg
1327
+
1328
+ btn_delete_preset.click(
1329
+ fn=delete_preset_handler,
1330
+ inputs=preset_dropdown,
1331
+ outputs=[preset_dropdown, preset_status]
1332
+ )
1333
+
1334
+ # Tab 2: Lịch sử
1335
+ def search_history(voice_filter, text_search):
1336
+ return get_history_list(voice_filter, text_search)
1337
+
1338
+ btn_search.click(
1339
+ fn=search_history,
1340
+ inputs=[filter_voice, search_text],
1341
+ outputs=history_display
1342
+ )
1343
+
1344
+ btn_refresh.click(
1345
+ fn=lambda: get_history_list(),
1346
+ outputs=history_display
1347
+ ).then(
1348
+ fn=get_processing_stats,
1349
+ outputs=stats_display
1350
+ )
1351
+
1352
+ btn_load_history.click(
1353
+ fn=load_history_item,
1354
+ inputs=history_select,
1355
+ outputs=[history_audio, history_text, history_voice, history_id, history_info]
1356
+ )
1357
+
1358
+ def clear_all():
1359
+ msg = clear_all_history()
1360
+ return get_history_list(), msg
1361
+
1362
+ btn_clear_all.click(
1363
+ fn=clear_all,
1364
+ outputs=[history_display, stats_display]
1365
+ )
1366
+
1367
+ # Tái sử dụng văn bản và settings
1368
+ def reuse_from_history(text, voice, record_id):
1369
+ if not record_id:
1370
+ return [text, voice, 1.0, 50, 0.95, 1.0, 0, 1.0, 0.15,
1371
+ "⚠️ Không có bản ghi để tải cài đặt"]
1372
+
1373
+ history = load_history()
1374
+ record = next((r for r in history if r.id == record_id), None)
1375
+
1376
+ if not record:
1377
+ return [text, voice, 1.0, 50, 0.95, 1.0, 0, 1.0, 0.15,
1378
+ "❌ Không tìm thấy bản ghi"]
1379
+
1380
+ settings = record.settings
1381
+ return [
1382
+ text,
1383
+ voice,
1384
+ settings.get('temperature', 1.0),
1385
+ settings.get('top_k', 50),
1386
+ settings.get('top_p', 0.95),
1387
+ settings.get('speed_ratio', 1.0),
1388
+ settings.get('pitch_shift', 0),
1389
+ settings.get('volume_gain', 1.0),
1390
+ settings.get('silence_duration', 0.15),
1391
+ f"✅ Đã tải văn bản và cài đặt từ bản ghi #{record_id[:8]}"
1392
+ ]
1393
+
1394
+ btn_reuse.click(
1395
+ fn=reuse_from_history,
1396
+ inputs=[history_text, history_voice, history_id],
1397
+ outputs=[
1398
+ text_input, voice_select,
1399
+ temperature_slider, top_k_slider, top_p_slider,
1400
+ speed_slider, pitch_slider, volume_slider, silence_slider,
1401
+ status_output
1402
+ ]
1403
+ )
1404
+
1405
+ # Export audio (download)
1406
+ def export_audio(record_id):
1407
+ if not record_id:
1408
+ return None, "⚠️ Không có audio để export"
1409
+
1410
+ history = load_history()
1411
+ record = next((r for r in history if r.id == record_id), None)
1412
+
1413
+ if not record or not record.audio_path:
1414
+ return None, "❌ Không tìm thấy file audio"
1415
+
1416
+ if not os.path.exists(record.audio_path):
1417
+ return None, "❌ File audio đã bị xóa"
1418
+
1419
+ return record.audio_path, f"✅ Đã export file audio #{record_id[:8]}"
1420
+
1421
+ btn_export.click(
1422
+ fn=export_audio,
1423
+ inputs=history_id,
1424
+ outputs=[history_audio, history_info]
1425
+ )
1426
+
1427
+ # Auto-refresh stats periodically
1428
+ def auto_refresh_stats():
1429
+ return get_processing_stats()
1430
+
1431
+ # Refresh stats every time a tab is clicked
1432
+ demo.load(
1433
+ fn=auto_refresh_stats,
1434
+ outputs=stats_display
1435
+ )
1436
+
1437
+ # Background processor thread
1438
+ def background_processor():
1439
+ """Xử lý queue tổng hợp trong background"""
1440
+ global is_processing
1441
+
1442
+ while True:
1443
+ task = processing_queue.get()
1444
+ if task is None:
1445
+ break
1446
+
1447
+ is_processing = True
1448
+ text, voice, settings = task
1449
+
1450
+ try:
1451
+ print(f"[Background] Bắt đầu tổng hợp: {text[:50]}...")
1452
+ result = synthesize_speech_internal(text, voice, settings)
1453
+
1454
+ if result:
1455
+ print(f"[Background] ✅ Hoàn thành: {result}")
1456
+ else:
1457
+ print(f"[Background] ❌ Thất bại")
1458
+
1459
+ except Exception as e:
1460
+ print(f"[Background] ❌ Lỗi: {e}")
1461
+ import traceback
1462
+ traceback.print_exc()
1463
+
1464
+ is_processing = False
1465
+ processing_queue.task_done()
1466
+
1467
+ # Khởi động background thread
1468
+ bg_thread = threading.Thread(target=background_processor, daemon=True)
1469
+ bg_thread.start()
1470
+
1471
+ if __name__ == "__main__":
1472
+ print(f"\n{'='*70}")
1473
+ print(f"🚀 VieNeu-TTS Studio Pro v{VERSION} đã sẵn sàng!")
1474
+ print(f"{'='*70}")
1475
+ print(f"📂 History Directory: {HISTORY_DIR}")
1476
+ print(f"🎛️ Presets Directory: {SETTINGS_DIR}")
1477
+ print(f"🎭 Voice Samples: {len(VOICE_SAMPLES)} giọng")
1478
+ print(f"⚙️ Device Info: {DEVICE_INFO}")
1479
+ print(f"📊 Default Presets: {len(DEFAULT_PRESETS)}")
1480
+ print(f"{'='*70}")
1481
+ print(f"\n🎯 Features:")
1482
+ print(f" ✅ Advanced Voice Customization")
1483
+ print(f" ✅ Preset Manager (Save/Load/Delete)")
1484
+ print(f" ✅ Audio Effects (Speed/Pitch/Volume)")
1485
+ print(f" ✅ Smart History with Search & Filter")
1486
+ print(f" ✅ Settings Reuse from History")
1487
+ print(f" ✅ Background Processing")
1488
+ print(f" ✅ Thread-Safe Operations")
1489
+ print(f" ✅ Auto-cleanup (100 records limit)")
1490
+ print(f"\n🌐 Starting Gradio interface...")
1491
+ print(f"{'='*70}\n")
1492
+
1493
+ # Compatible với cả Gradio cũ và mới
1494
+ try:
1495
+ demo.queue(max_size=20).launch(
1496
+ server_name="0.0.0.0",
1497
+ server_port=7860,
1498
+ show_error=True,
1499
+ share=False
1500
+ )
1501
+ except Exception as e:
1502
+ print(f"⚠️ Lỗi khởi động với queue, thử không queue: {e}")
1503
+ demo.launch(
1504
+ server_name="0.0.0.0",
1505
+ server_port=7860,
1506
+ show_error=True,
1507
+ share=False
1508
+ )