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,
|
||||
"user": "",
|
||||
"password": "",
|
||||
"name": "mi_red"
|
||||
"name": ""
|
||||
},
|
||||
"languages": {
|
||||
"enabled": [
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user