Civic data · 2025
Elecciones HN 2025
An independent count of the 2025 Honduras presidential election, extracted from the official actas by a local vision model and published department by department — alongside the CNE's own numbers, so anyone can compare the two.
What it claims, and what it does not
That sentence decided the architecture. A tool that cannot verify the source document has exactly one honest job: transcribe faithfully, show its work, and make the original reachable. So every number on the site is traceable back to the acta it came from, and the acta image sits one click away.
The site stopped updating on 19 December 2025, and says so in a banner rather than quietly going stale.
Coverage
The real figures from the pipeline's own report, not rounded for effect.
19,137
actas published by the CNE and pulled down.
16,895
actas read, validated and counted.
88.28%
coverage — and the shortfall is reported, not hidden.
2,245
actas left unprocessed, each one listed by id in the report.
Across 18 departments plus voto en el exterior, counting five parties: PNH, PLH, LIBRE, PINU and DC.
From a scanned acta to a number
Every stage writes its output to disk, so a bad reading can be found, corrected and re-run without repeating the whole pipeline.
A local model, deliberately. The actas are public, but running 19,137 of them through a hosted API would have been slow, expensive and someone else's dependency.
How a bad reading is caught
An acta has arithmetic built into it, and that arithmetic is what validates the OCR. Each acta is sorted into one of five buckets.
| Valid | Numbers parse, and the party totals sum to the gran total. Counted. |
|---|---|
| Empty gran total | The model read no overall total, so nothing can be checked against it. |
| Gran total exceeds 400 | A mesa cannot hold more than 400 votes. Anything above it is a misread digit, not a landslide. |
| Sum ≠ gran total | The parties add up to something other than the stated total — the single most useful signal that a digit was misread. |
| Other errors | Everything that fails for a reason not covered above, kept separate rather than swept into one pile. |
The correction tool
Actas that fail validation are not discarded and not guessed at. They go to a small editor built for one job: put the original PDF next to the extracted JSON and let a human fix the digit.
Filter by status
Work through one failure category at a time instead of hunting through 19,137 files.
PDF side by side
The original acta rendered next to the values, via react-pdf and pdfjs-dist.
Edit N and L
Both the numeric and written-out values an acta carries, with the total re-summed as you type.
Re-validated after
A corrected acta goes back through the same checks. A human edit is not a free pass.
The site
Screenshots go here.
Running the pipeline
$ python3 process_actas_ocr.py --single 16-004-04-001-16498.pdf ============================================================ ACTAS OCR PROCESSING ============================================================ Found 1 PDF file(s) to process → 16-004-04-001-16498.pdf Converting PDF to image... ✓ Running OCR analysis... ✓ Saving JSON result... ✓ ✓ 16-004-04-001-16498.pdf - Completed Processing Summary: Successfully processed: 1 ============================================================
Roughly two to three minutes per acta on local hardware. Multiply by 19,137 and the reason the pipeline is resumable, chunked and writes every stage to disk becomes obvious.
What the repository holds
| utils/ | The pipeline: fetching zonas, municipios, centros and mesas from the CNE; downloading actas; converting PDFs; running OCR; comparing votes; finding inconsistencies and unprocessed files. |
|---|---|
| app/client/ | The correction tool's React + Vite interface, with react-pdf for the side-by-side view. |
| app/server/ | A small Express server that reads and writes the acta JSON files on disk. |
| actas_data_cne/ | The CNE's own published figures, kept separate from the extracted ones so the two can be compared rather than merged. |
| actas_corrections/ | Human corrections, stored apart from the raw model output so the original reading is never lost. |
| reports/ | validation_report.txt, unprocessed_actas_report.json and the inconsistency lists that produced the coverage figures above. |