From 6599dfcc236604bb52a30b46d09c79d9cd7b269c Mon Sep 17 00:00:00 2001 From: nickpons666 Date: Fri, 6 Mar 2026 22:11:29 -0600 Subject: [PATCH] =?UTF-8?q?fix:=20optimizar=20cach=C3=A9=20de=20traducci?= =?UTF-8?q?=C3=B3n=20con=20hashes=20SHA256=20y=20normalizaci=C3=B3n=20de?= =?UTF-8?q?=20texto=20para=20estabilidad=20en=20producci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- botdiscord/config.py | 2 +- botdiscord/database.py | 142 +++++++++++++++++++++++++++++----------- botdiscord/translate.py | 35 +++++----- panel/main.py | 27 ++++++-- 4 files changed, 148 insertions(+), 58 deletions(-) diff --git a/botdiscord/config.py b/botdiscord/config.py index 1e6caca..3538ba6 100644 --- a/botdiscord/config.py +++ b/botdiscord/config.py @@ -34,7 +34,7 @@ def load_config(config_path: str = None) -> dict: "port": 3306, "user": "", "password": "", - "name": "mi_red" + "name": "" }, "languages": { "enabled": [ diff --git a/botdiscord/database.py b/botdiscord/database.py index a9f95ab..ad1307d 100644 --- a/botdiscord/database.py +++ b/botdiscord/database.py @@ -11,19 +11,42 @@ def get_connection(): db_type = get_db_type() if db_type == "mysql": - if _connection is None or not _connection.is_connected(): - db_config = get_db_config() + db_config = get_db_config() + + # Si no hay conexión o no está activa, intentamos conectar/reconectar + if _connection is None: try: _connection = mysql.connector.connect( - host=db_config.get("host", "localhost"), - port=db_config.get("port", 3306), - user=db_config.get("user", "root"), - password=db_config.get("password", ""), - database=db_config.get("name", "mi_red") + host=db_config.get("host"), + port=db_config.get("port"), + user=db_config.get("user"), + password=db_config.get("password"), + database=db_config.get("name"), + consume_results=True, + connection_timeout=5 ) except MySQLError as e: - print(f"Error connecting to MySQL: {e}") + print(f"CRITICAL: Could not establish initial MySQL connection: {e}") raise + + # Verificar salud de la conexión existente + try: + _connection.ping(reconnect=True, attempts=3, delay=1) + except MySQLError: + try: + _connection = mysql.connector.connect( + host=db_config.get("host"), + port=db_config.get("port"), + user=db_config.get("user"), + password=db_config.get("password"), + database=db_config.get("name"), + consume_results=True, + connection_timeout=5 + ) + except MySQLError as e: + print(f"CRITICAL: MySQL reconnection failed: {e}") + raise + return _connection else: import os @@ -107,16 +130,34 @@ def init_db(): created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (message_id) REFERENCES messages(message_id) ON DELETE CASCADE, UNIQUE KEY idx_msg_lang (message_id, target_lang)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') - # Tabla para traducciones de la interfaz web cursor.execute('''CREATE TABLE IF NOT EXISTS ui_translations (id INT AUTO_INCREMENT PRIMARY KEY, + text_hash VARCHAR(64) NOT NULL, original_text TEXT NOT NULL, target_lang VARCHAR(10) NOT NULL, translated_text TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY idx_ui_lang (original_text(255), target_lang)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') - + UNIQUE KEY idx_ui_hash_lang (text_hash, target_lang)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') + + # Migración automática: Añadir text_hash si no existe + try: + cursor.execute("SHOW COLUMNS FROM ui_translations LIKE 'text_hash'") + if not cursor.fetchone(): + print("Migrating ui_translations: Adding text_hash column...") + # Primero borramos el índice viejo si existe (basado en el código anterior) + try: + cursor.execute("ALTER TABLE ui_translations DROP INDEX idx_ui_lang") + except: pass + # Añadimos la columna y el nuevo índice + cursor.execute("ALTER TABLE ui_translations ADD COLUMN text_hash VARCHAR(64) NOT NULL AFTER id") + cursor.execute("ALTER TABLE ui_translations ADD UNIQUE KEY idx_ui_hash_lang (text_hash, target_lang)") + # Limpiamos la tabla para evitar conflictos de hash con datos viejos + cursor.execute("DELETE FROM ui_translations") + except Exception as e: + print(f"Migration warning: {e}") + + conn.commit() # Tabla para administradores del panel web cursor.execute('''CREATE TABLE IF NOT EXISTS admins (id INT AUTO_INCREMENT PRIMARY KEY, @@ -412,37 +453,64 @@ def get_cached_translation(message_id: int, target_lang: str) -> str: conn.close() return row[0] if row else None +import hashlib + +def _normalize_text(text: str) -> str: + """Normaliza el texto para que el caché sea consistente.""" + if not text: return "" + # Quitar espacios en extremos, normalizar saltos de línea y quitar espacios dobles + text = text.strip().replace('\r\n', '\n') + return " ".join(text.split()) + +def _get_text_hash(text: str) -> str: + """Genera un hash único para el texto normalizado.""" + norm = _normalize_text(text) + return hashlib.sha256(norm.encode('utf-8')).hexdigest() + def get_ui_translation(text: str, target_lang: str) -> str: db_type = get_db_type() - if db_type == "mysql": - conn = get_connection() - cursor = conn.cursor() - cursor.execute("SELECT translated_text FROM ui_translations WHERE original_text = %s AND target_lang = %s", (text, target_lang)) - row = cursor.fetchone() - cursor.close() - return row[0] if row else None - else: - conn = get_connection() - c = conn.cursor() - c.execute("SELECT translated_text FROM ui_translations WHERE original_text = ? AND target_lang = ?", (text, target_lang)) - row = c.fetchone() - conn.close() - return row[0] if row else None + text_hash = _get_text_hash(text) + try: + if db_type == "mysql": + conn = get_connection() + cursor = conn.cursor() + cursor.execute("SELECT translated_text FROM ui_translations WHERE text_hash = %s AND target_lang = %s", (text_hash, target_lang)) + row = cursor.fetchone() + cursor.close() + return row[0] if row else None + else: + conn = get_connection() + c = conn.cursor() + c.execute("SELECT translated_text FROM ui_translations WHERE text_hash = ? AND target_lang = ?", (text_hash, target_lang)) + row = c.fetchone() + conn.close() + return row[0] if row else None + except Exception as e: + print(f"Database error in get_ui_translation: {e}") + return None def save_ui_translation(text: str, target_lang: str, translated_text: str): db_type = get_db_type() - if db_type == "mysql": - conn = get_connection() - cursor = conn.cursor() - cursor.execute("INSERT INTO ui_translations (original_text, target_lang, translated_text) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE translated_text = %s", (text, target_lang, translated_text, translated_text)) - conn.commit() - cursor.close() - else: - conn = get_connection() - c = conn.cursor() - c.execute("INSERT OR REPLACE INTO ui_translations (original_text, target_lang, translated_text) VALUES (?, ?, ?)", (text, target_lang, translated_text)) - conn.commit() - conn.close() + text_hash = _get_text_hash(text) + try: + if db_type == "mysql": + conn = get_connection() + cursor = conn.cursor() + query = """INSERT INTO ui_translations (text_hash, original_text, target_lang, translated_text) + VALUES (%s, %s, %s, %s) + ON DUPLICATE KEY UPDATE translated_text = %s""" + cursor.execute(query, (text_hash, text, target_lang, translated_text, translated_text)) + conn.commit() + cursor.close() + else: + conn = get_connection() + c = conn.cursor() + c.execute("INSERT OR REPLACE INTO ui_translations (text_hash, original_text, target_lang, translated_text) VALUES (?, ?, ?, ?)", + (text_hash, text, target_lang, translated_text)) + conn.commit() + conn.close() + except Exception as e: + print(f"Database error in save_ui_translation: {e}") # Funciones para administradores def get_admins(): diff --git a/botdiscord/translate.py b/botdiscord/translate.py index ba20d45..b8350f3 100644 --- a/botdiscord/translate.py +++ b/botdiscord/translate.py @@ -94,24 +94,29 @@ async def translate_text(text: str, target_lang: str) -> str: return "".join(translated_segments) def translate_text_sync(text: str, target_lang: str) -> str: - """Versión síncrona de translate_text para usar en filtros de Jinja2.""" + """Versión síncrona de translate_text utilizando un hilo separado.""" if not text or not target_lang or target_lang == "es": return text - try: - import nest_asyncio - nest_asyncio.apply() - except ImportError: - # Si no está instalado, no podemos usar el bridge en FastAPI - return text - - try: - loop = asyncio.get_event_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - return loop.run_until_complete(translate_text(text, target_lang)) + import threading + + result = [] + def target(): + # Creamos un nuevo bucle de eventos exclusivo para este hilo + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + try: + res = new_loop.run_until_complete(translate_text(text, target_lang)) + result.append(res) + finally: + new_loop.close() + + # Ejecutamos la traducción en un hilo aparte para no interferir con el loop de FastAPI + thread = threading.Thread(target=target) + thread.start() + thread.join(timeout=15) # Esperamos máximo 15 segundos + + return result[0] if result else text def get_lang_mapping(bot_type: str = None) -> dict: load_lang_mappings(bot_type) diff --git a/panel/main.py b/panel/main.py index c3c5b11..c65aea7 100644 --- a/panel/main.py +++ b/panel/main.py @@ -11,10 +11,14 @@ from pydantic import BaseModel from passlib.hash import pbkdf2_sha256 as hasher from botdiscord.config import load_config, get_web_config, get_libretranslate_url, get_db_type +load_config() # Cargamos configuración inmediatamente + from botdiscord.database import ( - get_ui_translation, save_ui_translation, - get_admins, get_admin_by_username, add_admin, delete_admin + init_db, get_ui_translation, save_ui_translation, + get_admins, get_admin_by_username, add_admin, delete_admin, + _normalize_text ) +init_db() # Aseguramos que las tablas existan antes de que FastAPI atienda peticiones from botdiscord.translate import translate_text app = FastAPI(title="Panel de Configuración - Bots de Traducción") @@ -23,23 +27,36 @@ SCRIPT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) templates = Jinja2Templates(directory=os.path.join(SCRIPT_DIR, "panel", "templates")) +# Caché de memoria para traducciones UI (evita 50 queries SQL por página) +_ui_memory_cache = {} + # Filtro de traducción para Jinja2 def translate_filter(text, lang="es"): if lang == "es" or not text: return text - # Buscamos en caché de la base de datos (acceso rápido) - from botdiscord.database import get_ui_translation, save_ui_translation + # NORMALIZACIÓN inmediata + text = _normalize_text(text) + if not text: return "" + + # 1. Buscamos en caché de MEMORIA (RAM) - Ultra rápido + cache_key = f"{lang}:{text}" + if cache_key in _ui_memory_cache: + return _ui_memory_cache[cache_key] + + # 2. Buscamos en caché de la base de datos (acceso rápido) cached = get_ui_translation(text, lang) if cached: + _ui_memory_cache[cache_key] = cached return cached - # Si no está en caché, traducimos de forma síncrona para la UI + # 3. Si no está en ningún caché, traducimos de forma síncrona from botdiscord.translate import translate_text_sync translated = translate_text_sync(text, lang) if translated and translated != text: save_ui_translation(text, lang, translated) + _ui_memory_cache[cache_key] = translated return translated