fix: optimizar caché de traducción con hashes SHA256 y normalización de texto para estabilidad en producción
This commit is contained in:
@@ -34,7 +34,7 @@ def load_config(config_path: str = None) -> dict:
|
|||||||
"port": 3306,
|
"port": 3306,
|
||||||
"user": "",
|
"user": "",
|
||||||
"password": "",
|
"password": "",
|
||||||
"name": "mi_red"
|
"name": ""
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"enabled": [
|
"enabled": [
|
||||||
|
|||||||
@@ -11,19 +11,42 @@ def get_connection():
|
|||||||
db_type = get_db_type()
|
db_type = get_db_type()
|
||||||
|
|
||||||
if db_type == "mysql":
|
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:
|
try:
|
||||||
_connection = mysql.connector.connect(
|
_connection = mysql.connector.connect(
|
||||||
host=db_config.get("host", "localhost"),
|
host=db_config.get("host"),
|
||||||
port=db_config.get("port", 3306),
|
port=db_config.get("port"),
|
||||||
user=db_config.get("user", "root"),
|
user=db_config.get("user"),
|
||||||
password=db_config.get("password", ""),
|
password=db_config.get("password"),
|
||||||
database=db_config.get("name", "mi_red")
|
database=db_config.get("name"),
|
||||||
|
consume_results=True,
|
||||||
|
connection_timeout=5
|
||||||
)
|
)
|
||||||
except MySQLError as e:
|
except MySQLError as e:
|
||||||
print(f"Error connecting to MySQL: {e}")
|
print(f"CRITICAL: Could not establish initial MySQL connection: {e}")
|
||||||
raise
|
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
|
return _connection
|
||||||
else:
|
else:
|
||||||
import os
|
import os
|
||||||
@@ -107,16 +130,34 @@ def init_db():
|
|||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (message_id) REFERENCES messages(message_id) ON DELETE CASCADE,
|
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''')
|
UNIQUE KEY idx_msg_lang (message_id, target_lang)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''')
|
||||||
|
|
||||||
# Tabla para traducciones de la interfaz web
|
# Tabla para traducciones de la interfaz web
|
||||||
cursor.execute('''CREATE TABLE IF NOT EXISTS ui_translations
|
cursor.execute('''CREATE TABLE IF NOT EXISTS ui_translations
|
||||||
(id INT AUTO_INCREMENT PRIMARY KEY,
|
(id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
text_hash VARCHAR(64) NOT NULL,
|
||||||
original_text TEXT NOT NULL,
|
original_text TEXT NOT NULL,
|
||||||
target_lang VARCHAR(10) NOT NULL,
|
target_lang VARCHAR(10) NOT NULL,
|
||||||
translated_text TEXT NOT NULL,
|
translated_text TEXT NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
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
|
# Tabla para administradores del panel web
|
||||||
cursor.execute('''CREATE TABLE IF NOT EXISTS admins
|
cursor.execute('''CREATE TABLE IF NOT EXISTS admins
|
||||||
(id INT AUTO_INCREMENT PRIMARY KEY,
|
(id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -412,37 +453,64 @@ def get_cached_translation(message_id: int, target_lang: str) -> str:
|
|||||||
conn.close()
|
conn.close()
|
||||||
return row[0] if row else None
|
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:
|
def get_ui_translation(text: str, target_lang: str) -> str:
|
||||||
db_type = get_db_type()
|
db_type = get_db_type()
|
||||||
if db_type == "mysql":
|
text_hash = _get_text_hash(text)
|
||||||
conn = get_connection()
|
try:
|
||||||
cursor = conn.cursor()
|
if db_type == "mysql":
|
||||||
cursor.execute("SELECT translated_text FROM ui_translations WHERE original_text = %s AND target_lang = %s", (text, target_lang))
|
conn = get_connection()
|
||||||
row = cursor.fetchone()
|
cursor = conn.cursor()
|
||||||
cursor.close()
|
cursor.execute("SELECT translated_text FROM ui_translations WHERE text_hash = %s AND target_lang = %s", (text_hash, target_lang))
|
||||||
return row[0] if row else None
|
row = cursor.fetchone()
|
||||||
else:
|
cursor.close()
|
||||||
conn = get_connection()
|
return row[0] if row else None
|
||||||
c = conn.cursor()
|
else:
|
||||||
c.execute("SELECT translated_text FROM ui_translations WHERE original_text = ? AND target_lang = ?", (text, target_lang))
|
conn = get_connection()
|
||||||
row = c.fetchone()
|
c = conn.cursor()
|
||||||
conn.close()
|
c.execute("SELECT translated_text FROM ui_translations WHERE text_hash = ? AND target_lang = ?", (text_hash, target_lang))
|
||||||
return row[0] if row else None
|
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):
|
def save_ui_translation(text: str, target_lang: str, translated_text: str):
|
||||||
db_type = get_db_type()
|
db_type = get_db_type()
|
||||||
if db_type == "mysql":
|
text_hash = _get_text_hash(text)
|
||||||
conn = get_connection()
|
try:
|
||||||
cursor = conn.cursor()
|
if db_type == "mysql":
|
||||||
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 = get_connection()
|
||||||
conn.commit()
|
cursor = conn.cursor()
|
||||||
cursor.close()
|
query = """INSERT INTO ui_translations (text_hash, original_text, target_lang, translated_text)
|
||||||
else:
|
VALUES (%s, %s, %s, %s)
|
||||||
conn = get_connection()
|
ON DUPLICATE KEY UPDATE translated_text = %s"""
|
||||||
c = conn.cursor()
|
cursor.execute(query, (text_hash, text, target_lang, translated_text, translated_text))
|
||||||
c.execute("INSERT OR REPLACE INTO ui_translations (original_text, target_lang, translated_text) VALUES (?, ?, ?)", (text, target_lang, translated_text))
|
conn.commit()
|
||||||
conn.commit()
|
cursor.close()
|
||||||
conn.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
|
# Funciones para administradores
|
||||||
def get_admins():
|
def get_admins():
|
||||||
|
|||||||
@@ -94,24 +94,29 @@ async def translate_text(text: str, target_lang: str) -> str:
|
|||||||
return "".join(translated_segments)
|
return "".join(translated_segments)
|
||||||
|
|
||||||
def translate_text_sync(text: str, target_lang: str) -> str:
|
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":
|
if not text or not target_lang or target_lang == "es":
|
||||||
return text
|
return text
|
||||||
|
|
||||||
try:
|
import threading
|
||||||
import nest_asyncio
|
|
||||||
nest_asyncio.apply()
|
result = []
|
||||||
except ImportError:
|
def target():
|
||||||
# Si no está instalado, no podemos usar el bridge en FastAPI
|
# Creamos un nuevo bucle de eventos exclusivo para este hilo
|
||||||
return text
|
new_loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(new_loop)
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
res = new_loop.run_until_complete(translate_text(text, target_lang))
|
||||||
except RuntimeError:
|
result.append(res)
|
||||||
loop = asyncio.new_event_loop()
|
finally:
|
||||||
asyncio.set_event_loop(loop)
|
new_loop.close()
|
||||||
|
|
||||||
return loop.run_until_complete(translate_text(text, target_lang))
|
# 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:
|
def get_lang_mapping(bot_type: str = None) -> dict:
|
||||||
load_lang_mappings(bot_type)
|
load_lang_mappings(bot_type)
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
from passlib.hash import pbkdf2_sha256 as hasher
|
from passlib.hash import pbkdf2_sha256 as hasher
|
||||||
from botdiscord.config import load_config, get_web_config, get_libretranslate_url, get_db_type
|
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 (
|
from botdiscord.database import (
|
||||||
get_ui_translation, save_ui_translation,
|
init_db, get_ui_translation, save_ui_translation,
|
||||||
get_admins, get_admin_by_username, add_admin, delete_admin
|
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
|
from botdiscord.translate import translate_text
|
||||||
|
|
||||||
app = FastAPI(title="Panel de Configuración - Bots de Traducción")
|
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"))
|
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
|
# Filtro de traducción para Jinja2
|
||||||
def translate_filter(text, lang="es"):
|
def translate_filter(text, lang="es"):
|
||||||
if lang == "es" or not text:
|
if lang == "es" or not text:
|
||||||
return text
|
return text
|
||||||
|
|
||||||
# Buscamos en caché de la base de datos (acceso rápido)
|
# NORMALIZACIÓN inmediata
|
||||||
from botdiscord.database import get_ui_translation, save_ui_translation
|
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)
|
cached = get_ui_translation(text, lang)
|
||||||
if cached:
|
if cached:
|
||||||
|
_ui_memory_cache[cache_key] = cached
|
||||||
return 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
|
from botdiscord.translate import translate_text_sync
|
||||||
translated = translate_text_sync(text, lang)
|
translated = translate_text_sync(text, lang)
|
||||||
|
|
||||||
if translated and translated != text:
|
if translated and translated != text:
|
||||||
save_ui_translation(text, lang, translated)
|
save_ui_translation(text, lang, translated)
|
||||||
|
_ui_memory_cache[cache_key] = translated
|
||||||
|
|
||||||
return translated
|
return translated
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user