Entrada XXX: PINSAPIA aprende a leer documentos escaneados (OCR)
Hasta ahora, si le pasabas a PINSAPIA un PDF que era en realidad una foto o un escaneo —una hoja de examen fotocopiada, un apunte fotografiado con el móvil, un documento antiguo sin versión digital— el sistema no sacaba nada de él. No es que lo leyera mal: es que no había ningún texto que leer. Un PDF así no contiene letras, contiene una imagen, y hasta ahora nuestra extracción de texto solo sabía leer letras.
Esta semana hemos cerrado ese hueco añadiendo reconocimiento óptico de caracteres (OCR) a PINSAPIA. Cuenta cómo lo hemos hecho, por qué lo hemos hecho así, y cómo lo puedes reproducir tú si te hace falta en tu propio proyecto.
El problema, con precisión
Un PDF puede tener texto de dos maneras muy distintas, aunque a simple vista parezcan iguales:
- Texto seleccionable: el PDF lleva “por dentro” las letras codificadas. Puedes seleccionarlas con el ratón y copiarlas. Es lo normal en un documento hecho con Word, Google Docs o exportado desde una web.
- Solo imagen: el PDF es, en el fondo, una foto de una página. Puede que a la vista parezca un documento normal, pero no hay ni una sola letra codificada dentro: es un dibujo. Pasa con documentos escaneados o fotografiados.
pypdf, la librería que usábamos para extraer texto en PINSAPIA, solo sabe leer el primer tipo. Ante el segundo, no falla con un error llamativo: simplemente devuelve una cadena vacía, y el documento entero se trataba como si no tuviera contenido. Eso es justo el tipo de fallo silencioso que en este proyecto intentamos evitar siempre: mejor un aviso claro que un “no hay nada” sin explicación.
La solución: OCR como paso adicional, no como sustituto
La decisión importante no es “usar OCR”, que es obvia. La decisión importante es dónde y cuándo se usa.
En PINSAPIA hay un principio que se repite en cada función nueva que construimos: si algo se puede resolver con código determinista, no se le pide al modelo de IA que lo adivine. El OCR encaja exactamente en esa idea, así que el diseño ha sido:
- Se intenta extraer el texto de cada página del PDF de la forma normal, con
pypdf. - Si una página concreta devuelve texto vacío, solo esa página se convierte en imagen y se le pasa a Tesseract, el motor de OCR.
- El texto que Tesseract reconoce se trata exactamente igual que el texto normal a partir de ese punto: se indexa, se busca, se cita, se simplifica… según qué parte de PINSAPIA lo esté usando.
El modelo de IA no interviene en el reconocimiento del texto en ningún momento. Es Tesseract, un motor de OCR normal y determinista, el que decide qué dice la imagen. Esto importa: si dejáramos que el modelo “adivinara” el contenido de una imagen a partir de una descripción vaga, corremos el riesgo de que invente texto plausible que no está realmente en el documento. Aquí no hay margen para eso.
Como el OCR no tiene una pantalla propia ni un botón propio, no lo hemos montado como una aplicación aparte: es un parche dentro de la función de extracción de texto (rag.py) que ya usan tres piezas distintas de PINSAPIA (la búsqueda documental, el analizador de legibilidad y Texto Claro). Todas se benefician a la vez, sin duplicar nada.
Cómo reproducirlo, paso a paso
Esto es lo que ha hecho falta, en el orden en que lo hemos hecho.
1. Instalar Tesseract (el motor de OCR)
Tesseract es software libre, y en Windows se instala con un instalador normal, no con pip: es un programa aparte, no una librería de Python.
Descárgalo del instalador de la comunidad UB-Mannheim (el más usado y mantenido para Windows):
Durante la instalación, importante: en la pantalla de selección de componentes, marca el paquete de idioma español. Por defecto solo viene inglés.
2. Comprobar que Windows lo encuentra (el PATH)
Tras instalar, abre una ventana de PowerShell nueva y prueba:
powershell
tesseract --version
Si sale un error de “no se reconoce como un cmdlet”, Tesseract se ha instalado bien pero Windows no sabe dónde buscarlo. Se soluciona añadiéndolo al PATH de tu usuario:
powershell
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\Program Files\Tesseract-OCR", "User")
Cierra la ventana de PowerShell del todo (no basta con abrir una pestaña nueva) y abre una nueva para que el cambio se aplique. Comprueba de nuevo con tesseract --version y, para confirmar que el idioma español está disponible:
powershell
tesseract --list-langs
Debe aparecer spa en la lista.
3. Instalar las dos librerías de Python
bash
pip install pymupdf pytesseract
pytesseractes el puente entre Python y el programa Tesseract que acabas de instalar.PyMuPDF(se importa comopymupdf) convierte cada página del PDF en una imagen, que es lo que Tesseract necesita para poder leerla. Elegimos esta librería en vez de la alternativa más conocida (pdf2image) porquepdf2imagenecesita además un programa externo llamado Poppler instalado aparte a mano en Windows; PyMuPDF se instala entero con ese únicopip install, sin pasos manuales adicionales.
Y, como siempre, añadidas también a requirements.txt del proyecto para que quede registrado.
4. El código: dos piezas nuevas en rag.py
Una función nueva, _ocr_pagina_pdf, que convierte una página en imagen y la pasa por Tesseract:
python
import pymupdf
import pytesseract
from PIL import Image
IDIOMA_OCR = "spa"
ZOOM_OCR = 300 / 72 # resolución aproximada de 300 ppp
def _ocr_pagina_pdf(ruta, num_pagina):
"""Convierte una página de un PDF en imagen y la reconoce con Tesseract.
Nunca lanza una excepción hacia arriba: si algo falla, avisa y devuelve
una cadena vacía, igual que una página sin texto."""
try:
documento = pymupdf.open(ruta)
pagina = documento[num_pagina - 1]
matriz = pymupdf.Matrix(ZOOM_OCR, ZOOM_OCR)
pixmap = pagina.get_pixmap(matrix=matriz)
imagen = Image.frombytes("RGB", (pixmap.width, pixmap.height), pixmap.samples)
documento.close()
return pytesseract.image_to_string(imagen, lang=IDIOMA_OCR)
except Exception as error:
print(f"[rag] Aviso: OCR no disponible o ha fallado en la página {num_pagina} de '{ruta}': {error}")
return ""
Y el enganche dentro de la extracción normal de páginas: solo se llama al OCR cuando la página no tiene texto seleccionable.
python
def _extraer_paginas_pdf(ruta):
paginas = []
lector = pypdf.PdfReader(ruta)
for i, pagina in enumerate(lector.pages, start=1):
texto = pagina.extract_text() or ""
if not texto.strip():
texto_ocr = _ocr_pagina_pdf(ruta, i)
if texto_ocr.strip():
print(f"[rag] Página {i} de '{ruta}' sin texto seleccionable: recuperada por OCR ({len(texto_ocr)} caracteres).")
texto = texto_ocr
paginas.append((texto, i))
return paginas
Nótese lo que NO hace este código: no le pide nada al modelo de IA, no intenta “adivinar” si una página necesita OCR con ninguna heurística complicada, y si Tesseract falla por cualquier motivo (no está instalado, la imagen es ilegible, lo que sea) la página simplemente queda vacía y se avisa por consola, sin tirar abajo el resto de la extracción.
5. Probarlo antes de fiarte
Antes de subir nada a producción, hemos escrito una batería de 8 pruebas automáticas propias, con un PDF de prueba construido a propósito con una página-imagen (sin texto seleccionable) y otra con texto normal:
- Que una página con texto normal no llame al OCR en absoluto (para no gastar tiempo de más).
- Que una página-imagen se recupere correctamente vía OCR.
- Que el texto recuperado por OCR se junte bien con el texto normal del resto del documento.
- Que si el OCR falla (simulado a propósito), no rompa la extracción del resto de páginas.
- Que una ruta de archivo inexistente no lance una excepción.
Las 8 pasaron antes de tocar el servidor real.
6. La prueba de verdad: un documento escaneado real
Las pruebas automáticas usan una imagen generada por código, que no es lo mismo que un escaneo de verdad, con su ruido, su ligera inclinación y su resolución imperfecta. Así que la prueba definitiva ha sido generar un PDF de prueba con texto simulado en español y pasarlo por extraer_texto_documento() ya en el ordenador real (ROCKY, con Windows).
Resultado: el texto se reconoce correctamente en su mayor parte, con los fallos típicos y esperables de cualquier OCR sobre una imagen —alguna palabra pegada por el espaciado, alguna tilde perdida—. Esto no es un error del código: es el comportamiento normal de cualquier motor de OCR, y el texto que produce es perfectamente aprovechable para búsqueda, aunque no sea una transcripción perfecta letra por letra.
Lo que el OCR NO hace (y por qué lo decimos así de claro)
- No reconoce letra manuscrita. Tesseract está pensado para texto impreso o a máquina. Prometer que lee “cualquier escrito a mano” habría sido inventar una capacidad que no existe.
- No es perfecto. Como cualquier OCR, comete errores de reconocimiento, sobre todo con imágenes de baja calidad. El sistema está pensado para tolerar ese ruido en la búsqueda, no para depender de una transcripción exacta.
- No decide el modelo de IA. El reconocimiento lo hace siempre Tesseract, nunca el modelo de lenguaje. Esta distinción no es un detalle técnico menor: es la misma regla que aplicamos en cada función de PINSAPIA que puede resolverse con código determinista en vez de con una IA que “cree recordar” el contenido.
Próximo paso
Con el OCR funcionando de forma aislada, el paso que queda es arrancar la API completa de PINSAPIA con las tres bases de conocimiento reales cargadas y confirmar que las nuevas dependencias no rompen nada del arranque normal. Cuando esté confirmado, este componente queda cerrado del todo.



