Exploiter la puissance des LLM pour l'extraction de reçus

Comment j'ai utilisé le modèle GPT-4o d'OpenAI et Paddle OCR pour faciliter l'extraction et la mise en forme des données des ingrédients et des prix à partir des reçus des fournisseurs dans Bon Service.

8 juillet 2024

Bon Service est une application web qui permet aux chefs de rédiger, standardiser et partager leurs recettes avec les membres de leur équipe de cuisine. Un des principaux défis que nous avons rencontrés était de permettre un accès à des données réelles. Notre solution initiale était de permettre de saisir manuellement les ingrédients, les prix et d'autres informations pertinentes telles que la provenance, le nom du fournisseur et la date de la dernière mise à jour du prix.

Bien que cette approche soit utilisée pour la version gratuite, la version payante devrait offrir quelque chose de plus robuste, qui permettrait réellement de faire gagner du temps lors de la gestion et d'éliminer le fardeau de la saisie des données.

Pour y parvenir, nous devions extraire les ingrédients, les prix et d'autres informations pertinentes à partir des reçus des fournisseurs de nos clients. Cela m'a conduit au développement d'une simple API d'extraction de reçus utilisant Python, GPT-4o d'OpenAI et Paddle OCR.

L'étincelle d'inspiration

Mon collègue Rémi et moi étions en train de travailler sur un projet scolaire complètement différent au moment où OpenAI venait d'ajouter le modèle GPT-Vision à leur API. À la fin de la journée, nous avons décidé d'expérimenter avec le modèle et de voir s'il serait possible de le solliciter correctement pour extraire des ingrédients depuis des PDF et des images d'anciens reçus que j'avais sous la main.

Après environ 25 minutes, nous étions en mesure de voir qu'il serait théoriquement possible d'utiliser GPT pour formater les informations à condition de pouvoir lui fournir des données de bonne qualité. Cependant, GPT Vision n'arrivait pas à interpréter les reçus rapidement, surtout ceux qui n'étaient pas en anglais.

Cela m'a cependant donné l'idée d'explorer d'autres modèles OCR qui pourraient être mieux adaptés à la tâche et de fournir le texte extrait à GPT-4o pour formater les données.

Trouver le meilleur modèle OCR pour notre scénario d'utilisation

Tesseract OCR

Je devais d'abord trouver un modèle OCR capable d'extraire le texte des reçus. J'ai commencé par faire des recherches sur les modèles OCR open-source et j'ai trouvé Tesseract. Celui-ci semblait être un bon choix en fonction de ce qui était requis pour notre application. Cette librairie utilise TypeScript, qui est le langage que nous utilisons pour le projet. Il était donc possible de l'exécuter sur le même serveur que notre application.

Sur papier, Tesseract était un modèle intéressant, mais il présentait trois problèmes flagrants :

  • son exactitude en français était sous-optimale;

  • l'extraction de texte à partir de reçus manuscrits était presque impossible;

  • il ne pouvait pas traiter les PDF directement.

Cela signifiait que si je voulais utiliser Tesseract, je devais convertir les reçus PDF en images, puis extraire le texte de celles-ci. Cette tâche est rapidement devenue beaucoup plus compliquée que je ne le pensais, surtout en TypeScript.

En cherchant un moyen de convertir mes PDF, tous les signes m'ont dirigé vers Python. Python avait des moyens plus simples de faire les conversions, mais il offrait également une variété d'autres options pour l'OCR. Spontanément, j'ai décidé que j'allais donc réécrire l'API en Python.

PaddleOCR

Après quelques heures de recherches supplémentaires, je suis tombé sur PaddleOCR, un outil de reconnaissance optique de caractères (OCR) open-source développé par PaddlePaddle, une plateforme de deep learning créée par Baidu. PaddleOCR est conçu pour fournir une solution complète de détection et de reconnaissance de texte dans les images. Il prend en charge une multitude de langues et est reconnu pour son exactitude et son efficacité.

Les modèles de base sont très puissants et il pouvait traiter à la fois les images et les PDF directement. Cela m'a suffi pour l'essayer.

python
from paddleocr import PaddleOCR file_path = '/root/documents/receipts/text-receipt.pdf' _ocr = PaddleOCR( use_angle_cls=True, lang='fr', show_log=True, ) try: formated_text = extract_receipts(_ocr, file_path) except: # This is where the error would be handled once the added to the Flask API. return jsonify({"error": "Something went wrong when passing the file through OCR."}), 500 # Helper function to extract text from receipts, then format it to a giant string where chunks are separated by a newline. def extract_receipts(ocr, file_path): results = ocr.ocr(file_path, cls=True) txts = [] # PaddleORC returns a list of lists, where each sublist contains the text and the confidence score. for result in results: for line in result: txts.append(line[1][0]) return "\n".join(txts)

En quelques heures à peine, j'ai réécrit le code en Python et j'avais un prototype fonctionnel en main. Il ne me restait plus qu'à construire l'API en utilisant Flask et à fournir la sortie textuelle à GPT-4o pour formater les données et les retourner à mon application Next.js.

