import aiohttp import re import asyncio from botdiscord.config import get_libretranslate_url, get_languages from botdiscord.database import get_available_languages, get_bot_languages from utils.logger import discord_logger as log from utils.cache import cache_get, cache_set def load_lang_mappings(bot_type: str = None): global LANG_MAPPING, REVERSE_MAPPING, FLAG_MAPPING, _cached_bot_type, NAME_TO_CODE if bot_type: _cached_bot_type = bot_type available = get_available_languages() if not available: from botdiscord.config import get_languages available = get_languages() log.debug(f"Idiomas desde config: {available}") all_codes = [lang["code"] for lang in available] log.debug(f"Códigos disponibles: {all_codes}") if _cached_bot_type: active_codes = get_bot_languages(_cached_bot_type) log.debug(f"Códigos activos para {_cached_bot_type}: {active_codes}") if not active_codes: active_codes = all_codes else: active_codes = all_codes if not active_codes: active_codes = all_codes name_to_code = {lang["name"]: lang["code"] for lang in available if lang["code"] in active_codes} code_to_name = {lang["code"]: lang["name"] for lang in available if lang["code"] in active_codes} flag_dict = {lang["code"]: lang.get("flag", "") for lang in available} LANG_MAPPING = code_to_name NAME_TO_CODE = name_to_code FLAG_MAPPING = flag_dict REVERSE_MAPPING = code_to_name _cached_bot_type = None LANG_MAPPING = {} REVERSE_MAPPING = {} FLAG_MAPPING = {} NAME_TO_CODE = {} _translation_semaphore = asyncio.Semaphore(5) async def _translate_segment(session, url, segment, target_code): if not re.search(r'[a-zA-Z0-9]', segment): return segment payload = {"q": segment, "source": "auto", "target": target_code, "format": "html"} async with _translation_semaphore: try: async with session.post(url, json=payload, timeout=15) as resp: if resp.status == 200: data = await resp.json() return data.get("translatedText", segment) return segment except Exception: return segment _libretranslate_healthy: bool = True _last_health_check: float = 0 _consecutive_failures: int = 0 _MAX_FAILURES = 2 # Tolerar hasta 2 fallos antes de marcar como caído async def check_libretranslate_health(url: str) -> bool: """Verifica si LibreTranslate está disponible. Cachea el resultado 30 segundos.""" global _libretranslate_healthy, _last_health_check, _consecutive_failures import time now = time.monotonic() if now - _last_health_check < 30: return _libretranslate_healthy _last_health_check = now try: from urllib.parse import urlparse, urlunparse parsed = urlparse(url) # Tomamos solo scheme + netloc y construimos /languages directamente check_url = urlunparse((parsed.scheme, parsed.netloc, "/languages", "", "", "")) async with aiohttp.ClientSession() as session: async with session.get(check_url, timeout=10) as resp: if resp.status == 200: _consecutive_failures = 0 _libretranslate_healthy = True else: _consecutive_failures += 1 except Exception as e: _consecutive_failures += 1 log.warning(f"⚠️ LibreTranslate health check falló (intento {_consecutive_failures}/{_MAX_FAILURES}): {e}") if _consecutive_failures >= _MAX_FAILURES: _libretranslate_healthy = False log.warning("⚠️ LibreTranslate no está disponible tras varios intentos.") elif _consecutive_failures > 0: # Un solo fallo no bloquea el servicio _libretranslate_healthy = True return _libretranslate_healthy async def translate_text(text: str, target_lang: str) -> str: url = get_libretranslate_url() if not url: return text # Health Check: si LibreTranslate está caído, retornamos el texto con aviso if not await check_libretranslate_health(url): return f"⚠️ *Servicio de traducción en mantenimiento.* | {text}" target_code = NAME_TO_CODE.get(target_lang, target_lang) # Revisamos caché Redis antes de hacer cualquier petición redis_key = f"trans:{target_code}:{hash(text)}" cached = cache_get(redis_key) if cached: return cached segments = re.split(r'([.?!]+\s*|\n+)', text) try: async with aiohttp.ClientSession() as session: tasks = [_translate_segment(session, url, seg, target_code) for seg in segments] translated_segments = await asyncio.gather(*tasks) result = "".join(translated_segments) # Guardar en Redis por 24 horas if result != text: cache_set(redis_key, result, ttl=86400) return result except Exception: return text def translate_text_sync(text: str, target_lang: str) -> str: """Versión síncrona de translate_text utilizando un hilo separado.""" if not text or not target_lang or target_lang == "es": return text 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) return LANG_MAPPING.copy() def get_reverse_mapping(bot_type: str = None) -> dict: load_lang_mappings(bot_type) return REVERSE_MAPPING.copy() def get_flag_mapping(bot_type: str = None) -> dict: load_lang_mappings(bot_type) return FLAG_MAPPING.copy() def get_name_to_code(bot_type: str = None) -> dict: load_lang_mappings(bot_type) return NAME_TO_CODE.copy() def get_lang_flag(lang_code: str) -> str: return FLAG_MAPPING.get(lang_code, "")