Skip to content

imartinsorribes/gdpr-anonymization-engine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 

Repository files navigation

GDPR Anonymization Engine

Case study. Motor de anonimización forense de PDFs legales en español, 100 % on-premise, que combina regex + checksums matemáticos + NER + LLM local + OCR. Código propietario; este repositorio documenta el diseño y las decisiones técnicas.

Status Python PyMuPDF Ollama License


Contexto

Motor de anonimización forense desarrollado dentro del ecosistema Iceberg Datum Analytics para despachos de abogados y entornos sanitarios que manejan documentos con datos personales bajo el RGPD y la LOPD-GDD. El problema que resuelve es concreto: redactar documentos legales (escrituras, contratos, herencias, formularios) eliminando DNIs, nombres, direcciones, cuentas bancarias y otros identificadores de forma irreversible, sin que ningún dato salga del equipo del cliente.

El código no es público. Este repositorio documenta el cómo: la arquitectura, las decisiones de diseño y las trampas técnicas que se encontraron por el camino.

Por qué no bastaba con Presidio

Un primer abordaje natural sería apoyarse en Microsoft Presidio + spaCy es_core_news_lg + regex básicos. Esa receta funciona para el 70 % de los casos, pero falla en los que más importan:

  • Coreferencia inter-bloque: "el Sr. García" en la página 3 y "D. Antonio García" en la página 7 eran tratados como entidades distintas.
  • Ambigüedad por proximidad: un DNI escrito encima del nombre del solicitante, cuando en realidad pertenecía a la madre del solicitante (caso real en formularios de beca).
  • Validación débil: spaCy marca como PER apellidos infrecuentes que no lo son. Ningún sistema de NER es infalible.
  • Redacción con cajas movibles en vez de eliminación real del texto en el PDF — un PDF con "redacciones" arrastrables no es RGPD.

El rediseño del motor se apoya en tres ideas que resuelven los cuatro problemas:

  1. Pre-pass LLM para entender el documento antes de extraer.
  2. Checksums matemáticos (DNI mod-23, NIE, IBAN mod-97, NSS TGSS) con prioridad sobre el NER.
  3. Redacción forense con PyMuPDF.apply_redactions() eliminando texto e imágenes del PDF y sanitizando metadatos.

Arquitectura

El pipeline tiene 8 pasos. El diagrama de abajo es simplificado; la implementación real incluye caché LLM en SQLite, verificación OCR post-redacción y audit log GDPR en JSONL con SHA-256.

  PDF ENTRADA
      │
      ▼
  ┌──────────────────────────────────────────────┐
  │ 1a. Extracción paralela (texto + layout)     │
  │ 1b. Pre-pass NER de cabecera (coref global)  │
  │ 1c. Pre-pass LLM de contexto                 │
  │     → document_type + parties + claims       │
  └──────────────────┬───────────────────────────┘
                     ▼
  ┌──────────────────────────────────────────────┐
  │ 2. Detección por bloque                      │
  │    Primary:  LLM local (Qwen3:8b vía Ollama) │
  │    Validate: regex + checksum matemático     │
  │    Complement: NER (BSC-RoBERTa)             │
  └──────────────────┬───────────────────────────┘
                     ▼
  ┌──────────────────────────────────────────────┐
  │ 3. Consolidación de identidades              │
  │ 3a. Filtrar PER dentro de DOMICILIO          │
  │ 3b. Filtro LLM de entidades públicas         │
  │ 3c. Fusión de fragmentos PER/ORG             │
  │ 3d. Build de identidades + atributos         │
  └──────────────────┬───────────────────────────┘
                     ▼
  ┌──────────────────────────────────────────────┐
  │ 4. TUI interactiva [--interactivo]           │
  │    El operador revisa y marca cada identidad │
  │    como anonymize=true|false                 │
  └──────────────────┬───────────────────────────┘
                     ▼
  ┌──────────────────────────────────────────────┐
  │ 5-6. Redacción forense (PyMuPDF)             │
  │     + scrub de metadatos + garbage=4         │
  └──────────────────┬───────────────────────────┘
                     ▼
  ┌──────────────────────────────────────────────┐
  │ 7. Verificación post-redacción               │
  │    Texto nativo + OCR Tesseract              │
  │    → confirma que no queda PII visible       │
  └──────────────────┬───────────────────────────┘
                     ▼
  ┌──────────────────────────────────────────────┐
  │ 8. Audit log GDPR (art. 5.2 — accountability)│
  │    SHA-256 del original + SHA-256 del final  │
  │    + roles detectados + stats caché          │
  └──────────────────┬───────────────────────────┘
                     ▼
  PDF SALIDA + JSON correspondencia + audit_log.jsonl

