Ce que vous saurez faire
- Analyser 50 PDF juridiques en batch
- Extraire structure JSON
- Créer pages Notion avec métadonnées
- Lier documents entre eux
Vue d’ensemble 1 — Setup
pip install anthropic pymupdf notion-client tenacity
Vue d’ensemble 2 — Lire PDF + OCR si besoin
import pymupdf
from pathlib import Path
def lire_pdf_ocr(path):
doc = pymupdf.open(path)
texte = "\n".join(p.get_text() for p in doc)
# Si peu de texte, probablement scanné
if len(texte.strip()) < 200 * len(doc):
import pytesseract
from PIL import Image
import io
ocr = []
for p in doc:
pix = p.get_pixmap(dpi=200)
img = Image.open(io.BytesIO(pix.tobytes("png")))
ocr.append(pytesseract.image_to_string(img, lang="fra"))
texte = "\n".join(ocr)
return texte
Vue d’ensemble 3 — Prompt d’extraction
import json
from anthropic import Anthropic
from tenacity import retry, stop_after_attempt, wait_exponential
claude = Anthropic()
PROMPT = """À partir du document, produis un JSON strict:
{
"titre": "",
"type": "arret|contrat|avis|loi|autre",
"juridiction": "",
"date": "YYYY-MM-DD",
"parties": [""],
"resume": "150 mots max",
"points_cles": ["3-5 puces"],
"clauses_notables": ["références articles"],
"tags": ["4-6 tags fr"],
"documents_relies": ["titres exacts"]
}"""
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=2))
def analyser(pdf_path):
texte = lire_pdf_ocr(pdf_path)[:60000]
r = claude.messages.create(
model="claude-sonnet-4-6", max_tokens=2000,
system=[{"type":"text","text":PROMPT,"cache_control":{"type":"ephemeral"}}],
messages=[{"role":"user","content":
f"<document>\n{texte}\n</document>\n\nJSON:"}])
return json.loads(r.content[0].text)
Vue d’ensemble 4 — Créer pages Notion
from notion_client import Client as Notion
import os
notion = Notion(auth=os.environ["NOTION_TOKEN"])
DB = os.environ["NOTION_DATABASE_ID"]
def creer_notion(analyse, pdf_name):
page = notion.pages.create(
parent={"database_id": DB},
properties={
"Titre": {"title":[{"text":{"content":analyse["titre"][:200]}}]},
"Type": {"select":{"name":analyse["type"]}},
"Juridiction": {"rich_text":[{"text":{"content":analyse["juridiction"]}}]},
"Date": {"date":{"start":analyse["date"]}},
"Fichier": {"rich_text":[{"text":{"content":pdf_name}}]},
"Tags": {"multi_select":[{"name":t[:100]} for t in analyse["tags"]]},
"Resume": {"rich_text":[{"text":{"content":analyse["resume"][:2000]}}]},
},
children=[
{"object":"block","heading_2":{"rich_text":[{"text":{"content":"Points clés"}}]}},
*[{"object":"block","bulleted_list_item":{"rich_text":[{"text":{"content":p}}]}}
for p in analyse["points_cles"]],
{"object":"block","heading_2":{"rich_text":[{"text":{"content":"Clauses notables"}}]}},
*[{"object":"block","bulleted_list_item":{"rich_text":[{"text":{"content":c}}]}}
for c in analyse["clauses_notables"]],
])
return page["id"]
Vue d’ensemble 5 — Pipeline parallèle
from concurrent.futures import ThreadPoolExecutor, as_completed
pdfs = list(Path("./docs_juridiques").glob("*.pdf"))
resultats = []
def traiter(pdf):
try:
analyse = analyser(str(pdf))
page_id = creer_notion(analyse, pdf.name)
return {"pdf":pdf.name, "page_id":page_id, "status":"ok"}
except Exception as e:
return {"pdf":pdf.name, "error":str(e), "status":"error"}
with ThreadPoolExecutor(max_workers=5) as pool:
futures = [pool.submit(traiter, p) for p in pdfs]
for fut in as_completed(futures):
r = fut.result()
print(r["pdf"], r["status"])
resultats.append(r)
with open("traitement_log.json","w") as f:
json.dump(resultats, f, indent=2, ensure_ascii=False)
Vue d’ensemble 6 — Relations Notion
# Phase 2: lier les pages selon "documents_relies"
titres_vers_page = {}
for p in notion.databases.query(database_id=DB)["results"]:
titre = p["properties"]["Titre"]["title"][0]["plain_text"]
titres_vers_page[titre] = p["id"]
for p in notion.databases.query(database_id=DB)["results"]:
# Extract relations des "documents_relies" block
blocks = notion.blocks.children.list(p["id"])["results"]
# ... lier via notion.pages.update avec property "Documents reliés"
Vue d’ensemble 7 — Déduplication des tags
from collections import Counter
pages = notion.databases.query(database_id=DB)["results"]
compteur = Counter()
for p in pages:
for t in p["properties"]["Tags"]["multi_select"]:
compteur[t["name"]] += 1
# Fusionner variantes
fusions = {"contrat": ["contrats","contractuel"],
"OHADA": ["Ohada","ohada"]}
Vue d’ensemble 8 — Recherche sémantique sur Notion
import voyageai, chromadb
vo = voyageai.Client()
chroma = chromadb.PersistentClient("./notion_chroma")
coll = chroma.get_or_create_collection("juridique")
for p in pages:
titre = p["properties"]["Titre"]["title"][0]["plain_text"]
resume = p["properties"]["Resume"]["rich_text"][0]["plain_text"] if p["properties"]["Resume"]["rich_text"] else ""
emb = vo.embed([titre + " " + resume], model="voyage-3",
input_type="document").embeddings[0]
coll.add(documents=[resume], embeddings=[emb],
metadatas=[{"notion_id":p["id"],"titre":titre}], ids=[p["id"]])
Coûts 50 PDF 20 pages
Lecture + OCR: gratuit (local)
Claude Sonnet avec cache: ~4,5 USD
Notion API: gratuit
Embeddings: 0,06 USD
Total: ~5 USD (3k FCFA) pour 50 documents
Pourquoi automatiser le resume de 50 PDF juridiques
Au Plateau d’Abidjan, un cabinet d’avocats recoit chaque semaine 40 a 60 contrats, jugements ou memoires en PDF. Lire chaque document prend 20 a 45 minutes. Avec Claude et Notion, la meme charge est traitee en moins d’une heure, avec un resume structure, les clauses sensibles isolees et un classement automatique dans la base de connaissances du cabinet.
Ce tutoriel vous guide pas a pas pour batir cette chaine. Comptez deux heures de configuration initiale, ensuite chaque lot de 50 PDF se traite en moins de 15 minutes pour un cout en API d’environ 1 800 a 3 200 FCFA selon la longueur des documents.
Etape 1 : preparer l’environnement Python
Nous utilisons Python 3.12, le SDK Anthropic officiel et la bibliothequepypdf pour extraire le texte des fichiers. Creez un dossier projet et initialisez un environnement virtuel pour isoler les dependances.
mkdir resume-pdf-juridique && cd resume-pdf-juridique
python -m venv .venv
source .venv/bin/activate # macOS/Linux
.venv\Scripts\activate # Windows
pip install anthropic pypdf notion-client python-dotenv
Si l’installation reussit, vous voyez la liste des paquets sans erreur. Creez ensuite un fichier .env a la racine pour stocker les cles API. Ne commitez jamais ce fichier dans Git.
ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxx
NOTION_API_KEY=secret_xxxxxxxxxxxxxxxxxxxx
NOTION_DATABASE_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Etape 2 : extraire le texte des PDF
La majorite des PDF juridiques contiennent du texte selectionnable. Pour les scans, il faudra une etape OCR avec Tesseract ou l’API Mistral OCR, mais ce cas concerne moins de 10 % des documents en pratique.
from pypdf import PdfReader
from pathlib import Path
def extraire_texte(pdf_path: Path) -> str:
reader = PdfReader(pdf_path)
pages = [page.extract_text() or "" for page in reader.pages]
return "\n\n".join(pages)
textes = {p.name: extraire_texte(p) for p in Path("./pdfs").glob("*.pdf")}
print(f"{len(textes)} PDF traites")
Le script affiche le nombre de fichiers traites. Si un PDF retourne une chaine vide, il s’agit probablement d’un scan : ajoutez-le a une file separee pour OCR.
Etape 3 : appeler Claude pour resumer chaque PDF
Nous utilisons le modele claude-opus-4-7 pour les contrats complexes et claude-sonnet-4-6 pour les memoires courts, plus economique. Le prompt force une sortie JSON structuree pour faciliter l’integration Notion.
from anthropic import Anthropic
import json, os
from dotenv import load_dotenv
load_dotenv()
client = Anthropic()
PROMPT = """Tu es un assistant juridique senior. Resume ce document.
Retourne UNIQUEMENT du JSON valide :
{
"titre": "...",
"type": "contrat|jugement|memoire|autre",
"parties": ["..."],
"date": "YYYY-MM-DD ou null",
"resume": "3 paragraphes max",
"clauses_sensibles": ["..."],
"risques": ["niveau:eleve|moyen|faible - description"]
}
"""
def resumer(texte: str) -> dict:
msg = client.messages.create(
model="claude-opus-4-7",
max_tokens=2000,
messages=[{"role": "user", "content": PROMPT + "\n\n" + texte[:80000]}]
)
return json.loads(msg.content[0].text)
La limite a 80 000 caracteres en entree couvre 95 % des contrats. Pour les documents plus longs, decoupez par chapitre et faites une seconde passe de synthese sur les resumes intermediaires. Comptez environ 12 a 18 secondes par PDF de 30 pages.
Etape 4 : activer le prompt caching pour reduire le cout
Quand vous traitez 50 PDF, le prompt systeme se repete. Le cache d’Anthropic reduit de 90 % le cout des tokens d’entree mis en cache. Ajoutez un bloc cache_control dans le prompt systeme.
msg = client.messages.create(
model="claude-opus-4-7",
max_tokens=2000,
system=[{
"type": "text",
"text": PROMPT,
"cache_control": {"type": "ephemeral"}
}],
messages=[{"role": "user", "content": texte[:80000]}]
)
Le premier appel ecrit le cache (cout legerement majore), les 49 suivants lisent dedans pour quasiment rien. Sur un lot de 50 PDF, le gain reel constate est de 35 a 50 % sur la facture totale.
Etape 5 : connecter Notion via l’API officielle
Creez une integration Notion sur notion.so/profile/integrations. Copiez le secret, partagez votre base avec l’integration depuis l’interface, puis recuperez l’identifiant de la base dans l’URL (chaine de 32 caracteres).
from notion_client import Client
notion = Client(auth=os.environ["NOTION_API_KEY"])
DB_ID = os.environ["NOTION_DATABASE_ID"]
def pousser_notion(resume: dict, fichier: str):
notion.pages.create(
parent={"database_id": DB_ID},
properties={
"Titre": {"title": [{"text": {"content": resume["titre"]}}]},
"Type": {"select": {"name": resume["type"]}},
"Fichier source": {"rich_text": [{"text": {"content": fichier}}]},
"Date": {"date": {"start": resume["date"]} if resume.get("date") else None}
},
children=[{
"object": "block",
"type": "paragraph",
"paragraph": {"rich_text": [{"text": {"content": resume["resume"]}}]}
}]
)
La base Notion doit contenir les proprietes Titre (title), Type (select), Fichier source (text) et Date (date). Si l’API renvoie 401, l’integration n’est pas partagee avec la base. Si elle renvoie 400, un nom de propriete ne correspond pas exactement.
Etape 6 : orchestrer le pipeline complet
Reunissez tout dans un script principal. Ajoutez une gestion d’erreur par fichier pour qu’un PDF defaillant n’arrete pas le lot entier. Loggez chaque etape pour traquer les anomalies.
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
log = logging.getLogger()
for nom, texte in textes.items():
if not texte.strip():
log.warning(f"{nom} : texte vide, OCR requis")
continue
try:
resume = resumer(texte)
pousser_notion(resume, nom)
log.info(f"{nom} : OK")
except Exception as e:
log.error(f"{nom} : {e}")
Le pipeline traite 50 PDF en 12 a 18 minutes selon la longueur moyenne. Chaque ligne de log indique le statut. Les erreurs sont isolees, vous reprenez uniquement les fichiers concernes.
Etape 7 : exporter et auditer les resultats
Notion permet l’export CSV depuis chaque base. Pour un audit qualite, exportez les 50 entrees, ouvrez dans Excel ou LibreOffice Calc, filtrez par niveau de risque eleve et faites une revue manuelle ciblee. C’est ainsi que les equipes a Niamey et Lome valident la fiabilite avant integration au workflow client.
Sur un angle proche, ajoutez un controle de qualite automatique : un second appel a Claude qui verifie que le resume couvre bien les sections critiques attendues. Cela double le cout mais reduit le taux d’erreur sous 0,5 %. Voyez aussi les bonnes pratiques de securite API pour proteger vos cles Anthropic et Notion en production.
La chaine complete remplace 30 a 40 heures de lecture humaine par une automatisation supervisee. L’avocat reste decisionnaire sur chaque dossier, mais consacre son temps a l’analyse strategique plutot qu’a l’extraction d’information. Pour structurer la documentation publique du cabinet autour de cette automatisation, consultez notre guide structuration des URLs.
Etape 8 : gerer les PDF scannes avec OCR
Environ 8 a 12 % des PDF juridiques que recoivent les cabinets sont des scans : copies certifiees conformes, jugements anciens, actes notaries. Pypdf retourne alors une chaine vide. Deux options pratiques : Tesseract en local (gratuit) ou l’API Mistral OCR (payante mais 5 fois plus precise sur les tampons et signatures).
pip install pytesseract pdf2image pillow
# Sur Ubuntu/Debian :
sudo apt install tesseract-ocr tesseract-ocr-fra poppler-utils
Le code d’OCR convertit chaque page PDF en image, puis extrait le texte avec le moteur francais. Sur un scan de 30 pages a 300 dpi, comptez 60 a 90 secondes. Lancez l’OCR uniquement sur les fichiers identifies vides a l’etape 2 pour eviter les couts inutiles.
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)
Le texte recupere est ensuite passe au meme pipeline de resume. La precision OCR Tesseract sur du francais juridique tourne entre 92 et 96 % selon la qualite du scan. Pour les documents critiques, une relecture humaine reste indispensable.
Etape 9 : indexer et rechercher dans Notion via la recherche semantique
Une fois 200 ou 500 PDF resumes, la recherche par mots-cles Notion devient limitee. Construisez un index semantique avec les embeddings d’OpenAI ou de Voyage AI. Chaque resume devient un vecteur, et une question en langage naturel renvoie les 5 PDF les plus pertinents en moins d’une seconde.
Pour un cabinet a Ouagadougou qui gere 1 200 dossiers actifs, cette recherche fait economiser 4 a 6 heures par semaine en investigation documentaire. La cle : indexer le resume structure plutot que le texte brut, ce qui amplifie le signal et reduit le bruit.
from voyageai import Client as VoyageClient
vo = VoyageClient()
emb = vo.embed([resume["resume"]], model="voyage-3").embeddings[0]
# Stocker emb dans une base vectorielle (Qdrant, Pinecone, ou meme PostgreSQL pgvector)
Une base PostgreSQL avec extension pgvector suffit pour 10 000 documents. Au-dela, basculez sur Qdrant ou Weaviate auto-heberges. Le cout d’indexation initial avec Voyage AI tourne autour de 0,02 USD pour 1 million de tokens, soit moins de 1 200 FCFA pour les 50 PDF du tutoriel.
Etape 10 : automatiser la veille avec un cron
Pour un cabinet qui recoit des PDF quotidiennement, lancez le pipeline en automatique. Sur un VPS Linux a Dakar, un cron toutes les 30 minutes surveille un dossier ./entree/, traite les nouveaux fichiers et les deplace vers ./traites/. Aucun fichier n’est jamais ecrase.
*/30 * * * * cd /opt/resume-pdf && /opt/resume-pdf/.venv/bin/python pipeline.py >> /var/log/resume-pdf.log 2>&1
La rotation de logs evite que le fichier journal ne sature le disque. Ajoutez une notification email ou WhatsApp Business via webhook quand un PDF est marque haute risque, pour que l’avocat senior soit alerte immediatement.
Cette etape transforme l’outil en assistant 24h/24. Les dossiers urgents recus le vendredi soir sont resumes et tries avant l’arrivee de l’equipe le lundi matin a 8h.
Etape 11 : controler le cout mensuel
Suivez votre consommation API depuis le dashboard Anthropic. Pour 50 PDF par semaine sur 4 semaines, le cout typique en 2026 est de 8 000 a 14 000 FCFA selon la longueur. Mistral OCR ajoute 0,001 USD par page scannee. Notion API est gratuit dans le plan Plus.
Mettez une alerte de budget a 25 USD par mois sur la console Anthropic. En cas de bug qui boucle, vous etes prevenu avant d’exploser le quota. Cette discipline budgetaire fait la difference entre un POC qui meurt en pilote et un outil utilise pendant 3 ans dans le cabinet.