Construire l'API

Pour utiliser l'extracteur de reçus, j'ai dû écrire une simple API en utilisant Flask. Lors de l'appel de l'API, l'application Next.js doit passer le fichier en entrée ainsi que le nom du fournisseur. L'API devrait ensuite retourner les données extraites sous le format JSON.

J'ai choisi Flask car il était simple à mettre en place, de plus je l'avais utilisé pour un bot qui automatise les inscriptions à des classes de CrossFit. (Peut-être je vais écrire un article sur ça dans le futur).

Écrire l'application Flask

À partir du prototype initial, j'ai construit une simple application Flask pour gérer les demandes et l'extraction.

python
from flask import Flask, request, abort, jsonify from werkzeug.utils import secure_filename from paddleocr import PaddleOCR import os app = Flask(__name__) @app.route("/api/process-receipts", methods=["POST"]) def process_receipts(): # Initializing an OCR object on every request that way the API can deal with multiple requests at once. _ocr = PaddleOCR( use_angle_cls=True, lang='fr', show_log=True, ) # Get supplier name from header supplier = request.headers.get('X-Supplier') if 'file' not in request.files: return jsonify({"error": "No file part"}), 400 file = request.files['file'] if file.filename == '': return jsonify({"error": "No selected file"}), 400 filename = secure_filename(file.filename) file_path = os.path.join("/tmp", filename) file.save(file_path) try: formated_text = extract_receipts(_ocr, file_path) except: return jsonify({"error": "Something went wrong when passing the file through OCR."}), 500 os.remove(file_path) # Once the text has been extracted, we can pass it to GPT-4o to format the data. if __name__ == "__main__": app.run()

Ajouter une clé API et CORS pour plus de sécurité

Afin de protéger cette API contre les abus, j'ai ajouté une clé d'API et activé les CORS. De cette manière, seules les applications autorisées pourraient y faire des demandes.

Les clés API sont stockées dans une base de données et ne sont accessibles que par l'API elle-même. Si aucune clé valide n'est fournie, la demande sera rejetée.

Remarquez comment nous avons modifié l'en-tête 'X-Supplier' pour 'X-Supplier-Notes'.

Nous modifions l'en-tête car l'API va maintenant recevoir une string qui sera utilisée pour modifier l'invite de base. Il est donc maintenant possible d'ajouter des notes sur mesure, permettant ainsi de travailler avec n'importe quel fournisseur.

python
from flask import Flask, request, abort, jsonify from flask_cors import CORS from werkzeug.utils import secure_filename from data.dao import ApiDataManager from paddleocr import PaddleOCR import os app = Flask(__name__) # Added a database to store the API keys. db = ApiDataManager() # This will only allow requests coming from the same domain. CORS(app) @app.route("/api/process-receipts", methods=["POST"]) def process_receipts(): _ocr = PaddleOCR( use_angle_cls=True, lang='fr', show_log=True, ) # Added two new headers to the request app_name = request.headers.get('X-App-Name') app_api_key = request.headers.get('X-Api-Key') supplier_notes = request.headers.get('X-Supplier-Notes') if app_api_key != db.get_application_api_key(app_name): abort(401)

Connecter l'API à OpenAI

Pour utiliser l'API, nous devions la connecter au modèle GPT-4o d'OpenAI. Il s’agit d’un LLM puissant qui peut générer du texte en fonction d'une invite. Nous avons utilisé la bibliothèque Python openai pour nous connecter a leur API et envoyer la requête.

python
import os import openai import json import re class Inferencer: def __init__(self): self.__client = openai.Client() self.__client.api_key = os.getenv("OPENAI_API_KEY") def inference(self, additional_notes, receipts): # This is the base prompt that will be used to make sense of the text data sent to GPT-4o. # The additional_notes variable is a string that contains additional notes that will be added to the prompt. prompt = f"The following text is from a receipt, convert it to a JSON object.There might be some errors in the \ item description: For example, '5LB' might have been extracted as 'SLB' since the 5 closely resembles an S. \ Fix those errors. The JSON object should only contain the items and their price per unit (if there are \ two prices for the same item use the smallest of the 2). Units that are labeled GR should be changed to G. LT to L \ CL is not an origin tag it should be ignored. It is possible for items to not have an origin. The QUANTITY-UNIT \ should be in 2 unique fields also add a category field. The category should be logical with the item name, \ and should be in the language of the receipt. Use the following categories: 'Fruit & Légume', \ 'Viande', 'Poisson', 'Produit Laitier', 'Pâtisserie', 'Cannes', 'Congeler', 'Sec', 'Fines Herbes'.\ Make sure to filter duplicate items.If the item is not in the list use 'Autre'. A mushroom should be classified \ as 'Fruit & Légume' The JSON structure for an item should be: 'name': 'NAME OF PRODUCT', 'quantity': number, \ 'unit': 'UNIT OF PRODUCT', 'origin': 'ORIGIN TAG OF PRODUCT', 'category': 'ONE OF THE CATEGORY', 'price': number \ . Make sure to correct common french mistakes (e.g lls should be île) {additional_notes}" response = openai.chat.completions.create( model="gpt-4-0125-preview", messages=[ { "role": "user", "content": [ { "type": "text", "text": prompt }, { "type": "text", "text": receipts } ], } ], max_tokens=4096, ) # In order to receive a JSON object we need to remove the markdown. json_string = response.choices[0].message.content.replace("```json\n", "").replace("\n```", ""); # In some cases the JSON object was inside of an array, so I also need to remove that array. array = re.search(r'\[(.*?)\]', json_string, re.DOTALL).group(0) json_data = json.loads(array)

