Un comptable indépendant traite 500 factures par mois pour 30 clients. Saisir à la main dans le logiciel compta, c'est 3 jours de travail répétitif. Un LLM vision (Qwen-VL, InternVL) lit directement les PDF — scannés ou natifs — et extrait date, montant HT/TTC, fournisseur, TVA, code analytique. En local, donc sans envoyer les factures chez le fisc, pardon, chez OpenAI. Ce guide monte le pipeline.
Par Mohamed Meguedmi·Màj 2026-03-18·Testé sur Windows, macOS, Linux
#Ce qu'on automatise
À partir d'un PDF de facture, produire un JSON structuré qui peut être importé en batch dans Cegid, EBP, QuickBooks ou Axonaut. Précision cible : 98 %+ sur date, montants, SIRET ; 90 %+ sur le poste analytique.
#Stack (vision + LLM)
Qwen2.5-VL 7B / 72B
LLM vision d'Alibaba. Le meilleur rapport qualité/taille en open source. 7B suffit pour des factures classiques.
InternVL 2.5 8B
Alternative. Très bon sur tableaux complexes et documents manuscrits.
Tesseract (fallback)
Pour les PDF scannés basse qualité que la vision peine à lire.
pdf2image
Convertir PDF en images si nécessaire.
i
PDF natif vs scanné
Un PDF natif (texte sélectionnable) peut être traité par LLM texte, plus rapide. Un PDF scanné (image) nécessite un LLM vision. Détectez automatiquement avec PyMuPDF.
from pdf2image import convert_from_path
import pytesseract
def ocr_pdf(path):
images = convert_from_path(path, dpi=300)
return "\n".join(pytesseract.image_to_string(img, lang="fra") for img in images)
#2. Vision LLM (Qwen-VL via Ollama)
Télécharger le modèle
ollama pull qwen2.5vl:7b
vision.py
import base64, json, requests
from pdf2image import convert_from_path
from io import BytesIO
def pdf_to_images_b64(path):
images = convert_from_path(path, dpi=200)
b64s = []
for img in images:
buf = BytesIO()
img.save(buf, format="PNG")
b64s.append(base64.b64encode(buf.getvalue()).decode())
return b64s
VISION_PROMPT = """Tu extrais les informations d'une facture française.
Produis un JSON STRICT selon ce schéma, rien d'autre :
{
"fournisseur": {"nom": string, "siret": string|null, "tva_intra": string|null},
"facture": {"numero": string, "date": "YYYY-MM-DD", "echeance": "YYYY-MM-DD"|null},
"montants": {
"ht": number, "tva": number, "ttc": number, "devise": "EUR"|string
},
"tva_breakdown": [{"taux": number, "base": number, "montant": number}],
"lignes": [{"description": string, "quantite": number, "pu_ht": number, "total_ht": number}],
"iban": string|null
}
Règles :
- Si une donnée est absente ou illisible, mets null.
- Les montants en nombres (pas de "1 234,56 €" mais 1234.56).
- Date au format ISO YYYY-MM-DD."""
def extraire_facture(pdf_path):
images = pdf_to_images_b64(pdf_path)
r = requests.post("http://localhost:11434/api/chat", json={
"model": "qwen2.5vl:7b",
"messages": [{
"role": "user",
"content": VISION_PROMPT,
"images": images,
}],
"format": "json",
"stream": False,
"options": {"temperature": 0.0},
})
return json.loads(r.json()["message"]["content"])
#3. Schéma d'extraction
Le format JSON ci-dessus couvre 95 % des factures françaises. Adaptations fréquentes :
Acompte / solde
Ajouter un champ acompte_paye si vous gérez les acomptes.
Bon de commande / devis
Champ bon_commande_ref pour rapprochement.
Frais de port
Ligne spécifique ou champ port_ht.
Code analytique
Un poste supplémentaire "compte_analytique" que vous faites deviner au LLM à partir du libellé fournisseur.
#4. Validation et post-traitement
validate.py
def valider(facture):
erreurs = []
ht = facture['montants']['ht']
tva = facture['montants']['tva']
ttc = facture['montants']['ttc']
# Contrôle arithmétique
if abs((ht + tva) - ttc) > 0.02: # tolérance 2 cts
erreurs.append(f"HT+TVA ({ht+tva}) ≠ TTC ({ttc})")
# SIRET valide ? (14 chiffres, Luhn)
siret = facture['fournisseur'].get('siret')
if siret and not siret_valide(siret):
erreurs.append(f"SIRET invalide : {siret}")
# Date cohérente ?
from datetime import date
d = date.fromisoformat(facture['facture']['date'])
if d.year < 2000 or d > date.today():
erreurs.append(f"Date suspecte : {d}")
return erreurs
def siret_valide(s):
if not s.isdigit() or len(s) != 14: return False
total = sum(int(c) * (2 if i % 2 else 1) for i, c in enumerate(s[::-1]))
total = sum(d for n in range(total + 1) for d in str(n) if total % 10 == 0)
return total % 10 == 0 # approximation Luhn pour démo
→
Escalader les erreurs
Si valider() retourne des erreurs, mettez la facture en file "à vérifier manuellement". N'importez jamais automatiquement une facture avec une incohérence arithmétique.
#5. Traitement par lot
batch.py
from pathlib import Path
import json
def traiter_dossier(dossier_in, dossier_ok, dossier_a_verifier):
for pdf in Path(dossier_in).glob("*.pdf"):
try:
f = extraire_facture(str(pdf))
erreurs = valider(f)
dest = dossier_ok if not erreurs else dossier_a_verifier
(Path(dest) / (pdf.stem + ".json")).write_text(
json.dumps(f, ensure_ascii=False, indent=2)
)
pdf.rename(Path(dest) / pdf.name)
print(f"{'✓' if not erreurs else '!'} {pdf.name}")
except Exception as e:
print(f"✗ {pdf.name}: {e}")
#Intégration au logiciel compta
Cegid Loop / Expert
Import XML ou CSV via le module "Écritures". Mapper votre JSON vers leur format maison.
EBP
Import CSV standardisé. La plupart des champs correspondent directement.
Pennylane / QuickBooks
APIs REST documentées. Un wrapper Python autour pour pousser la facture validée.
Axonaut
API propre et moderne. Champs directement mappables.
!
Piste d'audit
Gardez toujours le PDF original ET l'extraction JSON. En cas de contrôle URSSAF/DGFiP, vous devez pouvoir remonter de l'écriture comptable à la pièce justificative.
Ce guide vous a aidé ?
Un retour, une erreur, une précision ? Faites-nous signe, ça améliore le guide pour tout le monde.