Files

217 lines
7.8 KiB
Python

"""Fail-safe translation nodes.
Simple keeps the compact text + src_lang UI and always translates to English.
Its src_lang combo is dynamically limited by the Local CPU Languages setting.
Advanced keeps the legacy full language list for compatibility.
"""
import logging
import time
from typing import Any, List
from . import settings as _settings
from .language_models import LOCAL_CPU_LANGUAGE_ORDER, normalize_local_languages
from .providers import ProviderError, RateLimitedError, _make_provider, cache, google_breaker
logger = logging.getLogger("ComfyUI.FailsafeTranslate")
LEGACY_LANGUAGES = [
"auto", "en", "ja", "zh-CN", "zh-TW", "ko", "fr", "de", "es", "it", "ru",
"pt", "nl", "pl", "tr", "ar", "hi", "bn", "pa", "jw", "ms", "vi", "th", "id",
"[No Translation]",
]
ADVANCED_FAIL_MODES = ["return_input", "return_cached", "return_error", "raise"]
_VALID_FINAL_BEHAVIORS = {"return_input", "return_error", "return_cached", "raise"}
def _simple_source_options():
cfg = _settings.all()
selected = set(normalize_local_languages(cfg.get("local_cpu_languages", [])))
options = ["auto"]
options.extend(code for code in LOCAL_CPU_LANGUAGE_ORDER if code in selected)
options.append("[No Translation]")
return options
def _coerce_retries(value: Any, default: int = 0) -> int:
try:
return max(0, int(float(value)))
except (TypeError, ValueError):
return default
def _coerce_wait(value: Any, default: float = 1.0) -> float:
try:
return max(0.0, float(value))
except (TypeError, ValueError):
return default
def _coerce_behavior(value: Any, fallback: str) -> str:
v = str(value).strip() if value is not None else ""
return v if v in _VALID_FINAL_BEHAVIORS else fallback
def _provider_message(name: str, message: Any) -> str:
msg = str(message or "provider error")
prefix = f"{name}:"
if msg.lower().startswith(prefix.lower()):
return msg[len(prefix):].lstrip()
return msg
def _attempt_provider(provider, text: str, retries: int, wait_sec: float) -> str:
for attempt in range(retries + 1):
try:
return provider.translate(text)
except RateLimitedError:
raise
except ProviderError as exc:
if exc.transient and attempt < retries:
time.sleep(wait_sec)
continue
raise exc
except Exception: # noqa: BLE001
exc = ProviderError("unexpected provider error", transient=True)
if attempt < retries:
time.sleep(wait_sec)
continue
raise exc
def _handle_final_failure(text, src_lang, dest_lang, behavior, errors):
if behavior == "return_input":
return (text,)
if behavior == "return_cached":
cached = cache.find_by_request(text, src_lang, dest_lang)
return (cached if cached is not None else text,)
if behavior == "return_error":
detail = "; ".join(f"{name}: {msg}" for name, msg, _ in errors) or "all providers unavailable"
return (f"[ERROR] Translation failed: {detail}",)
if errors:
name, msg, transient = errors[-1]
raise ProviderError(f"{name}: {msg}", transient=transient)
raise ProviderError("all providers unavailable", transient=False)
def _run_translation(text, src_lang, dest_lang, fail_mode, retries, retry_wait_sec):
if text is None:
text = ""
if src_lang == "[No Translation]" or not text.strip():
return (text,)
cfg = _settings.all()
fallback_enabled = bool(cfg["fallback_enabled"])
cooldown_minutes = cfg["google_cooldown_minutes"]
behavior = _coerce_behavior(fail_mode, cfg["final_failure_behavior"])
if retries is None:
retries = cfg["google_retries"]
if retry_wait_sec is None:
retry_wait_sec = cfg["retry_wait_seconds"]
retries = _coerce_retries(retries, cfg["google_retries"])
wait_sec = _coerce_wait(retry_wait_sec, cfg["retry_wait_seconds"])
cache.update_limits(cfg["cache_enabled"], cfg["cache_max_entries"])
providers: List[str] = ["Google"]
if fallback_enabled:
providers.append("LocalCPU")
errors: List[tuple] = []
for index, name in enumerate(providers):
if index > 0:
logger.info("[FailSafeTranslate] Using fallback: %s", name)
provider = _make_provider(name, src_lang, dest_lang, cfg)
if provider is None:
msg = "provider not available for this language pair/settings"
logger.warning("[FailSafeTranslate] %s: %s", name, msg)
errors.append((name, msg, False))
continue
if provider.is_google and google_breaker.in_cooldown():
msg = "in cooldown, skipped"
logger.warning(
"[FailSafeTranslate] Google: in cooldown (%.0f s remaining), skipping",
google_breaker.remaining_seconds(),
)
errors.append((name, msg, False))
continue
cache_source = getattr(provider, "cache_source", src_lang)
key = (text, cache_source, dest_lang, name)
cached = cache.get(key)
if cached is not None:
return (cached,)
try:
provider_retries = retries if provider.is_google else 0
result = _attempt_provider(provider, text, provider_retries, wait_sec)
cache.set(key, result)
logger.info("[FailSafeTranslate] %s: success", name)
return (result,)
except RateLimitedError as exc:
msg = _provider_message(name, exc.message)
if provider.is_google:
google_breaker.enter_cooldown(cooldown_minutes)
logger.warning(
"[FailSafeTranslate] Google: rate limited, cooldown %d min",
cooldown_minutes,
)
errors.append((name, msg, False))
except ProviderError as exc:
msg = _provider_message(name, exc.message)
logger.warning("[FailSafeTranslate] %s: %s", name, msg)
errors.append((name, msg, exc.transient))
except Exception: # noqa: BLE001
logger.warning("[FailSafeTranslate] %s: unexpected error", name, exc_info=True)
errors.append((name, "unexpected error", False))
return _handle_final_failure(text, src_lang, dest_lang, behavior, errors)
class FailSafeTranslateSimple:
@classmethod
def INPUT_TYPES(cls):
options = _simple_source_options()
default = "auto"
return {
"required": {
"text": ("STRING", {"multiline": True}),
"src_lang": (options, {"default": default}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "translate"
CATEGORY = "utils/text"
def translate(self, text: str, src_lang: str) -> tuple:
return _run_translation(text, src_lang, "en", None, None, None)
class FailSafeTranslateAdvanced:
@classmethod
def INPUT_TYPES(cls):
# Advanced intentionally retains the legacy full list. Local CPU
# fallback still only runs for enabled source languages -> English.
return {
"required": {
"text": ("STRING", {"multiline": True}),
"src_lang": ([l for l in LEGACY_LANGUAGES if l != "auto"], {"default": "en"}),
"dest_lang": (LEGACY_LANGUAGES,),
"fail_mode": (ADVANCED_FAIL_MODES,),
"retries": ("INT", {"default": 3, "min": 0, "max": 10}),
"retry_wait_sec": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 30.0}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "translate"
CATEGORY = "utils/text"
def translate(self, text, src_lang, dest_lang, fail_mode, retries, retry_wait_sec):
return _run_translation(text, src_lang, dest_lang, fail_mode, retries, retry_wait_sec)