Envoyer le reçu à GPT-4o

Une fois l'API connectée à OpenAI, nous pouvons envoyer le reçu au modèle et obtenir les ingrédients et les prix extraits. Nous avons utilisé la fonction extract_receipts pour envoyer le contenu texte des reçus à GPT-4o et ensuite retourner les données extraites dans un format JSON.

python
from gpt4 import Inferencer _gpt = Inferencer() try: print("sending to GPT-4...") interpreted_receipt_data = _gpt.inference(supplier_notes, formated_text) return jsonify(interpreted_receipt_data), 200 except: return jsonify({"error": "Something went wrong when converting the response from GPT-4."}), 500 if __name__ == "__main__": app.run()

Cette petite application fonctionnait étonnamment bien, elle renvoyait les données dans dans le même format sans jamais vraiment faire d'erreur. Il ne restait plus qu'à la déployer sur mon serveur personnel et à faire les appels depuis Bon Service.

Déploiement

Faire fonctionner PaddleOCR sur autre chose qu'une machine Linux était si compliqué que j'ai décidé d'utiliser un conteneur Docker. J'ai créé un Dockerfile qui construit un conteneur Python et installe les dépendances requises.

L'image python:3.10-slim est un bon point de départ pour le conteneur, mais j'ai dû installer quelques dépendances supplémentaires pour le faire fonctionner.

dockerfile
FROM python:3.10-slim WORKDIR /app RUN apt-get update && apt-get install -y \ libgomp1 \ && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y \ libgomp1 \ libgl1-mesa-glx \ libglib2.0-0 \ libsm6 \ libxext6 \ libxrender-dev \ && rm -rf /var/lib/apt/lists/* COPY app/ . RUN pip install -r requirements.txt CMD gunicorn --bind 0.0.0.0:5000 app:app

Jusqu'à présent, chaque fois qu'une requête était envoyée à l'API, PaddleOCR téléchargeait les modèles. Pour éviter cela, je les ajoute maintenant lors de la construction du conteneur. De cette manière, les modèles n'auraient pas besoin d'être téléchargés à chaque fois qu'une requête était faite.

dockerfile
FROM python:3.10-slim WORKDIR /app RUN apt-get update && apt-get install -y \ libgomp1 \ && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y \ libgomp1 \ libgl1-mesa-glx \ libglib2.0-0 \ libsm6 \ libxext6 \ libxrender-dev \ && rm -rf /var/lib/apt/lists/* COPY app/ . RUN pip install -r requirements.txt COPY models/ /root/.paddleocr/whl/ CMD gunicorn --bind 0.0.0.0:5000 app:app

Dans notre application Flask, nous devons simplement ajouter les lignes de code qui pointent vers les modèles de détection et de reconnaissance.

python
@app.route("/api/process-receipts", methods=["POST"]) def process_receipts(): _ocr = PaddleOCR( use_angle_cls=True, lang='fr', show_log=True, det_model_dir='/root/.paddleocr/whl/det/en_PP-OCRv3_det_infer/', rec_model_dir='/root/.paddleocr/whl/rec/latin_PP-OCRv3_rec_infer/', cls_model_dir='/root/.paddleocr/whl/cls/ch_ppocr_mobile_v2.0_cls_infer/' )

Puisque notre API est déployée avec Docker il serait donc facile d'utiliser un serveur web comme NGINX afin de créer un load balancer qui distribuerait les requêtes entre plusieurs conteneurs différents. Cela permettrait potentiellement de traiter les requêtes plus rapidement.

Cette application sera maintenant déployée sur un droplet DigitalOcean, mais vous pourriez aussi choisir votre propre VPS.

Conclusion

En conclusion, cette API est un outil puissant qui peut être utilisé pour extraire les ingrédients et les prix des reçus. Sa capacité à gérer différentes langues et différents formats de reçus en fait un atout précieux pour notre application.

J'espère que vous trouvez cet article utile et informatif. Si vous avez des questions ou des commentaires, n'hésitez pas à me contacter à hello@juliencm.dev. Je suis toujours heureux d'avoir de vos nouvelles !

Merci d'avoir lu mon blog !

Peace nerds,

Julien