Files

207 lines
6.5 KiB
Markdown

# count-api
Microservicio Go que cuenta servicios facturados en facturas médicas PDF.
Aplica OCR (Tesseract) + clustering espacial (DBSCAN) + clasificación ML (Random Forest vía ONNX Runtime).
Es un servicio **sin estado**: recibe PDFs por HTTP, procesa en memoria y devuelve el resultado. No persiste datos ni depende de bases de datos, colas ni almacenamiento de objetos.
La persistencia de resultados la maneja el servicio `extraction` (Python) a través de los endpoints `/count-jobs`.
---
## Endpoints
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/health` | Liveness — `{"status":"ok"}` |
| `GET` | `/ready` | Readiness — `{"status":"ready"}` |
| `GET` | `/openapi.json` | Especificación OpenAPI |
| `POST` | `/v1/count` | Conteo síncrono de PDFs |
### POST /v1/count
**Request**
```
Content-Type: multipart/form-data
Query: include_rejected=true|false (default: false)
Field: files — uno o más archivos PDF (máx. SYNC_MAX_FILES)
```
**Response 200**
```json
{
"results": [
{
"filename": "factura_001.pdf",
"service_count": 3,
"status": "ok",
"services": [
{ "text": "CONSULTA MEDICA", "probability": 0.91 },
{ "text": "LABORATORIO HEMOGRAMA", "probability": 0.87 },
{ "text": "RADIOGRAFIA TORAX", "probability": 0.83 }
]
}
],
"processed": 1,
"failed": 0
}
```
**Response 4xx/5xx**
```json
{ "error": "descripción del error" }
```
---
## Pipeline de procesamiento
```
PDF bytes (memoria)
│
▼ gen2brain/go-fitz (MuPDF)
Render de la primera página a imagen (OCR_DPI, default 300)
│
▼ otiai10/gosseract (Tesseract)
OCR palabra a palabra con coordenadas (X, Y, W, H)
│
▼ DBSCAN 1D sobre coordenada Y
Agrupación de palabras en filas candidatas
│
▼ 14 features por fila (pipeline v1.1.0)
first_token_is_code · first_token_is_service_code · has_service_table_pattern · has_header_keyword
currency_at_row_end · first_token_x_norm · is_likely_continuation · currency_count · integer_count
x_span_norm · y_norm · token_count · special_char_ratio · digit_ratio
│
▼ ONNX Runtime (Random Forest)
P(servicio) por fila → filtrar por threshold (inference.json)
│ Si el modelo no está disponible → heurística: todas las filas = servicio
▼
PDFResult { service_count, services[], status }
```
---
## Modelo ONNX
El modelo es opcional. Si no está disponible, el servicio arranca con una advertencia y usa heurística de conteo.
Los archivos deben montarse en `/app/models/`:
```
/app/models/model.onnx — modelo Random Forest exportado con skl2onnx
/app/models/inference.json — threshold y feature_columns
```
Para generar el modelo desde un `model.joblib` existente:
```bash
cd scripts
pip install -r ../requirements-export.txt
python export_onnx.py --joblib /ruta/model.joblib --out ../models/latest
```
**Despliegue:** los archivos no van en la imagen Docker; se montan desde `models/latest/`. Tras copiarlos al servidor:
```bash
chmod 644 models/latest/model.onnx models/latest/inference.json
docker compose restart count-api
docker logs ipsextractor-count-api 2>&1 | grep -E "modelo ONNX|WARN: modelo"
# Debe aparecer: modelo ONNX cargado: /app/models/model.onnx
```
Si el log sigue mostrando `WARN: modelo ML no disponible`, verifica dentro del contenedor:
```bash
docker exec ipsextractor-count-api ls -la /app/models/
```
Sin modelo cargado, **todas las filas OCR salen con probability=1.0 (100%)** — modo heurístico.
### Error: `requests feature 13 but input tensor has 8 features`
El `model.onnx` desplegado **no coincide** con el pipeline Go. El modelo v1.1.0 usa **14 features** (ver `features.go` y `export_onnx.py`). Si el ONNX fue exportado con otro esquema verás errores como `requests feature 13 but input tensor has 8 features`.
**Exportar desde el joblib v1.1.0:**
```bash
python export_onnx.py \
--joblib /ruta/services-count-facturas-medicas-ml/models/v1.1.0/model.joblib \
--metadata /ruta/services-count-facturas-medicas-ml/models/v1.1.0/model_metadata.json \
--out ../models/latest
```
---
## Variables de entorno
| Variable | Default | Descripción |
|----------|---------|-------------|
| `PORT` | `8080` | Puerto HTTP |
| `MODEL_PATH` | `/app/models/model.onnx` | Ruta al modelo ONNX |
| `INFERENCE_CONFIG` | `/app/models/inference.json` | Threshold y metadatos del modelo |
| `OPENAPI_PATH` | `api/openapi.yaml` | Ruta a la especificación OpenAPI |
| `OCR_DPI` | `300` | DPI de renderizado PDF para OCR |
| `OCR_LANG` | `spa` | Idioma Tesseract |
| `OCR_POOL_SIZE` | `0` (→ 4) | Tamaño del pool de clientes Tesseract |
| `DBSCAN_EPS` | `10` | Épsilon de clustering DBSCAN (píxeles en Y) |
| `DBSCAN_MIN_SAMPLES` | `1` | Mínimo de puntos por cluster DBSCAN |
| `SYNC_MAX_FILES` | `50` | Máximo de PDFs por request a `/v1/count` |
| `SYNC_PROCESS_CONCURRENCY` | `0` (→ OCR_POOL_SIZE) | Goroutines paralelas por request |
| `MAX_PDF_SIZE_MB` | `20` | Tamaño máximo permitido por PDF |
| `PDF_PROCESS_TIMEOUT_SEC` | `120` | Timeout por PDF |
| `LOG_PIPELINE_TIMING` | `false` | Loguear tiempos de cada etapa |
---
## Dependencias en producción (Docker)
- `libonnxruntime.so` — ONNX Runtime (copiado en build multi-etapa)
- `tesseract-ocr tesseract-ocr-spa` — OCR en español
- `libmupdf` — via go-fitz, renderizado de PDFs
Sin Redis, MinIO, NATS ni bases de datos.
---
## Estructura del código
```
cmd/api/ — punto de entrada del servidor HTTP
internal/
api/ — router y handler countSync
config/ — carga de variables de entorno
domain/ — tipos PDFResult, CountResponse, ServiceRow
model/ — sesión ONNX Runtime (Predictor)
ocr/ — pool de clientes Tesseract + renderizado MuPDF
pipeline/ — orquestación OCR → DBSCAN → features → ML
processor.go — ProcessPDF por documento
clustering.go — DBSCAN 1D en Y
features.go — 8 features por fila
api/openapi.yaml — especificación OpenAPI 3.1
models/latest/ — model.onnx + inference.json (gitignored, montar en Docker)
scripts/ — export_onnx.py para generar el modelo
Dockerfile — build multi-etapa: ONNX download + Go builder + runtime slim
```
---
## Arrancar localmente (Docker)
```bash
# Desde la raíz del repositorio:
docker compose up --build count-api
```
El servicio queda disponible en `http://localhost:8081`.
Para probar el endpoint:
```bash
curl -X POST http://localhost:8081/v1/count \
-F "files=@factura.pdf" | jq .
```