Aller au contenu principal

Annotations en batch

Passer à l’échelle

Traiter un document à la fois convient pour le prototypage. En production, vous devez traiter des centaines ou des milliers de documents par jour. Cette leçon couvre les stratégies de traitement par lots avec les annotations Document AI.

Traitement séquentiel avec gestion d’erreurs

La première approche, simple et robuste :

import os
import json
import time
from pathlib import Path
from mistralai import Mistral
from mistralai.extra import response_format_from_pydantic_model
from pydantic import BaseModel, Field

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

class ExtractionFacture(BaseModel):
    fournisseur: str = Field(..., description="Nom du fournisseur")
    numero: str = Field(..., description="Numéro de facture")
    montant_ttc: float = Field(..., description="Montant TTC")
    date: str = Field(..., description="Date (YYYY-MM-DD)")

def traiter_dossier_factures(dossier: str) -> list[dict]:
    """Traite toutes les factures d'un dossier."""
    resultats = []
    chemin = Path(dossier)
    fichiers = list(chemin.glob("*.pdf"))

    print(f"Traitement de {len(fichiers)} fichiers...")

    for i, fichier in enumerate(fichiers):
        print(f"  [{i+1}/{len(fichiers)}] {fichier.name}")

        try:
            import base64
            with open(fichier, "rb") as f:
                doc_b64 = base64.b64encode(f.read()).decode("utf-8")

            response = client.ocr.process(
                model="mistral-ocr-latest",
                document={
                    "type": "document_base64",
                    "document_base64": doc_b64
                },
                document_annotation_format=response_format_from_pydantic_model(
                    ExtractionFacture
                ),
                table_format="markdown"
            )

            for page in response.pages:
                if page.document_annotation:
                    facture = ExtractionFacture.model_validate_json(
                        page.document_annotation
                    )
                    resultats.append({
                        "fichier": fichier.name,
                        "fournisseur": facture.fournisseur,
                        "numero": facture.numero,
                        "montant_ttc": facture.montant_ttc,
                        "date": facture.date,
                        "statut": "ok"
                    })

        except Exception as e:
            resultats.append({
                "fichier": fichier.name,
                "statut": "erreur",
                "erreur": str(e)
            })

    return resultats

Traitement parallèle avec asyncio

Pour accélérer le traitement, utilisez les appels asynchrones :

import os
import asyncio
import base64
from pathlib import Path
from mistralai import Mistral
from mistralai.extra import response_format_from_pydantic_model

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

async def traiter_document(chemin_fichier: Path, schema) -> dict:
    """Traite un document de manière asynchrone."""
    try:
        with open(chemin_fichier, "rb") as f:
            doc_b64 = base64.b64encode(f.read()).decode("utf-8")

        response = client.ocr.process(
            model="mistral-ocr-latest",
            document={
                "type": "document_base64",
                "document_base64": doc_b64
            },
            document_annotation_format=response_format_from_pydantic_model(
                schema
            )
        )

        annotations = []
        for page in response.pages:
            if page.document_annotation:
                annotations.append(page.document_annotation)

        return {
            "fichier": chemin_fichier.name,
            "annotations": annotations,
            "statut": "ok"
        }

    except Exception as e:
        return {
            "fichier": chemin_fichier.name,
            "statut": "erreur",
            "erreur": str(e)
        }


async def traiter_batch(
    dossier: str,
    schema,
    max_concurrent: int = 5
) -> list[dict]:
    """Traite un dossier avec parallélisme contrôlé."""
    chemin = Path(dossier)
    fichiers = list(chemin.glob("*.pdf"))

    semaphore = asyncio.Semaphore(max_concurrent)

    async def traiter_avec_limite(fichier):
        async with semaphore:
            return await traiter_document(fichier, schema)

    taches = [traiter_avec_limite(f) for f in fichiers]
    resultats = await asyncio.gather(*taches)

    return list(resultats)

# Exécution
# resultats = asyncio.run(traiter_batch("./factures/", ExtractionFacture))

Service Batch Inference de Mistral

Pour les très gros volumes, Mistral propose un service de Batch Inference dédié, plus économique que les appels individuels :

import json

def preparer_batch_jsonl(dossier: str, sortie: str) -> int:
    """Prépare un fichier JSONL pour le batch processing."""
    chemin = Path(dossier)
    fichiers = list(chemin.glob("*.pdf"))
    compte = 0

    with open(sortie, "w") as f:
        for fichier in fichiers:
            with open(fichier, "rb") as pdf:
                doc_b64 = base64.b64encode(pdf.read()).decode("utf-8")

            requete = {
                "custom_id": fichier.stem,
                "model": "mistral-ocr-latest",
                "document": {
                    "type": "document_base64",
                    "document_base64": doc_b64
                }
            }

            f.write(json.dumps(requete) + "\n")
            compte += 1

    print(f"{compte} requêtes préparées dans {sortie}")
    return compte

Suivi et reporting

Pour un traitement en production, ajoutez du monitoring :

import time
from dataclasses import dataclass, field

@dataclass
class StatsBatch:
    total: int = 0
    succes: int = 0
    erreurs: int = 0
    temps_debut: float = field(default_factory=time.time)
    fichiers_erreur: list = field(default_factory=list)

    @property
    def duree(self) -> float:
        return time.time() - self.temps_debut

    @property
    def vitesse(self) -> float:
        if self.duree > 0:
            return self.succes / self.duree
        return 0.0

    def rapport(self) -> str:
        return (
            f"Traitement terminé en {self.duree:.1f}s\n"
            f"  Succès : {self.succes}/{self.total}\n"
            f"  Erreurs : {self.erreurs}\n"
            f"  Vitesse : {self.vitesse:.2f} docs/s\n"
            f"  Fichiers en erreur : {self.fichiers_erreur}"
        )

Export des résultats

Exportez vos résultats vers les formats courants :

import csv
import json

def exporter_csv(resultats: list[dict], chemin: str) -> None:
    """Exporte les résultats en CSV."""
    if not resultats:
        return

    cles = resultats[0].keys()

    with open(chemin, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=cles)
        writer.writeheader()
        writer.writerows(resultats)

    print(f"Export CSV : {chemin}")


def exporter_json(resultats: list[dict], chemin: str) -> None:
    """Exporte les résultats en JSON."""
    with open(chemin, "w", encoding="utf-8") as f:
        json.dump(resultats, f, ensure_ascii=False, indent=2)

    print(f"Export JSON : {chemin}")

Points clés à retenir

  • Le traitement séquentiel convient pour les petits volumes avec une bonne gestion d’erreurs
  • Le traitement parallèle avec asyncio et sémaphore accélère les gros volumes
  • Le service Batch Inference de Mistral est la solution la plus économique pour les très gros volumes
  • Ajoutez du monitoring et du reporting pour suivre la progression et identifier les erreurs
  • Exportez les résultats en CSV ou JSON pour l’intégration avec vos systèmes