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
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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user