Lesson 9 — Fiabiliser, maîtriser les coûts, projet final

Projet final : analyseur de justificatifs et résumeur de PDF

5 min read

This content is not translated yet. Showing the original version.

Projet final : analyseur de justificatifs et résumeur de PDF

Le cahier des charges

  1. Une photo de ticket ou de facture → un JSON avec commerçant, date (AAAA-MM-JJ), lignes, total TTC, TVA.
  2. Des alertes quand le code détecte une incohérence (somme, TVA, date, lisibilité).
  3. Un PDF → titre, résumé de 3 phrases, points clés, chiffres avec leur page.
  4. Aucun fichier ne reste dans l'API Files après l'exécution.
  5. Des tests qui tournent sans clé ni réseau.
flowchart TB
  subgraph "multimodal.py"
    PF["part_from() / uploader() / nettoyer()"]
    GE["generer() + réessais"]
    AJ["analyser_justificatif() + verifier()"]
    RP["resumer_pdf()"]
  end
  CLI["python multimodal.py justificatif|pdf|comparer|audio|video"] --> AJ
  CLI --> RP
  AJ --> PF
  RP --> PF
  AJ --> GE
  RP --> GE
  T["test_multimodal.py (FauxClient)"] -.-> AJ
  T -.-> RP

Les fichiers du lab

FichierRôle
multimodal.pytout le code : préparation des fichiers, appels, schémas, contrôles, ligne de commande
generer_exemples.pycrée les fichiers d'exemple fictifs
test_multimodal.py7 tests avec un faux client
requirements.txtdépendances
.env.example (dans files/)modèle de configuration (valeur factice)

Tester sans clé : le faux client

Un test unitaire vérifie une partie du code de façon automatique. Pour ne dépendre ni d'une clé ni du réseau, on remplace le vrai client par un faux qui renvoie des réponses écrites à l'avance :

files/test_multimodal.py (extrait)
class FauxClient:
    def __init__(self, textes, erreurs=0):
        self.textes, self.erreurs, self.appels = list(textes), erreurs, []
        self.models = NS(generate_content=self._generer)
        ...

def test_justificatif_incoherent_genere_des_alertes():
    faux = dict(TICKET_OK, total_ttc=12.0, date="14/10/2026", lisible=False)
    res = mm.analyser_justificatif(FauxClient([json.dumps(faux)]), ICI / "ticket.png")
    texte = " ".join(res["alertes"])
    assert "somme des lignes 9.5 != total 12.0" in texte
    assert "mauvais format" in texte and "vérification humaine" in texte

On teste ainsi notre logique (contrôles, choix inline/Files, nettoyage, réessais) sans payer d'appels.

Que vérifie un test qui utilise FauxClient ?

Quelles commandes lancer avant de considérer le projet terminé ?

Lancer

terminal
pytest -q                                      # sans clé
python multimodal.py justificatif ticket.png   # avec clé
python multimodal.py pdf rapport.pdf

Exercice : traiter un dossier entier

Écrivez un script lot_justificatifs.py qui analyse toutes les images *.png et *.jpg d'un dossier et écrit un fichier justificatifs.csv (une ligne par fichier : nom, commerçant, date, total, alertes). Un fichier en erreur ne doit pas arrêter le lot.

Voir la solution
lot_justificatifs.py
import csv
import sys
from pathlib import Path

from google import genai

import multimodal as mm

dossier = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
client = genai.Client()
with open("justificatifs.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.writer(f)
    w.writerow(["fichier", "commercant", "date", "total_ttc", "alertes"])
    for img in sorted([*dossier.glob("*.png"), *dossier.glob("*.jpg")]):
        try:
            r = mm.analyser_justificatif(client, img)
            j = r["justificatif"]
            w.writerow([img.name, j["commercant"], j["date"], j["total_ttc"], " ; ".join(r["alertes"])])
        except Exception as e:
            w.writerow([img.name, "", "", "", f"ERREUR {type(e).__name__}"])
        print(img.name, "ok")

Avec les fichiers du lab, vous obtenez deux lignes : le ticket (9.5) et la facture (87.6), sans alerte. Ouvrez le CSV dans un tableur pour la validation humaine.

Résultat obtenu

Tests automatiques, exécutés le 2026-10-15 sans clé :

pytest -v
test_multimodal.py::test_justificatif_coherent PASSED
test_multimodal.py::test_justificatif_incoherent_genere_des_alertes PASSED
test_multimodal.py::test_json_invalide_leve_une_erreur PASSED
test_multimodal.py::test_mime_et_choix_inline_ou_files PASSED
test_multimodal.py::test_pdf_via_files_puis_nettoyage PASSED
test_multimodal.py::test_comparaison_etiquette_chaque_image PASSED
test_multimodal.py::test_reessai_sur_503 PASSED
7 passed in 0.33s

Exécution réelle, python multimodal.py justificatif ticket.png (gemini-3.5-flash) :

sortie (extrait)
{
  "justificatif": {
    "type": "ticket", "commercant": "BOULANGERIE DU CANAL", "date": "2026-10-14", "numero": "004521",
    "lignes": [
      {"libelle": "2 x Croissant", "montant": 2.6},
      {"libelle": "1 x Baguette tradition", "montant": 1.3},
      {"libelle": "1 x Café allongé", "montant": 2.1},
      {"libelle": "1 x Jus d'orange", "montant": 3.5}
    ],
    "total_ttc": 9.5, "tva": 0.86, "devise": "EUR", "lisible": true
  },
  "alertes": [],
  "tokens": {"entree": 1123, "sortie": 133}
}

Toutes les valeurs attendues sont retrouvées (ticket 9,50 ; facture 87,60 ; rapport 412 000 EUR et 450 000 EUR d'objectif), et client.files.list() ne contient plus aucun fichier du lab.

  • Les 7 tests passent sans clé
  • L'exécution réelle retrouve les valeurs attendues des trois documents
  • Je sais étendre le lab à un dossier entier avec gestion des erreurs
  • Je prévois une validation humaine avant toute écriture comptable