diff --git a/docker-compose.yml b/docker-compose.yml index 0dce079..ef38819 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -105,6 +105,7 @@ services: - STRIPE_PRO_PRICE_ID=${STRIPE_PRO_PRICE_ID:-} - STRIPE_BUSINESS_PRICE_ID=${STRIPE_BUSINESS_PRICE_ID:-} - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-} + - GOOGLE_CLOUD_API_KEY=${GOOGLE_CLOUD_API_KEY:-} volumes: - uploads_data:/app/uploads - outputs_data:/app/outputs diff --git a/routes/translate_routes.py b/routes/translate_routes.py index 1e91504..3f8907e 100644 --- a/routes/translate_routes.py +++ b/routes/translate_routes.py @@ -836,6 +836,47 @@ def _estimate_pages(file_path: Path, file_extension: str) -> int: return 1 +class _GoogleCloudWithFallback: + """Tries Google Cloud API first, falls back to deep_translator on error. + + This avoids a hard crash when the Cloud API key is invalid, quota is + exceeded, or the network is unreachable. The legacy GoogleTranslator + (deep_translator, free, no key) is used as a best-effort fallback. + """ + + def __init__(self, cloud_adapter, legacy_provider): + self.cloud = cloud_adapter + self.legacy = legacy_provider + self.provider_name = "google_cloud_with_fallback" + + def translate(self, text, target_language, source_language="auto"): + try: + return self.cloud.translate(text, target_language, source_language) + except Exception as e: + logger.warning( + "google_cloud_failed_fallback_to_legacy", + error=str(e)[:200], + ) + return self.legacy.translate(text, target_language, source_language) + + def translate_batch( + self, texts, target_language, source_language="auto", **kwargs + ): + try: + return self.cloud.translate_batch( + texts, target_language, source_language + ) + except Exception as e: + logger.warning( + "google_cloud_batch_failed_fallback_to_legacy", + error=str(e)[:200], + text_count=len(texts), + ) + return self.legacy.translate_batch( + texts, target_language, source_language + ) + + async def _run_translation_job( job_id: str, input_path: Path, @@ -953,6 +994,9 @@ async def _run_translation_job( # "google" (default classic mode): use Google Cloud API key if available, # otherwise fall back to deep_translator (legacy, no key). + # When the Cloud API key IS present, wrap it in a fallback so that + # quota / network errors don't kill the job — deep_translator is tried + # as a best-effort second chance. if _p == "google": gc_key = _cfg( getattr(_admin_cfg.google_cloud, "api_key", None), @@ -960,8 +1004,13 @@ async def _run_translation_job( ) if gc_key: from services.providers.google_cloud_provider import LegacyGoogleCloudAdapter - translation_provider = LegacyGoogleCloudAdapter(gc_key) - logger.info("google_provider_using_cloud_api", job_id=job_id) + from services.translation_service import GoogleTranslationProvider + cloud_adapter = LegacyGoogleCloudAdapter(gc_key) + legacy_fallback = GoogleTranslationProvider() + translation_provider = _GoogleCloudWithFallback( + cloud_adapter, legacy_fallback + ) + logger.info("google_provider_using_cloud_api_with_fallback", job_id=job_id) else: logger.info("google_provider_no_cloud_key_using_legacy", job_id=job_id) @@ -1142,7 +1191,7 @@ async def _run_translation_job( if attempted > 0: ratio = changed / attempted logger.info(f"Job {job_id}: translation stats — {changed}/{attempted} texts changed ({ratio:.0%})") - if ratio < 0.15 and changed == 0: + if changed == 0: error_msg = ( f"Translation failed: 0 out of {attempted} texts were translated. " f"The provider ({provider}) may be unavailable or misconfigured. " @@ -1151,6 +1200,12 @@ async def _run_translation_job( logger.error(f"Job {job_id}: {error_msg}") tracker.set_error(error_msg) return + elif ratio < 0.05: + # Very suspicious — likely partial failure, warn but don't block + logger.warning( + f"Job {job_id}: suspiciously low translation rate: " + f"{changed}/{attempted} ({ratio:.1%})" + ) if user_id: await tier_quota_service.increment_on_success(user_id)