Files
office_translator/_bmad-output/implementation-artifacts/spec-quick-wins-pipeline-llm.md
sepehr 526c87348f
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m20s
feat(translation): quality pipeline overhaul + new features (audit 2026-08-29)
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)
2026-08-29 18:38:09 +02:00

6.7 KiB
Raw Blame History

title, type, created, status, context
title type created status context
Quick wins pipeline LLM : prompt, connexions, cache, event loop bugfix 2026-08-26 draft

Intent

Problem: Le pipeline de traduction LLM souffre de 4 défauts vérifiés : (1) dans les 3 providers LLM, un prompt personnalisé/glossaire remplace le prompt par défaut — la paire de langues et les règles métier disparaissent de la requête ; (2) chaque segment traduit ouvre une nouvelle connexion TCP+TLS (requests.post nu) ; (3) le cache de traduction Redis/LRU (services/translation_cache.py) est écrit et testé mais jamais branché — re-traduire un fichier re-facture 100 % des appels ; (4) 7 appels bloquants (SHA-256 de fichiers, DB, disque, sonde réseau Google) s'exécutent directement dans l'event loop et peuvent geler tout le serveur.

Approach: 4 correctifs chirurgicaux dans la couche providers (openai/minimax/deepseek) et le runner de jobs de translate_routes.py, sans changement d'interface publique ni des translators.

Boundaries & Constraints

Always: L'interface TranslationProvider / TranslationRequest / TranslationResponse reste inchangée ; la paire de langues doit toujours figurer dans le system prompt ; le cache reste piloté par env (REDIS_CACHE_TTL, LRU_CACHE_MAXSIZE, REDIS_URL) ; tout échec du cache est silencieux (fallback LRU, puis poursuite sans cache) ; from_cache=True sur les réponses servies du cache.

Ask First: modifier build_full_prompt (services/glossary_service.py) ; modifier les schémas providers ; introduire un nouveau backend de cache.

Never: Pas de batch multi-segments LLM (chantier suivant) ; pas de refonte de la double couche providers legacy/nouvelle ; pas de modification des translators/ ; pas de nouvel endpoint ; pas de persistance des jobs.

I/O & Edge-Case Matrix

Scenario Input / State Expected Output / Behavior Error Handling
Prompt custom actif glossaire et/ou prompt utilisateur System prompt = prompt par défaut formaté (avec paire de langues) + contexte custom en complément N/A
Segment déjà en cache même texte + langues + provider + hash prompt Réponse from_cache=True, 0 requête HTTP N/A
Segment répété dans un même document 2e occurrence après succès de la 1ʳᵉ Servie par le cache (le batch est séquentiel) N/A
Redis indisponible get_cache() init ou set() en échec Fallback LRU RAM puis non-bloquant ; traduction réussie quand même Log warning, jamais d'exception
Provider en erreur échec API après retries Rien n'est écrit dans le cache Erreur existante propagée
Fichier 50 Mo + glossaire configuré job lancé pendant que d'autres requêtes arrivent SHA-256, zip-safety, DB, settings et sonde Google hors event loop (asyncio.to_thread) Erreurs existantes propagées

Code Map

  • services/providers/openai_provider.py -- _build_system_prompt :120-128 (bug du remplacement), requests.post :263, translate_text :426-455 (point d'entrée cache)
  • services/providers/minimax_provider.py -- prompt inline :181-183, requests.post :117, translate_text :168+
  • services/providers/deepseek_provider.py -- prompt inline :164-166, requests.post :111, translate_text :151+
  • services/translation_cache.py -- API prête et testée : make_cache_key :45, hash_prompt :77, get_cache() :402 (Redis auto + fallback LRU) — aucun appelant hors tests
  • routes/translate_routes.py -- appels bloquants dans coroutines : calculate_sha256 :858/:871, validate_zip_safety :884, _load_admin_settings :1174, get_glossary_terms :1191, get_prompt_content :1206, _google_cloud_key_valid :1242 (fait une traduction HTTP de test synchrone)
  • tests/test_providers/ -- 8 fichiers existants, dont test_minimax_provider.py (récent) : patterns de mock à réutiliser

Tasks & Acceptance

Execution:

  • services/providers/openai_provider.py -- (1) _build_system_prompt : concaténer le prompt par défaut formaté + le custom au lieu de return custom_prompt ; (2) requests.Session d'instance réutilisée dans _make_api_request ; (3) translate_text : lookup get_cache() + make_cache_key(..., custom_prompt_hash=hash_prompt(custom_prompt)) avant l'appel, cache.set après succès -- corrige le bug qualité, la latence et le coût
  • services/providers/minimax_provider.py -- mêmes 3 changements (prompt construit inline :181-183)
  • services/providers/deepseek_provider.py -- mêmes 3 changements (prompt construit inline :164-166)
  • routes/translate_routes.py -- envelopper les 7 appels bloquants listés dans la Code Map dans asyncio.to_thread -- l'event loop reste réactif pendant les jobs
  • tests/test_providers/ -- nouveaux tests couvrant la matrice I/O : le system prompt contient la paire de langues et le custom (×3 providers) ; cache hit → 0 appel HTTP et from_cache=True ; erreur provider → rien mis en cache ; Redis down → traduction quand même

Acceptance Criteria:

  • Given un custom_prompt sans mention de langue, when traduction via openai/minimax/deepseek, then le payload contient « from {source} to {target} » et le contenu custom.
  • Given un segment déjà en cache (même clé), when re-traduit, then aucune requête HTTP n'est émise vers l'API LLM.
  • Given Redis down, when traduction, then réponse normale via LRU ou sans cache, sans exception remontée au job.
  • Given la suite de tests existante, when pytest -x, then 0 régression.

Spec Change Log

Design Notes

Concaténation du prompt (les 3 providers) :

system_prompt = DEFAULT_TRANSLATION_PROMPT.format(
    source_lang=source_lang_name, target_lang=target_lang_name
)
if custom_prompt:
    system_prompt += (
        "\n\nAdditional context and instructions from the user "
        f"(comply without overriding the language pair above):\n{custom_prompt}"
    )

La requests.Session est créée dans __init__ du provider (le pooling urllib3 est thread-safe ; les translators appellent translate_text depuis 6 threads). Clé de cache : réutiliser make_cache_key(text, target_language, source_language, self._provider_name, custom_prompt_hash=hash_prompt(custom_prompt)) — le hash du prompt isole déjà les glossaires différents ; user_id absent des metadata aujourd'hui → valeur par défaut « anon » (partage inter-utilisateurs acceptable : mémoire de traduction standard, TTL 24 h).

Verification

Commands:

  • pytest tests/test_providers/ -x -- expected: succès, nouveaux tests inclus
  • pytest -x -- expected: succès complet, 0 régression