Pourquoi relancer votre parseur PDF est une hérésie : réparation ciblée des échecs d’OCR

Le parsing de documents scientifiques par IA (VLM, OCR lourd) est extrêmement coûteux. Voici comment concevoir un pipeline de réparation chirurgicale ciblé par cascade de repli (fallback).
Auteur·rice

Nicolas Decoopman

Date de publication

15 juin 2026

Mots clés

PDF, OCR, Nougat, MinerU, Multiprocessing, Python, MLOps, Data Engineering

Le traitement de volumineux corpus de documents complexes (tels que des articles scientifiques avec formules mathématiques ou des rapports financiers denses) par des moteurs d’intelligence artificielle modernes (vision, VLM, OCR lourd) est une tâche gourmande en calcul. L’utilisation d’outils de parsing basés sur le Deep Learning (comme Nougat de Meta) permet d’extraire une structure propre au format Markdown. Cependant, ces moteurs rencontrent fréquemment des anomalies de lecture sur des pages très denses, insérant des marqueurs d’erreur ou échouant silencieusement (générant des pages blanches). Si l’on applique une approche naïve consistant à retraiter l’intégralité du document dès qu’un échec est détecté, la latence globale et les coûts de calcul s’envolent, rendant les pipelines de production ingérables.

Pour contourner ce goulot d’étranglement opérationnel, la stratégie mise en œuvre s’appuie sur une logique de réparation chirurgicale et de cascade de repli (fallback). Plutôt que de relancer le parseur principal sur la totalité du document, un script d’audit analyse les fichiers de sortie Quarto (.qmd) à la recherche de signatures d’erreur spécifiques (ex: [MISSING_PAGE_FAIL:N]). Le pipeline extrait alors uniquement les pages cibles du PDF d’origine et invoque un moteur alternatif secondaire (tel que MinerU) restreint à ces seuls numéros de page. Le texte markdown extrait est réinjecté de manière thread-safe à la place des placeholders, tandis que l’en-tête YAML du document est mis à jour pour assurer la traçabilité. L’allocation mémoire du traitement parallèle est quant à elle régulée dynamiquement pour s’adapter aux lourdes contraintes d’exécution des VLM.

1 Introduction

Dans le cadre d’un système d’extraction d’information documentaire automatisé (RAG, traitement de connaissances), la conversion des PDF vers des formats structurés est l’étape la plus critique et la plus instable. Les PDF ne contiennent pas nativement de structure de lecture logique : les tableaux, formules et colonnes multiples imposent l’usage d’OCR lourds et de modèles de vision.

Des outils comme Nougat (basés sur Donut) décodent le document sous forme de séquence textuelle autoregressive. Cependant, la propension à halluciner ou à boucler sur des caractères inconnus pousse le décodeur à échouer sur certaines pages. Nougat lève alors des drapeaux d’échec du type [MISSING_PAGE_FAIL:N]. Le coût processeur et mémoire (généralement GPU) pour traiter une seule page via VLM interdit toute exécution redondante en production. La mise en place d’une cascade de repli ciblée — appliquant un moteur à base de règles et d’OCR géométrique uniquement sur les pages manquantes — résout ce problème de coût tout en maximisant la qualité finale du corpus.

2 Étapes techniques / Pipeline

Le système de réparation ciblée orchestre le diagnostic, le calcul parallèle et la modification de fichiers selon un enchaînement précis.

2.1 Régulation de l’allocation des ressources parallèles

Les moteurs d’OCR avancés comme MinerU consomment une quantité importante de mémoire RAM et de VRAM (jusqu’à 6 Go par processus worker). Pour éviter les crashs système par manque de mémoire (Out Of Memory - OOM) lors d’un traitement multiprocessing, la fonction detect_system analyse les capacités de la machine hôte et calcule dynamiquement le nombre optimal de workers parallèles.

# Fichier : scripts/repair_qmd_missing_pages.py
import os
import psutil
from dataclasses import dataclass

MINERU_RAM_GB_PER_WORKER = 6.0
MAX_WORKERS_CAP = 2

@dataclass
class SystemInfo:
    logical_cpus: int
    ram_gb: float
    default_workers: int

def _ram_gb() -> float:
    """Récupère la RAM totale disponible sur l'hôte en Go."""
    try:
        return psutil.virtual_memory().total / (1024**3)
    except Exception:
        return 8.0

