Nous avons été confrontés à un cas d’usage impliquant une comparaison d’appels d’offres, avec des PDF volumineux contenant de nombreuses pages et une grande quantité d’informations. Les documents à traiter comprenaient à la fois des PDF natifs et des dossiers scannés puis convertis en PDF.
L’objectif était donc de trouver une solution efficace permettant d’extraire les données souhaitées et de les structurer sous forme de JSON.
La méthode la plus performante que nous avons identifiée consistait à utiliser l’API d’OpenAI. En fournissant le ou les documents en entrée, et en élaborant un prompt bien travaillé, il était possible d’obtenir en retour une réponse directement formatée selon nos besoins.
Prérequis :
- Python > 3.8
- Modèles (Azure) OpenAI : GPT-4o
Extraction depuis un PDF
Dans un premier temps, nous avons envisagé d'utiliser directement l’API d’OpenAI. Pour cela, il était nécessaire d’extraire le texte du PDF, car l’API n’est pas capable de lire les fichiers PDF directement. Il fallait donc extraire et transmettre l’intégralité du texte contenu dans le document.
Nous extrayons le texte du PDF avec le code suivant :
import PyPDF2
def extract_text_from_pdf(file_path):
with open(file_path, "rb") as pdf_file:
pdf_reader = PyPDF2.PdfReader(pdf_file)
text = ""
for page in pdf_reader.pages:
text += page.extract_text()
return text
Puis nous envoyons le texte à l’API, ici nous utilisons l’API OpenAI via Azure.
DEPLOYMENT_NAME = "your-deployment-name"
client = AzureOpenAI(
api_key="your-api-key",
api_version="your-api-version",
azure_endpoint="your-openai-api-endpoint",
)
def analyze_pdf_content(pdf_text):
response = client.chat.completions.create(
model=DEPLOYMENT_NAME,
messages=[
{
"role": "system",
"content": """
VOTRE PROMPT,
"""
},
{"role": "user", "content": f"Analyse ce contenu de PDF et extrait les informations : {pdf_text}"},
],
max_tokens=4096,
)
return response.choices[0].message.content
Cette méthode fonctionne très bien car elle nous renvoie bien notre JSON avec les infos que l’on donne à OpenAI.
Dans ce cas, notre prompt a été conçu pour effectuer une recherche spécifique au sein de documents partageant une thématique et des objectifs communs. Il a été rédigé de manière claire et concise pour en maximiser la compréhension, et structuré en sections distinctes pour plus de clarté. Le voici juste ici.
1. Lire chaque page du fichier PDF et extraire les données structurées comme les numéros de prix, libellés, quantités, prix unitaires, montants totaux, etc.
2. Organiser ces données en une hiérarchie JSON en fonction des numéros de prix :
- Les numéros simples (ex. 1, 2, 3) représentent les niveaux principaux (lots).
- Les numéros avec sous-sections (ex. 1.1, 2.1) doivent être organisés comme des sous-niveaux.
- Les numéros à plusieurs niveaux (ex. 1.1.1) doivent être ajoutés sous le niveau précédent sous un champ "children".
3. Ajouter toutes les lignes, y compris les sous-totaux et les totaux. Si des champs sont absents (ex. prix unitaire ou montant HT), ils doivent avoir la valeur `null`.
4. Si des commentaires sont présents entre les lignes de données, les inclure sous le champ "comments".
5. Si le nom de la société est introuvable dans le fichier, utiliser le nom du fichier comme valeur par défaut pour la société.
6. Générer un fichier JSON structuré avec un ID unique pour chaque ligne extraite. La sortie doit être un fichier JSON contenant :
- "lot number" : numéro du lot.
- "society" : nom de la société.
- "quote name" : nom du devis.
- "data" : liste hiérarchique d'objets représentant les lignes de données extraites.
Voici le résultat que nous avons obtenu depuis le PDF :
{
"lot number": "xxx",
"society": "xxx",
"quote name": "xxx",
"items": [
{
"id": 1,
"number": "xxx",
"name": "xxx",
"sub_total": "xxx",
"children": [
{
"id": 2,
"number": "xxx",
"description": "xxx",
"unit": "xxx",
"quantity": "xxx",
"unit_price": "xxx",
"total_ht": "xxx"
},
{
"id": 3,
"number": "xxx",
"description": "xxx",
"unit": "xxx",
"quantity": "xxx",
"unit_price": "xxx",
"total_ht": "xxx"
}
]
}
{
...
}
]
}
Malheureusement, cela ne suffit pas dans notre cas, car parmi les documents à analyser, certains étaient des documents scannés. Cela a rendu la lecture et l'extraction des informations impossibles.
Pour pallier ce problème, nous avons dû passer par le module Vision d’OpenAI. Ce module est en quelque sorte ce qui va permettre à l’intelligence artificielle de “voir” le document, et ainsi pouvoir le comprendre et l’analyser. Il est actuellement embarqué par le modèle GPT-4o, modèle que nous utilisons pour cet exemple.
Extraire depuis une image
La solution que nous avons donc trouvée pour uniformiser l’analyse de document est de passer par Vision en ne donnant que des images à analyser. Dans un premier temps, nous avons testé avec une seule image. Il faut savoir que l’API OpenAI n’est capable d’interpréter seulement les images encodées en Base64. Nous avons donc créé une fonction pour gérer cela :
def local_image_to_data_url(image_path):
"""Convertir une image locale en URL de données base64."""
mime_type, _ = guess_type(image_path)
if mime_type is None:
mime_type = "application/octet-stream"
with open(image_path, "rb") as image_file:
base64_encoded_data = base64.b64encode(image_file.read()).decode("utf-8")
return f"data:{mime_type};base64,{base64_encoded_data}"
Cette fonction permet de générer l’image en base64, nous allons donc passer cette image quasiment de la même manière que précédemment.
Dans l’exemple ci-dessous, nous avons commencé par découper notre PDF, pour en prendre seulement la première page. Nous avons converti cette première page en .png puis converti en base64 avant de la donner à OpenAI.
def analyze_img(data_url):
response = client.chat.completions.create(
model=DEPLOYMENT_NAME,
messages=[
{
"role": "system",
"content": """
PROMPT
""",
},
{
"role": "user",
"content": [
{"type": "text", "text": "Analyse cette image et extrait les informations."},
{"type": "image_url", "image_url": {"url": data_url}},
],
},
],
temperature=0,
max_tokens=4096,
)
description = response.choices[0].message.content
print(description)
Voilà le résultat obtenu :
{
"lot number": "xxx",
"society": "xxx",
"quote name": "xxx",
"items": [
{
"id": "xxx",
"number": "xxx",
"name": "xxx",
"sub_total": "null",
"children": [
{
"id": "xxx",
"number": "xxx",
"description": "xxx",
"unit": "xxx",
"quantity": "xxx",
"unit_price": "xxx",
"total_ht": "xxx"
},
{
"id": "xxx",
"number": "xxx",
"description": "xxx",
"unit": "xxx",
"quantity": "xxx",
"unit_price": "xxx",
"total_ht": "xxx"
},
{
"id": "xxx",
"number": "xxx",
"name": "xxx",
"children": [
{
"id": "xxx",
"number": "xxx",
"location": "xxx",
"unit": "xxx",
"quantity": "xxx",
"unit_price": "xxx",
"total_ht": "xxx"
}
]
}
]
}
]
}
Le résultat que nous avons obtenu est plutôt positif mais cela ne suffit pas encore car actuellement, le résultat ne fonctionne que pour une seule image et non le document PDF entier.
Extraire les données depuis plusieurs images
Pour extraire les informations, la solution que nous avons adoptée consiste à convertir toutes les pages en images à l’aide d’une fonction. Ces images sont ensuite encodées en base64, puis transmises à l’API.
Nous avons donc créé une fonction pour passer d’un PDF en plusieurs images :
def pdf2img(pdf_path):
"""Convertir chaque page d'un PDF en une image."""
images = convert_from_path(pdf_path, dpi=300)
image_paths = []
for i, image in enumerate(images):
output_path = f"page_{i + 1}.jpg"
image.save(output_path, "JPEG")
image_paths.append(output_path)
return image_paths
La fonction d’analyse du contenu diffère légèrement de la précédente, car elle nécessite de recevoir directement un tableau d’URLs. Dans l’élément "content" du corps de la requête envoyée, une boucle est effectuée sur type_url afin de transmettre toutes les images encodées en base64 à l’API.
data_urls = [local_image_to_data_url(path) for path in image_paths]
def analyze_multiple_images(data_urls):
"""Envoyer plusieurs images pour analyse."""
# Construire les messages avec plusieurs images
messages = [
{
"role": "system",
"content": """
PROMPT
""",
},
{
"role": "user",
"content": [{"type": "image_url", "image_url": {"url": url}} for url in data_urls],
},
]
# Envoyer la requête
response = client.chat.completions.create(
model=DEPLOYMENT_NAME,
messages=messages,
max_tokens=4000,
temperature=0,
)
# Afficher la réponse (le JSON attendu)
description = response.choices[0].message.content
print(description)
Le résultat obtenu est le résultat souhaité depuis le début.
{
"lots": [
{
"lot_number": "xxx",
"lot_name": "xxx",
"sub_lots": [],
"total_ht_eur": "xxx"
},
{
"lot_number": "xxx",
"lot_name": "xxx",
"sub_lots": [],
"total_ht_eur": "xxx"
},
{
"lot_number": "xxx",
"lot_name": "xxx",
"sub_lots": [],
"total_ht_eur": "xxx"
},
{
"lot_number": "xxx",
"lot_name": "xxx",
"sub_lots": [],
"total_ht_eur": "xxx"
}
],
"total_ht_eur": "xxx",
"tva_20_percent": "xxx",
"total_ttc_eur": "xxx"
}
Conclusion
Lors de l’analyse de documents avec Azure OpenAI, il est essentiel de considérer les coûts liés au traitement, en particulier pour des requêtes volumineuses impliquant plusieurs images ou un grand nombre de tokens. Il faut bien avoir en tête que les requêtes vont dans les deux sens (envoi et réception).
Utiliser un modèle Azure OpenAI performant peut accélérer le processus grâce à un débit élevé de traitement, mais cela augmente également la consommation de ressources et les dépenses.
Vous souhaitez bénéficier d'experts, de développeurs, ou d'une formation sur Azure et ses API ? Nous sommes à votre écoute. Contactez-nous !