feat(translation): quality pipeline overhaul + new features (audit 2026-08-29)
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m20s

Translation quality & format preservation:
- Word: merge adjacent same-format runs into one unit (sentence-level
  coherence like inline-tag handling); translate comments/balloons;
  dedupe textbox collection (was translated twice); RTL no longer
  overrides center/justify alignment; CJK/Arabic font hints (eastAsia/cs)
- PPTX: chart translations now actually reach the output file
  (ChartPart.blob is read-only — rewrite chart XML in the saved ZIP);
  CJK typeface hints (a:ea)
- Excel: sheet renames no longer break references — rewrite cell
  formulas (3D/quoted), defined names, data validations, cond. formats
- PDF: bold/italic honored (hebo/heit/hebi); table cells never merge;
  unchanged blocks left untouched (typography preserved, fixes duplicate
  hyperlinks); attempted/changed stats + route gate now cover PDF;
  CJK font paths; scanned PDFs via Mistral OCR (detection + admin settings)

Features:
- formality param (formal/informal) + automatic regional-variant prompts
- output_mode=bilingual docx (source above translation)
- per-user translation memory on Redis (falls back to LRU), context-hashed
- QA report + 0-100 confidence score in job status; L0 on by default
- OpenAI-compatible providers: whole chunk in ONE numbered-JSON request
  (~15x fewer calls) with per-item fallback; base prompt always present
  (custom prompt no longer replaces translation instructions)

Infra & marketing alignment:
- plan-based engine gating + vision gating (closes paid-engine leak);
  /providers/available filtered per plan; 107 languages exposed
- zh-CN/zh-TW validation fixed; libmagic disabled on Windows (native crash)
- admin: Mistral OCR settings + engine status dashboard; httpx<0.28 pin
  (TestClient breakage); Prometheus test fixture fixed
- marketing docs aligned with code (PDF+OCR, retention, engines, pricing)
- security: .env.ionos/.env.production/provider_settings.json removed

Tests: 1173 passed / 0 failed (6 network tests deselected: free Google
endpoint temporarily blocked from this machine)
This commit is contained in:
2026-08-29 18:38:09 +02:00
parent 992f13d53c
commit 526c87348f
87 changed files with 6996 additions and 1024 deletions

View File

