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
{
"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
{ "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:
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:
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:
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:
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ñollibmupdf— 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)
# Desde la raíz del repositorio:
docker compose up --build count-api
El servicio queda disponible en http://localhost:8081.
Para probar el endpoint:
curl -X POST http://localhost:8081/v1/count \
-F "files=@factura.pdf" | jq .