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

86 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 'Quick wins pipeline LLM : prompt, connexions, cache, event loop'
type: 'bugfix'
created: '2026-08-26'
status: 'draft'
context: []
---
<frozen-after-approval reason="human-owned intent — do not modify unless human renegotiates">
## 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 |
</frozen-after-approval>
## 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) :
```python
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