feat: Add .env.example configuration file and integrate Redis service into Docker Compose for improved task management. Update desktop application to support document indexing and extraction workflows, enhancing user experience with a new extraction wizard and stepper components. Revise README and documentation for clarity on setup and usage.

This commit is contained in:
erik
2026-07-05 22:39:07 -05:00
parent 9aca926988
commit d95f8f6ad8
70 changed files with 9807 additions and 499 deletions
@@ -0,0 +1,339 @@
# Services Count — API de Facturas Médicas
![Go](https://img.shields.io/badge/Go-1.23-blue)
Microservicio de inferencia en Go que cuenta servicios facturados en facturas médicas PDF. El entrenamiento del modelo vive en el repositorio hermano [services-count-facturas-medicas-ml](https://github.com/builker-col/services-count-facturas-medicas-ml); este repositorio despliega la API HTTP y los workers de procesamiento.
## Características
- Conteo **síncrono** (1–50 PDFs por petición) y **asíncrono** (hasta 50 000 PDFs por job).
- Pipeline de inferencia: OCR → clustering DBSCAN → Random Forest vía ONNX.
- Escalado horizontal de workers con Docker Compose.
- Especificación OpenAPI y descriptor de herramientas para agentes AI.
## Arquitectura
El sistema se compone de una API HTTP, workers consumidores y tres servicios de infraestructura (Redis, NATS JetStream y MinIO), definidos en [`docker-compose.yml`](docker-compose.yml).
```mermaid
flowchart TB
Client[Cliente_o_Agente]
API[API_Go_8080]
Worker[Worker_Go_xN]
Redis[(Redis)]
NATS[NATS_JetStream]
MinIO[(MinIO)]
Pipeline[OCR_Tesseract_ONNX]
Client -->|sync_POST_v1_count| API
Client -->|async_POST_v1_jobs| API
API --> Pipeline
API --> Redis
API --> MinIO
API -->|publish| NATS
NATS -->|consume| Worker
Worker --> MinIO
Worker --> Pipeline
Worker --> Redis
```
### Pipeline de inferencia
Por cada PDF se procesa la primera página siguiendo este flujo (`internal/pipeline`):
```mermaid
flowchart LR
PDF[PDF_primera_pagina] --> Render[Render_MuPDF]
Render --> OCR[Tesseract_spa]
OCR --> Cluster[DBSCAN_filas]
Cluster --> Features[8_features]
Features --> Infer[ONNX_RandomForest]
Infer --> Result[service_count_y_services]
```
1. **Render** — La primera página se rasteriza con MuPDF (go-fitz) a 300 DPI.
2. **OCR** — Tesseract extrae cajas de texto en español.
3. **Clustering** — DBSCAN agrupa palabras en filas candidatas.
4. **Features** — Se calculan 8 atributos por fila (códigos, moneda, posición, etc.).
5. **Inferencia** — Un Random Forest exportado a ONNX clasifica cada fila como servicio o no.
## Componentes
| Componente | Binario / servicio | Paquete | Responsabilidad |
|------------|-------------------|---------|-----------------|
| API HTTP | `cmd/api` | `internal/api` | Endpoints REST, gestión de jobs, health probes |
| Worker | `cmd/worker` | `internal/worker` | Consume NATS, descarga PDFs de MinIO, persiste resultados |
| Pipeline | — | `internal/pipeline` | Orquestación OCR → clustering → features → inferencia |
| Modelo ONNX | — | `internal/model` | Carga `model.onnx` e `inference.json` |
| OCR | — | `internal/ocr` | Render PDF (go-fitz) + Tesseract (gosseract) |
| Estado de jobs | Redis 7 | `internal/store` | Metadatos, resultados y progreso (TTL 72 h) |
| Cola | NATS JetStream | `internal/queue` | Stream `PDF_JOBS`, subject `pdf.process.*` |
| Object storage | MinIO | `internal/storage` | Almacenamiento de PDFs en jobs asíncronos |
### Estructura del repositorio
```
├── cmd/
│ ├── api/ # Punto de entrada HTTP
│ └── worker/ # Punto de entrada worker
├── internal/
│ ├── api/ # Servidor HTTP (chi)
│ ├── config/ # Variables de entorno
│ ├── domain/ # Tipos de dominio
│ ├── model/ # Inferencia ONNX
│ ├── ocr/ # Extracción OCR
│ ├── pipeline/ # Procesamiento por PDF
│ ├── queue/ # NATS JetStream
│ ├── storage/ # Cliente MinIO
│ ├── store/ # Estado en Redis
│ └── worker/ # Runner del worker
├── api/openapi.yaml # Especificación OpenAPI 3.1
├── mcp/tool.json # Descriptor de herramientas para agentes
├── web/ # Cliente demo (embebido en la API en /demo/)
├── scripts/ # Export ONNX, paridad con Python
└── models/latest/ # model.onnx + inference.json (montado en Docker)
```
## Modos de operación
| Modo | Endpoint principal | Límite | Cuándo usarlo |
|------|-------------------|--------|---------------|
| Síncrono | `POST /v1/count` | 50 PDFs | Pruebas, integraciones en tiempo real, lotes pequeños |
| Asíncrono | `POST /v1/jobs` + poll | 50 000 PDFs/job | MinIO + NATS + workers (path producción) |
**Flujo asíncrono (producción):**
1. Crear job (`POST /v1/jobs`) — shell vacío, prefijo MinIO o lista de claves.
2. Subir PDFs por chunks (`POST /v1/jobs/{jobID}/upload`, máx. 500 por chunk) → **MinIO Put** → **NATS Publish**.
3. Workers consumen la cola, descargan de MinIO, OCR, guardan en Redis.
4. Consultar progreso (`GET /v1/jobs/{jobID}?wait_ms=30000` para long-polling).
5. Obtener resultados paginados (`GET /v1/jobs/{jobID}/results?cursor=&limit=`).
## API REST
Especificación completa: `GET /openapi.json` o [`api/openapi.yaml`](api/openapi.yaml).
| Método | Ruta | Descripción |
|--------|------|-------------|
| `GET` | `/health` | Liveness probe |
| `GET` | `/ready` | Readiness probe |
| `POST` | `/v1/count` | Conteo síncrono (multipart `files`) |
| `POST` | `/v1/jobs` | Crear job asíncrono |
| `POST` | `/v1/jobs/{jobID}/upload` | Subir chunk de PDFs |
| `GET` | `/v1/jobs/{jobID}` | Estado y progreso del job |
| `GET` | `/v1/jobs/{jobID}/results` | Resultados paginados |
| `DELETE` | `/v1/jobs/{jobID}` | Cancelar job |
**Parámetros relevantes:**
- `include_rejected` (query) — Si es `true`, incluye todas las filas OCR con el campo `is_service`.
- Jobs asíncronos devuelven `202 Accepted`; la cola rechaza con `429` si supera `QUEUE_MAX_PENDING`.
**Respuesta por PDF:**
```json
{
"filename": "factura.pdf",
"service_count": 2,
"status": "ok",
"services": [
{ "text": "890201 Consulta ...", "probability": 0.94 }
]
}
```
## Stack tecnológico
| Área | Tecnología |
|------|------------|
| Lenguaje | Go 1.23 |
| HTTP router | [go-chi/chi](https://github.com/go-chi/chi) v5 |
| OCR | go-fitz (MuPDF) + gosseract (Tesseract) |
| ML | ONNX Runtime + Random Forest exportado |
| Cola | NATS JetStream |
| Estado | Redis 7 |
| Storage | MinIO |
| Contenedores | Docker multi-stage ([`Dockerfile`](Dockerfile)) |
## Inicio rápido
### 1. Exportar el modelo ONNX
Desde el repositorio de entrenamiento, generar los artefactos de inferencia:
```bash
pip install -r requirements-export.txt
python scripts/export_onnx.py \
--joblib ../services-count/models/latest/model.joblib \
--metadata ../services-count/models/v1.0.0/model_metadata.json
```
Esto produce `models/latest/model.onnx` e `inference.json`.
### 2. Levantar con Docker Compose
```bash
docker compose up --build
```
| Servicio | Puerto / acceso |
|----------|-----------------|
| API | `http://localhost:8080` |
| MinIO Console | `http://localhost:9002` |
| Workers | 1 réplica inicial; autoscaler sube hasta 8 según cola |
| Autoscaler | Escala `worker` vía Docker socket según `PendingCount` en Redis |
### 3. Probar
```bash
# Síncrono
curl -F "files=@factura.pdf" http://localhost:8080/v1/count
# Asíncrono (PDFs ya en MinIO bajo un prefijo)
curl -X POST http://localhost:8080/v1/jobs \
-H "Content-Type: application/json" \
-d '{"prefix":"jobs/batch-1/"}'
```
## Cliente web de ejemplo
Con la API en ejecución, abre **[http://localhost:8080/demo/](http://localhost:8080/demo/)** para probar la integración desde el navegador. Los archivos estáticos se sirven desde la propia API (mismo origen, sin CORS).
| Pestaña | Demuestra |
|---------|-----------|
| **Síncrono** | `POST /v1/count` con subida multipart de hasta 50 PDFs |
| **Asíncrono** | `POST /v1/jobs` → upload → polling de estado → resultados paginados |
El modo asíncrono requiere que workers, Redis, NATS y MinIO estén levantados (igual que con `docker compose up`). Código fuente en [`web/`](web/).
## Desarrollo local
Requisitos adicionales a Go 1.23+:
- Tesseract con datos de idioma `spa`
- ONNX Runtime (la variable `ONNXRUNTIME_SHARED_LIBRARY` apunta a la librería compartida)
- Redis, NATS y MinIO en ejecución
```bash
go build -o api ./cmd/api
go build -o worker ./cmd/worker
```
Configurar las variables de entorno para apuntar a los servicios locales (ver tabla siguiente).
## Configuración
Variables definidas en [`internal/config/config.go`](internal/config/config.go):
| Variable | Default | Descripción |
|----------|---------|-------------|
| `PORT` | `8080` | Puerto de la API |
| `REDIS_URL` | `redis://redis:6379/0` | Conexión Redis |
| `NATS_URL` | `nats://nats:4222` | Conexión NATS |
| `NATS_STREAM` | `PDF_JOBS` | Nombre del stream JetStream |
| `NATS_SUBJECT_PREFIX` | `pdf.process` | Prefijo de subjects |
| `MINIO_ENDPOINT` | `minio:9000` | Endpoint MinIO |
| `MINIO_ACCESS_KEY` | `minioadmin` | Access key |
| `MINIO_SECRET_KEY` | `minioadmin` | Secret key |
| `MINIO_BUCKET` | `pdf-count` | Bucket de PDFs |
| `MINIO_USE_SSL` | `false` | Usar SSL con MinIO |
| `MODEL_PATH` | `/app/models/model.onnx` | Ruta al modelo ONNX |
| `INFERENCE_CONFIG` | `/app/models/inference.json` | Metadata de inferencia |
| `OCR_DPI` | `300` | DPI de renderizado PDF (150 reduce latencia ~2–4×) |
| `OCR_LANG` | `spa` | Idioma Tesseract |
| `DBSCAN_EPS` | `10` | Parámetro epsilon de DBSCAN |
| `DBSCAN_MIN_SAMPLES` | `1` | Mínimo de muestras DBSCAN |
| `SYNC_MAX_FILES` | `50` | Máximo de PDFs en modo síncrono |
| `JOB_MAX_FILES` | `50000` | Máximo de PDFs por job |
| `QUEUE_MAX_PENDING` | `100000` | Límite de mensajes pendientes en cola |
| `WORKER_CONCURRENCY` | `4` | Goroutines paralelas por worker |
| `MAX_PDF_SIZE_MB` | `20` | Tamaño máximo por PDF |
| `PDF_PROCESS_TIMEOUT_SEC` | `120` | Timeout de procesamiento por PDF |
| `JOB_RESULT_TTL_HOURS` | `72` | TTL de resultados en Redis |
| `OCR_POOL_SIZE` | `4` (API) / `WORKER_CONCURRENCY` (worker) | Clientes Tesseract reutilizables en pool |
| `SYNC_PROCESS_CONCURRENCY` | `4` (o `OCR_POOL_SIZE`) | PDFs procesados en paralelo en `/v1/count` |
| `JOB_INLINE_PROCESSING` | `false` | `true` procesa uploads en la API sin MinIO (solo dev local) |
| `LOG_PIPELINE_TIMING` | `false` | Logs de duración por etapa del pipeline |
| `NATS_FETCH_MAX_WAIT_MS` | `300` | Tiempo máximo de espera del worker al leer NATS |
## Rendimiento
El cuello de botella de `/v1/count` es el **OCR con Tesseract**, no la inferencia ONNX. Los clientes Tesseract se crean una vez al arranque y se reutilizan desde un pool (`internal/ocr/pool.go`).
| Etapa | Tiempo típico (1 PDF) |
|-------|------------------------|
| Render PDF (MuPDF) | ~100–300 ms |
| OCR Tesseract | ~3–8 s |
| Clustering + ONNX | ~10–50 ms |
**Latencia esperada (síncrono):** ~3–10 s por PDF. Varios PDFs se procesan en paralelo (`SYNC_PROCESS_CONCURRENCY`, default 4). Ejemplo: 20 PDFs ≈ `ceil(20/4) × 2.85s ≈ 15s`.
**Latencia esperada (asíncrono por upload):** path producción MinIO → NATS → worker. Con `WORKER_CONCURRENCY=4` y autoscaler activo, 16 PDFs ≈ `ceil(16/4) × 2.85s` más overhead Put/Get (~15–25 s). 100 PDFs escala hasta 8 workers (32 slots OCR).
**Asíncrono por prefix MinIO** (`POST /v1/jobs {"prefix":"..."}`): mismos workers; los PDFs ya están en el bucket.
**Ajustes para reducir latencia:**
- `OCR_DPI=150` — reduce tiempo de render y OCR (~2–4× más rápido) con posible ligera pérdida de precisión. Validar con `scripts/compare_parity.py` antes de usar en producción.
- `OCR_POOL_SIZE=4` y `WORKER_CONCURRENCY=4` — paralelismo por réplica worker.
- Upload pipeline publica en NATS tras cada Put (no al final del lote).
- `LOG_PIPELINE_TIMING=true` — logs por etapa: `ocr_render`, `ocr_tesseract`, `cluster`, `infer`, `total`.
- `NATS_FETCH_MAX_WAIT_MS=300` — el worker recoge mensajes con menor espera.
- `GET /v1/jobs/{id}?wait_ms=30000` — long-polling en lugar de poll cada 500 ms.
## Escalado
### Autoscaler (docker compose)
El servicio `autoscaler` lee `PendingCount` en Redis cada 5 s y ajusta réplicas `worker`:
```
desired = clamp(ceil(pending / WORKER_SLOTS), MIN_WORKERS, MAX_WORKERS)
```
| Variable | Default | Descripción |
|----------|---------|-------------|
| `MIN_WORKERS` | `1` | Réplicas mínimas |
| `MAX_WORKERS` | `8` | Réplicas máximas |
| `WORKER_SLOTS` | `4` | PDFs en paralelo por réplica (`WORKER_CONCURRENCY`) |
| `SCALE_DOWN_IDLE_SEC` | `60` | Segundos sin cola antes de bajar 1 réplica |
Requiere montar `/var/run/docker.sock` (solo entorno local). En Kubernetes usar HPA sobre lag de NATS o métricas custom.
### Escalado manual
```bash
docker compose up --scale worker=8 -d
```
Cada worker procesa hasta `WORKER_CONCURRENCY` PDFs en paralelo. NATS JetStream distribuye los mensajes entre réplicas con política work-queue.
## Integración con agentes (MCP)
[`mcp/tool.json`](mcp/tool.json) es un **descriptor declarativo** de herramientas para agentes AI (Cursor, etc.). No incluye un servidor MCP embebido.
| Herramienta | Modo | Flujo |
|-------------|------|-------|
| `count_pdf_services` | Síncrono | `POST /v1/count` con multipart |
| `count_pdf_services_batch` | Asíncrono | Crear job → poll estado → resultados paginados |
## Paridad con Python
Scripts para validar que la inferencia Go reproduce los resultados del pipeline Python original:
```bash
set SERVICES_COUNT_ROOT=../services-count
python scripts/parity_baseline.py
python scripts/compare_parity.py
```
## Troubleshooting
| Síntoma | Causa probable | Acción |
|---------|------------------|--------|
| API no arranca | Modelo ONNX ausente | Ejecutar `scripts/export_onnx.py` y verificar `models/latest/` |
| `/ready` falla | Redis o NATS no disponibles | Comprobar healthchecks en `docker compose ps` |
| Jobs en `pending` sin avance | Workers no consumen cola | Verificar logs del worker y conectividad a NATS/MinIO |
| Job atascado en `running` con N/N procesados | Race en metadata del job (versiones antiguas) | Reconstruir con `docker compose up --build`; `GetJob` ahora finaliza jobs completados |
| `429` al crear job | Cola saturada | Escalar workers o esperar a que baje `QUEUE_MAX_PENDING` |
| Resultados vacíos o erróneos | OCR o modelo desactualizado | Revisar `OCR_LANG=spa`, reexportar ONNX desde el repo ML |