Extracción inteligente de datos de nóminas españolas con OCR, visión por computador e inteligencia artificial generativa. El sistema no solo extrae información: detecta cuando la IA puede estar equivocada y marca los resultados para revisión humana.
- Visión y alcance
- Stack tecnológico
- Arquitectura
- Estructura del proyecto
- Requisitos previos
- Instalación y ejecución
- Variables de entorno
- API
- Estrategia de validación y guardrails
- Base de datos
- Decisiones técnicas
- Estado actual y próximos pasos
- Testing
- Licencia
NominaFlow permite a un usuario subir una nómina en formato PDF o imagen (JPEG/PNG) y obtener, de forma automatizada, los siguientes datos estructurados:
- Nombre del trabajador
- Nombre de la empresa
- Ingresos brutos
- Ingresos netos
- Fecha o perÃodo de la nómina
- IBAN
El diferenciador del producto es que no confÃa ciegamente en la IA. Cada extracción pasa por un motor de validación que verifica coherencia lógica, formato de campos y consistencia interna. Si algo falla, el resultado se marca como needs_review para revisión humana.
Este repositorio corresponde a un challenge técnico desarrollado para Kontaktu AI.
| TecnologÃa | Propósito |
|---|---|
| Python 3.10+ | Lenguaje principal del servidor |
| FastAPI | Framework web moderno para la API REST |
| Uvicorn | Servidor ASGI de alto rendimiento |
| Pydantic 2 | Validación de modelos y contratos de API |
| SQLAlchemy 2.0 | ORM para persistencia en SQLite |
| SQLite | Base de datos embebida, sin infraestructura adicional |
| PyMuPDF | Renderizado de PDF a imágenes para el modelo de visión |
| OpenAI SDK (GitHub Models) | Cliente para llamar a modelos de lenguaje y visión |
| uv | Gestor de paquetes y entornos virtuales de Python |
| TecnologÃa | Propósito |
|---|---|
| Next.js 16 | Framework React con App Router |
| React 19 | Biblioteca de interfaces de usuario |
| TypeScript 5 | Tipado estático en todo el frontend |
| Tailwind CSS 4 | Framework de utilidades CSS |
| react-dropzone | Componente de arrastrar y soltar archivos |
| sonner | Notificaciones y toasts |
| lucide-react | IconografÃa vectorial |
| TecnologÃa | Propósito |
|---|---|
| npm workspaces | Organización del monorepo frontend |
| Turbo | Orquestación de builds y tareas con caché |
| Prettier | Formateo de código |
| ESLint | Análisis estático de código |
Flujo resumido:
- El usuario arrastra un archivo al navegador.
- El frontend lo envÃa al backend y recibe un
file_idúnico. - El backend convierte el PDF a imágenes y las envÃa al modelo de visión.
- El modelo devuelve un JSON con los datos extraÃdos.
- El motor de validación analiza cada campo y asigna un nivel de confianza.
- El resultado se guarda en SQLite y se devuelve al frontend.
.
├── backend/
│ ├── app/
│ │ ├── db/
│ │ │ ├── crud.py # Operaciones contra SQLite
│ │ │ └── database.py # Modelos SQLAlchemy y conexión
│ │ ├── models/
│ │ │ └── schemas.py # Modelos Pydantic
│ │ ├── routes/
│ │ │ ├── extraction.py # Endpoints de extracción y consulta
│ │ │ └── upload.py # Endpoint de subida de archivos
│ │ ├── services/
│ │ │ ├── ocr.py # Conversión PDF/imagen + llamada al LLM
│ │ │ └── validator.py # Reglas de validación de negocio
│ │ └── main.py # Punto de entrada de FastAPI
│ ├── uploads/ # Archivos subidos temporalmente
│ ├── nominaflow.db # Base de datos SQLite
│ ├── pyproject.toml # Dependencias de Python
│ └── .env # Variables de entorno del backend
│
├── frontend/
│ └── next-monorepo/
│ ├── apps/web/
│ │ ├── app/
│ │ │ ├── page.tsx # Pantalla principal
│ │ │ └── result/[fileId]/
│ │ │ └── page.tsx # Pantalla de resultados
│ │ ├── components/
│ │ │ └── upload-zone.tsx # Zona de arrastrar y soltar
│ │ ├── lib/
│ │ │ └── utils.ts # Utilidades compartidas
│ │ └── next.config.ts # Proxy hacia el backend
│ └── packages/ui/ # Componentes compartidos del monorepo
│
├── README.md
└── kanban.md
- Python 3.10 o superior
- Node.js 20 o superior
- npm 11 o superior
- Cuenta de GitHub con token de acceso para GitHub Models
- Entra al directorio:
cd backend- Crea el entorno virtual e instala las dependencias con uv:
uv sync- Copia el archivo de ejemplo y configura tu token:
cp .env.example .envEdita .env y añade tu token de GitHub Models:
GITHUB_TOKEN=tu_token_aqui- Levanta el servidor:
uv run uvicorn app.main:app --reload --port 8001La documentación interactiva de la API estará disponible en:
http://localhost:8001/docs
- Entra al directorio:
cd frontend- Instala las dependencias:
npm install- Levanta el servidor de desarrollo:
npm run devLa aplicación estará disponible en:
http://localhost:3000
El frontend está configurado para redirigir las peticiones a /api/* hacia el backend en http://127.0.0.1:8001.
| Variable | Descripción | Requerida |
|---|---|---|
GITHUB_TOKEN |
Token de GitHub para acceder a GitHub Models | SÃ |
DATABASE_URL |
URL de conexión a la base de datos. Por defecto: SQLite local | No |
Actualmente la URL del backend está hardcodeada en frontend/next-monorepo/apps/web/next.config.ts. Se recomienda moverla a una variable de entorno en próximas iteraciones.
Sube un archivo PDF o imagen y devuelve un identificador único.
Request: multipart/form-data con el campo file.
Response:
{
"file_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"filename": "nomina_junio.pdf",
"mime_type": "application/pdf",
"status": "received",
"message": "Archivo recibido correctamente. Procesando...",
"size_bytes": 124578,
"created_at": "2026-07-16T10:00:00"
}Procesa el archivo con OCR + LLM, valida los datos y guarda el resultado.
Response:
{
"validated_data": {
"es_nomina": true,
"nombre_trabajador": "Juan Pérez GarcÃa",
"nombre_empresa": "Acme SL",
"ingresos_brutos": 2500.0,
"ingresos_netos": 2000.0,
"fecha_nomina": "06/2026",
"iban": "ES9121000418450200051332"
},
"flags": [],
"overall_confidence": "high",
"status": "validated",
"flag_count": {
"errors": 0,
"warnings": 0,
"total": 0
},
"extraction_metadata": {
"file_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"filename": "nomina_junio.pdf",
"llm_confidence": "high",
"raw_llm_response": "...",
"saved_to_database": true
}
}Recupera una extracción previamente guardada.
Lista las extracciones guardadas, ordenadas por fecha descendente.
El sistema implementa varias capas de control para reducir el riesgo de errores de la IA:
Antes de extraer datos, el modelo de visión clasifica si el documento es realmente una nómina española mediante el campo es_nomina. Si el documento no es una nómina, el resultado se marca como needs_review y se omite la validación de campos individuales.
- IBAN: debe tener 24 caracteres, comenzar por
ESy contener solo dÃgitos tras el prefijo. - Fecha: debe tener formato
MM/YYYYoDD/MM/YYYY, no puede ser futura ni anterior a 2020. - Nombres: al menos dos palabras y sin secuencias numéricas sospechosas.
- Empresa: no vacÃa, más de dos caracteres y no solo números.
- Los ingresos netos deben ser menores que los brutos.
- Se alerta si la deducción es menor al 5% o mayor al 50%.
- Se detecta si netos y brutos son iguales.
| Estado | Criterio |
|---|---|
high |
Sin errores ni advertencias |
medium |
Al menos una advertencia |
low |
Al menos un error |
El estado final puede ser validated o needs_review.
Cada respuesta cruda del LLM se almacena en el campo raw_llm_response, lo que permite investigar posteriormente por qué un campo fue extraÃdo de cierta manera.
Tabla principal: extractions
| Columna | Tipo | Descripción |
|---|---|---|
id |
TEXT (PK) | UUID del archivo |
filename |
TEXT | Nombre original del archivo |
mime_type |
TEXT | Tipo MIME |
size_bytes |
INTEGER | Tamaño en bytes |
es_nomina |
BOOLEAN | Indica si el documento es una nómina |
nombre_trabajador |
TEXT | Nombre del empleado |
nombre_empresa |
TEXT | Nombre de la empresa |
ingresos_brutos |
REAL | Ingresos brutos |
ingresos_netos |
REAL | Ingresos netos |
fecha_nomina |
TEXT | PerÃodo de la nómina |
iban |
TEXT | IBAN del trabajador |
raw_llm_response |
TEXT | Respuesta cruda del modelo |
validation_flags |
TEXT (JSON) | Flags de validación serializados |
overall_confidence |
TEXT | high / medium / low |
status |
TEXT | validated / needs_review / error |
created_at |
DATETIME | Fecha de creación |
updated_at |
DATETIME | Fecha de última actualización |
FastAPI ofrece validación automática con Pydantic, documentación OpenAPI generada y soporte nativo para dependencias asÃncronas. Esto reduce el código repetitivo y acelera el desarrollo de APIs tipadas.
Las nóminas españolas tienen layouts muy variables. Un modelo de visión generalista entiende el contexto visual del documento, tolera escaneos de baja calidad y extrae información semántica sin necesidad de plantillas por empresa.
Para un challenge técnico y un MVP, SQLite elimina la necesidad de configurar un servidor de base de datos. Es portable, se versiona fácilmente en desarrollo y es suficiente para el volumen de datos esperado.
Next.js proporciona enrutamiento, optimización de fuentes e imágenes, y un proxy reverso útil para conectar frontend y backend durante el desarrollo sin problemas de CORS.
Permite separar la aplicación web de componentes y configuraciones compartidas, escalar a múltiples aplicaciones si es necesario y aprovechar el caché de builds de Turbo.
- Subida de archivos PDF, JPEG y PNG con validación de tipo y tamaño.
- Extracción de datos mediante GPT-4o Vision.
- Validación de campos con reglas de negocio.
- Persistencia en SQLite con consulta de resultados.
- Frontend con drag-and-drop y pantalla de resultados.
- Guardrail de clasificación de documento no-nómina.
- Forzar salida JSON válida del modelo mediante
response_format={"type": "json_object"}. - Validación completa del checksum del IBAN español.
- Rate limiting y logging estructurado.
- Tests unitarios e integración con pytest.
- Reintentos con backoff ante fallos del LLM.
- Capa de API centralizada en el frontend con manejo de errores robusto.
- Validación de respuestas de API con Zod.
Actualmente no hay tests automatizados en el repositorio. Para añadirlos en el futuro:
cd backend
uv run pytestCasos prioritarios a cubrir:
- Validador de IBAN, fechas e ingresos.
- Endpoint de subida con archivos válidos e inválidos.
- Clasificación de documentos que no son nóminas.
Este proyecto se desarrolla como parte de un challenge técnico y no tiene una licencia de código abierto definida. Todos los derechos pertenecen a su autor.