Projet final : analyseur de justificatifs et résumeur de PDF
Le cahier des charges
- Une photo de ticket ou de facture → un JSON avec commerçant, date (AAAA-MM-JJ), lignes, total TTC, TVA.
- Des alertes quand le code détecte une incohérence (somme, TVA, date, lisibilité).
- Un PDF → titre, résumé de 3 phrases, points clés, chiffres avec leur page.
- Aucun fichier ne reste dans l'API Files après l'exécution.
- 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
| Fichier | Rôle |
|---|---|
| multimodal.py | tout le code : préparation des fichiers, appels, schémas, contrôles, ligne de commande |
| generer_exemples.py | crée les fichiers d'exemple fictifs |
| test_multimodal.py | 7 tests avec un faux client |
| requirements.txt | dé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 :
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 texteOn teste ainsi notre logique (contrôles, choix inline/Files, nettoyage, réessais) sans payer d'appels.
Que vérifie un test qui utilise FauxClient ?
Le faux client renvoie ce qu'on lui dit : il ne mesure pas le modèle. Il garantit que notre code réagit bien à chaque cas, y compris les erreurs. La qualité du modèle se vérifie par l'exécution réelle sur des exemples connus.
Quelles commandes lancer avant de considérer le projet terminé ?
Les deux sont complémentaires : pytest prouve que la logique est juste, l'exécution réelle prouve que le modèle lit correctement vos documents. Le schéma ne garantit que la forme.
Lancer
pytest -q # sans clé
python multimodal.py justificatif ticket.png # avec clé
python multimodal.py pdf rapport.pdfExercice : 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
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é :
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.33sExécution réelle, python multimodal.py justificatif ticket.png (gemini-3.5-flash) :
{
"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