# 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 . ```