Sistema de análisis y verificación electoral de las actas E-14 de la elección presidencial de Colombia (31 de mayo de 2026). Descarga las actas escaneadas publicadas por la Registraduría, las lee (IA + etiquetado humano), valida su coherencia matemática y señala las actas con posibles inconsistencias para revisión ciudadana.
Propósito. Herramienta de auditoría y transparencia electoral que trabaja exclusivamente con datos públicos publicados por la Registraduría Nacional del Estado Civil. No modifica resultados ni accede a sistemas privados; solo lee, contrasta y visualiza información ya divulgada. Una acta "sospechosa" es una acta cuya aritmética no cuadra a la lectura, no una acusación de fraude: requiere verificación humana.
- ¿Qué hace?
- Arquitectura y fases
- Estructura del repositorio
- Requisitos
- Instalación
- Descargar las actas (PDFs)
- Pipeline de procesamiento
- Aplicaciones web de etiquetado
- APIs públicas descubiertas
- El formulario E-14
- Análisis estadístico
- Notas importantes
La Registraduría publica 121.041 actas E-14 (de 122.020 mesas) como PDFs escaneados. Cada acta es el formulario físico que los jurados llenan a mano con el conteo de votos de su mesa.
El reto: leer números manuscritos de forma confiable a gran escala. La estrategia del proyecto:
- Descargar las actas desde la API pública (índice
allTransmissionCodes.json→ URL de cada PDF). - Leer cada acta. El E-14 tiene redundancia matemática:
suma(candidatos) + blanco + nulo + no_marcadas == suma_total(o== votos en urna).- Si la ecuación cuadra → la lectura es correcta automáticamente (etiqueta limpia, gratis).
- Si no cuadra → candidata a inconsistencia → revisión humana.
- Validar y etiquetar (IA profesora Qwen2.5-VL + etiquetado humano colaborativo).
- Visualizar las actas sospechosas en una web de auditoría.
- (Futuro) Escalar a las 121.041 actas con un modelo Donut destilado.
El detector de inconsistencias es la validación matemática: tras un voto de mayoría de varias lecturas (self-consistency), las actas que siguen sin cuadrar son las que merecen ojo humano.
API Registraduría Lectura Auditoría
┌───────────────────┐ ┌───────────────────────┐ ┌──────────────────┐
│ allTransmission │ PDFs │ Qwen2.5-VL (profesor) │ │ Web etiquetado │
│ Codes.json │ ──────► │ + validación │ ───► │ + dashboard │
│ (índice 121.041) │ │ matemática E-14 │ │ (humano revisa) │
└───────────────────┘ └───────────────────────┘ └──────────────────┘
│ │
▼ ▼
resultado_clean.jsonl resultado_sospechosas.jsonl
(etiquetas para Donut) (candidatas a revisión)
| Fase | Estado | Descripción |
|---|---|---|
| 1. Descarga estratificada | ✅ | Muestra geográficamente diversa: 250 actas/departamento + 2 deptos completos de holdout (Tolima, Caldas) |
| 2. Auto-etiquetado | 🔄 | Notebook Qwen2.5-VL-7B + validación matemática (self-consistency 3×, voto mayoría) |
| 3. Entrenar Donut | ⏳ | Modelo rápido destilado a partir de etiquetas limpias |
| 4. Inferencia masiva | ⏳ | Las 121.041 actas |
| 5. Dashboard web | 🔄 | Visualización de actas sospechosas con evidencia |
El estado de trabajo detallado, decisiones de diseño y resultados de experimentos están en PROYECTO.md (bitácora técnica).
SafeVote/
├── PROYECTO.md # Bitácora técnica detallada (lee esto para el contexto completo)
├── README.md # Este archivo
│
├── cache/ # Índices JSON de la API (¡se necesitan para descargar!)
│ ├── allTransmissionCodes.json # CLAVE: mapeo mesa → hash SHA256 del PDF (~35 MB)
│ ├── departmentsTree.json # Árbol dept → mun → zona → puesto → mesas
│ └── allMviewGetProgress...json # Progreso de publicación
│
├── scraper/ # Cliente de la API de la Registraduría
│ ├── registraduria_client.py # Cliente de descarga por índice de hashes
│ ├── api_endpoints.py # Mapa de todos los endpoints públicos
│ └── requirements.txt
│
├── pipeline/ # Descarga + OCR por departamento
│ ├── download_dept.py # Descarga TODAS las actas de un departamento
│ ├── ocr_dept.py # OCR con Tesseract (precisión ~60-70%, obsoleto vs IA)
│ └── preprocess.py # Preprocesamiento de imágenes
│
├── training/ # FASE 1-2: muestra de entrenamiento + auto-etiquetado
│ ├── download_training_sample.py # Descarga estratificada (toda Colombia)
│ ├── manifest_train.csv # Metadata de cada acta de entrenamiento
│ ├── manifest_holdout.csv # Metadata del holdout (Tolima + Caldas)
│ ├── fase2_autolabel_v2.ipynb # Notebook actual (Qwen2.5-VL + validación)
│ ├── make_notebook_autolabel_v2.py # Generador del notebook
│ └── pdfs_train/ , pdfs_holdout/ # PDFs descargados (IGNORADOS por git, ver abajo)
│
├── precon/ # Resultados oficiales PRECON
│ ├── download_precon.py # Descarga todos los JSONs de resultados
│ ├── analyze_precon.py # Análisis estadístico (Benford, participación, histórico)
│ └── cache/ # ~1.224 JSONs (nacional, departamentos, municipios)
│
├── analyzer/
│ └── fraud_detector.py # Reglas de detección de inconsistencias
│
├── labeling_app/ # App web de etiquetado v1 — Flask + PostgreSQL/SQLite
│ ├── app.py # Versión producción (Postgres + PyMuPDF)
│ ├── app_sqlite_local.py # Versión local (SQLite)
│ ├── templates/ # UI con zoom por casilla
│ ├── docker-compose.yml , Dockerfile , nginx/
│ └── DEPLOY.md # Guía de despliegue
│
├── safevote-web/ # App web de etiquetado v2 — Next.js + NestJS + Postgres
│ ├── api/ # Backend NestJS 10 + Prisma
│ ├── web/ # Frontend Next.js 14 + react-pdf (render en navegador)
│ ├── docker-compose.yml # db + api + web
│ ├── nginx/ # Reverse proxy + TLS
│ └── README.md
│
├── resultados/ # Salidas de OCR (CSV)
├── analyze_now.py # Análisis rápido de progreso
├── run_pipeline.py # Orquestador (descarga + OCR)
├── gen_full_manifest.py # Genera manifest completo
├── inspect_csv.py , inspect_pdf.py , test_*.py # Utilidades de inspección
- Python 3.11+ (el proyecto se desarrolló con 3.13)
- Tesseract OCR 5.x con
spa.traineddata(solo para el pipeline OCR clásico; la IA no lo necesita) - Poppler (para
pdf2image) - Para las apps web: Docker + Docker Compose (o Node.js 20 + PostgreSQL 16 para correr a mano)
- Para el auto-etiquetado (Fase 2): GPU — pensado para Google Colab Pro (A100)
httpx[http2] aiofiles pytesseract pdf2image Pillow pandas numpy scipy pymupdf
git clone https://github.com/Manuuell/ojoalvoto.git
cd ojoalvoto
# Entorno virtual
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Dependencias
pip install -r scraper/requirements.txt
# Tesseract + Poppler (solo si vas a usar el OCR clásico)
# macOS:
brew install tesseract tesseract-lang poppler
# Ubuntu:
# sudo apt install tesseract-ocr tesseract-ocr-spa poppler-utilsLos PDFs no se versionan en git (son ~2.9 GB y regenerables). Están en
.gitignore. Cualquiera que clone el repo reconstruye los datos con estos scripts a partir del índicecache/allTransmissionCodes.json, que sí está en el repo.
Descarga ~250 actas por departamento a training/pdfs_train/ y reserva Tolima + Caldas completos como holdout. Genera los manifests CSV.
python training/download_training_sample.py --per-dept 250| Flag | Default | Descripción |
|---|---|---|
--per-dept |
250 | Actas por departamento |
--concurrency |
20 | Descargas en paralelo |
Descarga todas las actas de un departamento a pdfs/{dept}/...
python pipeline/download_dept.py --dept 60 # 60 = AMAZONASCódigos de departamento: 01 Antioquia · 05 Bolívar · 07 Boyacá · 09 Caldas · 11 Cauca · 15 Cundinamarca · 16 Bogotá · 23 Nariño · 29 Tolima · 31 Valle · 60 Amazonas · 88 Consulados … (lista completa en los scripts).
Ambos scripts son idempotentes: si un PDF ya existe y pesa >500 bytes, lo saltan, así que puedes reanudar sin re-descargar.
python precon/download_precon.py # nacional + 34 deptos + ~1.189 municipiospython pipeline/download_dept.py --dept 60 # 1. descargar
python pipeline/preprocess.py # 2. preprocesar imágenes
python pipeline/ocr_dept.py --dept 60 # 3. OCR → CSV en resultados/El OCR de números manuscritos rinde ~60-70%, insuficiente para auditoría. Por eso el proyecto migró a un VLM (Qwen2.5-VL) con validación matemática.
- Sube
training/pdfs_train.zipa tu Google Drive (safevote/). - Abre
training/fase2_autolabel_v2.ipynben Colab Pro (A100). - Corre el notebook. Es reanudable y acumula entre departamentos.
Salidas (en Drive safevote/):
labels_sc.jsonl— lecturas crudas con voto de mayoríaresultado_clean.jsonl— actas consistentes → etiquetas para entrenar Donutresultado_sospechosas.jsonl— actas que no cuadran → revisión en la web
Config validada (lo que funciona, ver detalles en PROYECTO.md):
| Parámetro | Valor |
|---|---|
| Modelo | Qwen2.5-VL-7B |
| Resolución | max_pixels = 2560·28·28 (~2M) — bajarla mata la precisión |
| Recorte | encabezado superior, ancho completo |
| Lectura | self-consistency: 3 lecturas + voto de mayoría (temp 0.3) → yield 32% → 53% |
| Validación | suma(cand)+blanco+nulo+no_marc == suma_total (o == votos_urna) |
Como el techo de la IA es ~40-53%, se complementa con etiquetado humano (etiquetas perfectas que entrenan mejor a Donut). Hay dos versiones:
- Flask + Gunicorn + PostgreSQL (
app.py) o SQLite (app_sqlite_local.py) - Render del PDF en el servidor con PyMuPDF + caché
- Cola con bloqueo de 15 min (reparte actas sin repetir)
- Visor con zoom por casilla calibrado, navegación con Enter
- Reglas:
urna > votantesovotos > votantes→ posible inconsistencia
cd labeling_app
cp .env.example .env
docker compose up --build # o: pip install -r requirements.txt && python app_sqlite_local.pyGuía de despliegue: labeling_app/DEPLOY.md.
- Frontend: Next.js 14 + React +
react-pdf(renderiza el PDF en el navegador) - Backend: NestJS 10 + Prisma
- DB: PostgreSQL 16, cola con
FOR UPDATE SKIP LOCKED - Infra: Docker Compose (db + api + web) + nginx + Certbot
- Landing con estadísticas en vivo (se refrescan cada 15 s desde
/api/stats)
cd safevote-web
cp .env.example .env
docker compose up --build # api en :3000, web en :4500Endpoints NestJS: GET /api/actas/next · GET /api/actas/:id/pdf · POST /api/actas/:id/{submit,flag,skip} · GET /api/stats · GET /api/flagged
Más detalle: safevote-web/README.md.
Todas son GET públicos, sin autenticación (servidos por CDN, cache 60 s).
BASE = https://divulgacione14presidente.registraduria.gov.co/assets/temis
| Endpoint | Descripción |
|---|---|
/divipol_json/allTransmissionCodes.json |
Clave: mapeo mesa → hash SHA256 del PDF (~35 MB) |
/divipol_json/departmentsTree.json |
Árbol completo dept→mun→zona→puesto→mesa |
/divipol_json/allDepartments.json |
34 departamentos |
/divipol_json/allMviewGetProgress...json |
Progreso de publicación |
URL de un PDF E-14:
GET {BASE}/pdf/{dept}/{mun}/{zona:03d}/{stand}/{mesa:03d}/PRE/{sha256}.pdf?uuid={uuid4}
sha256=expectedNamedelallTransmissionCodes.jsonzonase rellena a 3 dígitos conzfill(3)para la URL (en el índice viene sin ceros)uuides solo cache-busting, no es autenticación
BASE = https://resultados.registraduria.gov.co/json/ACT/PR/
/00.json nacional · /{DD}.json departamento · /{DD}{MMM}.json municipio.
PRECON no tiene datos por mesa (URLs de 7+ dígitos dan 404). El PDF E-14 es la única fuente a nivel de mesa.
Cada acta tiene 3 páginas:
- Página 1 — Nivelación de la mesa (
votantes E-11,votos en urna,incinerados) + candidatos 1-7 - Página 2 — Candidatos 8-13 +
en blanco/nulos/no marcadas/ SUMA TOTAL - Página 3 — Firmas de jurados + ¿hubo recuento?
Los votos se escriben en celdas de 3 cajitas [centena][decena][unidad]. Una casilla vacía es un punto (•) = 0:
[•][5][5] = 055 = 55 votos
[•][•][2] = 002 = 2 votos
[•][•][•] = 000 = 0 votos
Renderizado a 300 DPI: ~3705 × 10842 px.
precon/analyze_precon.py corre, sobre los resultados oficiales:
- Ley de Benford — distribución del primer dígito por departamento/candidato (anomalías con Chi² alto en Nariño, Boyacá, Cundinamarca, Cauca, Valle…)
- Participación anormal — municipios muy por encima/debajo del promedio nacional (57.89%)
- Histórico del conteo — saltos anómalos en la curva de carga de mesas
python precon/analyze_precon.pyEstas anomalías estadísticas son indicios para priorizar revisión, no pruebas. Se cruzan con la lectura de las actas E-14 para verificar caso por caso.
- Datos pesados fuera de git.
pdfs/,training/pdfs_*,*.pdf,*.zip,*.tar.gzestán en.gitignore. Se regeneran con los scripts de descarga. El índicecache/allTransmissionCodes.jsonsí se versiona porque es lo que permite reconstruir todo. - Uso responsable. Trabaja solo con datos públicos. Una acta marcada es una acta a verificar, no una denuncia. Cualquier conclusión pública debe basarse en revisión humana de la evidencia.
- Bitácora técnica. El detalle de decisiones, experimentos y resultados está en PROYECTO.md.
Proyecto de auditoría ciudadana · Elección Presidencial Colombia 2026 · Datos: Registraduría Nacional del Estado Civil.