Alhazen-OCR : lire les documents institutionnels arabes
Un modèle OCR open-weight compact pour les formulaires, factures et documents administratifs arabes formels. Entraîné sur des données aux licences propres, prévu pour tourner là où résident déjà les archives sensibles.
La numérisation des documents arabes reste un point fragile. Formulaires, factures et courriers scannés s'accumulent plus vite qu'on ne peut les saisir, et l'OCR grand public peine souvent sur les mises en page que banques, ministères ou hôpitaux traitent vraiment. Nous publions aujourd'hui Alhazen-OCR, un modèle open-weight pensé pour cette charge : les documents institutionnels arabes formels.
TL;DR
Alhazen-OCR est un adaptateur QLoRA compact pour les formulaires, factures et pages à forte part manuscrite en arabe. Sur le split de validation, le taux d'erreur caractère passe de 0,79 → 0,28 CER face à Qwen3-VL-2B-Instruct (WER 0,95 → 0,45), et s'améliore aussi sur le manuscrit KHATT externe (1,89 → 1,12 CER). Poids et code d'évaluation sont ouverts. Données d'entraînement : context212/context212-alhazen-ocr.
Le nom vient d'Ibn al-Haytham (Alhazen), le savant du XIe siècle qui a fondé la science de l'optique.
Le problème : l'arabe institutionnel reste enfermé dans des pixels
La recherche en OCR s'appuie encore beaucoup sur les écritures latines et les scans grand public. Les documents arabes formels n'entrent pas dans ce cadre. La mise en page va de droite à gauche. Les lettres se lient. Les tableaux sont denses. Les en-têtes sont souvent bilingues. Cachets et annotations manuscrites apparaissent sur des formulaires autrement imprimés.
Les API cloud fermées aident quand les données peuvent sortir du bâtiment. Beaucoup d'institutions ne le peuvent pas. Les grands modèles documents généraux sont souvent trop lourds pour tourner on-premise à côté des archives. Les équipes réglementées ont besoin d'un modèle plus petit, aux licences propres, et calé sur les documents qu'elles traitent vraiment.
Résultats
Évalué en CER/WER (plus bas étant mieux) sur le split de validation de context212/context212-alhazen-ocr et sur le benchmark externe ahmedheakl/arocrbench_khatt (50 échantillons chacun, décodage greedy) :
| Modèle | Eval CER ↓ | Eval WER ↓ | KHATT CER ↓ | KHATT WER ↓ |
|---|---|---|---|---|
| Qwen3-VL-2B-Instruct (base) | 0.792 | 0.947 | 1.893 | 1.801 |
| Alhazen-OCR | 0.281 | 0.448 | 1.121 | 1.215 |
Le fine-tuning fait passer l'erreur caractère in-distribution de 0,79 → 0,28 CER et le WER de 0,95 → 0,45. Sur le manuscrit KHATT externe, CER/WER s'améliorent aussi face à la base non fine-tunée (1,89 → 1,12 CER). Un mélange trop synthétique avait auparavant régressé sur KHATT ; cette version utilise un mélange plus manuscrit (~45 % paragraphes KHATT, ~35 % imprimé synthétique, ~20 % factures), sans chevauchement de transcriptions avec le bench KHATT.
Une epoch suffit. La seconde ne plafonne pas ; elle diverge. Formulaires et factures imprimés restent la cible ; KHATT est meilleur qu'avant mais encore dur (CER au-dessus de 1,0 = beaucoup de lignes fausses). Mieux vaut une baseline honnête qu'une démo triée sur le volet.
Utilisation avec vLLM
Alhazen-OCR est un adaptateur LoRA. On sert la base vision-langage 2B ouverte avec l'adaptateur branché, puis on appelle l'endpoint chat compatible OpenAI.
vllm serve Qwen/Qwen3-VL-2B-Instruct \
--enable-lora \
--lora-modules alhazen=context212/alhazen-ocr \
--limit-mm-per-prompt '{"image": 1}' \
--mm-processor-cache-gb 0 \
--no-enable-prefix-caching
import base64
from pathlib import Path
import requests
ENDPOINT = "http://localhost:8000/v1/chat/completions"
MODEL = "alhazen" # nom du module LoRA passé à --lora-modules
image_path = Path("formulaire.png") # scan, facture ou page rendue
image_base64 = base64.b64encode(image_path.read_bytes()).decode("utf-8")
payload = {
"model": MODEL,
"messages": [
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{image_base64}"
},
},
{
"type": "text",
"text": (
"Extract all the text from this image, "
"preserving the original reading order."
),
},
],
}
],
"max_tokens": 2048,
"temperature": 0.0,
}
response = requests.post(ENDPOINT, json=payload, timeout=120)
text = response.json()["choices"][0]["message"]["content"]
print(text)
Pour un PDF, rendez chaque page en image d'abord (environ 200 DPI convient), puis une page par requête. Gardez temperature à 0 pour une transcription déterministe.
Liens
- Modèle :
context212/alhazen-ocr(Apache-2.0) - Dataset :
context212/context212-alhazen-ocr - Code : entraînement, données, évaluation sur
github.com/context212/atlas-ocr