diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,54 +1,593 @@ -# app.py cho Hugging Face Spaces +# secure_tts_proxy_app_final.py # -*- coding: utf-8 -*- -import os, re, time, random, json, tempfile, shutil -from pathlib import Path +import sys, os, re, time, random, json, base64, hashlib import requests -import gradio as gr +import socks +import socket +import uuid +import platform +import warnings +from typing import List, Dict, Optional, Tuple +import concurrent.futures +import urllib3 +from PySide6.QtWidgets import ( + QApplication, QWidget, QLabel, QLineEdit, QTextEdit, QPushButton, + QFileDialog, QVBoxLayout, QHBoxLayout, QSlider, QComboBox, QCheckBox, + QMessageBox, QProgressBar, QGroupBox, QTabWidget, QFrame, QSplitter, + QMenuBar, QMenu, QStatusBar, QDialog, QDialogButtonBox, QFormLayout, + QProgressDialog, QInputDialog, QListWidget, QListWidgetItem +) +from PySide6.QtCore import Qt, QThread, Signal, QSettings, QTimer, QUrl +from PySide6.QtGui import QFont, QPalette, QColor, QAction, QIcon, QDesktopServices from pydub import AudioSegment import natsort -import asyncio -from concurrent.futures import ThreadPoolExecutor -import hashlib +import tempfile +import shutil -# ==================== HÀM GỐC ==================== -def merge_audio_files(input_folder, format, output_filename, silence_ms=300): - audio_files = [f for f in os.listdir(input_folder) if f.lower().endswith(f".{format.lower()}")] - if not audio_files: - return None - audio_files = natsort.natsorted(audio_files) - combined = AudioSegment.from_file(os.path.join(input_folder, audio_files[0]), format=format.lower()) - for audio_file in audio_files[1:]: - audio = AudioSegment.from_file(os.path.join(input_folder, audio_file), format=format.lower()) - combined += AudioSegment.silent(duration=silence_ms) - combined += audio - output_path = f"{output_filename}.{format.lower()}" - combined.export(output_path, format=format.lower()) - return output_path +warnings.filterwarnings("ignore") +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -def check_api_key(api_key): - try: - res = requests.get( - "https://api.elevenlabs.io/v1/user", - headers={"xi-api-key": api_key.strip()}, - timeout=10 - ) - if res.status_code == 200: - sub = res.json().get("subscription", {}) +# ==================== HÀM ẨN THÔNG TIN MÁY TÍNH ==================== +class PrivacyProtector: + """Lớp bảo vệ quyền riêng tư và ẩn thông tin máy tính""" + + @staticmethod + def get_random_user_agent() -> str: + """Tạo User-Agent ngẫu nhiên""" + browsers = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + ] + return random.choice(browsers) + + @staticmethod + def get_obfuscated_headers(api_key: str) -> Dict: + """Tạo headers với thông tin bị ẩn""" + return { + "xi-api-key": api_key, + "Content-Type": "application/json", + "User-Agent": PrivacyProtector.get_random_user_agent(), + "Accept": "application/json", + "Accept-Language": "en-US,en;q=0.9", + "Accept-Encoding": "gzip, deflate, br", + "Connection": "keep-alive", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "cross-site", + "DNT": "1", + "Sec-GPC": "1" + } + + @staticmethod + def create_secure_session(proxy_manager=None, use_proxy=False) -> requests.Session: + """Tạo session requests an toàn với proxy""" + session = requests.Session() + + # Cấu hình timeout + session.timeout = 30 + + # Xóa thông tin mặc định + session.headers.clear() + + # Thêm headers bảo mật + session.headers.update({ + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + "User-Agent": PrivacyProtector.get_random_user_agent() + }) + + # Thêm proxy nếu được yêu cầu + if use_proxy and proxy_manager: + proxy_url = proxy_manager.get_random_proxy() + if proxy_url: + proxies = proxy_manager.get_proxy_for_requests(proxy_url) + if proxies: + session.proxies.update(proxies) + + return session + +# ==================== PROXY MANAGER NÂNG CAO ==================== +class AdvancedProxyManager: + """Quản lý proxy nâng cao với tính năng tự động xoay và chọn lọc""" + + def __init__(self): + self.proxy_list = [] + self.alive_proxies = [] + self.current_proxy = None + self.test_url = "https://httpbin.org/ip" + self.timeout = 10 + self.auto_check = True + self.proxy_scores = {} + self.proxy_fail_count = {} + self.max_fails = 3 + self.last_check_time = {} + + def add_proxy(self, proxy: str): + """Thêm proxy vào danh sách""" + if proxy and proxy not in self.proxy_list: + proxy = self.standardize_proxy(proxy) + self.proxy_list.append(proxy) + self.proxy_scores[proxy] = 100 + self.proxy_fail_count[proxy] = 0 + print(f"[ProxyManager] Added proxy: {proxy}") + + def standardize_proxy(self, proxy: str) -> str: + """Chuẩn hóa định dạng proxy""" + proxy = proxy.strip() + + if not re.match(r'^[a-zA-Z]+://', proxy): + if ':' in proxy: + # Kiểm tra nếu có authentication + if '@' in proxy: + # Định dạng user:pass@host:port + auth_part, host_part = proxy.split('@', 1) + if ':' in auth_part and ':' in host_part: + proxy = f"http://{proxy}" + else: + # Giả sử là host:port + proxy = f"http://{proxy}" + else: + # Định dạng host:port + proxy = f"http://{proxy}" + + return proxy + + def parse_proxy_url(self, proxy_url: str) -> Tuple[str, Optional[str], Optional[str], str, int]: + """Phân tích proxy URL để lấy thông tin""" + try: + if "://" not in proxy_url: + proxy_url = "http://" + proxy_url + + proxy_scheme, rest = proxy_url.split("://", 1) + username = password = host = port = None + + # Extract authentication if exists + if "@" in rest: + auth, rest = rest.split("@", 1) + if ":" in auth: + username, password = auth.split(":", 1) + else: + username = auth + + # Extract host and port + if ":" in rest: + host, port_str = rest.split(":", 1) + try: + port = int(port_str) + except ValueError: + port = 8080 if proxy_scheme in ["http", "https"] else 1080 + else: + host = rest + port = 8080 if proxy_scheme in ["http", "https"] else 1080 + + return proxy_scheme, username, password, host, port + except: + return "http", None, None, "", 8080 + + def check_proxy(self, proxy_url: str) -> Tuple[bool, str, float]: + """Kiểm tra proxy có hoạt động không và trả về response time""" + start_time = time.time() + try: + proxy_scheme, username, password, host, port = self.parse_proxy_url(proxy_url) + + if not host or port == 0: + return False, "Invalid host or port", 0 + + # Test HTTP/HTTPS proxy + if proxy_scheme in ["http", "https"]: + proxies = {"http": proxy_url, "https": proxy_url} + + try: + response = requests.get( + self.test_url, + proxies=proxies, + timeout=self.timeout, + verify=False + ) + + if response.status_code == 200: + response_time = time.time() - start_time + return True, f"Working ({response_time:.2f}s)", response_time + else: + return False, f"HTTP {response.status_code}", 0 + + except requests.exceptions.Timeout: + return False, "Timeout", 0 + except requests.exceptions.ConnectionError: + return False, "Connection failed", 0 + except Exception as e: + return False, f"Error: {str(e)}", 0 + + # Test SOCKS proxy + elif proxy_scheme in ["socks4", "socks5"]: + try: + import socks + + socks.set_default_proxy( + socks.SOCKS5 if proxy_scheme == "socks5" else socks.SOCKS4, + host, port, + username=username, + password=password + ) + + original_socket = socket.socket + socket.socket = socks.socksocket + + try: + test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + test_socket.settimeout(self.timeout) + test_socket.connect(("httpbin.org", 80)) + test_socket.close() + + socket.socket = original_socket + response_time = time.time() - start_time + return True, f"SOCKS Working ({response_time:.2f}s)", response_time + except: + socket.socket = original_socket + return False, "SOCKS Connection failed", 0 + + except ImportError: + return False, "PySocks not installed", 0 + except Exception as e: + return False, f"SOCKS Error: {str(e)}", 0 + + return False, f"Unsupported type: {proxy_scheme}", 0 + + except Exception as e: + return False, f"Check Error: {str(e)}", 0 + + def check_all_proxies(self, max_workers: int = 20) -> List[str]: + """Kiểm tra tất cả proxy trong danh sách""" + if not self.proxy_list: + print("[ProxyManager] No proxies to check") + return [] + + print(f"[ProxyManager] Checking {len(self.proxy_list)} proxies...") + self.alive_proxies = [] + checked_proxies = [] + + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + future_to_proxy = { + executor.submit(self.check_proxy, proxy): proxy + for proxy in self.proxy_list + } + + for future in concurrent.futures.as_completed(future_to_proxy): + proxy = future_to_proxy[future] + try: + is_alive, message, response_time = future.result() + + if is_alive: + self.alive_proxies.append(proxy) + self.proxy_scores[proxy] = min(100, self.proxy_scores.get(proxy, 100) + 10) + self.proxy_fail_count[proxy] = 0 + print(f"✅ {proxy} - {message}") + else: + self.proxy_scores[proxy] = max(0, self.proxy_scores.get(proxy, 100) - 20) + self.proxy_fail_count[proxy] = self.proxy_fail_count.get(proxy, 0) + 1 + print(f"❌ {proxy} - {message}") + + checked_proxies.append(proxy) + self.last_check_time[proxy] = time.time() + + except Exception as e: + print(f"❌ {proxy} - Check error: {str(e)}") + + print(f"[ProxyManager] Found {len(self.alive_proxies)} alive proxies") + + # Xóa proxy thất bại quá nhiều lần + self.remove_failed_proxies() + + return self.alive_proxies + + def remove_failed_proxies(self): + """Xóa proxy thất bại quá nhiều lần""" + to_remove = [] + for proxy, fails in self.proxy_fail_count.items(): + if fails >= self.max_fails: + to_remove.append(proxy) + + for proxy in to_remove: + if proxy in self.proxy_list: + self.proxy_list.remove(proxy) + if proxy in self.alive_proxies: + self.alive_proxies.remove(proxy) + self.proxy_scores.pop(proxy, None) + self.proxy_fail_count.pop(proxy, None) + print(f"[ProxyManager] Removed failed proxy: {proxy}") + + def get_best_proxy(self) -> Optional[str]: + """Lấy proxy tốt nhất dựa trên điểm số""" + if not self.alive_proxies: + if self.auto_check: + print("[ProxyManager] No alive proxies, running auto-check...") + self.check_all_proxies(max_workers=10) + + if not self.alive_proxies: + print("[ProxyManager] No alive proxies available") + return None + + # Sắp xếp proxy theo điểm số + scored_proxies = [] + for proxy in self.alive_proxies: + score = self.proxy_scores.get(proxy, 50) + # Ưu tiên proxy chưa được dùng gần đây + last_used = self.last_check_time.get(proxy, 0) + time_since_last = time.time() - last_used + adjusted_score = score + (time_since_last / 60) # Thêm điểm cho proxy lâu chưa dùng + + scored_proxies.append((proxy, adjusted_score)) + + # Chọn proxy có điểm cao nhất + scored_proxies.sort(key=lambda x: x[1], reverse=True) + best_proxy = scored_proxies[0][0] + + self.current_proxy = best_proxy + print(f"[ProxyManager] Selected best proxy: {best_proxy} (score: {scored_proxies[0][1]:.1f})") + return best_proxy + + def get_proxy_for_requests(self, proxy_url: str = None) -> Optional[Dict[str, str]]: + """Chuẩn bị proxy cho requests""" + if not proxy_url: + proxy_url = self.get_best_proxy() + if not proxy_url: + return None + + try: + proxy_scheme, username, password, host, port = self.parse_proxy_url(proxy_url) + + if proxy_scheme in ["http", "https"]: + if username and password: + proxy_str = f"{proxy_scheme}://{username}:{password}@{host}:{port}" + else: + proxy_str = f"{proxy_scheme}://{host}:{port}" + + return {"http": proxy_str, "https": proxy_str} + + elif proxy_scheme in ["socks4", "socks5"]: + import socks + socks.set_default_proxy( + socks.SOCKS5 if proxy_scheme == "socks5" else socks.SOCKS4, + host, port, + True, username, password + ) + socket.socket = socks.socksocket + return None + + return None + + except Exception as e: + print(f"[ProxyManager] Error preparing proxy: {e}") + return None + + def rotate_proxy(self): + """Xoay sang proxy khác""" + old_proxy = self.current_proxy + new_proxy = self.get_best_proxy() + + if new_proxy and new_proxy != old_proxy: + print(f"[ProxyManager] Rotated proxy: {old_proxy} -> {new_proxy}") + self.current_proxy = new_proxy + elif new_proxy: + print(f"[ProxyManager] Already using best proxy: {new_proxy}") + else: + print("[ProxyManager] No proxy available for rotation") + + return self.current_proxy + + def mark_proxy_failed(self, proxy_url: str): + """Đánh dấu proxy thất bại""" + if proxy_url in self.proxy_scores: + self.proxy_scores[proxy_url] = max(0, self.proxy_scores[proxy_url] - 30) + self.proxy_fail_count[proxy_url] = self.proxy_fail_count.get(proxy_url, 0) + 1 + print(f"[ProxyManager] Marked proxy as failed: {proxy_url}") + + # Xóa khỏi alive proxies nếu có + if proxy_url in self.alive_proxies: + self.alive_proxies.remove(proxy_url) + + # Xoay proxy nếu đang dùng proxy này + if self.current_proxy == proxy_url: + self.rotate_proxy() + + def mark_proxy_success(self, proxy_url: str, response_time: float = 0): + """Đánh dấu proxy thành công""" + if proxy_url in self.proxy_scores: + bonus = 20 if response_time < 2 else 10 if response_time < 5 else 5 + self.proxy_scores[proxy_url] = min(100, self.proxy_scores[proxy_url] + bonus) + self.proxy_fail_count[proxy_url] = 0 + + # Thêm vào alive proxies nếu chưa có + if proxy_url not in self.alive_proxies: + self.alive_proxies.append(proxy_url) + + def load_proxies_from_file(self, file_path: str = "proxies.txt"): + """Tải proxy từ file""" + if not os.path.exists(file_path): + print(f"[ProxyManager] File not found: {file_path}") + return False + + try: + with open(file_path, 'r', encoding='utf-8') as f: + proxies = [line.strip() for line in f.readlines() if line.strip()] + + for proxy in proxies: + self.add_proxy(proxy) + + print(f"[ProxyManager] Loaded {len(proxies)} proxies from {file_path}") + return True + except Exception as e: + print(f"[ProxyManager] Error loading proxies: {e}") + return False + + def save_proxies_to_file(self, file_path: str = "proxies.txt"): + """Lưu proxy vào file""" + try: + with open(file_path, 'w', encoding='utf-8') as f: + for proxy in self.proxy_list: + f.write(f"{proxy}\n") + print(f"[ProxyManager] Saved {len(self.proxy_list)} proxies to {file_path}") + return True + except Exception as e: + print(f"[ProxyManager] Error saving proxies: {e}") + return False + + def clear_proxies(self): + """Xóa toàn bộ proxy""" + self.proxy_list = [] + self.alive_proxies = [] + self.current_proxy = None + self.proxy_scores = {} + self.proxy_fail_count = {} + print("[ProxyManager] Cleared all proxies") + + def get_stats(self) -> Dict: + """Lấy thống kê proxy""" + total = len(self.proxy_list) + alive = len(self.alive_proxies) + + return { + "total": total, + "alive": alive, + "dead": total - alive, + "alive_percentage": (alive / total * 100) if total > 0 else 0, + "current": self.current_proxy, + "avg_score": sum(self.proxy_scores.values()) / len(self.proxy_scores) if self.proxy_scores else 0 + } + +# ==================== HÀM KIỂM TRA API KEY NÂNG CAO ==================== +def check_api_key_advanced(api_key: str, proxy_manager=None, use_proxy=False, timeout=15) -> Dict: + """Kiểm tra API key với retry mechanism và xử lý lỗi chi tiết""" + max_retries = 2 + for attempt in range(max_retries + 1): + try: + # Tạo session + session = requests.Session() + session.timeout = timeout + + # Headers + headers = { + "xi-api-key": api_key, + "Content-Type": "application/json", + "User-Agent": PrivacyProtector.get_random_user_agent(), + "Accept": "application/json" + } + + # Cấu hình proxy + if use_proxy and proxy_manager: + proxy_url = proxy_manager.get_best_proxy() + if proxy_url: + proxies = proxy_manager.get_proxy_for_requests(proxy_url) + if proxies: + session.proxies.update(proxies) + + # Độ trễ ngẫu nhiên + time.sleep(random.uniform(1, 3)) + + # Gửi request + response = session.get( + "https://api.elevenlabs.io/v1/user", + headers=headers, + timeout=timeout, + verify=False + ) + + # Xử lý response + if response.status_code == 200: + data = response.json() + subscription = data.get("subscription", {}) + + character_limit = subscription.get("character_limit", 0) + character_count = subscription.get("character_count", 0) + remaining = max(0, character_limit - character_count) + + return { + "valid": True, + "remaining": remaining, + "total": character_limit, + "used": character_count, + "tier": subscription.get("tier", "free"), + "is_fresh": remaining >= 1000, + "attempt": attempt + 1 + } + + elif response.status_code == 401: + return { + "valid": False, + "error": "Invalid API key (Unauthorized)", + "status_code": 401 + } + + elif response.status_code == 429: + if attempt < max_retries: + wait_time = 5 * (attempt + 1) + time.sleep(wait_time) + continue + return { + "valid": False, + "error": "Rate limited", + "status_code": 429 + } + + else: + return { + "valid": False, + "error": f"HTTP Error {response.status_code}", + "status_code": response.status_code + } + + except requests.exceptions.Timeout: + if attempt < max_retries: + time.sleep(3) + continue return { - "valid": True, - "remaining": sub.get("character_limit", 0) - sub.get("character_count", 0), - "total": sub.get("character_limit", 0) + "valid": False, + "error": "Connection timeout", + "status": "timeout" } - return {"valid": False, "message": f"Status code: {res.status_code}"} - except Exception as e: - return {"valid": False, "message": str(e)} + + except requests.exceptions.ConnectionError: + if attempt < max_retries: + time.sleep(3) + continue + return { + "valid": False, + "error": "Connection failed", + "status": "connection_error" + } + + except Exception as e: + if attempt < max_retries: + time.sleep(3) + continue + return { + "valid": False, + "error": f"Unexpected error: {str(e)}", + "status": "error" + } + + return { + "valid": False, + "error": "Max retries exceeded", + "status": "max_retries" + } -def generate_voice(text, api_key, voice_id, model_id, - stability=0.7, similarity=0.8, style=0.0, speed=0.75, speaker_boost=True): - time.sleep(random.uniform(0.5, 1.5)) # Giảm delay để nhanh hơn +# ==================== HÀM TẠO VOICE NÂNG CAO ==================== +def generate_voice_advanced(text: str, api_key: str, voice_id: str, model_id: str, + stability=0.7, similarity=0.8, style=0.0, speed=0.75, + speaker_boost=True, proxy_manager=None, use_proxy=False) -> Optional[bytes]: + """Tạo giọng nói với proxy và retry mechanism nâng cao""" + + max_retries = 3 url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}" - headers = {"xi-api-key": api_key.strip(), "Content-Type": "application/json"} + payload = { "text": text, "model_id": model_id, @@ -60,603 +599,1764 @@ def generate_voice(text, api_key, voice_id, model_id, "use_speaker_boost": speaker_boost } } + + for attempt in range(max_retries + 1): + try: + # Tạo session mới mỗi lần retry + session = requests.Session() + session.timeout = 45 + + # Headers + headers = { + "xi-api-key": api_key, + "Content-Type": "application/json", + "User-Agent": PrivacyProtector.get_random_user_agent(), + "Accept": "audio/mpeg" + } + + current_proxy = None + # Cấu hình proxy + if use_proxy and proxy_manager: + current_proxy = proxy_manager.get_best_proxy() + if current_proxy: + proxies = proxy_manager.get_proxy_for_requests(current_proxy) + if proxies: + session.proxies.update(proxies) + + # Độ trễ trước khi request + delay = random.uniform(2, 5) if attempt == 0 else random.uniform(10, 20) + time.sleep(delay) + + # Gửi request + response = session.post( + url, + headers=headers, + json=payload, + timeout=45, + verify=False + ) + + # Xử lý response + if response.status_code == 200: + audio_content = response.content + + # Kiểm tra kích thước file + if len(audio_content) > 2048: # File hợp lệ phải > 2KB + # Đánh dấu proxy thành công + if current_proxy and proxy_manager: + proxy_manager.mark_proxy_success(current_proxy) + + return audio_content + else: + # File quá nhỏ, có thể bị chặn + print(f"[VoiceGen] Audio too small: {len(audio_content)} bytes") + if current_proxy and proxy_manager: + proxy_manager.mark_proxy_failed(current_proxy) + + if attempt < max_retries: + time.sleep(10) + continue + + elif response.status_code == 429: + # Rate limit + wait_time = 30 + (attempt * 10) + print(f"[VoiceGen] Rate limited, waiting {wait_time}s...") + time.sleep(wait_time) + continue + + elif response.status_code == 403: + # Bị chặn + print(f"[VoiceGen] Blocked (403)") + if current_proxy and proxy_manager: + proxy_manager.mark_proxy_failed(current_proxy) + + if attempt < max_retries: + time.sleep(15) + continue + + else: + print(f"[VoiceGen] Error {response.status_code}: {response.text[:100]}") + if attempt < max_retries: + time.sleep(10) + continue + + except requests.exceptions.Timeout: + print(f"[VoiceGen] Timeout on attempt {attempt + 1}") + if attempt < max_retries: + time.sleep(10) + continue + + except requests.exceptions.ConnectionError: + print(f"[VoiceGen] Connection error") + if attempt < max_retries: + time.sleep(10) + continue + + except Exception as e: + print(f"[VoiceGen] Error: {str(e)}") + if attempt < max_retries: + time.sleep(10) + continue + + print(f"[VoiceGen] All attempts failed for text: {text[:50]}...") + return None + +# ==================== HÀM XỬ LÝ ÂM THANH ==================== +def merge_audio_files(input_folder, format, output_filename, silence_ms=300): + """Merge các file audio và kiểm tra kích thước""" try: - res = requests.post(url, headers=headers, json=payload, timeout=30) - if res.status_code == 200: - return res.content - elif res.status_code == 429: - time.sleep(2) # Giảm thời gian chờ + audio_files = [f for f in os.listdir(input_folder) if f.endswith(f".{format.lower()}")] + if not audio_files: return None - else: - return None - except: + + audio_files = natsort.natsorted(audio_files) + combined = AudioSegment.from_file(os.path.join(input_folder, audio_files[0]), format=format.lower()) + + for audio_file in audio_files[1:]: + audio = AudioSegment.from_file(os.path.join(input_folder, audio_file), format=format.lower()) + combined += AudioSegment.silent(duration=silence_ms) + combined += audio + + output_path = f"{output_filename}.{format.lower()}" + combined.export(output_path, format=format.lower()) + + # KIỂM TRA FILE 1KB VÀ TẠO LẠI + if os.path.exists(output_path) and os.path.getsize(output_path) <= 1024: + print(f"⚠️ File chỉ có {os.path.getsize(output_path)} bytes, tạo lại...") + os.remove(output_path) + + # Tạo lại với chất lượng cao hơn + combined.export(output_path, format=format.lower(), + parameters=["-q:a", "0"]) # Highest quality + + print(f"✅ Đã tạo lại file, kích thước mới: {os.path.getsize(output_path)} bytes") + + return output_path + except Exception as e: + print(f"❌ Lỗi merge audio: {str(e)}") return None - return None -def parse_text_blocks(raw_text, max_length=200): +def parse_text_blocks(raw_text, max_length=250): + """Phân chia văn bản thành các block""" blocks = [] - current = "" - for s in re.split(r'(?<=[.!?])\s+', raw_text): - if len(current) + len(s) <= max_length: - if current: - current += " " + s - else: - current = s + sentences = re.split(r'(?<=[.!?])\s+', raw_text) + + current_block = "" + for sentence in sentences: + if len(current_block) + len(sentence) <= max_length: + current_block += " " + sentence if current_block else sentence else: - if current: - blocks.append(current.strip()) - current = s - if current: - blocks.append(current.strip()) + if current_block: + blocks.append(current_block.strip()) + current_block = sentence + + if current_block: + blocks.append(current_block.strip()) + return blocks def estimate_credit(text): + """Ước tính credit cần thiết""" return len(text) + 50 -def ms_to_srt_time(ms): - h = ms // 3600000 - m = (ms % 3600000) // 60000 - s = (ms % 60000) // 1000 - ms = ms % 1000 - return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}" - def create_srt(voice_dir, texts, silence_ms=300): + """Tạo file phụ đề SRT""" files_audio = natsort.natsorted([f for f in os.listdir(voice_dir) if f.startswith("voice_")]) current_time = 0 srt_lines = [] + for idx, (fname, text) in enumerate(zip(files_audio, texts), start=1): try: audio = AudioSegment.from_file(os.path.join(voice_dir, fname)) start = current_time end = start + len(audio) + + # Format thời gian + def ms_to_time(ms): + h = ms // 3600000 + m = (ms % 3600000) // 60000 + s = (ms % 60000) // 1000 + ms = ms % 1000 + return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}" + srt_lines.append(str(idx)) - srt_lines.append(f"{ms_to_srt_time(start)} --> {ms_to_srt_time(end)}") + srt_lines.append(f"{ms_to_time(start)} --> {ms_to_time(end)}") srt_lines.append(text.strip()) srt_lines.append("") + current_time = end + silence_ms except: continue + if srt_lines: srt_path = os.path.join(voice_dir, "output_full.srt") with open(srt_path, "w", encoding="utf-8") as f: f.write("\n".join(srt_lines)) return srt_path + return None -def extract_api_keys_from_content(content): - """Trích xuất API keys từ nội dung""" - api_keys = [] - - # Tìm tất cả chuỗi bắt đầu bằng sk_ - pattern = r'sk_[a-zA-Z0-9]{20,}' - matches = re.findall(pattern, content) - api_keys.extend(matches) - - # Tìm trong định dạng KEY=sk_... - lines = content.splitlines() - for line in lines: - line = line.strip() - if '=' in line: - parts = line.split('=', 1) - if len(parts) == 2 and parts[1].strip().startswith('sk_'): - api_keys.append(parts[1].strip()) - - # Loại bỏ trùng lặp - unique_keys = [] - for key in api_keys: - if key and key not in unique_keys: - unique_keys.append(key) - - return unique_keys - -def extract_voice_ids_from_content(content): - """Trích xuất Voice IDs từ nội dung""" - voice_ids = [] - - # Voice ID thường có độ dài cố định - pattern = r'[a-zA-Z0-9]{20,}' - matches = re.findall(pattern, content) - - lines = content.splitlines() - for line in lines: - line = line.strip() - if len(line) > 15 and ' ' not in line: # Voice ID thường không có khoảng trắng - voice_ids.append(line) - - # Loại bỏ trùng lặp - unique_voices = [] - for vid in voice_ids: - if vid and vid not in unique_voices: - unique_voices.append(vid) - - return unique_voices[:10] # Giới hạn 10 voice IDs - -def get_output_folder(): - """Tạo thư mục output tạm thời""" - output_dir = os.path.join(tempfile.gettempdir(), "elevenlabs_tts", hashlib.md5(str(time.time()).encode()).hexdigest()[:8]) - os.makedirs(output_dir, exist_ok=True) - return output_dir - -def check_all_api_keys(api_keys_text): - """Kiểm tra tất cả API keys""" - api_keys = [k.strip() for k in api_keys_text.splitlines() if k.strip()] - results = [] - - if not api_keys: - return "❌ Vui lòng nhập ít nhất một API key!" - - for i, key in enumerate(api_keys, 1): - result = check_api_key(key) - if result.get("valid"): - results.append(f"✓ Key {i}: Hợp lệ | Còn lại: {result['remaining']:,}/{result['total']:,} ký tự") - else: - results.append(f"✗ Key {i}: Không hợp lệ ({result.get('message', 'Unknown error')})") +# ==================== THREAD TẠO VOICE ==================== +class VoiceGenerationThread(QThread): + """Thread tạo giọng nói với proxy và retry mechanism""" - return "\n".join(results) - -async def process_tts_async(api_keys_text, voice_id, text_input, model_id, - format_type, stability, similarity, style, speed, - speaker_boost, progress=gr.Progress()): - """Xử lý TTS không đồng bộ""" - try: - # Chuẩn bị dữ liệu - api_keys = [k.strip() for k in api_keys_text.splitlines() if k.strip()] - if not api_keys: - raise ValueError("Vui lòng nhập ít nhất một API key!") - - if not voice_id.strip(): - raise ValueError("Vui lòng nhập Voice ID!") - - if not text_input.strip(): - raise ValueError("Vui lòng nhập văn bản cần chuyển đổi!") - - # Tạo thư mục output - output_dir = get_output_folder() - - # Phân tích văn bản - texts = parse_text_blocks(text_input.strip()) - if not texts: - raise ValueError("Không thể phân tích văn bản!") - - progress(0, desc="🔄 Đang khởi tạo...") - - # Kiểm tra API keys - valid_keys = [] - progress(0.1, desc="🔍 Đang kiểm tra API keys...") - - for key in api_keys: - info = check_api_key(key) - if info.get("valid") and info["remaining"] > 600: - valid_keys.append([key, info["remaining"]]) - - if not valid_keys: - raise ValueError("Không có API key hợp lệ hoặc đủ hạn ngạch!") - - # Xử lý từng đoạn văn bản - audio_files = [] - current_key_index = 0 - - for i, text in enumerate(texts): - progress_percent = 0.1 + (i / len(texts)) * 0.7 - progress(progress_percent, desc=f"🎤 Đang tạo đoạn {i+1}/{len(texts)}...") + progress_signal = Signal(str) + progress_value = Signal(int) + progress_max = Signal(int) + finished_signal = Signal(bool, str) + api_key_checked = Signal(int, bool, dict) + + def __init__(self, api_keys, voice_id, texts, settings, fmt, model_id, output_dir, + proxy_manager=None, use_proxy=False): + super().__init__() + self.api_keys = api_keys + self.voice_id = voice_id + self.texts = texts + self.settings = settings + self.fmt = fmt + self.model_id = model_id + self.output_dir = output_dir + self.proxy_manager = proxy_manager + self.use_proxy = use_proxy + self.should_stop = False + + def stop(self): + """Dừng thread""" + self.should_stop = True + + def run(self): + try: + # Tạo thư mục output + os.makedirs(self.output_dir, exist_ok=True) - success = False - attempts = 0 + # Xóa file cũ + for f in os.listdir(self.output_dir): + if f.startswith("voice_") or f.startswith("output_"): + try: + os.remove(os.path.join(self.output_dir, f)) + except: + pass - while attempts < len(valid_keys) and not success: - key, remaining = valid_keys[current_key_index] - need = estimate_credit(text) + # Bước 1: Kiểm tra API keys + self.progress_signal.emit("🔍 Kiểm tra API keys...") + valid_keys = [] + + for i, key in enumerate(self.api_keys, 1): + if self.should_stop: + self.finished_signal.emit(False, "Đã dừng bởi người dùng") + return - if remaining < need: - valid_keys.pop(current_key_index) - current_key_index %= max(len(valid_keys), 1) - attempts += 1 - continue + self.progress_signal.emit(f"Kiểm tra API key {i}/{len(self.api_keys)}...") - audio = generate_voice( - text, key, voice_id.strip(), model_id, - stability=stability/100, - similarity=similarity/100, - style=style/100, - speed=speed/100, - speaker_boost=speaker_boost - ) + # Độ trễ giữa các lần check + time.sleep(random.uniform(1, 3)) - if audio: - filename = f"voice_{i+1:03d}.{format_type.lower()}" - filepath = os.path.join(output_dir, filename) - with open(filepath, "wb") as f: - f.write(audio) - audio_files.append(filepath) - valid_keys[current_key_index][1] -= need - success = True + # Kiểm tra API key + info = check_api_key_advanced(key, self.proxy_manager, self.use_proxy) + + if info.get("valid") and info["remaining"] > 500: + valid_keys.append({ + "key": key, + "remaining": info["remaining"], + "tier": info.get("tier", "free"), + "fresh": info.get("is_fresh", False) + }) + self.api_key_checked.emit(i, True, info) + self.progress_signal.emit(f"✓ Key {i}: {info['remaining']:,} chars") else: - valid_keys.pop(current_key_index) - current_key_index %= max(len(valid_keys), 1) - attempts += 1 + self.api_key_checked.emit(i, False, info) + self.progress_signal.emit(f"✗ Key {i}: {info.get('error', 'Invalid')}") - if not success: - raise ValueError(f"Không thể tạo đoạn {i+1} với tất cả API keys!") - - # Merge audio files - progress(0.8, desc="🔗 Đang merge file audio...") - merged_file = merge_audio_files(output_dir, format_type, os.path.join(output_dir, "output_full")) - - # Tạo file SRT - progress(0.9, desc="📝 Đang tạo file phụ đề...") - srt_file = create_srt(output_dir, texts) - - # Đọc file đã tạo - output_files = [] - if merged_file and os.path.exists(merged_file): - output_files.append(merged_file) - - if srt_file and os.path.exists(srt_file): - output_files.append(srt_file) - - # Thêm các file audio riêng lẻ - for audio_file in audio_files[:3]: # Chỉ hiển thị 3 file đầu - if os.path.exists(audio_file): - output_files.append(audio_file) - - progress(1.0, desc="✅ Hoàn thành!") - - return { - "message": f"✅ Đã tạo thành công {len(texts)} đoạn audio!", - "output_dir": output_dir, - "files": output_files - } - - except Exception as e: - return { - "message": f"❌ Lỗi: {str(e)}", - "output_dir": None, - "files": [] - } - -# ==================== GRADIO INTERFACE ==================== -with gr.Blocks(title="🎤 ElevenLabs TTS Pro - Web Edition", theme=gr.themes.Soft()) as demo: - gr.Markdown(""" - # 🎤 ElevenLabs TTS Pro - Web Edition - **Công cụ chuyển văn bản thành giọng nói chuyên nghiệp** - - > Phiên bản Web chạy trên Hugging Face Spaces - """) - - with gr.Tabs(): - with gr.TabItem("⚙️ Cấu hình chính"): - with gr.Row(): - with gr.Column(scale=2): - gr.Markdown("### 🔑 Cấu hình API Keys") - api_keys_text = gr.Textbox( - label="API Keys (mỗi key một dòng)", - placeholder="sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nsk_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy", - lines=5, - info="Nhập một hoặc nhiều API keys từ ElevenLabs" - ) - - with gr.Row(): - check_api_btn = gr.Button("🔍 Kiểm tra API Keys", variant="secondary") - clear_api_btn = gr.Button("🗑️ Xóa", variant="secondary") - - api_check_result = gr.Textbox(label="Kết quả kiểm tra", interactive=False, lines=3) + if not valid_keys: + self.progress_signal.emit("❌ Không có API key hợp lệ") + self.finished_signal.emit(False, "Không có API key hợp lệ") + return + + # Sắp xếp API keys: fresh trước, nhiều credits trước + valid_keys.sort(key=lambda x: (x["fresh"], x["remaining"]), reverse=True) + + self.progress_signal.emit(f"✅ Đã tìm thấy {len(valid_keys)} API keys hợp lệ") + self.progress_max.emit(len(self.texts)) + + # Bước 2: Tạo âm thanh cho từng block + for i, text in enumerate(self.texts, 1): + if self.should_stop: + self.finished_signal.emit(False, "Đã dừng bởi người dùng") + return + + success = False + attempts = 0 + max_attempts = min(len(valid_keys) * 2, 20) + + while not success and attempts < max_attempts: + attempts += 1 - # File upload cho API keys - api_file = gr.File( - label="📁 Tải API Keys từ file", - file_types=[".txt", ".json", ".env"], - type="binary" - ) + # Chọn API key theo round-robin + key_idx = (attempts - 1) % len(valid_keys) + key_info = valid_keys[key_idx] - with gr.Column(scale=1): - gr.Markdown("### 🎤 Cài đặt Voice") - voice_id = gr.Textbox( - label="Voice ID", - placeholder="21m00Tcm4TlvDq8ikWAM", - info="Voice ID từ ElevenLabs" - ) + # Kiểm tra credits + needed = estimate_credit(text) + if key_info["remaining"] < needed: + self.progress_signal.emit(f"⚠ Key {key_idx+1} hết credits") + valid_keys.pop(key_idx) + if not valid_keys: + break + continue - # File upload cho Voice IDs - voice_file = gr.File( - label="📁 Tải Voice IDs từ file", - file_types=[".txt", ".json"], - type="binary" - ) + self.progress_signal.emit(f"🎤 Block {i}/{len(self.texts)} - Attempt {attempts}...") - model_id = gr.Dropdown( - label="Model", - choices=["eleven_turbo_v2_5", "eleven_flash_v2_5", "eleven_multilingual_v2"], - value="eleven_multilingual_v2" + # Tạo âm thanh + audio = generate_voice_advanced( + text, key_info["key"], self.voice_id, self.model_id, + proxy_manager=self.proxy_manager, + use_proxy=self.use_proxy, + **self.settings ) - format_type = gr.Dropdown( - label="Định dạng file", - choices=["MP3", "WAV", "OGG", "FLAC"], - value="MP3" - ) - - gr.Markdown("### ⚙️ Thông số Giọng nói") - with gr.Row(): - stability = gr.Slider(minimum=0, maximum=100, value=95, label="Stability") - similarity = gr.Slider(minimum=0, maximum=100, value=80, label="Similarity") - - with gr.Row(): - style = gr.Slider(minimum=0, maximum=100, value=40, label="Style") - speed = gr.Slider(minimum=70, maximum=120, value=80, label="Speed") + if audio: + # Lưu file + filename = os.path.join(self.output_dir, f"voice_{i:03d}.{self.fmt.lower()}") + with open(filename, "wb") as f: + f.write(audio) + + # Cập nhật credits + key_info["remaining"] -= needed + + self.progress_value.emit(i) + self.progress_signal.emit(f"✅ Đã tạo block {i}") + success = True + break + else: + # Thất bại, chờ và thử lại + wait_time = random.uniform(5, 15) + self.progress_signal.emit(f"⚠ Thử lại sau {wait_time:.1f}s...") + time.sleep(wait_time) + + if not success: + self.progress_signal.emit(f"❌ Không thể tạo block {i}") + self.finished_signal.emit(False, f"Lỗi tại block {i}") + return - speaker_boost = gr.Checkbox(label="Speaker Boost", value=True) - - with gr.TabItem("📝 Văn bản"): - text_input = gr.Textbox( - label="Văn bản cần chuyển đổi", - placeholder="Nhập văn bản cần chuyển đổi thành giọng nói tại đây...", - lines=10 + # Bước 3: Merge files + self.progress_signal.emit("🔗 Đang merge file audio...") + merged_file = merge_audio_files( + self.output_dir, self.fmt, + os.path.join(self.output_dir, "output_full") ) - with gr.Row(): - text_file = gr.File( - label="📁 Tải văn bản từ file", - file_types=[".txt", ".json"], - type="binary" - ) - clear_text_btn = gr.Button("🗑️ Xóa văn bản", variant="secondary") - - text_stats = gr.Textbox(label="📊 Thống kê", interactive=False, lines=2) + if merged_file and os.path.exists(merged_file) and os.path.getsize(merged_file) > 1024: + # Tạo phụ đề + self.progress_signal.emit("📝 Đang tạo file phụ đề...") + create_srt(self.output_dir, self.texts) + + self.progress_signal.emit(f"🎉 Hoàn thành! File: {merged_file}") + self.finished_signal.emit(True, f"Hoàn thành! File: {merged_file}") + else: + self.progress_signal.emit("❌ Lỗi merge file audio") + self.finished_signal.emit(False, "Lỗi merge file") - with gr.TabItem("🚀 Xử lý & Kết quả"): - with gr.Row(): - with gr.Column(): - process_btn = gr.Button("🚀 Bắt đầu Tạo Giọng nói", variant="primary", size="lg") - status_text = gr.Textbox(label="Trạng thái", interactive=False, lines=3) - - gr.Markdown("### 📁 Kết quả") - output_files = gr.File( - label="File đã tạo", - file_count="multiple", - interactive=False - ) - - output_message = gr.Markdown("") - with gr.Column(): - gr.Markdown("### 📊 Logs xử lý") - progress_bar = gr.Progress() - logs_text = gr.Textbox( - label="Logs", - interactive=False, - lines=15, - max_lines=50 - ) + except Exception as e: + self.progress_signal.emit(f"❌ Lỗi hệ thống: {str(e)}") + self.finished_signal.emit(False, f"Lỗi: {str(e)}") + +# ==================== PROXY DIALOG ==================== +class ProxyManagerDialog(QDialog): + """Dialog quản lý proxy""" - # ==================== CALLBACK FUNCTIONS ==================== + proxies_updated = Signal() - def update_text_stats(text): - """Cập nhật thống kê văn bản""" - char_count = len(text) - if text.strip(): - blocks = parse_text_blocks(text) - block_count = len(blocks) - estimated_credits = char_count + (block_count * 50) - return f"📊 {char_count:,} ký tự | {block_count} đoạn | Ước tính: {estimated_credits:,} credits" - return "📊 0 ký tự | 0 đoạn | Ước tính: 0 credits" + def __init__(self, proxy_manager, parent=None): + super().__init__(parent) + self.proxy_manager = proxy_manager + self.setWindowTitle("🌐 Quản lý Proxy") + self.setMinimumSize(800, 600) + self.setup_ui() - def clear_api_keys(): - """Xóa API keys""" - return "", "✅ Đã xóa API keys" + def setup_ui(self): + layout = QVBoxLayout(self) + + # Tab widget + self.tab_widget = QTabWidget() + + # Tab 1: Quản lý proxy + self.setup_manage_tab() + + # Tab 2: Kiểm tra proxy + self.setup_check_tab() + + # Tab 3: Lấy proxy từ web + self.setup_fetch_tab() + + layout.addWidget(self.tab_widget) + + # Nút đóng + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) - def clear_text(): - """Xóa văn bản""" - return "", update_text_stats("") + def setup_manage_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # Text edit để nhập proxy + self.proxy_text = QTextEdit() + self.proxy_text.setPlaceholderText( + "Nhập proxy (mỗi dòng một proxy):\n" + "http://user:pass@ip:port\n" + "socks5://user:pass@ip:port\n" + "ip:port (tự động thêm http://)\n\n" + "Ví dụ:\n" + "http://user:pass@192.168.1.1:8080\n" + "socks5://192.168.1.2:1080\n" + "45.77.123.99:8080" + ) + layout.addWidget(QLabel("Danh sách proxy:")) + layout.addWidget(self.proxy_text) + + # Nút thao tác + btn_layout = QHBoxLayout() + + load_btn = QPushButton("📁 Tải từ file") + load_btn.clicked.connect(self.load_proxies) + + save_btn = QPushButton("💾 Lưu vào file") + save_btn.clicked.connect(self.save_proxies) + + add_btn = QPushButton("➕ Thêm Proxy") + add_btn.clicked.connect(self.add_proxies) + + clear_btn = QPushButton("🗑️ Xóa tất cả") + clear_btn.clicked.connect(self.clear_proxies) + + btn_layout.addWidget(load_btn) + btn_layout.addWidget(save_btn) + btn_layout.addWidget(add_btn) + btn_layout.addWidget(clear_btn) + + layout.addLayout(btn_layout) + + # Hiển thị thông tin + self.stats_label = QLabel("Tổng: 0 | Sống: 0 | Chết: 0") + self.stats_label.setStyleSheet("font-weight: bold; color: #2c3e50;") + layout.addWidget(self.stats_label) + + # Danh sách proxy sống + self.alive_list = QListWidget() + layout.addWidget(QLabel("Proxy sống:")) + layout.addWidget(self.alive_list) + + self.tab_widget.addTab(tab, "📝 Quản lý") + self.load_current_proxies() + + def setup_check_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # Nút kiểm tra + self.check_btn = QPushButton("🔍 Kiểm tra tất cả proxy") + self.check_btn.clicked.connect(self.check_all_proxies) + layout.addWidget(self.check_btn) + + # Progress + self.progress = QProgressBar() + layout.addWidget(self.progress) + + # Kết quả + self.results_text = QTextEdit() + self.results_text.setReadOnly(True) + layout.addWidget(QLabel("Kết quả kiểm tra:")) + layout.addWidget(self.results_text) + + self.tab_widget.addTab(tab, "✅ Kiểm tra") - def process_api_file(file): - """Xử lý file API keys""" - if file is None: - return "", "⚠ Chưa chọn file" + def setup_fetch_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # Nguồn proxy + source_layout = QHBoxLayout() + source_layout.addWidget(QLabel("Nguồn:")) + + self.source_combo = QComboBox() + self.source_combo.addItems([ + "proxyscrape.com - HTTP", + "proxyscrape.com - SOCKS4", + "proxyscrape.com - SOCKS5", + "proxy-list.download - HTTP", + "proxy-list.download - SOCKS4", + "proxy-list.download - SOCKS5" + ]) + source_layout.addWidget(self.source_combo) + + fetch_btn = QPushButton("🌐 Lấy proxy") + fetch_btn.clicked.connect(self.fetch_proxies) + source_layout.addWidget(fetch_btn) + + layout.addLayout(source_layout) + + # URL tùy chỉnh + url_layout = QHBoxLayout() + url_layout.addWidget(QLabel("URL:")) + + self.custom_url = QLineEdit() + self.custom_url.setPlaceholderText("https://example.com/proxy-list.txt") + url_layout.addWidget(self.custom_url) + + fetch_custom_btn = QPushButton("🌐 Lấy từ URL") + fetch_custom_btn.clicked.connect(self.fetch_custom_proxies) + url_layout.addWidget(fetch_custom_btn) + + layout.addLayout(url_layout) + + # Thông tin + info_label = QLabel( + "Proxy miễn phí thường bị chặn bởi ElevenLabs.\n" + "Để tạo âm thanh thành công, nên sử dụng proxy premium/residential." + ) + info_label.setStyleSheet("color: #e74c3c; padding: 10px;") + layout.addWidget(info_label) + + # Premium proxy links + links_layout = QHBoxLayout() + links_label = QLabel("Proxy Premium đề xuất:") + links_layout.addWidget(links_label) + + brightdata_btn = QPushButton("BrightData") + brightdata_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://brightdata.com"))) + + oxylabs_btn = QPushButton("Oxylabs") + oxylabs_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://oxylabs.io"))) + + smartproxy_btn = QPushButton("SmartProxy") + smartproxy_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://smartproxy.com"))) + links_layout.addWidget(brightdata_btn) + links_layout.addWidget(oxylabs_btn) + links_layout.addWidget(smartproxy_btn) + links_layout.addStretch() + + layout.addLayout(links_layout) + + self.tab_widget.addTab(tab, "🌐 Lấy Proxy") + + def load_current_proxies(self): + """Tải proxy hiện tại""" + if self.proxy_manager.proxy_list: + self.proxy_text.setPlainText("\n".join(self.proxy_manager.proxy_list)) + self.update_stats() + + def update_stats(self): + """Cập nhật thống kê""" + stats = self.proxy_manager.get_stats() + self.stats_label.setText( + f"Tổng: {stats['total']} | " + f"Sống: {stats['alive']} | " + f"Chết: {stats['dead']} | " + f"Hiện tại: {stats['current'][:30] if stats['current'] else 'None'}" + ) + + # Cập nhật danh sách proxy sống + self.alive_list.clear() + for proxy in self.proxy_manager.alive_proxies[:50]: # Hiển thị tối đa 50 proxy + item = QListWidgetItem(proxy) + self.alive_list.addItem(item) + + def load_proxies(self): + """Tải proxy từ file""" + file_path, _ = QFileDialog.getOpenFileName( + self, "Tải proxy từ file", "", "Text files (*.txt);;All files (*.*)" + ) + + if file_path: + try: + self.proxy_manager.load_proxies_from_file(file_path) + self.load_current_proxies() + self.proxies_updated.emit() + QMessageBox.information(self, "Thành công", f"Đã tải {len(self.proxy_manager.proxy_list)} proxy") + except Exception as e: + QMessageBox.critical(self, "Lỗi", f"Lỗi: {str(e)}") + + def save_proxies(self): + """Lưu proxy vào file""" + file_path, _ = QFileDialog.getSaveFileName( + self, "Lưu proxy vào file", "proxies.txt", "Text files (*.txt)" + ) + + if file_path: + try: + self.proxy_manager.save_proxies_to_file(file_path) + QMessageBox.information(self, "Thành công", f"Đã lưu {len(self.proxy_manager.proxy_list)} proxy") + except Exception as e: + QMessageBox.critical(self, "Lỗi", f"Lỗi: {str(e)}") + + def add_proxies(self): + """Thêm proxy từ text""" + text = self.proxy_text.toPlainText().strip() + if not text: + return + + proxies = [p.strip() for p in text.splitlines() if p.strip()] + added = 0 + + for proxy in proxies: + if proxy and proxy not in self.proxy_manager.proxy_list: + self.proxy_manager.add_proxy(proxy) + added += 1 + + if added > 0: + self.proxies_updated.emit() + self.update_stats() + QMessageBox.information(self, "Thành công", f"Đã thêm {added} proxy mới") + + def clear_proxies(self): + """Xóa tất cả proxy""" + reply = QMessageBox.question( + self, "Xác nhận", + "Xóa tất cả proxy?", + QMessageBox.Yes | QMessageBox.No + ) + + if reply == QMessageBox.Yes: + self.proxy_manager.clear_proxies() + self.load_current_proxies() + self.proxies_updated.emit() + + def check_all_proxies(self): + """Kiểm tra tất cả proxy""" + if not self.proxy_manager.proxy_list: + QMessageBox.warning(self, "Cảnh báo", "Không có proxy để kiểm tra") + return + + self.check_btn.setEnabled(False) + self.results_text.clear() + self.progress.setMaximum(len(self.proxy_manager.proxy_list)) + self.progress.setValue(0) + + # Thread kiểm tra + self.check_thread = ProxyCheckThread(self.proxy_manager) + self.check_thread.progress_signal.connect(self.update_check_progress) + self.check_thread.finished_signal.connect(self.on_check_finished) + self.check_thread.start() + + def update_check_progress(self, proxy, is_alive, message): + """Cập nhật tiến trình kiểm tra""" + timestamp = time.strftime("%H:%M:%S") + if is_alive: + self.results_text.append(f'[{timestamp}] ✅ {proxy} - {message}') + else: + self.results_text.append(f'[{timestamp}] ❌ {proxy} - {message}') + + self.progress.setValue(self.progress.value() + 1) + + def on_check_finished(self): + """Khi kiểm tra hoàn tất""" + self.check_btn.setEnabled(True) + self.update_stats() + self.results_text.append("\n✅ Kiểm tra hoàn tất!") + QMessageBox.information(self, "Hoàn tất", "Đã kiểm tra tất cả proxy") + + def fetch_proxies(self): + """Lấy proxy từ web""" + sources = { + 0: "https://api.proxyscrape.com/v2/?request=getproxies&protocol=http&timeout=10000&country=all&ssl=all&anonymity=all", + 1: "https://api.proxyscrape.com/v2/?request=getproxies&protocol=socks4&timeout=10000&country=all", + 2: "https://api.proxyscrape.com/v2/?request=getproxies&protocol=socks5&timeout=10000&country=all", + 3: "https://www.proxy-list.download/api/v1/get?type=http", + 4: "https://www.proxy-list.download/api/v1/get?type=socks4", + 5: "https://www.proxy-list.download/api/v1/get?type=socks5" + } + + url = sources.get(self.source_combo.currentIndex()) + if url: + self.do_fetch_proxies(url) + + def fetch_custom_proxies(self): + """Lấy proxy từ URL tùy chỉnh""" + url = self.custom_url.text().strip() + if url: + self.do_fetch_proxies(url) + + def do_fetch_proxies(self, url): + """Thực hiện lấy proxy""" try: - content = file.read().decode('utf-8', errors='ignore') - api_keys = extract_api_keys_from_content(content) - - if api_keys: - return "\n".join(api_keys), f"✅ Đã tải {len(api_keys)} API keys từ file" + response = requests.get(url, timeout=30, verify=False) + if response.status_code == 200: + lines = response.text.strip().splitlines() + added = 0 + + for line in lines: + line = line.strip() + if line and ':' in line and not line.startswith('#'): + parts = line.split(':') + if len(parts) >= 2: + ip = parts[0].strip() + port = parts[1].strip() + + # Xác định protocol + if 'socks' in url.lower(): + if 'socks4' in url.lower(): + proxy = f"socks4://{ip}:{port}" + else: + proxy = f"socks5://{ip}:{port}" + else: + proxy = f"http://{ip}:{port}" + + if proxy not in self.proxy_manager.proxy_list: + self.proxy_manager.add_proxy(proxy) + added += 1 + + if added > 0: + self.load_current_proxies() + self.proxies_updated.emit() + QMessageBox.information(self, "Thành công", f"Đã thêm {added} proxy mới từ web") + else: + QMessageBox.warning(self, "Cảnh báo", "Không tìm thấy proxy mới") else: - return "", "⚠ Không tìm thấy API keys trong file" + QMessageBox.critical(self, "Lỗi", f"HTTP Error: {response.status_code}") + except Exception as e: - return "", f"❌ Lỗi: {str(e)}" + QMessageBox.critical(self, "Lỗi", f"Lỗi: {str(e)}") + +class ProxyCheckThread(QThread): + """Thread kiểm tra proxy""" + + progress_signal = Signal(str, bool, str) + finished_signal = Signal() - def process_voice_file(file): - """Xử lý file Voice IDs""" - if file is None: - return "", "⚠ Chưa chọn file" + def __init__(self, proxy_manager): + super().__init__() + self.proxy_manager = proxy_manager + + def run(self): + self.proxy_manager.check_all_proxies(max_workers=10) + self.finished_signal.emit() + +# ==================== ỨNG DỤNG CHÍNH ==================== +class SecureTTSApp(QWidget): + """Ứng dụng TTS bảo mật với proxy""" + + def __init__(self): + super().__init__() + self.settings = QSettings("ElevenLabsTTS", "SecureTTS") + self.setWindowTitle("🎤 ElevenLabs TTS Pro - Secure with Proxy") + self.resize(1100, 800) - try: - content = file.read().decode('utf-8', errors='ignore') - voice_ids = extract_voice_ids_from_content(content) + # Proxy manager + self.proxy_manager = AdvancedProxyManager() + self.use_proxy = self.settings.value("use_proxy", "false") == "true" + + # Output directory + self.output_dir = self.settings.value("output_dir", "voices") + os.makedirs(self.output_dir, exist_ok=True) + + self.setup_ui() + self.load_settings() + self.update_proxy_status() + + def setup_ui(self): + """Thiết lập giao diện""" + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(10, 10, 10, 10) + main_layout.setSpacing(10) + + # Header + header = QLabel("🎤 ELEVENLABS TTS - SECURE EDITION") + header.setFont(QFont("Arial", 16, QFont.Bold)) + header.setStyleSheet("color: #2c3e50; padding: 10px;") + header.setAlignment(Qt.AlignCenter) + main_layout.addWidget(header) + + # Status bar + status_layout = QHBoxLayout() + + self.proxy_status = QLabel("🌐 Proxy: TẮT") + self.proxy_status.setStyleSheet(""" + QLabel { + background-color: #e74c3c; + color: white; + padding: 5px 10px; + border-radius: 10px; + font-weight: bold; + } + """) + + self.api_status = QLabel("🔑 API: 0 keys") + self.api_status.setStyleSheet(""" + QLabel { + background-color: #3498db; + color: white; + padding: 5px 10px; + border-radius: 10px; + font-weight: bold; + } + """) + + self.text_status = QLabel("📝 Text: 0 chars") + self.text_status.setStyleSheet(""" + QLabel { + background-color: #2ecc71; + color: white; + padding: 5px 10px; + border-radius: 10px; + font-weight: bold; + } + """) + + status_layout.addWidget(self.proxy_status) + status_layout.addWidget(self.api_status) + status_layout.addWidget(self.text_status) + status_layout.addStretch() + + main_layout.addLayout(status_layout) + + # Tab widget + self.tabs = QTabWidget() + + # Tab 1: Cấu hình + self.setup_config_tab() + + # Tab 2: Văn bản + self.setup_text_tab() + + # Tab 3: Proxy + self.setup_proxy_tab() + + # Tab 4: Output + self.setup_output_tab() + + main_layout.addWidget(self.tabs) + + # Control buttons + control_layout = QHBoxLayout() + + self.clear_btn = QPushButton("🗑️ Xóa tất cả") + self.clear_btn.clicked.connect(self.clear_all) + + self.check_api_btn = QPushButton("🔍 Kiểm tra API") + self.check_api_btn.clicked.connect(self.check_api_keys) + + self.export_btn = QPushButton("📁 Export") + self.export_btn.clicked.connect(self.export_files) + + self.start_btn = QPushButton("🚀 Bắt đầu tạo giọng nói") + self.start_btn.setStyleSheet(""" + QPushButton { + background-color: #2ecc71; + color: white; + font-weight: bold; + padding: 10px 20px; + border-radius: 5px; + } + QPushButton:hover { + background-color: #27ae60; + } + """) + self.start_btn.clicked.connect(self.start_tts_generation) + + control_layout.addWidget(self.clear_btn) + control_layout.addWidget(self.check_api_btn) + control_layout.addWidget(self.export_btn) + control_layout.addStretch() + control_layout.addWidget(self.start_btn) + + main_layout.addLayout(control_layout) + + # Progress bar + self.progress_bar = QProgressBar() + main_layout.addWidget(self.progress_bar) + + # Log output + self.log_text = QTextEdit() + self.log_text.setReadOnly(True) + self.log_text.setMaximumHeight(150) + main_layout.addWidget(self.log_text) + + def setup_config_tab(self): + """Tab cấu hình""" + tab = QWidget() + layout = QVBoxLayout(tab) + + # API keys + api_group = QGroupBox("🔑 API Keys") + api_layout = QVBoxLayout() + + self.api_text = QTextEdit() + self.api_text.setPlaceholderText("Nhập API keys (mỗi key một dòng)\nsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + self.api_text.textChanged.connect(self.update_api_status) + api_layout.addWidget(self.api_text) + + api_btn_layout = QHBoxLayout() + load_api_btn = QPushButton("📁 Tải từ file") + load_api_btn.clicked.connect(self.load_api_keys) + + save_api_btn = QPushButton("💾 Lưu keys") + save_api_btn.clicked.connect(self.save_api_keys) + + api_btn_layout.addWidget(load_api_btn) + api_btn_layout.addWidget(save_api_btn) + api_btn_layout.addStretch() + + api_layout.addLayout(api_btn_layout) + api_group.setLayout(api_layout) + layout.addWidget(api_group) + + # Voice settings + voice_group = QGroupBox("🎤 Voice Settings") + voice_layout = QVBoxLayout() + + # Voice ID + voice_id_layout = QHBoxLayout() + voice_id_layout.addWidget(QLabel("Voice ID:")) + + self.voice_input = QLineEdit() + self.voice_input.setPlaceholderText("21m00Tcm4TlvDq8ikWAM") + voice_id_layout.addWidget(self.voice_input) + + voice_layout.addLayout(voice_id_layout) + + # Model và Format + model_layout = QHBoxLayout() + model_layout.addWidget(QLabel("Model:")) + + self.model_combo = QComboBox() + self.model_combo.addItems(["eleven_multilingual_v2", "eleven_turbo_v2", "eleven_monolingual_v1"]) + model_layout.addWidget(self.model_combo) + + model_layout.addWidget(QLabel("Format:")) + + self.format_combo = QComboBox() + self.format_combo.addItems(["mp3", "wav", "ogg", "flac"]) + model_layout.addWidget(self.format_combo) + + voice_layout.addLayout(model_layout) + + # Voice parameters + params_group = QGroupBox("Voice Parameters") + params_layout = QVBoxLayout() + + # Stability + stability_layout = QHBoxLayout() + stability_layout.addWidget(QLabel("Stability:")) + self.stability_slider = QSlider(Qt.Horizontal) + self.stability_slider.setRange(0, 100) + self.stability_slider.setValue(70) + stability_layout.addWidget(self.stability_slider) + self.stability_label = QLabel("0.70") + stability_layout.addWidget(self.stability_label) + self.stability_slider.valueChanged.connect( + lambda v: self.stability_label.setText(f"{v/100:.2f}") + ) + params_layout.addLayout(stability_layout) + + # Similarity + similarity_layout = QHBoxLayout() + similarity_layout.addWidget(QLabel("Similarity:")) + self.similarity_slider = QSlider(Qt.Horizontal) + self.similarity_slider.setRange(0, 100) + self.similarity_slider.setValue(80) + similarity_layout.addWidget(self.similarity_slider) + self.similarity_label = QLabel("0.80") + similarity_layout.addWidget(self.similarity_label) + self.similarity_slider.valueChanged.connect( + lambda v: self.similarity_label.setText(f"{v/100:.2f}") + ) + params_layout.addLayout(similarity_layout) + + # Speed + speed_layout = QHBoxLayout() + speed_layout.addWidget(QLabel("Speed:")) + self.speed_slider = QSlider(Qt.Horizontal) + self.speed_slider.setRange(50, 150) + self.speed_slider.setValue(100) + speed_layout.addWidget(self.speed_slider) + self.speed_label = QLabel("1.00") + speed_layout.addWidget(self.speed_label) + self.speed_slider.valueChanged.connect( + lambda v: self.speed_label.setText(f"{v/100:.2f}") + ) + params_layout.addLayout(speed_layout) + + params_group.setLayout(params_layout) + voice_layout.addWidget(params_group) + + voice_group.setLayout(voice_layout) + layout.addWidget(voice_group) + + layout.addStretch() + self.tabs.addTab(tab, "⚙️ Cấu hình") + + def setup_text_tab(self): + """Tab văn bản""" + tab = QWidget() + layout = QVBoxLayout(tab) + + self.text_edit = QTextEdit() + self.text_edit.setPlaceholderText("Nhập văn bản cần chuyển thành giọng nói tại đây...") + self.text_edit.textChanged.connect(self.update_text_status) + layout.addWidget(self.text_edit) + + # Text controls + text_controls = QHBoxLayout() + + load_text_btn = QPushButton("📁 Tải văn bản") + load_text_btn.clicked.connect(self.load_text_file) + + save_text_btn = QPushButton("💾 Lưu văn bản") + save_text_btn.clicked.connect(self.save_text_file) + + clear_text_btn = QPushButton("🗑️ Xóa văn bản") + clear_text_btn.clicked.connect(self.clear_text) + + text_controls.addWidget(load_text_btn) + text_controls.addWidget(save_text_btn) + text_controls.addWidget(clear_text_btn) + text_controls.addStretch() + + layout.addLayout(text_controls) + + # Text stats + self.text_stats = QLabel("0 ký tự | 0 đoạn | Ước tính: 0 credits") + self.text_stats.setStyleSheet("color: #7f8c8d; font-style: italic;") + layout.addWidget(self.text_stats) + + self.tabs.addTab(tab, "📝 Văn bản") + + def setup_proxy_tab(self): + """Tab proxy""" + tab = QWidget() + layout = QVBoxLayout(tab) + + # Proxy toggle + toggle_layout = QHBoxLayout() + self.proxy_toggle_btn = QPushButton("🌐 Bật Proxy") + self.proxy_toggle_btn.setStyleSheet(""" + QPushButton { + background-color: #2ecc71; + color: white; + padding: 10px 20px; + font-weight: bold; + border-radius: 5px; + } + """) + self.proxy_toggle_btn.clicked.connect(self.toggle_proxy) + + self.rotate_proxy_btn = QPushButton("🔄 Xoay Proxy") + self.rotate_proxy_btn.clicked.connect(self.rotate_proxy) + + self.manage_proxy_btn = QPushButton("🔧 Quản lý Proxy") + self.manage_proxy_btn.clicked.connect(self.manage_proxies) + + toggle_layout.addWidget(self.proxy_toggle_btn) + toggle_layout.addWidget(self.rotate_proxy_btn) + toggle_layout.addWidget(self.manage_proxy_btn) + toggle_layout.addStretch() + + layout.addLayout(toggle_layout) + + # Current proxy info + self.proxy_info = QTextEdit() + self.proxy_info.setReadOnly(True) + self.proxy_info.setMaximumHeight(80) + layout.addWidget(QLabel("Thông tin proxy hiện tại:")) + layout.addWidget(self.proxy_info) + + # Proxy stats + self.proxy_stats = QLabel("Tổng: 0 | Sống: 0 | Chết: 0") + self.proxy_stats.setStyleSheet("font-weight: bold; color: #2c3e50;") + layout.addWidget(self.proxy_stats) + + # Proxy tips + tips_label = QLabel( + "💡 Mẹo sử dụng proxy:\n" + "1. Proxy miễn phí thường bị chặn bởi ElevenLabs\n" + "2. Sử dụng proxy premium/residential để có kết quả tốt nhất\n" + "3. Thường xuyên kiểm tra và xoay proxy\n" + "4. Nếu âm thanh không tạo được, thử tắt proxy và chạy lại" + ) + tips_label.setStyleSheet(""" + background-color: #fff3cd; + padding: 10px; + border-radius: 5px; + color: #856404; + """) + layout.addWidget(tips_label) + + layout.addStretch() + self.tabs.addTab(tab, "🌐 Proxy") + + def setup_output_tab(self): + """Tab output""" + tab = QWidget() + layout = QVBoxLayout(tab) + + # Output directory + dir_layout = QHBoxLayout() + dir_layout.addWidget(QLabel("Thư mục output:")) + + self.output_dir_label = QLabel(self.output_dir) + self.output_dir_label.setStyleSheet("color: #27ae60; font-weight: bold;") + dir_layout.addWidget(self.output_dir_label, 1) + + select_dir_btn = QPushButton("📁 Chọn thư mục") + select_dir_btn.clicked.connect(self.select_output_dir) + + open_dir_btn = QPushButton("📂 Mở thư mục") + open_dir_btn.clicked.connect(self.open_output_dir) + + dir_layout.addWidget(select_dir_btn) + dir_layout.addWidget(open_dir_btn) + + layout.addLayout(dir_layout) + + # Recent files + self.file_list = QListWidget() + self.file_list.itemDoubleClicked.connect(self.open_selected_file) + layout.addWidget(QLabel("File đã tạo:")) + layout.addWidget(self.file_list) + + # File controls + file_controls = QHBoxLayout() + + refresh_btn = QPushButton("🔄 Làm mới") + refresh_btn.clicked.connect(self.refresh_file_list) + + play_btn = QPushButton("▶️ Phát") + play_btn.clicked.connect(self.play_selected_file) + + delete_btn = QPushButton("🗑️ Xóa") + delete_btn.clicked.connect(self.delete_selected_file) + + file_controls.addWidget(refresh_btn) + file_controls.addWidget(play_btn) + file_controls.addWidget(delete_btn) + file_controls.addStretch() + + layout.addLayout(file_controls) + + # Clear logs button + clear_logs_btn = QPushButton("🗑️ Xóa logs") + clear_logs_btn.clicked.connect(self.clear_logs) + layout.addWidget(clear_logs_btn) + + layout.addStretch() + self.tabs.addTab(tab, "📊 Output") + + def load_settings(self): + """Tải cài đặt""" + self.voice_input.setText(self.settings.value("voice_id", "")) + self.model_combo.setCurrentText(self.settings.value("model", "eleven_multilingual_v2")) + self.format_combo.setCurrentText(self.settings.value("format", "mp3")) + self.stability_slider.setValue(int(self.settings.value("stability", 70))) + self.similarity_slider.setValue(int(self.settings.value("similarity", 80))) + self.speed_slider.setValue(int(self.settings.value("speed", 100))) + + def save_settings(self): + """Lưu cài đặt""" + self.settings.setValue("voice_id", self.voice_input.text()) + self.settings.setValue("model", self.model_combo.currentText()) + self.settings.setValue("format", self.format_combo.currentText()) + self.settings.setValue("stability", self.stability_slider.value()) + self.settings.setValue("similarity", self.similarity_slider.value()) + self.settings.setValue("speed", self.speed_slider.value()) + self.settings.setValue("output_dir", self.output_dir) + self.settings.setValue("use_proxy", "true" if self.use_proxy else "false") + + def update_proxy_status(self): + """Cập nhật trạng thái proxy""" + if self.use_proxy: + self.proxy_status.setText("🌐 Proxy: BẬT") + self.proxy_status.setStyleSheet(""" + QLabel { + background-color: #2ecc71; + color: white; + padding: 5px 10px; + border-radius: 10px; + font-weight: bold; + } + """) + self.proxy_toggle_btn.setText("🌐 Tắt Proxy") + self.proxy_toggle_btn.setStyleSheet(""" + QPushButton { + background-color: #e74c3c; + color: white; + padding: 10px 20px; + font-weight: bold; + border-radius: 5px; + } + QPushButton:hover { + background-color: #c0392b; + } + """) + else: + self.proxy_status.setText("🌐 Proxy: TẮT") + self.proxy_status.setStyleSheet(""" + QLabel { + background-color: #e74c3c; + color: white; + padding: 5px 10px; + border-radius: 10px; + font-weight: bold; + } + """) + self.proxy_toggle_btn.setText("🌐 Bật Proxy") + self.proxy_toggle_btn.setStyleSheet(""" + QPushButton { + background-color: #2ecc71; + color: white; + padding: 10px 20px; + font-weight: bold; + border-radius: 5px; + } + QPushButton:hover { + background-color: #27ae60; + } + """) + + # Update proxy info + stats = self.proxy_manager.get_stats() + self.proxy_stats.setText( + f"Tổng: {stats['total']} | " + f"Sống: {stats['alive']} | " + f"Chết: {stats['dead']} | " + f"Điểm TB: {stats['avg_score']:.1f}" + ) + + if stats['current']: + proxy_info = f"Proxy hiện tại: {stats['current']}\n" + proxy_info += f"Alive proxies: {stats['alive']}/{stats['total']}\n" + proxy_info += f"Tỉ lệ sống: {stats['alive_percentage']:.1f}%" + self.proxy_info.setText(proxy_info) + else: + self.proxy_info.setText("Chưa có proxy nào được chọn") + + def update_api_status(self): + """Cập nhật trạng thái API""" + keys = [k.strip() for k in self.api_text.toPlainText().splitlines() if k.strip()] + self.api_status.setText(f"🔑 API: {len(keys)} keys") + + def update_text_status(self): + """Cập nhật trạng thái văn bản""" + text = self.text_edit.toPlainText() + char_count = len(text) + + if text.strip(): + blocks = parse_text_blocks(text) + block_count = len(blocks) + credit_estimate = char_count + (block_count * 50) - if voice_ids: - return voice_ids[0], f"✅ Đã tải {len(voice_ids)} Voice IDs (sử dụng đầu tiên)" - else: - return "", "⚠ Không tìm thấy Voice IDs trong file" - except Exception as e: - return "", f"❌ Lỗi: {str(e)}" + self.text_stats.setText( + f"{char_count:,} ký tự | " + f"{block_count} đoạn | " + f"Ước tính: {credit_estimate:,} credits" + ) + else: + self.text_stats.setText("0 ký tự | 0 đoạn | Ước tính: 0 credits") + + self.text_status.setText(f"📝 Text: {char_count:,} chars") - def process_text_file(file): - """Xử lý file văn bản""" - if file is None: - return "", "⚠ Chưa chọn file" + def log_message(self, message, level="info"): + """Ghi log message""" + timestamp = time.strftime("%H:%M:%S") - try: - content = file.read().decode('utf-8', errors='ignore') + if level == "error": + color = "#e74c3c" + prefix = "❌" + elif level == "success": + color = "#2ecc71" + prefix = "✅" + elif level == "warning": + color = "#f39c12" + prefix = "⚠️" + else: + color = "#3498db" + prefix = "ℹ️" + + html = f'[{timestamp}] {prefix} {message}' + self.log_text.append(html) + + # Auto-scroll to bottom + scrollbar = self.log_text.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def toggle_proxy(self): + """Bật/tắt proxy""" + self.use_proxy = not self.use_proxy + + if self.use_proxy: + # Kiểm tra nếu có proxy sống + if not self.proxy_manager.alive_proxies: + reply = QMessageBox.question( + self, "Kiểm tra proxy", + "Chưa có proxy sống. Bạn có muốn kiểm tra proxy ngay bây giờ?", + QMessageBox.Yes | QMessageBox.No + ) + + if reply == QMessageBox.Yes: + self.manage_proxies() + return + else: + self.use_proxy = False + return - # Thử phân tích JSON + self.log_message("Đã bật chế độ sử dụng proxy", "success") + else: + self.log_message("Đã tắt chế độ sử dụng proxy", "info") + + self.update_proxy_status() + self.save_settings() + + def rotate_proxy(self): + """Xoay proxy""" + new_proxy = self.proxy_manager.rotate_proxy() + if new_proxy: + self.log_message(f"Đã xoay sang proxy: {new_proxy}", "info") + self.update_proxy_status() + else: + self.log_message("Không có proxy để xoay", "warning") + + def manage_proxies(self): + """Mở dialog quản lý proxy""" + dialog = ProxyManagerDialog(self.proxy_manager, self) + dialog.proxies_updated.connect(self.update_proxy_status) + dialog.exec() + + def load_api_keys(self): + """Tải API keys từ file""" + file_path, _ = QFileDialog.getOpenFileName( + self, "Tải API keys", "", "Text files (*.txt);;All files (*.*)" + ) + + if file_path: try: - data = json.loads(content) - if isinstance(data, dict) and 'text' in data: - content = data['text'] - elif isinstance(data, dict) and 'content' in data: - content = data['content'] - elif isinstance(data, list): - content = '\n'.join(str(item) for item in data) - except: - pass # Không phải JSON, giữ nguyên - - return content, f"✅ Đã tải văn bản ({len(content):,} ký tự)" - except Exception as e: - return "", f"❌ Lỗi: {str(e)}" + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Parse API keys + keys = [] + for line in content.splitlines(): + line = line.strip() + if line.startswith('sk_'): + keys.append(line) + elif '=' in line and 'sk_' in line: + # Try to extract from key=value format + parts = line.split('=', 1) + if len(parts) == 2 and parts[1].strip().startswith('sk_'): + keys.append(parts[1].strip()) + + if keys: + current_keys = self.api_text.toPlainText().strip() + if current_keys: + keys = list(set(current_keys.splitlines() + keys)) + + self.api_text.setPlainText('\n'.join(keys)) + self.log_message(f"Đã tải {len(keys)} API keys", "success") + else: + self.log_message("Không tìm thấy API keys hợp lệ trong file", "warning") + + except Exception as e: + self.log_message(f"Lỗi tải file: {str(e)}", "error") - def process_tts(api_keys_text, voice_id, text_input, model_id, format_type, - stability, similarity, style, speed, speaker_boost, progress=gr.Progress()): - """Xử lý TTS chính""" - logs = [] + def save_api_keys(self): + """Lưu API keys vào file""" + keys = [k.strip() for k in self.api_text.toPlainText().splitlines() if k.strip()] + if not keys: + QMessageBox.warning(self, "Cảnh báo", "Không có API keys để lưu") + return - def log_message(msg): - logs.append(f"[{time.strftime('%H:%M:%S')}] {msg}") - return "\n".join(logs[-20:]) # Giữ 20 dòng cuối + file_path, _ = QFileDialog.getSaveFileName( + self, "Lưu API keys", "api_keys.txt", "Text files (*.txt)" + ) - try: - # Kiểm tra đầu vào - if not api_keys_text.strip(): - return { - status_text: "❌ Vui lòng nhập API keys!", - logs_text: log_message("❌ Lỗi: Chưa nhập API keys"), - output_files: None, - output_message: "### ❌ Lỗi: Vui lòng nhập API keys" - } + if file_path: + try: + with open(file_path, 'w', encoding='utf-8') as f: + f.write('\n'.join(keys)) + self.log_message(f"Đã lưu {len(keys)} API keys", "success") + except Exception as e: + self.log_message(f"Lỗi lưu file: {str(e)}", "error") + + def load_text_file(self): + """Tải văn bản từ file""" + file_path, _ = QFileDialog.getOpenFileName( + self, "Tải văn bản", "", "Text files (*.txt);;All files (*.*)" + ) + + if file_path: + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + current_text = self.text_edit.toPlainText() + if current_text: + reply = QMessageBox.question( + self, "Xác nhận", + "Thêm vào văn bản hiện tại hay thay thế?", + QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel + ) + + if reply == QMessageBox.Yes: + self.text_edit.setPlainText(current_text + "\n\n" + content) + elif reply == QMessageBox.No: + self.text_edit.setPlainText(content) + else: + self.text_edit.setPlainText(content) + + self.log_message(f"Đã tải văn bản từ {os.path.basename(file_path)}", "success") + + except Exception as e: + self.log_message(f"Lỗi tải file: {str(e)}", "error") + + def save_text_file(self): + """Lưu văn bản vào file""" + text = self.text_edit.toPlainText().strip() + if not text: + QMessageBox.warning(self, "Cảnh báo", "Không có văn bản để lưu") + return + + file_path, _ = QFileDialog.getSaveFileName( + self, "Lưu văn bản", "text.txt", "Text files (*.txt)" + ) + + if file_path: + try: + with open(file_path, 'w', encoding='utf-8') as f: + f.write(text) + self.log_message("Đã lưu văn bản", "success") + except Exception as e: + self.log_message(f"Lỗi lưu file: {str(e)}", "error") + + def clear_text(self): + """Xóa văn bản""" + self.text_edit.clear() + self.log_message("Đã xóa văn bản", "info") + + def select_output_dir(self): + """Chọn thư mục output""" + dir_path = QFileDialog.getExistingDirectory(self, "Chọn thư mục", self.output_dir) + if dir_path: + self.output_dir = dir_path + self.output_dir_label.setText(dir_path) + self.save_settings() + self.refresh_file_list() + self.log_message(f"Thư mục output: {dir_path}", "info") + + def open_output_dir(self): + """Mở thư mục output""" + if os.path.exists(self.output_dir): + os.startfile(self.output_dir) + else: + self.log_message("Thư mục không tồn tại", "error") + + def refresh_file_list(self): + """Làm mới danh sách file""" + self.file_list.clear() + + if os.path.exists(self.output_dir): + files = [] + for f in os.listdir(self.output_dir): + if f.endswith(('.mp3', '.wav', '.ogg', '.flac', '.srt', '.txt')): + files.append(f) - if not voice_id.strip(): - return { - status_text: "❌ Vui lòng nhập Voice ID!", - logs_text: log_message("❌ Lỗi: Chưa nhập Voice ID"), - output_files: None, - output_message: "### ❌ Lỗi: Vui lòng nhập Voice ID" - } + files.sort(key=lambda x: os.path.getmtime(os.path.join(self.output_dir, x)), reverse=True) - if not text_input.strip(): - return { - status_text: "❌ Vui lòng nhập văn bản!", - logs_text: log_message("❌ Lỗi: Chưa nhập văn bản"), - output_files: None, - output_message: "### ❌ Lỗi: Vui lòng nhập văn bản" - } + for f in files[:50]: # Hiển thị tối đa 50 file + size = os.path.getsize(os.path.join(self.output_dir, f)) + item = QListWidgetItem(f"{f} ({size:,} bytes)") + item.setData(Qt.UserRole, f) + self.file_list.addItem(item) + + def open_selected_file(self, item): + """Mở file được chọn""" + filename = item.data(Qt.UserRole) + filepath = os.path.join(self.output_dir, filename) + + if os.path.exists(filepath): + os.startfile(filepath) + + def play_selected_file(self): + """Phát file được chọn""" + item = self.file_list.currentItem() + if item: + self.open_selected_file(item) + + def delete_selected_file(self): + """Xóa file được chọn""" + item = self.file_list.currentItem() + if item: + filename = item.data(Qt.UserRole) + filepath = os.path.join(self.output_dir, filename) - logs_text_value = log_message("🚀 Bắt đầu xử lý TTS...") - status_text_value = "🔄 Đang khởi tạo..." - progress(0, desc="Đang khởi tạo...") + reply = QMessageBox.question( + self, "Xác nhận", + f"Xóa file {filename}?", + QMessageBox.Yes | QMessageBox.No + ) - # Xử lý không đồng bộ - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) + if reply == QMessageBox.Yes: + try: + os.remove(filepath) + self.refresh_file_list() + self.log_message(f"Đã xóa file {filename}", "info") + except Exception as e: + self.log_message(f"Lỗi xóa file: {str(e)}", "error") + + def clear_logs(self): + """Xóa logs""" + self.log_text.clear() + self.log_message("Đã xóa logs", "info") + + def check_api_keys(self): + """Kiểm tra API keys""" + keys = [k.strip() for k in self.api_text.toPlainText().splitlines() if k.strip()] + if not keys: + QMessageBox.warning(self, "Cảnh báo", "Không có API keys để kiểm tra") + return + + self.log_message(f"Kiểm tra {len(keys)} API keys...", "info") + + valid_count = 0 + total_remaining = 0 + + for i, key in enumerate(keys, 1): + info = check_api_key_advanced(key, self.proxy_manager, self.use_proxy) - result = loop.run_until_complete( - process_tts_async( - api_keys_text, voice_id, text_input, model_id, format_type, - stability, similarity, style, speed, speaker_boost, progress - ) - ) + if info.get("valid"): + valid_count += 1 + remaining = info.get("remaining", 0) + total_remaining += remaining + + status = f"Key {i}: ✅ Hợp lệ | Còn lại: {remaining:,} ký tự" + if remaining < 1000: + status += " ⚠️ (SẮP HẾT)" + self.log_message(status, "warning") + else: + self.log_message(status, "success") + else: + error = info.get("error", "Unknown error") + self.log_message(f"Key {i}: ❌ Không hợp lệ - {error}", "error") - loop.close() + # Độ trễ giữa các lần check + if i < len(keys): + time.sleep(random.uniform(2, 4)) + + # Tổng kết + self.log_message("="*50, "info") + if valid_count > 0: + avg_remaining = total_remaining // valid_count + self.log_message(f"📊 TỔNG KẾT: {valid_count}/{len(keys)} key hợp lệ", "success") + self.log_message(f"📈 Tổng ký tự còn lại: {total_remaining:,}", "info") + self.log_message(f"📊 Trung bình mỗi key: {avg_remaining:,} ký tự", "info") - logs_text_value = log_message(result["message"]) + if avg_remaining < 1000: + self.log_message("⚠️ CẢNH BÁO: Ký tự còn lại thấp, cần thêm API keys!", "warning") + else: + self.log_message("❌ KHÔNG CÓ API KEY NÀO HỢP LỆ!", "error") + self.log_message("👉 Kiểm tra lại API keys hoặc thử tắt proxy", "info") + + def export_files(self): + """Export files""" + if not os.path.exists(self.output_dir): + QMessageBox.warning(self, "Cảnh báo", "Thư mục output không tồn tại") + return + + export_dir = QFileDialog.getExistingDirectory(self, "Chọn thư mục export") + if export_dir: + try: + exported = 0 + for f in os.listdir(self.output_dir): + if f.endswith(('.mp3', '.wav', '.ogg', '.flac', '.srt')): + src = os.path.join(self.output_dir, f) + dst = os.path.join(export_dir, f) + shutil.copy2(src, dst) + exported += 1 + + self.log_message(f"Đã export {exported} file đến {export_dir}", "success") + except Exception as e: + self.log_message(f"Lỗi export: {str(e)}", "error") + + def clear_all(self): + """Xóa tất cả""" + reply = QMessageBox.question( + self, "Xác nhận", + "Xóa tất cả API keys và văn bản?", + QMessageBox.Yes | QMessageBox.No + ) + + if reply == QMessageBox.Yes: + self.api_text.clear() + self.text_edit.clear() + self.log_text.clear() + self.progress_bar.setValue(0) + self.log_message("Đã xóa tất cả dữ liệu", "info") + + def start_tts_generation(self): + """Bắt đầu tạo TTS""" + # Kiểm tra API keys + api_keys = [k.strip() for k in self.api_text.toPlainText().splitlines() if k.strip()] + if not api_keys: + QMessageBox.warning(self, "Lỗi", "Vui lòng nhập ít nhất một API key") + self.tabs.setCurrentIndex(0) + return + + # Kiểm tra Voice ID + voice_id = self.voice_input.text().strip() + if not voice_id: + QMessageBox.warning(self, "Lỗi", "Vui lòng nhập Voice ID") + self.tabs.setCurrentIndex(0) + return + + # Kiểm tra văn bản + text = self.text_edit.toPlainText().strip() + if not text: + QMessageBox.warning(self, "Lỗi", "Vui lòng nhập văn bản") + self.tabs.setCurrentIndex(1) + return + + # Kiểm tra proxy nếu đang bật + if self.use_proxy and not self.proxy_manager.alive_proxies: + reply = QMessageBox.question( + self, "Cảnh báo", + "Không có proxy sống. Bạn có muốn:\n" + "1. Tắt proxy và tiếp tục (IP thực sẽ lộ)\n" + "2. Kiểm tra proxy trước\n" + "3. Hủy", + QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel + ) - if result["files"]: - status_text_value = "✅ Hoàn thành!" - 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](#)" - return { - status_text: status_text_value, - logs_text: logs_text_value, - output_files: result["files"], - output_message: message - } + if reply == QMessageBox.Yes: + self.use_proxy = False + self.update_proxy_status() + elif reply == QMessageBox.No: + self.manage_proxies() + return else: - status_text_value = "❌ Thất bại" - message = f"### ❌ Thất bại\n\n{result['message']}" - return { - status_text: status_text_value, - logs_text: logs_text_value, - output_files: None, - output_message: message - } - - except Exception as e: - error_msg = f"❌ Lỗi hệ thống: {str(e)}" - return { - status_text: "❌ Lỗi hệ thống", - logs_text: log_message(error_msg), - output_files: None, - output_message: f"### ❌ Lỗi hệ thống\n\n{str(e)}" - } + return + + # Parse văn bản + texts = parse_text_blocks(text) + + # Chuẩn bị settings + settings = { + "stability": self.stability_slider.value() / 100, + "similarity": self.similarity_slider.value() / 100, + "style": 0.0, + "speed": self.speed_slider.value() / 100, + "speaker_boost": True + } + + # Hiển thị thông tin + self.log_message("="*60, "info") + self.log_message("🚀 BẮT ĐẦU TẠO GIỌNG NÓI", "info") + self.log_message(f"🔑 API keys: {len(api_keys)}", "info") + self.log_message(f"🎤 Voice ID: {voice_id}", "info") + self.log_message(f"🤖 Model: {self.model_combo.currentText()}", "info") + self.log_message(f"📁 Format: {self.format_combo.currentText()}", "info") + self.log_message(f"📝 Số đoạn văn bản: {len(texts)}", "info") + self.log_message(f"🌐 Proxy: {'BẬT' if self.use_proxy else 'TẮT'}", "info") + if self.use_proxy: + stats = self.proxy_manager.get_stats() + self.log_message(f"🌐 Proxy sống: {stats['alive']}/{stats['total']}", "info") + self.log_message(f"📂 Thư mục output: {self.output_dir}", "info") + self.log_message("="*60, "info") + + # Tạo thread + self.voice_thread = VoiceGenerationThread( + api_keys=api_keys, + voice_id=voice_id, + texts=texts, + settings=settings, + fmt=self.format_combo.currentText(), + model_id=self.model_combo.currentText(), + output_dir=self.output_dir, + proxy_manager=self.proxy_manager if self.use_proxy else None, + use_proxy=self.use_proxy + ) + + # Kết nối signals + self.voice_thread.progress_signal.connect(self.log_message) + self.voice_thread.progress_value.connect(self.progress_bar.setValue) + self.voice_thread.progress_max.connect(self.progress_bar.setMaximum) + self.voice_thread.finished_signal.connect(self.on_tts_finished) + self.voice_thread.api_key_checked.connect(self.on_api_key_checked) + + # Vô hiệu hóa nút + self.start_btn.setEnabled(False) + self.start_btn.setText("⏳ Đang xử lý...") + + # Bắt đầu thread + self.voice_thread.start() + + def on_api_key_checked(self, index, valid, info): + """Khi API key được kiểm tra""" + if valid: + remaining = info.get("remaining", 0) + self.log_message(f"Key {index}: {remaining:,} ký tự còn lại", "success") + else: + error = info.get("error", "Invalid") + self.log_message(f"Key {index}: {error}", "error") + + def on_tts_finished(self, success, message): + """Khi TTS hoàn thành""" + self.start_btn.setEnabled(True) + self.start_btn.setText("🚀 Bắt đầu tạo giọng nói") + + if success: + self.log_message(message, "success") + self.refresh_file_list() + + # Hỏi có mở thư mục không + reply = QMessageBox.question( + self, "Thành công", + "Đã tạo âm thanh thành công!\n\nMở thư mục output?", + QMessageBox.Yes | QMessageBox.No + ) + + if reply == QMessageBox.Yes: + self.open_output_dir() + else: + self.log_message(message, "error") + + # Đề xuất giải pháp + if self.use_proxy: + self.log_message("💡 Gợi ý: Thử tắt proxy và chạy lại", "info") - # ==================== EVENT HANDLERS ==================== - - # Text stats update - text_input.change( - fn=update_text_stats, - inputs=[text_input], - outputs=[text_stats] - ) - - # API keys check - check_api_btn.click( - fn=check_all_api_keys, - inputs=[api_keys_text], - outputs=[api_check_result] - ) - - # Clear buttons - clear_api_btn.click( - fn=clear_api_keys, - inputs=[], - outputs=[api_keys_text, api_check_result] - ) - - clear_text_btn.click( - fn=clear_text, - inputs=[], - outputs=[text_input, text_stats] - ) - - # File upload handlers - api_file.upload( - fn=process_api_file, - inputs=[api_file], - outputs=[api_keys_text, api_check_result] - ).then( - fn=check_all_api_keys, - inputs=[api_keys_text], - outputs=[api_check_result] - ) - - voice_file.upload( - fn=process_voice_file, - inputs=[voice_file], - outputs=[voice_id, status_text] - ) - - text_file.upload( - fn=process_text_file, - inputs=[text_file], - outputs=[text_input, status_text] - ).then( - fn=update_text_stats, - inputs=[text_input], - outputs=[text_stats] - ) - - # Main TTS process - process_btn.click( - fn=process_tts, - inputs=[ - api_keys_text, voice_id, text_input, model_id, format_type, - stability, similarity, style, speed, speaker_boost - ], - outputs=[status_text, logs_text, output_files, output_message] - ) + def closeEvent(self, event): + """Xử lý khi đóng ứng dụng""" + self.save_settings() + + # Dừng thread nếu đang chạy + if hasattr(self, 'voice_thread') and self.voice_thread.isRunning(): + self.voice_thread.stop() + self.voice_thread.wait(2000) + + event.accept() -# ==================== LAUNCH APP ==================== +# ==================== MAIN ==================== if __name__ == "__main__": - demo.queue() # Enable queuing for async operations - demo.launch( - server_name="0.0.0.0", - server_port=7860, - share=False, - debug=False - ) \ No newline at end of file + app = QApplication(sys.argv) + app.setStyle("Fusion") + + # Palette + palette = QPalette() + palette.setColor(QPalette.Window, QColor(240, 242, 245)) + palette.setColor(QPalette.WindowText, QColor(44, 62, 80)) + palette.setColor(QPalette.Base, QColor(255, 255, 255)) + palette.setColor(QPalette.AlternateBase, QColor(236, 240, 241)) + palette.setColor(QPalette.ToolTipBase, QColor(44, 62, 80)) + palette.setColor(QPalette.ToolTipText, QColor(236, 240, 241)) + palette.setColor(QPalette.Text, QColor(44, 62, 80)) + palette.setColor(QPalette.Button, QColor(236, 240, 241)) + palette.setColor(QPalette.ButtonText, QColor(44, 62, 80)) + palette.setColor(QPalette.BrightText, Qt.red) + palette.setColor(QPalette.Link, QColor(52, 152, 219)) + palette.setColor(QPalette.Highlight, QColor(52, 152, 219)) + palette.setColor(QPalette.HighlightedText, Qt.white) + + app.setPalette(palette) + + window = SecureTTSApp() + window.show() + + sys.exit(app.exec()) \ No newline at end of file