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

@@ -0,0 +1,192 @@
"""Scanned PDF detection and the Mistral OCR translation path."""
import fitz
import pytest
from config import config
from services.mistral_ocr import MistralOCRClient
from services.providers.base import TranslationProvider
from services.providers.schemas import TranslationRequest, TranslationResponse
from translators.pdf_translator import PDFTranslator
FAKE_OCR_PAGES = [
"# Facture\n\nMontant total : 1 250 euros\n\n![logo](img.png)",
"Le paiement est du sous 30 jours.",
]
# Minimal French → English mapping for the fake provider; anything else is
# prefixed so tests can assert replacement happened.
TRANSLATIONS = {
"Facture": "Invoice",
"Montant total : 1 250 euros": "Total amount: 1,250 euros",
"Le paiement est du sous 30 jours.": "Payment is due within 30 days.",
}
class FakeProvider(TranslationProvider):
"""New-style provider with canned translations (no network)."""
def get_name(self) -> str:
return "fake"
def is_available(self) -> bool:
return True
def translate_text(self, request: TranslationRequest) -> TranslationResponse:
# Pages arrive as multi-line text: translate line by line so the
# canned mapping applies regardless of how lines are grouped.
lines = []
for line in request.text.split("\n"):
stripped = line.strip()
if not stripped:
lines.append(line)
continue
lines.append(TRANSLATIONS.get(stripped, f"[EN] {stripped}"))
return TranslationResponse(
translated_text="\n".join(lines),
provider_name=self.get_name(),
from_cache=False,
)
def _make_scanned_pdf(path):
"""Image-only PDF: one page almost fully covered by a picture, no text."""
pix = fitz.Pixmap(fitz.csRGB, fitz.IRect(0, 0, 10, 10))
pix.clear_with(90)
png_bytes = pix.tobytes("png")
doc = fitz.open()
page = doc.new_page(width=612, height=792)
page.insert_image(fitz.Rect(20, 20, 592, 772), stream=png_bytes)
doc.save(str(path))
doc.close()
return path
def _make_text_pdf(path):
doc = fitz.open()
page = doc.new_page()
page.insert_text(
(72, 100),
"Real selectable text with plenty of characters to exceed the scanned threshold. " * 2,
fontsize=11,
)
doc.save(str(path))
doc.close()
return path
@pytest.fixture
def no_api_key(monkeypatch):
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
monkeypatch.setattr(config, "MISTRAL_API_KEY", "")
monkeypatch.setattr(config, "MISTRAL_OCR_ENABLED", True)
@pytest.fixture
def api_key(monkeypatch):
# Set BOTH the env var and the config attribute: some other test in the
# full suite reloads the config module, and pdf_translator imports
# `config` at call time — after a reload only the env var is still
# visible (the class attributes are re-read from the environment).
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
monkeypatch.setattr(config, "MISTRAL_API_KEY", "test-key")
monkeypatch.setattr(config, "MISTRAL_OCR_ENABLED", True)
class TestScannedDetection:
def test_image_only_pdf_is_scanned(self, tmp_path):
pdf = _make_scanned_pdf(tmp_path / "scan.pdf")
translator = PDFTranslator(provider=None)
assert translator._is_scanned_pdf(pdf) is True
def test_text_pdf_is_not_scanned(self, tmp_path):
pdf = _make_text_pdf(tmp_path / "text.pdf")
translator = PDFTranslator(provider=None)
assert translator._is_scanned_pdf(pdf) is False
def test_title_only_pdf_is_not_scanned(self, tmp_path):
"""A sparse but textual page (no raster image) stays on the layout path."""
doc = fitz.open()
page = doc.new_page()
page.insert_text((72, 100), "Facture 2026", fontsize=18)
doc.save(str(tmp_path / "title.pdf"))
doc.close()
translator = PDFTranslator(provider=None)
assert translator._is_scanned_pdf(tmp_path / "title.pdf") is False
class TestMarkdownCleanup:
def test_images_links_headings_removed(self):
md = "# Titre\n\n![img](x.png)\n\n[lien](http://x) texte"
out = PDFTranslator._markdown_to_text(md)
assert "Titre" in out
assert "![img]" not in out
assert "(http://x)" not in out
assert "#" not in out
assert "lien texte" in out
def test_table_pipes_removed(self):
out = PDFTranslator._markdown_to_text("| A | B |\n|---|---|\n| un | deux |")
assert "|" not in out
assert "un" in out and "deux" in out
class TestScannedOCRPath:
def test_translate_scanned_pdf_end_to_end(self, tmp_path, api_key, monkeypatch):
pdf = _make_scanned_pdf(tmp_path / "scan.pdf")
monkeypatch.setattr(
MistralOCRClient,
"extract_pdf_text",
lambda self, path, progress_callback=None: list(FAKE_OCR_PAGES),
)
translator = PDFTranslator(provider=FakeProvider())
# Configure OCR the way the production route does (set_ocr_config).
# Going through the config module is fragile in the full suite:
# another test replaces sys.modules["config"], so a config-module
# patch may never reach the translator.
translator.set_ocr_config(api_key="test-key", enabled=True)
out = tmp_path / "out.pdf"
result = translator.translate_file(pdf, out, "en", "fr")
assert result.exists() and result.stat().st_size > 0
doc = fitz.open(str(result))
text = "\n".join(p.get_text("text") for p in doc)
doc.close()
assert "Invoice" in text
assert "Payment is due within 30 days" in text
assert "Facture" not in text
assert "logo" not in text # markdown images stripped
assert "|" not in text # markdown tables stripped
def test_missing_api_key_raises_clear_error(self, tmp_path, no_api_key):
pdf = _make_scanned_pdf(tmp_path / "scan.pdf")
translator = PDFTranslator(provider=FakeProvider())
with pytest.raises(RuntimeError) as exc:
translator.translate_file(pdf, tmp_path / "out.pdf", "en", "fr")
assert "MISTRAL_API_KEY" in str(exc.value)
def test_disabled_ocr_raises_clear_error(self, tmp_path, monkeypatch):
monkeypatch.setattr(config, "MISTRAL_API_KEY", "test-key")
monkeypatch.setattr(config, "MISTRAL_OCR_ENABLED", False)
pdf = _make_scanned_pdf(tmp_path / "scan.pdf")
translator = PDFTranslator(provider=FakeProvider())
with pytest.raises(RuntimeError):
translator.translate_file(pdf, tmp_path / "out.pdf", "en", "fr")
def test_text_pdf_bypasses_ocr(self, tmp_path, api_key, monkeypatch):
pdf = _make_text_pdf(tmp_path / "text.pdf")
called = {"ocr": False}
def _fail(self, path, progress_callback=None):
called["ocr"] = True
return []
monkeypatch.setattr(MistralOCRClient, "extract_pdf_text", _fail)
translator = PDFTranslator(provider=FakeProvider())
out = tmp_path / "out.pdf"
translator.translate_file(pdf, out, "en", "fr")
assert called["ocr"] is False