@@ -285,6 +285,67 @@ async def get_admin_dashboard(admin_id: str = Depends(require_admin)):
"last_check": None,
}
# OCR status (scanned PDFs): configured via admin settings or env key.
try:
from services.mistral_ocr import MistralOCRClient
settings = load_settings()
mistral_key = (settings.mistral.api_key or "").strip() or os.getenv(
"MISTRAL_API_KEY", ""
).strip()
ocr_configured = bool(mistral_key) and (
settings.mistral.enabled
# enabled=False in a saved-but-untouched settings file must not
# hide an env-configured OCR (same rule as the translate route)
or not (settings.mistral.api_key or "").strip()
)
providers_status["mistral_ocr"] = {
"name": "mistral_ocr",
"available": ocr_configured,
"error": None
if ocr_configured
else "Non configuré — les PDF scannés seront refusés (MISTRAL_API_KEY)",
"last_check": None,
# No live call here: health is derived from configuration.
"config_only": True,
}
except Exception as e:
providers_status["mistral_ocr"] = {
"name": "mistral_ocr",
"available": False,
"error": str(e)[:100],
"last_check": None,
}
# LLM/translation engines configured via admin settings (key presence
# only — no live call, keys are never exposed).
try:
settings = load_settings()
def _engine_status(section_name: str, env_var: str, label: str):
section = getattr(settings, section_name, None)
key_set = bool(
((getattr(section, "api_key", None) or "").strip())
or os.getenv(env_var, "").strip()
)
providers_status[section_name] = {
"name": section_name,
"available": key_set,
"error": None if key_set else f"Clé absente ({env_var})",
"last_check": None,
"config_only": True,
"label": label,
}
_engine_status("deepl", "DEEPL_API_KEY", "DeepL")
_engine_status("openrouter", "OPENROUTER_API_KEY", "Traduction IA Éco")
_engine_status("openrouter_premium", "OPENROUTER_API_KEY", "Traduction IA Premium")
_engine_status("openai", "OPENAI_API_KEY", "OpenAI")
_engine_status("zai", "ZAI_API_KEY", "Grok (xAI)")
_engine_status("google_cloud", "GOOGLE_CLOUD_API_KEY", "Google Cloud")
except Exception as e:
logger.warning(f"admin dashboard engines status failed: {e}")
return {
"timestamp": health_status.get("timestamp"),
"status": health_status.get("status"),
@@ -641,13 +702,24 @@ async def update_default_provider(
provider: str = Form(...),
admin_id: str = Depends(require_admin),
):
"""Update the default translation provider"""
"""Update the default translation provider.
The allowed list mirrors the providers actually wired in the translate
route (and declared in SettingsConfig) so the admin can set any of them as
the default — previously google_cloud, openrouter_premium, deepseek and
minimax were wrongly rejected here even though they are fully supported.
"""
valid_providers = [
"google",
"google_cloud",
"deepl",
"openai",
"openrouter",
"openrouter_premium",
"deepseek",
"minimax",
"zai",
# Mode aliases resolved at translation time.
"classic",
"llm",
]
@@ -852,6 +924,7 @@ class SettingsConfig(BaseModel):
deepseek: ProviderSettings = ProviderSettings()
minimax: ProviderSettings = ProviderSettings()
zai: ProviderSettings = ProviderSettings()
mistral: ProviderSettings = ProviderSettings() # OCR Mistral (PDF scannés)
smtp: SmtpSettings = SmtpSettings()
fallback_chain: str = "google,google_cloud,deepl,openrouter,openrouter_premium,openai,deepseek,zai"
fallback_chain_classic: str = "google,google_cloud,deepl"
@@ -924,6 +997,7 @@ async def get_settings(admin_id: str = Depends(require_admin)):
payload["zai"] = _merge_env(settings.zai, key_env="ZAI_API_KEY", model_env="ZAI_MODEL", url_env="ZAI_BASE_URL", default_model="grok-2-1212", default_url="https://api.x.ai/v1")
payload["google_cloud"] = _merge_env(settings.google_cloud, key_env="GOOGLE_CLOUD_API_KEY")
payload["mistral"] = _merge_env(settings.mistral, key_env="MISTRAL_API_KEY", model_env="MISTRAL_OCR_MODEL", default_model="mistral-ocr-latest")
# SMTP: merge from env vars, but never expose password
smtp_data = settings.smtp.model_dump()
@@ -958,6 +1032,7 @@ async def get_settings(admin_id: str = Depends(require_admin)):
"zai": bool(os.getenv("ZAI_API_KEY", "").strip()),
"google_cloud": bool(os.getenv("GOOGLE_CLOUD_API_KEY", "").strip()),
"mistral": bool(os.getenv("MISTRAL_API_KEY", "").strip()),
"smtp": bool(os.getenv("SMTP_HOST", "").strip()),
}
return JSONResponse(
@@ -1029,6 +1104,49 @@ async def test_provider(
status_code=200, content={"available": True, "test_result": result}
)
elif provider == "mistral":
api_key = _key(provider_config.api_key, "MISTRAL_API_KEY")
if not api_key:
return JSONResponse(
status_code=400,
content={
"available": False,
"error": "Aucune clé API Mistral trouvée (JSON ou .env MISTRAL_API_KEY).",
},
)
import requests as _requests
resp = _requests.get(
"https://api.mistral.ai/v1/models",
headers={"Authorization": f"Bearer {api_key}"},
timeout=10,
)
if resp.ok:
n_models = len(resp.json().get("data", []))
return JSONResponse(
status_code=200,
content={
"available": True,
"test_result": f"Clé valide — {n_models} modèles accessibles",
},
)
elif resp.status_code in (401, 403):
return JSONResponse(
status_code=resp.status_code,
content={
"available": False,
"error": f"Clé API Mistral invalide (HTTP {resp.status_code}).",
},
)
else:
return JSONResponse(
status_code=500,
content={
"available": False,
"error": f"Erreur Mistral HTTP {resp.status_code}: {resp.text[:200]}",
},
)
elif provider == "google_cloud":
api_key = _key(provider_config.api_key, "GOOGLE_CLOUD_API_KEY")
if not api_key: