# Servicio OCR para lectura de comprobantes — Especificación Este documento describe el microservicio que hay que desplegar (en Coolify u otro host con Docker) para que SirPremium deje de depender únicamente de Gemini Vision para leer comprobantes de pago. El flujo pasa a ser: ``` imagen del comprobante → [ESTE SERVICIO OCR] → texto plano → Gemini (solo texto) → JSON estructurado ``` Gemini deja de "ver" la imagen; solo recibe texto y lo ordena en JSON. Esto evita el punto único de falla que tenías hoy (si Gemini Vision falla, cae el 100% de las lecturas) y reduce el consumo de cuota, porque los modelos de texto son más baratos y menos limitados que los de visión. ## 1. Qué tiene que exponer el servicio Un único endpoint HTTP: ``` POST /extract ``` ### Request ``` Content-Type: application/json Authorization: Bearer ``` ```json { "image_base64": "", "mime_type": "image/jpeg" } ``` - `mime_type` puede ser `image/jpeg`, `image/png`, `image/webp` o `image/heic` (son los formatos que ya acepta la app hoy). - El token se valida por header `Authorization: Bearer`. Si no coincide, responder `401`. ### Response — éxito (HTTP 200) ```json { "success": true, "text": "Transferencia exitosa\nBre-B\nComprobante 24152758409697903...\nValor de la transferencia\n$1.000,00\nEnviaste LEDYS LEON QUINTERO\nA la llave @LEON8909\nEntidad BANCOLOMBIA\n..." } ``` - `text`: todo el texto detectado en la imagen, tal cual lo lee el OCR (no hace falta que estructure nada — de eso se encarga Gemini después). Si detecta texto en varias líneas/bloques, mejor mantener saltos de línea, ayuda a Gemini a interpretar el layout. ### Response — error (HTTP 4xx/5xx) ```json { "success": false, "error": "descripción corta del error" } ``` Casos que deben devolver error controlado (no un 500 crudo): - Imagen corrupta / no decodificable. - Imagen sin texto detectable (`"error": "sin_texto_detectado"`). - `mime_type` no soportado. ### Timeouts Laravel llama con un timeout de **20 segundos**. El servicio debe responder bien antes de eso — el OCR no debería tardar más de 2-5 segundos por imagen en CPU. ## 2. Motor de OCR recomendado Para desplegar rápido en Coolify, la opción más simple es un contenedor Docker con: - **Tesseract OCR** (`tesseract-ocr` + paquete de idioma `spa`) envuelto en una API pequeña (Python + FastAPI, o Node + Express). Es la opción de menor esfuerzo. - Si la precisión con las capturas de pantalla de apps bancarias (fondos oscuros, fuentes pequeñas) resulta insuficiente, migrar el mismo contrato de API a **PaddleOCR** (más preciso, pero imagen Docker más pesada — necesita Python + PaddlePaddle). El contrato HTTP (`POST /extract`) es el mismo sin importar cuál motor uses por dentro — así que se puede empezar con Tesseract y cambiar el motor después sin tocar el lado de Laravel. ### Preprocesamiento recomendado antes de correr OCR Mejora mucho la precisión con capturas de pantalla de apps: 1. Convertir a escala de grises. 2. Aumentar contraste / binarizar (umbral adaptativo). 3. Escalar la imagen si es muy pequeña (mínimo ~1000px de ancho). ## 3. Variables de entorno del servicio (sugeridas) ``` OCR_TOKEN= OCR_LANG=spa # idioma para Tesseract PORT=8000 ``` ## 4. Ejemplo mínimo de referencia (Python + FastAPI + Tesseract) Esto es solo un punto de partida — no es la implementación final, es para no arrancar de cero. **Dockerfile** ```dockerfile FROM python:3.11-slim RUN apt-get update && apt-get install -y --no-install-recommends \ tesseract-ocr tesseract-ocr-spa libgl1 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . EXPOSE 8000 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] ``` **requirements.txt** ``` fastapi uvicorn[standard] pytesseract pillow python-multipart ``` **app.py** ```python import base64, io, os from fastapi import FastAPI, Header, HTTPException from pydantic import BaseModel from PIL import Image, ImageOps import pytesseract app = FastAPI() OCR_TOKEN = os.environ.get("OCR_TOKEN", "") LANG = os.environ.get("OCR_LANG", "spa") class ExtractRequest(BaseModel): image_base64: str mime_type: str = "image/jpeg" def preprocesar(img: Image.Image) -> Image.Image: img = img.convert("L") # escala de grises img = ImageOps.autocontrast(img) if img.width < 1000: ratio = 1000 / img.width img = img.resize((1000, int(img.height * ratio))) return img @app.post("/extract") def extract(req: ExtractRequest, authorization: str = Header(default="")): if OCR_TOKEN and authorization != f"Bearer {OCR_TOKEN}": raise HTTPException(status_code=401, detail="token invalido") try: raw = base64.b64decode(req.image_base64) img = Image.open(io.BytesIO(raw)) except Exception: return {"success": False, "error": "imagen invalida o corrupta"} img = preprocesar(img) texto = pytesseract.image_to_string(img, lang=LANG).strip() if not texto: return {"success": False, "error": "sin_texto_detectado"} return {"success": True, "text": texto} ``` ## 5. Despliegue en Coolify 1. Crear un nuevo recurso tipo "Application" apuntando a un repo/carpeta con el `Dockerfile` de arriba (o el que termines usando). 2. Configurar la variable de entorno `OCR_TOKEN` con un valor secreto largo (ej: generado con `openssl rand -hex 32`). 3. Publicar el servicio con dominio propio (ej: `https://ocr.tu-dominio.com`) o IP interna si Coolify y el hosting de Laravel comparten red — cualquiera de las dos formas sirve, Laravel solo necesita una URL alcanzable por HTTPS. 4. Probar con: ```bash curl -X POST https://ocr.tu-dominio.com/extract \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"image_base64":"'"$(base64 -w0 comprobante.jpg)"'","mime_type":"image/jpeg"}' ``` ## 6. Qué necesito de ti para conectar Laravel Una vez el servicio esté arriba, en el panel de SirPremium (Configuración del Chat → sección "OCR de comprobantes", ya agregada) solo tienes que llenar: - **URL del servicio OCR** → ej: `https://ocr.tu-dominio.com` - **Token** → el mismo valor que pusiste en `OCR_TOKEN` - Activar el checkbox "Habilitar OCR antes de enviar a la IA" Laravel hace el resto: llama a `{URL}/extract`, si responde con texto se lo pasa a Gemini en modo texto (`estructurarTexto`), y si el OCR falla o no está habilitado, cae automáticamente al método anterior (Gemini leyendo la imagen directamente) — no se rompe nada si el servicio OCR está caído o aún no lo has desplegado.