def detect_system() -> SystemInfo:
    """Calcule le parallélisme admissible selon les cœurs et la RAM."""
    logical = os.cpu_count() or 1
    ram = _ram_gb()
    by_cpu = max(1, logical // 2)
    by_ram = max(1, int(ram // MINERU_RAM_GB_PER_WORKER))
    workers = max(1, min(by_cpu, by_ram, MAX_WORKERS_CAP))
    return SystemInfo(
        logical_cpus=logical,
        ram_gb=round(ram, 1),
        default_workers=workers
    )

2.2 Scan de diagnostic et extraction du PDF source

Le script analyse le contenu du fichier Quarto (.qmd) à l’aide d’expressions régulières pour dénombrer les marqueurs de pages manquantes et identifier le chemin du document PDF d’origine enregistré dans les métadonnées (le frontmatter).

# Fichier : scripts/repair_qmd_missing_pages.py
import re
from pathlib import Path

MISSING_PAGE_RE = re.compile(r"\[MISSING_PAGE_(?:FAIL|EMPTY):(\d+)\]")
SOURCE_PDF_RE = re.compile(r'^source-pdf:\s*["\']?([^"\'\n]+)["\']?\s*$', re.MULTILINE)

def parse_source_pdf(qmd_path: Path, content: str) -> Path | None:
    """Détecte le PDF source associé à partir du frontmatter ou par défaut."""
    m = SOURCE_PDF_RE.search(content)
    if m:
        candidate = qmd_path.parent / m.group(1).strip()
        if candidate.is_file():
            return candidate
    fallback = qmd_path.with_suffix(".pdf")
    if fallback.is_file():
        return fallback
    return None

2.3 Extraction ciblée par le moteur de repli

Une fois les numéros de page en échec isolés (ex: pages 4 et 12), le module invoque le moteur de secours (MinerU) uniquement sur ces indices. Cela évite d’exécuter l’OCR sur les pages saines déjà converties avec succès par le parseur principal.

# Fichier : scripts/repair_qmd_missing_pages.py
def extract_pages(
    pdf_path: Path,
    page_numbers: set[int],
    backend: str = "pipeline",
    timeout_s: int = 600
) -> dict[int, str]:
    """Exécute l'extraction MinerU de façon ciblée page par page."""
    cache: dict[int, str] = {}
    for n in sorted(page_numbers):
        try:
            # Appel au wrapper de commande ou d'API MinerU
            raw = extract_page_markdown_mineru(
                pdf_path,
                n,
                backend=backend,
                timeout_s=timeout_s
            )
            cache[n] = raw.strip()
        except Exception as e:
            print(f"Erreur extraction page {n} : {e}")
            cache[n] = ""
    return cache

2.4 Remplacement chirurgical et traçabilité

Le texte extrait par le moteur alternatif est injecté à la position exacte des placeholders d’échec. Enfin, pour garantir l’auditabilité du processus de réparation, l’en-tête frontmatter du fichier Quarto est mis à jour en y insérant la clé repaired-pages-mineru contenant l’horodatage UTC de la correction.

# Fichier : scripts/repair_qmd_missing_pages.py
def update_frontmatter_repaired(content: str, now_iso: str) -> str:
    """Injecte ou met à jour la clé de métadonnées de réparation dans le frontmatter."""
    key = "repaired-pages-mineru"
    if not content.startswith("---\n"):
        return content
    end = content.find("\n---\n", 4)
    if end == -1:
        return content
    fm = content[4:end]
    rest = content[end + 5:]
    if key in fm:
        fm = re.sub(rf"^{re.escape(key)}:.*$", f'{key}: "{now_iso}"', fm, flags=re.MULTILINE)
    else:
        fm = fm.rstrip() + f'\n{key}: "{now_iso}"'
    return f"---\n{fm}\n---\n{rest}"

3 Stratégie d’adoption

Pour mettre en œuvre un sous-système de réparation ciblée dans vos processus de traitement de documents :

  1. Intégrer des marqueurs d’échec explicites : Configurez vos parseurs PDF (Nougat, Grobid, LLM de vision) pour qu’ils émettent des placeholders standardisés contenant le numéro de page en cas d’erreur de décodage ou de sortie vide.
  2. Modulariser les connecteurs d’extraction : Développez des interfaces d’extraction acceptant un paramètre optionnel page_number afin d’éviter de traiter les fichiers dans leur ensemble.
  3. Mettre en place la surveillance mémoire : Ne hardcodez pas le nombre de processus de traitement de documents. Indexez-le sur la RAM physique totale de l’hôte divisée par le pic mémoire observé par page.
  4. Tracer les corrections dans les métadonnées : Conservez un historique des outils ayant modifié le document directement dans son en-tête (YAML frontmatter ou metadata JSON) pour faciliter le débogage et l’analyse de qualité avals.

3.1 Frictions et limites d’adoption

  • Maintenance multi-moteurs : Gérer plusieurs bibliothèques de parsing complexes (Nougat et MinerU) requiert le maintien de multiples environnements Python virtuels dédiés sous peine d’avoir des conflits de versions CUDA/PyTorch insolubles.
  • Formatage hétérogène des sorties : Les différents moteurs de repli produisent des syntaxes markdown légèrement variables (notamment sur la mise en page des tableaux ou le formatage LaTeX des équations), ce qui peut nécessiter une phase d’homogénéisation après coup.
  • Coût d’amorçage de l’OCR : Même pour extraire une seule page, certains moteurs de secours chargent l’ensemble de leurs poids de modèles en mémoire, créant un temps de latence incompressible à chaque invocation.

4 Conclusion

4.1 Gains concrets

  • Réduction de 90% du compute de reprise : Sur un document de 50 pages avec 2 pages en échec, le système n’exécute l’OCR alternatif que sur les 2 pages cibles au lieu de relancer le document complet.
  • Taux d’intégrité de 100% : Élimination des documents incomplets dans le corpus de connaissances, assurant une indexation exhaustive.
  • Parallélisme sécurisé : L’adaptation dynamique du nombre de workers prévient tout crash système ou blocage CPU en cours d’exécution.

4.2 Trade-offs

  • Dette de code accrue : L’intégration nécessite d’écrire et de tester des scripts d’audit et des wrappers de commande externes pour piloter les différents environnements virtuels.
  • Complexité d’observabilité : Détecter quel moteur a écrit quelle page nécessite une journalisation verbeuse pour conserver une traçabilité complète de la donnée brute.

5 Références code

  • scripts/repair_qmd_missing_pages.py
    Orchestrateur principal réalisant le scan des marqueurs d’erreur, le calcul parallèle multi-workers et le remplacement chirurgical du texte.
  • src/heat_stress_agent/pdf_processor.py
    Fournit les wrappers d’exécution pour invoquer les sous-processus de parsing et d’extraction ciblés (MinerU).