feat(q1): D3a - raster fallback + text copy to quarantine on PDF failure

Étape D3 du sprint Q-1 (sous-commit 3/3 pour process_pdf, finalise D).

Décision B du consolidé v2 : fallback raster SYSTÉMATIQUE (option 3a
validée par Dom). Si redact_pdf_vector rate :

1. Tente redact_pdf_raster avec les mêmes paramètres
2. Si raster OK :
   - outputs["pdf_raster"] est rempli
   - flag pdf_vector_fallback_to_raster (severity=partial) → signale
     au DPO que le PDF livré est en qualité raster (moins précis)
3. Si raster rate aussi :
   - flag pdf_redaction_failed avec détail des 2 erreurs
4. Décision A finalisée : si quarantine_mgr fourni, le .pseudonymise.txt
   est copié dans quarantine_dir/ pour autoportance opérateur
   (un seul dossier à consulter au lieu de naviguer entre 2)

Import ajouté : shutil (stdlib).

Rétro-compat préservée : si quarantine_mgr is None, le fallback raster
est tenté quand même (RGPD-friendly), mais sans flag ni copie texte.

Le bloc "also_make_raster_burn" qui suit reste inchangé — un appelant
qui veut un raster systématique en plus du vector continue de le forcer
via ce flag.

Ref: docs/coordination/inbox/for-dom/2026-05-29_consolide_pseudocode-Q1-v2.md §3 Décisions A+B, §10

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 18:42:59 +02:00
parent 4aef17be90
commit 0d20d131ee

View File

@@ -17,6 +17,7 @@ import json
import logging
import os
import re
import shutil
import sys
from concurrent.futures import ProcessPoolExecutor
@@ -4674,19 +4675,54 @@ def process_pdf(
redact_pdf_vector(pdf_path, anon.audit, vec_path, ocr_word_map=ocr_word_map)
outputs["pdf_vector"] = str(vec_path)
except Exception as e:
# Q-1 D2 : ne plus avaler silencieusement. Le texte (.pseudonymise.txt)
# est déjà sorti avant ce bloc — donc on log + flag quarantaine PDF
# (severity=partial). Le fallback raster + copie texte arrivent en D3.
# Q-1 D2/D3 : ne plus avaler silencieusement. Le texte (.pseudonymise.txt)
# est déjà sorti avant ce bloc.
log.warning("PDF vector redaction failed for %s: %s", pdf_path.name, e)
if quarantine_mgr is not None:
quarantine_mgr.flag(
doc_name=pdf_path.stem,
reason="pdf_redaction_failed",
detail=str(e),
severity="partial",
exc=e,
# D3a : Décision B du consolidé v2 — fallback raster systématique
raster_fallback_ok = False
raster_err: Optional[Exception] = None
try:
ras_fb_path = out_dir / f"{base}.redacted_raster.pdf"
redact_pdf_raster(
pdf_path, anon.audit, ras_fb_path,
ogc_label=ogc_label, ocr_word_map=ocr_word_map,
)
# Note : pas de raise — texte anonymisé déjà disponible, partial OK
outputs["pdf_raster"] = str(ras_fb_path)
raster_fallback_ok = True
log.info("PDF raster fallback OK for %s", pdf_path.name)
except Exception as e2:
raster_err = e2
log.warning("PDF raster fallback also failed for %s: %s", pdf_path.name, e2)
if quarantine_mgr is not None:
if raster_fallback_ok:
# Vector raté mais raster OK : qualité moindre, signalée explicitement
quarantine_mgr.flag(
doc_name=pdf_path.stem,
reason="pdf_vector_fallback_to_raster",
detail=f"vector failed ({e}); raster fallback succeeded",
severity="partial",
exc=e,
)
else:
quarantine_mgr.flag(
doc_name=pdf_path.stem,
reason="pdf_redaction_failed",
detail=f"vector failed ({e}); raster also failed ({raster_err})",
severity="partial",
exc=e,
)
# Décision A finalisée : copier le texte en quarantaine pour autoportance
# (l'opérateur peut tout consulter depuis un seul dossier)
try:
quarantine_mgr.quarantine_dir.mkdir(parents=True, exist_ok=True)
shutil.copy(txt_path, quarantine_mgr.quarantine_dir / txt_path.name)
except Exception as copy_err:
log.warning("Could not copy text to quarantine for %s: %s",
pdf_path.name, copy_err)
# Note : pas de raise — texte anonymisé disponible (et copié si quarantine_mgr)
if also_make_raster_burn and fitz is not None:
ras_path = out_dir / f"{base}.redacted_raster.pdf"
redact_pdf_raster(pdf_path, anon.audit, ras_path, ogc_label=ogc_label, ocr_word_map=ocr_word_map)