Una factura fotografiada aún no es texto
Una ferretería fotografía una factura en papel y quiere el total en su base de datos. La foto son solo píxeles. Un programa lector tiene que encontrar primero las palabras.
El lector devuelve cada trozo de texto con su lugar en la página. Luego tu código los ordena y saca los campos.
Al final de esta lección
Nota
Puedo convertir una página de cuadros de texto en líneas en orden de lectura y sacar sus campos con su confianza.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Cuadro | un trozo de texto y el lugar donde está en la página | |
| Orden de lectura | el orden en que una persona leería los cuadros | |
| Campo | una etiqueta y el valor que va a su lado | Total: 46,50 |
| Confianza | lo seguro que está el lector, de 0 a 100 | |
| Tolerancia | cuánto pueden separarse dos cuadros y seguir en una línea |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Una página es una lista de cuadros
Un lector de páginas no te da un solo bloque de texto. Te da una lista de cuadros. Cada cuadro tiene un texto, su lugar (x a lo ancho, y a lo alto) y una confianza de 0 a 100.
La lista no viene en un orden útil. Dos cuadros que parecen una línea pueden estar a unos píxeles de distancia.
Cuatro cuadros de una página
| Texto | x | y | Confianza |
|---|---|---|---|
| Total: | 40 | 200 | 98 |
| 46,50 | 120 | 201 | 61 |
| Cliente: | 40 | 60 | 97 |
| Marta | 120 | 62 | 95 |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
El orden de lectura necesita una tolerancia
Para leer una página ordenas los cuadros de arriba abajo, luego los agrupas en líneas y después ordenas cada línea de izquierda a derecha.
Los cuadros de una línea casi nunca tienen exactamente la misma y. Por eso un cuadro se une a una línea cuando su y está dentro de una tolerancia respecto al primer cuadro de la línea. Aquí se usa una tolerancia de 5 píxeles.
Un error frecuente
Advertencia
Mucha gente ordena los cuadros solo por y y los lee en ese orden. Un valor unos píxeles más arriba que su etiqueta sale entonces antes. Primero agrupa los cuadros en líneas con una tolerancia, y luego ordena cada línea por x.
Un campo es una etiqueta y su valor
En un formulario, una etiqueta como Total: va seguida de su valor en la misma línea. Para sacar un campo, busca un cuadro que acabe en dos puntos y toma el siguiente de la línea.
Guarda la confianza junto al valor. Una cifra baja significa que una persona debe comprobarlo, porque un dígito mal leído parece igual que uno correcto.
Escribe el ayudante del modelo, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Los tokens de una respuesta, o null si no trae ninguno.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el ayudante del modelo, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, 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(\"estado \" + res.status);' ' const d = await res.json();' ' return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de la página, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const boxes = [' ' { text: \"46,50\", x: 120, y: 201, conf: 61 },' ' { text: \"Total:\", x: 40, y: 200, conf: 98 },' ' { text: \"Factura\", x: 40, y: 10, conf: 99 },' ' { text: \"Marta\", x: 120, y: 62, conf: 95 },' ' { text: \"Cliente:\", x: 40, y: 60, conf: 97 },' ' { text: \"12/03/2026\", x: 120, y: 99, conf: 88 },' ' { text: \"Fecha:\", x: 40, y: 100, conf: 98 },' '];' > page3.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de la página, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function lines(list, tol) {' ' const sorted = list.slice().sort(function (a, b) { return a.y - b.y || a.x - b.x; });' ' const out = [];' ' for (const b of sorted) {' ' const last = out[out.length - 1];' ' if (last && Math.abs(b.y - last[0].y) <= tol) last.push(b);' ' else out.push([b]);' ' }' ' return out.map(function (l) { return l.sort(function (a, b) { return a.x - b.x; }); });' '}' >> page3.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de la página, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function fields(ls) {' ' const found = {};' ' for (const l of ls) {' ' for (let i = 0; i < l.length - 1; i++) {' ' if (l[i].text.endsWith(\":\")) found[l[i].text.slice(0, -1).toLowerCase()] = { value: l[i + 1].text, conf: l[i + 1].conf };' ' }' ' }' ' return found;' '}' >> page3.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de la página, parte 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const ls = lines(boxes, 5);' ' for (const l of ls) console.log(l.map(function (b) { return b.text; }).join(\" \"));' ' const f = fields(ls);' ' console.log(JSON.stringify(f));' ' const low = Object.keys(f).filter(function (k) { return f[k].conf < 90; });' ' console.log(\"revisar a mano: \" + low.join(\", \"));' ' const q = \"Escribe una pregunta corta que pida a una persona confirmar un total leído con poca confianza.\";' ' const reply = await ask([{ role: \"user\", content: q }], \"Comprueba este total con la hoja en papel: ¿son 46,50 euros?\", 40);' ' console.log(\"respuesta: \" + reply.text);' '}' 'main();' >> page3.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Lee la página y saca los campos
docker exec -w /home/node/project agent node page3.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Las líneas que imprime
| Línea | Qué muestra |
|---|---|
| Cliente: Marta | Dos cuadros unidos en una línea, primero la etiqueta |
| Fecha: 12/03/2026 | y 99 e y 100 están dentro de la tolerancia de 5 |
| {"cliente":{...},"fecha":{...},"total":{...}} | Cada campo con su valor y su confianza |
| revisar a mano: fecha, total | Los dos valores por debajo de 90 |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.