207 lines
6.5 KiB
Markdown
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 .
|
|
```
|