Decisiones técnicas destacadas

Pre-pass LLM de contexto antes de extraer entidades

Caso real que motivó el diseño: un formulario de beca donde el DNI de la madre aparece antes que su nombre, en la sección "Familiares". Por proximidad simple, la heurística asigna ese DNI al solicitante (cuyo encabezado está arriba). Error grave.

El pre-pass LLM lee el documento entero y devuelve un JSON estructurado:

{
  "document_type": "formulario_beca",
  "parties": [
    {"role": "solicitante", "name": "..."},
    {"role": "madre",       "name": "..."}
  ],
  "attribute_claims": [
    {"type": "DNI", "text": "...", "owner_name": "..."}
  ]
}

Con los attribute_claims ya sabemos a quién pertenece cada dato antes de recorrer el documento bloque a bloque. La heurística de proximidad pasa a ser fallback, no autoridad.

Checksum matemático > heurística ML

DNIs, NIEs, IBAN (mod-97) y Números de Seguridad Social se validan con el checksum oficial de cada formato, no con probabilidades. Resultado: cuando una entidad pasa el checksum, la probabilidad de falso positivo es matemáticamente cero.

Esto invierte la prioridad respecto a un pipeline clásico: el regex validado gana al NER probabilístico. Solo se confía en el NER para entidades no verificables por reglas duras (nombres, direcciones, organizaciones).

Redacción forense (no cajas movibles)

PyMuPDF expone dos APIs: añadir anotaciones de redacción (cajas visibles pero removibles) o aplicar redacciones (eliminación real del texto y reescritura del PDF). La segunda es la única aceptable bajo RGPD: un PDF con rectángulos negros encima del texto sigue teniendo el texto debajo, y cualquiera con Acrobat lo extrae.

La implementación recorre cada bloque, llama a page.add_redact_annot con las coordenadas exactas de la entidad, y después ejecuta page.apply_redactions() que fuerza la reescritura. Se termina con doc.set_metadata({}) + doc.save(garbage=4, deflate=True) para eliminar metadatos residuales (autor, historial, thumbnails, XMP).

Verificación post-redacción con OCR

Un paso que muchos pipelines omiten: después de redactar, volver a leer el PDF con OCR (Tesseract sobre el render) y buscar patrones regex de identificadores. Si queda alguno, el pipeline falla explícitamente en lugar de entregar un documento aparentemente limpio. Es la diferencia entre "confío en que redacté bien" y "sé que redacté bien".

Audit log GDPR con hashes criptográficos

Cada ejecución añade una línea a audit_log.jsonl con timestamp, SHA-256 del PDF original, SHA-256 del redactado, tipo de documento y estadísticas. Conservar ese log es evidencia documental del cumplimiento del artículo 5.2 (principio de responsabilidad proactiva) ante una auditoría.

100 % on-premise (¿por qué?)

El LLM utilizado es Qwen3:8b ejecutado localmente vía Ollama (GPU AMD, Vulkan backend). Nada sale del equipo. Esto no es un nice-to-have: para un despacho que maneja escrituras, enviar el texto a una API externa (OpenAI, Anthropic, Gemini) convertiría al proveedor del LLM en encargado del tratamiento — con todo lo que eso implica en transferencias internacionales, DPA, etc. La opción on-premise elimina el problema de raíz.

Stack

