Une facture photographiée n’est pas du texte
Une boutique photographie une facture papier et veut le total dans sa base de données. La photo n’est que des pixels. Un programme de lecture doit d’abord trouver les mots.
Il rend chaque morceau de texte avec sa place sur la page. Votre code les remet ensuite dans l’ordre et en tire les champs.
À la fin de cette leçon
Remarque
Je peux transformer une page de zones de texte en lignes dans l’ordre de lecture et en tirer les champs avec leur confiance.
Mots clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Zone | un morceau de texte et sa place sur la page | |
| Ordre de lecture | l’ordre dans lequel une personne lirait les zones | |
| Champ | une étiquette et la valeur à côté | Total : 46,50 |
| Confiance | le degré de certitude du lecteur, de 0 à 100 | |
| Tolérance | l’écart permis entre deux zones d’une même ligne |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une page est une liste de zones
Un lecteur de pages ne vous rend pas un seul bloc de texte. Il vous rend une liste de zones. Chaque zone porte un texte, sa place (x vers la droite, y vers le bas) et une confiance de 0 à 100.
La liste n’est dans aucun ordre utile. Deux zones qui semblent former une ligne peuvent être à quelques pixels d’écart.
Quatre zones d’une page
| Texte | x | y | Confiance |
|---|---|---|---|
| Total : | 40 | 200 | 98 |
| 46,50 | 120 | 201 | 61 |
| Nom : | 40 | 60 | 97 |
| Amina | 120 | 62 | 95 |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
L’ordre de lecture demande une tolérance
Pour lire une page, vous triez les zones de haut en bas. Vous les groupez ensuite en lignes. Puis vous triez chaque ligne de gauche à droite.
Les zones d’une ligne sont rarement au même y exact. Une zone rejoint donc une ligne quand son y est à moins d’une tolérance de la première zone de la ligne. Ici, la tolérance est de 5 pixels.
Une erreur courante
Attention
Beaucoup trient les zones par y seul et les lisent dans cet ordre. Une valeur un peu plus haute que son étiquette passe alors avant elle. Groupez d’abord les zones en lignes avec une tolérance, puis triez chaque ligne par x.
Un champ est une étiquette et sa valeur
Dans un formulaire, une étiquette comme Total : est suivie de sa valeur sur la même ligne. Pour tirer un champ, cherchez une zone qui finit par deux-points et prenez la zone suivante de la ligne.
Gardez la confiance avec la valeur. Un chiffre faible veut dire qu’une personne doit vérifier : un chiffre mal lu ressemble exactement à un chiffre juste.
Écrire l’assistant du modèle, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Les jetons d’une réponse, ou null si elle n’en donne pas.' 'function compter(donnees, nom) {' ' return donnees.usage && typeof donnees.usage[nom] === \"number\" ? donnees.usage[nom] : null;' '}' > model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire l’assistant du modèle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function demander(messages, echantillon, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"statut \" + res.status);' ' const d = await res.json();' ' return { texte: d.choices[0].message.content, entree: compter(d, \"prompt_tokens\"), sortie: compter(d, \"completion_tokens\"), enregistre: false };' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' return { texte: echantillon, entree: null, sortie: null, enregistre: true };' ' }' '}' 'module.exports = { demander };' >> model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script de la page, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' 'const boites = [' ' { texte: \"46,50\", x: 120, y: 201, conf: 61 },' ' { texte: \"Total :\", x: 40, y: 200, conf: 98 },' ' { texte: \"Facture\", x: 40, y: 10, conf: 99 },' ' { texte: \"Amina\", x: 120, y: 62, conf: 95 },' ' { texte: \"Nom :\", x: 40, y: 60, conf: 97 },' ' { texte: \"12/03/2026\", x: 120, y: 99, conf: 88 },' ' { texte: \"Date :\", x: 40, y: 100, conf: 98 },' '];' > page3.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script de la page, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function lignes(liste, tol) {' ' const triees = liste.slice().sort(function (a, b) { return a.y - b.y || a.x - b.x; });' ' const sortie = [];' ' for (const b of triees) {' ' const derniere = sortie[sortie.length - 1];' ' if (derniere && Math.abs(b.y - derniere[0].y) <= tol) derniere.push(b);' ' else sortie.push([b]);' ' }' ' return sortie.map(function (l) { return l.sort(function (a, b) { return a.x - b.x; }); });' '}' >> page3.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script de la page, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function champs(ls) {' ' const trouves = {};' ' for (const l of ls) {' ' for (let i = 0; i < l.length - 1; i++) {' ' if (l[i].texte.endsWith(\":\")) trouves[l[i].texte.slice(0, -1).trim().toLowerCase()] = { valeur: l[i + 1].texte, conf: l[i + 1].conf };' ' }' ' }' ' return trouves;' '}' >> page3.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script de la page, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const ls = lignes(boites, 5);' ' for (const l of ls) console.log(l.map(function (b) { return b.texte; }).join(\" \"));' ' const c = champs(ls);' ' console.log(JSON.stringify(c));' ' const faibles = Object.keys(c).filter(function (k) { return c[k].conf < 90; });' ' console.log(\"à vérifier à la main : \" + faibles.join(\", \"));' ' const q = \"Écrivez une question courte qui demande à une personne de confirmer un total lu avec une faible confiance.\";' ' const reponse = await demander([{ role: \"user\", content: q }], \"Merci de comparer ce total avec la page papier : est-ce bien 46,50 ?\", 40);' ' console.log(\"réponse : \" + reponse.texte);' '}' 'main();' >> page3.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Lire la page et tirer les champs
docker exec -w /home/node/project agent node page3.jsAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Les lignes affichées
| Ligne | Ce qu’elle montre |
|---|---|
| Nom : Amina | Deux zones jointes en une ligne, l’étiquette d’abord |
| Date : 12/03/2026 | y 99 et y 100 sont dans la tolérance de 5 |
| {"nom":{...},"date":{...},"total":{...}} | Chaque champ avec sa valeur et sa confiance |
| à vérifier à la main : date, total | Les deux valeurs sous 90 |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.