mr-don88 commited on
Commit
d4f5948
·
verified ·
1 Parent(s): ed5e6d1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +650 -38
app.py CHANGED
@@ -1,50 +1,662 @@
1
- ---
2
- title: ElevenLabs TTS Pro
3
- emoji: 🎤
4
- colorFrom: blue
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 4.0.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
 
12
- # 🎤 ElevenLabs TTS Pro
 
 
 
 
 
 
 
 
13
 
14
- Công cụ chuyển văn bản thành giọng nói chuyên nghiệp sử dụng API của ElevenLabs.
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- ## 🚀 Tính năng
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- - 🤖 Hỗ trợ nhiều API keys của ElevenLabs
19
- - 🎭 Tạo giọng nói với nhiều thông số tùy chỉnh
20
- - 📝 Hỗ trợ văn bản dài, tự động chia đoạn
21
- - 💾 Xuất file audio (MP3, WAV, OGG, FLAC)
22
- - 📄 Tạo file phụ đề SRT tự động
23
- - 📁 Tải cấu hình từ file (TXT, JSON, ENV)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- ## 🔧 Cách sử dụng
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- 1. **Nhập API Keys**: Lấy từ [ElevenLabs](https://elevenlabs.io/) và nhập vào ô API Keys
28
- 2. **Nhập Voice ID**: Voice ID từ ElevenLabs (VD: `21m00Tcm4TlvDq8ikWAM`)
29
- 3. **Nhập văn bản**: Văn bản cần chuyển thành giọng nói
30
- 4. **Tùy chỉnh thông số**: Stability, Similarity, Style, Speed
31
- 5. **Nhấn "Bắt đầu Tạo Giọng nói"**
32
 
33
- ## 📁 Định dạng file hỗ trợ
 
 
 
 
 
34
 
35
- - **API Keys**: `.txt`, `.json`, `.env`
36
- - **Voice IDs**: `.txt`, `.json`
37
- - **Văn bản**: `.txt`, `.json`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- ## ⚠️ Lưu ý
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
- - Cần có API key hợp lệ từ ElevenLabs
42
- - Giới hạn tự theo gói subscription của bạn
43
- - File tạo ra sẽ được lưu tạm thời và tự động xóa sau phiên làm việc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
- ## 🛠️ Công nghệ sử dụng
 
 
 
 
46
 
47
- - Gradio: Giao diện web
48
- - ElevenLabs API: TTS engine
49
- - Pydub: Xử audio
50
- - Python 3.8+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py cho Hugging Face Spaces
2
+ # -*- coding: utf-8 -*-
 
 
 
 
 
 
 
 
3
 
4
+ import os, re, time, random, json, tempfile, shutil
5
+ from pathlib import Path
6
+ import requests
7
+ import gradio as gr
8
+ from pydub import AudioSegment
9
+ import natsort
10
+ import asyncio
11
+ from concurrent.futures import ThreadPoolExecutor
12
+ import hashlib
13
 
14
+ # ==================== HÀM GỐC ====================
15
+ def merge_audio_files(input_folder, format, output_filename, silence_ms=300):
16
+ audio_files = [f for f in os.listdir(input_folder) if f.lower().endswith(f".{format.lower()}")]
17
+ if not audio_files:
18
+ return None
19
+ audio_files = natsort.natsorted(audio_files)
20
+ combined = AudioSegment.from_file(os.path.join(input_folder, audio_files[0]), format=format.lower())
21
+ for audio_file in audio_files[1:]:
22
+ audio = AudioSegment.from_file(os.path.join(input_folder, audio_file), format=format.lower())
23
+ combined += AudioSegment.silent(duration=silence_ms)
24
+ combined += audio
25
+ output_path = f"{output_filename}.{format.lower()}"
26
+ combined.export(output_path, format=format.lower())
27
+ return output_path
28
 
29
+ def check_api_key(api_key):
30
+ try:
31
+ res = requests.get(
32
+ "https://api.elevenlabs.io/v1/user",
33
+ headers={"xi-api-key": api_key.strip()},
34
+ timeout=10
35
+ )
36
+ if res.status_code == 200:
37
+ sub = res.json().get("subscription", {})
38
+ return {
39
+ "valid": True,
40
+ "remaining": sub.get("character_limit", 0) - sub.get("character_count", 0),
41
+ "total": sub.get("character_limit", 0)
42
+ }
43
+ return {"valid": False, "message": f"Status code: {res.status_code}"}
44
+ except Exception as e:
45
+ return {"valid": False, "message": str(e)}
46
 
47
+ def generate_voice(text, api_key, voice_id, model_id,
48
+ stability=0.7, similarity=0.8, style=0.0, speed=0.75, speaker_boost=True):
49
+ time.sleep(random.uniform(0.5, 1.5)) # Giảm delay để nhanh hơn
50
+ url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
51
+ headers = {"xi-api-key": api_key.strip(), "Content-Type": "application/json"}
52
+ payload = {
53
+ "text": text,
54
+ "model_id": model_id,
55
+ "voice_settings": {
56
+ "stability": stability,
57
+ "similarity_boost": similarity,
58
+ "style": style,
59
+ "speed": speed,
60
+ "use_speaker_boost": speaker_boost
61
+ }
62
+ }
63
+ try:
64
+ res = requests.post(url, headers=headers, json=payload, timeout=30)
65
+ if res.status_code == 200:
66
+ return res.content
67
+ elif res.status_code == 429:
68
+ time.sleep(2) # Giảm thời gian chờ
69
+ return None
70
+ else:
71
+ return None
72
+ except:
73
+ return None
74
+ return None
75
 
76
+ def parse_text_blocks(raw_text, max_length=200):
77
+ blocks = []
78
+ current = ""
79
+ for s in re.split(r'(?<=[.!?])\s+', raw_text):
80
+ if len(current) + len(s) <= max_length:
81
+ if current:
82
+ current += " " + s
83
+ else:
84
+ current = s
85
+ else:
86
+ if current:
87
+ blocks.append(current.strip())
88
+ current = s
89
+ if current:
90
+ blocks.append(current.strip())
91
+ return blocks
92
 
93
+ def estimate_credit(text):
94
+ return len(text) + 50
 
 
 
95
 
96
+ def ms_to_srt_time(ms):
97
+ h = ms // 3600000
98
+ m = (ms % 3600000) // 60000
99
+ s = (ms % 60000) // 1000
100
+ ms = ms % 1000
101
+ return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
102
 
103
+ def create_srt(voice_dir, texts, silence_ms=300):
104
+ files_audio = natsort.natsorted([f for f in os.listdir(voice_dir) if f.startswith("voice_")])
105
+ current_time = 0
106
+ srt_lines = []
107
+ for idx, (fname, text) in enumerate(zip(files_audio, texts), start=1):
108
+ try:
109
+ audio = AudioSegment.from_file(os.path.join(voice_dir, fname))
110
+ start = current_time
111
+ end = start + len(audio)
112
+ srt_lines.append(str(idx))
113
+ srt_lines.append(f"{ms_to_srt_time(start)} --> {ms_to_srt_time(end)}")
114
+ srt_lines.append(text.strip())
115
+ srt_lines.append("")
116
+ current_time = end + silence_ms
117
+ except:
118
+ continue
119
+ if srt_lines:
120
+ srt_path = os.path.join(voice_dir, "output_full.srt")
121
+ with open(srt_path, "w", encoding="utf-8") as f:
122
+ f.write("\n".join(srt_lines))
123
+ return srt_path
124
+ return None
125
 
126
+ def extract_api_keys_from_content(content):
127
+ """Trích xuất API keys từ nội dung"""
128
+ api_keys = []
129
+
130
+ # Tìm tất cả chuỗi bắt đầu bằng sk_
131
+ pattern = r'sk_[a-zA-Z0-9]{20,}'
132
+ matches = re.findall(pattern, content)
133
+ api_keys.extend(matches)
134
+
135
+ # Tìm trong định dạng KEY=sk_...
136
+ lines = content.splitlines()
137
+ for line in lines:
138
+ line = line.strip()
139
+ if '=' in line:
140
+ parts = line.split('=', 1)
141
+ if len(parts) == 2 and parts[1].strip().startswith('sk_'):
142
+ api_keys.append(parts[1].strip())
143
+
144
+ # Loại bỏ trùng lặp
145
+ unique_keys = []
146
+ for key in api_keys:
147
+ if key and key not in unique_keys:
148
+ unique_keys.append(key)
149
+
150
+ return unique_keys
151
 
152
+ def extract_voice_ids_from_content(content):
153
+ """Trích xuất Voice IDs từ nội dung"""
154
+ voice_ids = []
155
+
156
+ # Voice ID thường có độ dài cố định
157
+ pattern = r'[a-zA-Z0-9]{20,}'
158
+ matches = re.findall(pattern, content)
159
+
160
+ lines = content.splitlines()
161
+ for line in lines:
162
+ line = line.strip()
163
+ if len(line) > 15 and ' ' not in line: # Voice ID thường không có khoảng trắng
164
+ voice_ids.append(line)
165
+
166
+ # Loại bỏ trùng lặp
167
+ unique_voices = []
168
+ for vid in voice_ids:
169
+ if vid and vid not in unique_voices:
170
+ unique_voices.append(vid)
171
+
172
+ return unique_voices[:10] # Giới hạn 10 voice IDs
173
 
174
+ def get_output_folder():
175
+ """Tạo thư mục output tạm thời"""
176
+ output_dir = os.path.join(tempfile.gettempdir(), "elevenlabs_tts", hashlib.md5(str(time.time()).encode()).hexdigest()[:8])
177
+ os.makedirs(output_dir, exist_ok=True)
178
+ return output_dir
179
 
180
+ def check_all_api_keys(api_keys_text):
181
+ """Kiểm tra tất cả API keys"""
182
+ api_keys = [k.strip() for k in api_keys_text.splitlines() if k.strip()]
183
+ results = []
184
+
185
+ if not api_keys:
186
+ return "❌ Vui lòng nhập ít nhất một API key!"
187
+
188
+ for i, key in enumerate(api_keys, 1):
189
+ result = check_api_key(key)
190
+ if result.get("valid"):
191
+ results.append(f"✓ Key {i}: Hợp lệ | Còn lại: {result['remaining']:,}/{result['total']:,} ký tự")
192
+ else:
193
+ results.append(f"✗ Key {i}: Không hợp lệ ({result.get('message', 'Unknown error')})")
194
+
195
+ return "\n".join(results)
196
+
197
+ async def process_tts_async(api_keys_text, voice_id, text_input, model_id,
198
+ format_type, stability, similarity, style, speed,
199
+ speaker_boost, progress=gr.Progress()):
200
+ """Xử lý TTS không đồng bộ"""
201
+ try:
202
+ # Chuẩn bị dữ liệu
203
+ api_keys = [k.strip() for k in api_keys_text.splitlines() if k.strip()]
204
+ if not api_keys:
205
+ raise ValueError("Vui lòng nhập ít nhất một API key!")
206
+
207
+ if not voice_id.strip():
208
+ raise ValueError("Vui lòng nhập Voice ID!")
209
+
210
+ if not text_input.strip():
211
+ raise ValueError("Vui lòng nhập văn bản cần chuyển đổi!")
212
+
213
+ # Tạo thư mục output
214
+ output_dir = get_output_folder()
215
+
216
+ # Phân tích văn bản
217
+ texts = parse_text_blocks(text_input.strip())
218
+ if not texts:
219
+ raise ValueError("Không thể phân tích văn bản!")
220
+
221
+ progress(0, desc="🔄 Đang khởi tạo...")
222
+
223
+ # Kiểm tra API keys
224
+ valid_keys = []
225
+ progress(0.1, desc="🔍 Đang kiểm tra API keys...")
226
+
227
+ for key in api_keys:
228
+ info = check_api_key(key)
229
+ if info.get("valid") and info["remaining"] > 600:
230
+ valid_keys.append([key, info["remaining"]])
231
+
232
+ if not valid_keys:
233
+ raise ValueError("Không có API key hợp lệ hoặc đủ hạn ngạch!")
234
+
235
+ # Xử lý từng đoạn văn bản
236
+ audio_files = []
237
+ current_key_index = 0
238
+
239
+ for i, text in enumerate(texts):
240
+ progress_percent = 0.1 + (i / len(texts)) * 0.7
241
+ progress(progress_percent, desc=f"🎤 Đang tạo đoạn {i+1}/{len(texts)}...")
242
+
243
+ success = False
244
+ attempts = 0
245
+
246
+ while attempts < len(valid_keys) and not success:
247
+ key, remaining = valid_keys[current_key_index]
248
+ need = estimate_credit(text)
249
+
250
+ if remaining < need:
251
+ valid_keys.pop(current_key_index)
252
+ current_key_index %= max(len(valid_keys), 1)
253
+ attempts += 1
254
+ continue
255
+
256
+ audio = generate_voice(
257
+ text, key, voice_id.strip(), model_id,
258
+ stability=stability/100,
259
+ similarity=similarity/100,
260
+ style=style/100,
261
+ speed=speed/100,
262
+ speaker_boost=speaker_boost
263
+ )
264
+
265
+ if audio:
266
+ filename = f"voice_{i+1:03d}.{format_type.lower()}"
267
+ filepath = os.path.join(output_dir, filename)
268
+ with open(filepath, "wb") as f:
269
+ f.write(audio)
270
+ audio_files.append(filepath)
271
+ valid_keys[current_key_index][1] -= need
272
+ success = True
273
+ else:
274
+ valid_keys.pop(current_key_index)
275
+ current_key_index %= max(len(valid_keys), 1)
276
+ attempts += 1
277
+
278
+ if not success:
279
+ raise ValueError(f"Không thể tạo đoạn {i+1} với tất cả API keys!")
280
+
281
+ # Merge audio files
282
+ progress(0.8, desc="🔗 Đang merge file audio...")
283
+ merged_file = merge_audio_files(output_dir, format_type, os.path.join(output_dir, "output_full"))
284
+
285
+ # Tạo file SRT
286
+ progress(0.9, desc="📝 Đang tạo file phụ đề...")
287
+ srt_file = create_srt(output_dir, texts)
288
+
289
+ # Đọc file đã tạo
290
+ output_files = []
291
+ if merged_file and os.path.exists(merged_file):
292
+ output_files.append(merged_file)
293
+
294
+ if srt_file and os.path.exists(srt_file):
295
+ output_files.append(srt_file)
296
+
297
+ # Thêm các file audio riêng lẻ
298
+ for audio_file in audio_files[:3]: # Chỉ hiển thị 3 file đầu
299
+ if os.path.exists(audio_file):
300
+ output_files.append(audio_file)
301
+
302
+ progress(1.0, desc="✅ Hoàn thành!")
303
+
304
+ return {
305
+ "message": f"✅ Đã tạo thành công {len(texts)} đoạn audio!",
306
+ "output_dir": output_dir,
307
+ "files": output_files
308
+ }
309
+
310
+ except Exception as e:
311
+ return {
312
+ "message": f"❌ Lỗi: {str(e)}",
313
+ "output_dir": None,
314
+ "files": []
315
+ }
316
+
317
+ # ==================== GRADIO INTERFACE ====================
318
+ with gr.Blocks(title="🎤 ElevenLabs TTS Pro - Web Edition", theme=gr.themes.Soft()) as demo:
319
+ gr.Markdown("""
320
+ # 🎤 ElevenLabs TTS Pro - Web Edition
321
+ **Công cụ chuyển văn bản thành giọng nói chuyên nghiệp**
322
+
323
+ > Phiên bản Web chạy trên Hugging Face Spaces
324
+ """)
325
+
326
+ with gr.Tabs():
327
+ with gr.TabItem("⚙️ Cấu hình chính"):
328
+ with gr.Row():
329
+ with gr.Column(scale=2):
330
+ gr.Markdown("### 🔑 Cấu hình API Keys")
331
+ api_keys_text = gr.Textbox(
332
+ label="API Keys (mỗi key một dòng)",
333
+ placeholder="sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nsk_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
334
+ lines=5,
335
+ info="Nhập một hoặc nhiều API keys từ ElevenLabs"
336
+ )
337
+
338
+ with gr.Row():
339
+ check_api_btn = gr.Button("🔍 Kiểm tra API Keys", variant="secondary")
340
+ clear_api_btn = gr.Button("🗑️ Xóa", variant="secondary")
341
+
342
+ api_check_result = gr.Textbox(label="Kết quả kiểm tra", interactive=False, lines=3)
343
+
344
+ # File upload cho API keys
345
+ api_file = gr.File(
346
+ label="📁 Tải API Keys từ file",
347
+ file_types=[".txt", ".json", ".env"],
348
+ type="binary"
349
+ )
350
+
351
+ with gr.Column(scale=1):
352
+ gr.Markdown("### 🎤 Cài đặt Voice")
353
+ voice_id = gr.Textbox(
354
+ label="Voice ID",
355
+ placeholder="21m00Tcm4TlvDq8ikWAM",
356
+ info="Voice ID từ ElevenLabs"
357
+ )
358
+
359
+ # File upload cho Voice IDs
360
+ voice_file = gr.File(
361
+ label="📁 Tải Voice IDs từ file",
362
+ file_types=[".txt", ".json"],
363
+ type="binary"
364
+ )
365
+
366
+ model_id = gr.Dropdown(
367
+ label="Model",
368
+ choices=["eleven_turbo_v2_5", "eleven_flash_v2_5", "eleven_multilingual_v2"],
369
+ value="eleven_multilingual_v2"
370
+ )
371
+
372
+ format_type = gr.Dropdown(
373
+ label="Định dạng file",
374
+ choices=["MP3", "WAV", "OGG", "FLAC"],
375
+ value="MP3"
376
+ )
377
+
378
+ gr.Markdown("### ⚙️ Thông số Giọng nói")
379
+ with gr.Row():
380
+ stability = gr.Slider(minimum=0, maximum=100, value=95, label="Stability")
381
+ similarity = gr.Slider(minimum=0, maximum=100, value=80, label="Similarity")
382
+
383
+ with gr.Row():
384
+ style = gr.Slider(minimum=0, maximum=100, value=40, label="Style")
385
+ speed = gr.Slider(minimum=70, maximum=120, value=80, label="Speed")
386
+
387
+ speaker_boost = gr.Checkbox(label="Speaker Boost", value=True)
388
+
389
+ with gr.TabItem("📝 Văn bản"):
390
+ text_input = gr.Textbox(
391
+ label="Văn bản cần chuyển đổi",
392
+ placeholder="Nhập văn bản cần chuyển đổi thành giọng nói tại đây...",
393
+ lines=10
394
+ )
395
+
396
+ with gr.Row():
397
+ text_file = gr.File(
398
+ label="📁 Tải văn bản từ file",
399
+ file_types=[".txt", ".json"],
400
+ type="binary"
401
+ )
402
+ clear_text_btn = gr.Button("🗑️ Xóa văn bản", variant="secondary")
403
+
404
+ text_stats = gr.Textbox(label="📊 Thống kê", interactive=False, lines=2)
405
+
406
+ with gr.TabItem("🚀 Xử lý & Kết quả"):
407
+ with gr.Row():
408
+ with gr.Column():
409
+ process_btn = gr.Button("🚀 Bắt đầu Tạo Giọng nói", variant="primary", size="lg")
410
+ status_text = gr.Textbox(label="Trạng thái", interactive=False, lines=3)
411
+
412
+ gr.Markdown("### 📁 Kết quả")
413
+ output_files = gr.File(
414
+ label="File đã tạo",
415
+ file_count="multiple",
416
+ interactive=False
417
+ )
418
+
419
+ output_message = gr.Markdown("")
420
+ with gr.Column():
421
+ gr.Markdown("### 📊 Logs xử lý")
422
+ progress_bar = gr.ProgressBar()
423
+ logs_text = gr.Textbox(
424
+ label="Logs",
425
+ interactive=False,
426
+ lines=15,
427
+ max_lines=50
428
+ )
429
+
430
+ # ==================== CALLBACK FUNCTIONS ====================
431
+
432
+ def update_text_stats(text):
433
+ """Cập nhật thống kê văn bản"""
434
+ char_count = len(text)
435
+ if text.strip():
436
+ blocks = parse_text_blocks(text)
437
+ block_count = len(blocks)
438
+ estimated_credits = char_count + (block_count * 50)
439
+ return f"📊 {char_count:,} ký tự | {block_count} đoạn | Ước tính: {estimated_credits:,} credits"
440
+ return "📊 0 ký tự | 0 đoạn | Ước tính: 0 credits"
441
+
442
+ def clear_api_keys():
443
+ """Xóa API keys"""
444
+ return "", "✅ Đã xóa API keys"
445
+
446
+ def clear_text():
447
+ """Xóa văn bản"""
448
+ return "", update_text_stats("")
449
+
450
+ def process_api_file(file):
451
+ """Xử lý file API keys"""
452
+ if file is None:
453
+ return "", "⚠ Chưa chọn file"
454
+
455
+ try:
456
+ content = file.read().decode('utf-8', errors='ignore')
457
+ api_keys = extract_api_keys_from_content(content)
458
+
459
+ if api_keys:
460
+ return "\n".join(api_keys), f"✅ Đã tải {len(api_keys)} API keys từ file"
461
+ else:
462
+ return "", "⚠ Không tìm thấy API keys trong file"
463
+ except Exception as e:
464
+ return "", f"❌ Lỗi: {str(e)}"
465
+
466
+ def process_voice_file(file):
467
+ """Xử lý file Voice IDs"""
468
+ if file is None:
469
+ return "", "⚠ Chưa chọn file"
470
+
471
+ try:
472
+ content = file.read().decode('utf-8', errors='ignore')
473
+ voice_ids = extract_voice_ids_from_content(content)
474
+
475
+ if voice_ids:
476
+ return voice_ids[0], f"✅ Đã tải {len(voice_ids)} Voice IDs (sử dụng đầu tiên)"
477
+ else:
478
+ return "", "��� Không tìm thấy Voice IDs trong file"
479
+ except Exception as e:
480
+ return "", f"❌ Lỗi: {str(e)}"
481
+
482
+ def process_text_file(file):
483
+ """Xử lý file văn bản"""
484
+ if file is None:
485
+ return "", "⚠ Chưa chọn file"
486
+
487
+ try:
488
+ content = file.read().decode('utf-8', errors='ignore')
489
+
490
+ # Thử phân tích JSON
491
+ try:
492
+ data = json.loads(content)
493
+ if isinstance(data, dict) and 'text' in data:
494
+ content = data['text']
495
+ elif isinstance(data, dict) and 'content' in data:
496
+ content = data['content']
497
+ elif isinstance(data, list):
498
+ content = '\n'.join(str(item) for item in data)
499
+ except:
500
+ pass # Không phải JSON, giữ nguyên
501
+
502
+ return content, f"✅ Đã tải văn bản ({len(content):,} ký tự)"
503
+ except Exception as e:
504
+ return "", f"❌ Lỗi: {str(e)}"
505
+
506
+ def process_tts(api_keys_text, voice_id, text_input, model_id, format_type,
507
+ stability, similarity, style, speed, speaker_boost, progress=gr.Progress()):
508
+ """Xử lý TTS chính"""
509
+ logs = []
510
+
511
+ def log_message(msg):
512
+ logs.append(f"[{time.strftime('%H:%M:%S')}] {msg}")
513
+ return "\n".join(logs[-20:]) # Giữ 20 dòng cuối
514
+
515
+ try:
516
+ # Kiểm tra đầu vào
517
+ if not api_keys_text.strip():
518
+ return {
519
+ status_text: "❌ Vui lòng nhập API keys!",
520
+ logs_text: log_message("❌ Lỗi: Chưa nhập API keys"),
521
+ output_files: None,
522
+ output_message: "### ❌ Lỗi: Vui lòng nhập API keys"
523
+ }
524
+
525
+ if not voice_id.strip():
526
+ return {
527
+ status_text: "❌ Vui lòng nhập Voice ID!",
528
+ logs_text: log_message("❌ Lỗi: Chưa nhập Voice ID"),
529
+ output_files: None,
530
+ output_message: "### ❌ Lỗi: Vui lòng nhập Voice ID"
531
+ }
532
+
533
+ if not text_input.strip():
534
+ return {
535
+ status_text: "❌ Vui lòng nhập văn bản!",
536
+ logs_text: log_message("❌ Lỗi: Chưa nhập văn bản"),
537
+ output_files: None,
538
+ output_message: "### ❌ Lỗi: Vui lòng nhập văn bản"
539
+ }
540
+
541
+ logs_text_value = log_message("🚀 Bắt đầu xử lý TTS...")
542
+ status_text_value = "🔄 Đang khởi tạo..."
543
+ progress(0, desc="Đang khởi tạo...")
544
+
545
+ # Xử lý không đồng bộ
546
+ loop = asyncio.new_event_loop()
547
+ asyncio.set_event_loop(loop)
548
+
549
+ result = loop.run_until_complete(
550
+ process_tts_async(
551
+ api_keys_text, voice_id, text_input, model_id, format_type,
552
+ stability, similarity, style, speed, speaker_boost, progress
553
+ )
554
+ )
555
+
556
+ loop.close()
557
+
558
+ logs_text_value = log_message(result["message"])
559
+
560
+ if result["files"]:
561
+ status_text_value = "✅ Hoàn thành!"
562
+ message = f"### ✅ Hoàn thành!\n\nĐã tạo {len([f for f in result['files'] if 'voice_' in str(f)])} file audio\n[📥 Tải xuống file đã tạo](#)"
563
+ return {
564
+ status_text: status_text_value,
565
+ logs_text: logs_text_value,
566
+ output_files: result["files"],
567
+ output_message: message
568
+ }
569
+ else:
570
+ status_text_value = "❌ Thất bại"
571
+ message = f"### ❌ Thất bại\n\n{result['message']}"
572
+ return {
573
+ status_text: status_text_value,
574
+ logs_text: logs_text_value,
575
+ output_files: None,
576
+ output_message: message
577
+ }
578
+
579
+ except Exception as e:
580
+ error_msg = f"❌ Lỗi hệ thống: {str(e)}"
581
+ return {
582
+ status_text: "❌ Lỗi hệ thống",
583
+ logs_text: log_message(error_msg),
584
+ output_files: None,
585
+ output_message: f"### ❌ Lỗi hệ thống\n\n{str(e)}"
586
+ }
587
+
588
+ # ==================== EVENT HANDLERS ====================
589
+
590
+ # Text stats update
591
+ text_input.change(
592
+ fn=update_text_stats,
593
+ inputs=[text_input],
594
+ outputs=[text_stats]
595
+ )
596
+
597
+ # API keys check
598
+ check_api_btn.click(
599
+ fn=check_all_api_keys,
600
+ inputs=[api_keys_text],
601
+ outputs=[api_check_result]
602
+ )
603
+
604
+ # Clear buttons
605
+ clear_api_btn.click(
606
+ fn=clear_api_keys,
607
+ inputs=[],
608
+ outputs=[api_keys_text, api_check_result]
609
+ )
610
+
611
+ clear_text_btn.click(
612
+ fn=clear_text,
613
+ inputs=[],
614
+ outputs=[text_input, text_stats]
615
+ )
616
+
617
+ # File upload handlers
618
+ api_file.upload(
619
+ fn=process_api_file,
620
+ inputs=[api_file],
621
+ outputs=[api_keys_text, api_check_result]
622
+ ).then(
623
+ fn=check_all_api_keys,
624
+ inputs=[api_keys_text],
625
+ outputs=[api_check_result]
626
+ )
627
+
628
+ voice_file.upload(
629
+ fn=process_voice_file,
630
+ inputs=[voice_file],
631
+ outputs=[voice_id, status_text]
632
+ )
633
+
634
+ text_file.upload(
635
+ fn=process_text_file,
636
+ inputs=[text_file],
637
+ outputs=[text_input, status_text]
638
+ ).then(
639
+ fn=update_text_stats,
640
+ inputs=[text_input],
641
+ outputs=[text_stats]
642
+ )
643
+
644
+ # Main TTS process
645
+ process_btn.click(
646
+ fn=process_tts,
647
+ inputs=[
648
+ api_keys_text, voice_id, text_input, model_id, format_type,
649
+ stability, similarity, style, speed, speaker_boost
650
+ ],
651
+ outputs=[status_text, logs_text, output_files, output_message]
652
+ )
653
+
654
+ # ==================== LAUNCH APP ====================
655
+ if __name__ == "__main__":
656
+ demo.queue() # Enable queuing for async operations
657
+ demo.launch(
658
+ server_name="0.0.0.0",
659
+ server_port=7860,
660
+ share=False,
661
+ debug=False
662
+ )