Files
sirpremiumv2/docs/ocr-service-spec.md
T
LizandroandClaude Sonnet 5 7afd22d581 feat: leer comprobantes via OCR externo + Gemini solo-texto
Antes, si Gemini Vision fallaba (cuota/API), el 100% de los comprobantes
del chat web quedaban sin poder leerse. Ahora el job intenta primero un
servicio OCR externo configurable (imagen -> texto) y le pasa el texto
a Gemini para que solo lo estructure en JSON, más barato y sin la cuota
de visión. Si el OCR no está habilitado o falla, cae al método anterior
(Gemini leyendo la imagen directamente) sin romper nada.

Agrega configuración en el panel (URL/token/habilitado + probar conexión)
y la spec del servicio OCR a desplegar en docs/ocr-service-spec.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 21:49:34 +00:00

211 lines
6.7 KiB
Markdown

# 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 <OCR_TOKEN>
```
```json
{
"image_base64": "<imagen en base64, SIN el prefijo data: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=<token secreto que Laravel debe enviar en el Authorization header>
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 <OCR_TOKEN>" \
-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.