Componente Tecnología
Lenguaje Python 3.12
Manipulación PDF PyMuPDF 1.24+
NER BSC-RoBERTa (mrm8488/bert-spanish-cased-finetuned-ner)
Motor NLP spaCy (es_core_news_md) + Presidio
LLM local Qwen3:8b via Ollama (Vulkan backend en AMD)
OCR Tesseract (spa language pack)
Inferencia GPU torch-directml para AMD
Caché LLM SQLite
Tests pytest (112 tests: validators, patterns, identity builder, LLM cache, regresiones)
UI (además de CLI) Flutter (Windows desktop)

Resultados (sobre datos sintéticos)

Sobre un corpus sintético generado con reglas controladas:

  • Cero falsos positivos en DNI/NIE/IBAN/NSS gracias a los checksums.
  • Recuperación de ambigüedades de propietario que la heurística de proximidad no resolvía (caso DNI-antes-de-nombre).
  • Pipeline reproducible con 112 tests en verde, cada uno con su regresión documentada.

Las métricas sobre documentos reales de clientes son propietarias y no se comparten.

Lecciones arquitectónicas destacadas

  • Combinar capas con prioridades distintas funciona mejor que apilarlas con la misma confianza. Checksum > regex > NER > LLM es un orden que compensa las debilidades de cada capa con las fortalezas de la siguiente.
  • El LLM rinde más como primer lector que como último detector. Usarlo para entender el documento antes de extraer (pre-pass) cambia la calidad del pipeline mucho más que colocarlo al final como validador.
  • Un pipeline de redacción sin verificación post-redacción es una afirmación de fe. La verificación OCR puede parecer paranoia, pero es lo que convierte el proyecto en defendible ante un auditor.
  • On-premise no es solo "privacidad", es una decisión de arquitectura jurídica. Simplifica el contrato con el cliente y elimina la necesidad de DPA con terceros.
  • Qwen3:8b corre con 6 GB de VRAM y ~50 tok/s en una AMD RX 6700 XT con Vulkan. Eso cambia por completo el cálculo de viabilidad económica frente a usar APIs cloud.

Estado del código

El código fuente no es público y forma parte del producto comercial de Iceberg Datum Analytics. Este repositorio documenta el diseño y las decisiones arquitectónicas; no contiene los módulos de producción.

Screenshots

Capturas generadas sobre herencia_falsa.pdf, un documento 100 % sintético producido por generar_herencia_falsa.py. Ningún dato real.

Pre-pass LLM + UI de revisión

La UI desktop muestra las identidades extraídas con el rol detectado por el LLM (causante, cónyuge supérstite, heredero forzoso, notario) y permite al operador decidir cuáles se anonimizan y cuáles se preservan. En este ejemplo, el notario se marca como PRESERVAR porque su identidad es pública y debe constar en la escritura.

Review UI

Redacción forense — antes y después

Comparativa directa del PDF original frente al redactado. Las cajas negras no son anotaciones movibles: el texto subyacente se ha eliminado del PDF con PyMuPDF.apply_redactions() y los metadatos se han saneado con scrub() + garbage=4.

Forensic redaction

Ejecución del pipeline

Log completo del pipeline sobre el PDF de ejemplo. Destaca el contador "26 via claim LLM, 34 via proximidad": el pre-pass LLM resolvió 26 de 60 atribuciones que la heurística por proximidad habría asignado mal. La última línea confirma la verificación post-redacción vía OCR.

CLI output


Desarrollado como parte del ecosistema de soluciones Iceberg Datum Analytics.

Autor

Iñaki Martín Sorribes — Estudiante de Ciencia de Datos en la Universitat de València y co-fundador de Iceberg Datum Analytics. Portfolio: github.com/imartinsorribes.

Licencia

Este case study está publicado bajo CC BY-SA 4.0 — puedes citarlo, adaptarlo y construir sobre él siempre que mantengas la atribución y la misma licencia.

El código fuente del motor es propietario y no está cubierto por esta licencia.

About

Case study: on-premise GDPR/LOPD PDF anonymization engine for Spanish legal documents — LLM + NER + checksums + forensic redaction

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors