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:
@@ -17,6 +17,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
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)
|
redact_pdf_vector(pdf_path, anon.audit, vec_path, ocr_word_map=ocr_word_map)
|
||||||
outputs["pdf_vector"] = str(vec_path)
|
outputs["pdf_vector"] = str(vec_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Q-1 D2 : ne plus avaler silencieusement. Le texte (.pseudonymise.txt)
|
# Q-1 D2/D3 : ne plus avaler silencieusement. Le texte (.pseudonymise.txt)
|
||||||
# est déjà sorti avant ce bloc — donc on log + flag quarantaine PDF
|
# est déjà sorti avant ce bloc.
|
||||||
# (severity=partial). Le fallback raster + copie texte arrivent en D3.
|
|
||||||
log.warning("PDF vector redaction failed for %s: %s", pdf_path.name, e)
|
log.warning("PDF vector redaction failed for %s: %s", pdf_path.name, 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,
|
||||||
|
)
|
||||||
|
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 quarantine_mgr is not None:
|
||||||
|
if raster_fallback_ok:
|
||||||
|
# Vector raté mais raster OK : qualité moindre, signalée explicitement
|
||||||
quarantine_mgr.flag(
|
quarantine_mgr.flag(
|
||||||
doc_name=pdf_path.stem,
|
doc_name=pdf_path.stem,
|
||||||
reason="pdf_redaction_failed",
|
reason="pdf_vector_fallback_to_raster",
|
||||||
detail=str(e),
|
detail=f"vector failed ({e}); raster fallback succeeded",
|
||||||
severity="partial",
|
severity="partial",
|
||||||
exc=e,
|
exc=e,
|
||||||
)
|
)
|
||||||
# Note : pas de raise — texte anonymisé déjà disponible, partial OK
|
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:
|
if also_make_raster_burn and fitz is not None:
|
||||||
ras_path = out_dir / f"{base}.redacted_raster.pdf"
|
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)
|
redact_pdf_raster(pdf_path, anon.audit, ras_path, ogc_label=ogc_label, ocr_word_map=ocr_word_map)
|
||||||
|
|||||||
Reference in New Issue
Block a user