Proof of Work

The measured accuracy — broken down by field, document type and confidence — how it got there, and where it goes next.

← Overlays  ·  The method stack  ·  Updated 6 Sep 2026

01The headline is three numbers, not one

Measured on the frozen v5 test set — 6,406 hand-labelled rows across 427 timesheets, scored by the same harness on every run. Reproduce it: score_both.py gt_v5_recon.jsonl qwen7b mistral24b olmocr2.

73.2%
ALL-5 null-matched
98.2%
row recovery
37.2%
ALL-5 strict
42.3%
strict ceiling
Read these together. Row recovery 98.2% — of every dated row on the sheets, we find and align 98 in 100. ALL-5 null-matched 73.2% — on a recovered row, all five fields agree with the label 73% of the time, giving a correct blank cell the credit it deserves (a rest day IS blank). ALL-5 strict 37.2% looks low only because of the strict ceiling: just 42.3% of rows have all five fields filled in the first place (most rows are rest days or partial). So 37.2% is 88% of the maximum a strict all-five score can reach — the strict number is near its mathematical limit, which is why null-matched is the honest measure of reading.

02Measured their way vs measured our way

Vendors quote character-level accuracy on clean printed text ("99% printed, 90%+ handwritten"). That is a real number — but it answers "did it read the glyphs," not "did it produce a correct payroll row." Here is the same frozen 6,406-row set scored four ways, for the union and each model alone.

Modelchar-level
(their way)
field-exactrow ALL-5
(our headline)
row recovery
qwen2.5vl:7b — alone84.8%85.8%70.3%90.3%
olmocr2:7b — alone80.8%80.8%67.7%85.7%
mistral-small-24b — alone73.7%72.1%54.8%82.9%
3-model union91.8%92.8%73.2%98.2%

And the vendor claim is specifically for clear printed text. Split our union’s character-level accuracy by document type:

Document typeChar-level (union)vs vendor claim
printed_table97.9%≈ their "99% printed"
spreadsheet93.7%chars read — but wrong columns
handwritten_form86.5%≈ their "90%+ handwritten"
whole corpus91.8%phone photos, mixed quality
So there is no accuracy gap — there is a metric gap. Measured the vendor’s way, our union reads 97.9% of characters on clean printed pages and 91.8% across the whole messy corpus — squarely at their claimed level. Our headline is lower on purpose: 73.2% is the fraction of rows where all five fields are simultaneously correct and in the right columns — the only thing that actually protects a paycheck. The spreadsheet row proves the point: 93.7% of characters read correctly, yet only 4.4% of rows usable — every glyph right, in the wrong column. A character score cannot see that error; ours is built to.
One more read of the table: olmocr2 alone scores 80.8% char-level on our corpus, not 99% — because our timesheets are nothing like the clean documents a vendor benchmarks on. No single model is at its brochure number here; the union is what recovers the difference (+7pt char, +8pt recovery over the best single model).

03Accuracy by field — the component breakdown

The 3-model union, per field. strict (must match a real value)   null-matched (a correct blank counts).

FieldStrictNull-matchedWhat limits it
date98.2%
98.2%
near-solved; the weekday cross-check pins it
time_in67.5%
90.8%
handwriting; : misread as ./;
time_out65.0%
88.5%
as above
break_minutes53.2%
87.0%
often unwritten; minutes vs hours ambiguity
total_hours49.1%
84.6%
the weakest field — recomputed & cross-checked
ALL-5 (row)37.2%
73.2%
every field must agree at once
Where to push: date is effectively solved (98.2%); the gains left are in total_hours (84.6%) and break_minutes (87.0%) — which is exactly why the pipeline recomputes hours from in/out/break rather than trusting the written total, and flags the disagreements. Predicted summation consistency is 75.2% (2,591/3,444 rows) — a built-in error detector.

04What the 3-model union buys over one model

Same test set, same harness — single best model vs the union.

ConfigurationdateALL-5 null-matchedrow recovery
qwen2.5vl:7b — single90.3%70.3%90.3%
3-model union (+ mistral-24b + olmocr2)98.2%73.2%98.2%
gain+7.9pt+2.9pt+7.9pt
Diminishing returns are real and measured — more models HURT. A 4th model (gemma3) drops the union to 72.3%; a majority-vote combiner instead of first-non-null drops it to 70.5%. The 3-model union is saturated: the next gain does not come from more models.

05Accuracy by document type — where the real weakness is

The shipped pipeline scored per template (140-file frozen set). "Conditional" = accuracy on the rows we recovered.

TemplateFilesRowsALL-5RecoveryConditional
printed_table1935364.9%77.9%83.3%
handwritten_form1121,11854.2%88.6%61.2%
punch_card67922.8%38.0%60.0%
spreadsheet3684.4%100.0%4.4%
The counter-intuitive finding: handwriting is not the weak family — it is mid-pack (54.2%) with the best row recovery. The catastrophic cases are spreadsheets (finds every row, gets every field wrong → a column-assignment problem, not a reading one) and punch cards (38% recovery → their AM/PM/OT punch pairs don't fit a single in/out slot). These are distinct, fixable failure modes — and they need dedicated handling, not better handwriting OCR. (spreadsheet n=3, punch n=6 — directional, not settled.)

06The progression — how 27% became 73%

Every point is a measured run on the frozen harness. The wins came from the pipeline, not from bigger models.

The pipeline journey on the core metric was 27.4% → 74.4%, and all of it came from pipeline work — period inference, row union, consensus, abstention — and none from fine-tuning.

07The LoRA record — five clean losses, and exactly why

Fine-tuning was tested seriously, five times, and reported honestly each time.

AttemptBase / poolResult
v1 (#31)olmOCR-2-7B, 33 filesbelow zero-shotloss
v2 (#33/35)Qwen2.5-VL-7B, 37 fileszero measurable gainloss
v3b (#57)schema-aligned, 42 files / 466 rowsdate 28.7% vs pipeline 93.6%; 51 hallucinated rowsloss
v6 (#115)2,606 scans (14× prior)12.2% exact / 18.7% coverageloss
The root cause is a data problem, not a model problem. Our inference prompt asks the model to emit a row for every date line, including blank rest days — and on this corpus most rows genuinely ARE blank (only ~1 in 11 rows carries a total). Fine-tuning on that distribution teaches the adapter to "predict null", which it then does even on populated rows. The all-rows prompt is right for inference (it drove recovery to 98%) and actively harmful as a training target at this pool size. Fine-tuning also can't beat a strong generalist at this scale. LoRA is parked — not abandoned — until the three conditions in the roadmap are met.

08The roadmap — how we systematically improve, from here

Grounded in what the 115 measured experiments actually showed. Ordered by expected return.

Recursive learning — the accretive loop (highest ROI, already half-built)

Pipeline ML — the proven levers with gains left

LoRA / fine-tuning — the conditions that would make it win

What NOT to spend on (measured dead ends)

Keep doing

  • 3-model union (first-non-null)
  • Cross-model abstention
  • Weekday/date cross-checks
  • Recompute hours, flag disagreements
  • HR corrections → labelled pool

Stop / don't start

  • A 4th model — gemma3 drops it to 72.3%
  • Majority-vote combiner — 70.5%
  • General LoRA at current scale — 5 losses
  • num_ctx sweeps — null result
  • Chasing the strict ALL-5 number — near its 42.3% ceiling

09Live experiment — should the union change?

Running · 6 Sep   A standing question: with new open models shipping monthly, should we swap the weakest member of the union, or expand to a 4th/5th model? We survey Hugging Face + the Ollama registry and bench the strongest candidates on the same frozen harness — apples-to-apples, on the fleet, no cloud.

The five candidates under test

ModelSourceWhy it's a candidate
qwen3-vl:8bAlibaba (late 2025)Newest Qwen VL — replaced qwen2.5-vl at the top; strongest CJK/multilingual. Direct swap candidate.
minicpm-v:8bOpenBMBMiniCPM-V 4.5 — widely reported best open document-OCR at 8B.
granite3.2-vision:2bIBMBuilt specifically for tables/charts/document extraction — targets our spreadsheet weakness.
llama3.2-vision:11bMetaStrong general VLM — added for error diversity, not raw OCR.
moondream:v2Moondream2GB tiny model — the cheap floor; is a small model good enough on printed forms?

The method (identical to every number on this page)

Each candidate runs the same PROMPT_V6 on a stratified small batch of the frozen v5 set, on the free fleet box (monster4), scored the same four ways. Then we score union variants — the current three, each candidate added as a 4th, and each candidate swapped for the current weakest member (mistral-24b). A candidate only earns a place if it moves row recovery or null-matched on held-out data — the bar every past change had to clear, and the bar that has rejected a 4th model and every LoRA so far.

Baseline on this batch (the bar to beat)

Configchar-levelfield-exactrow ALL-5recovery
qwen2.5vl:7b79.4%79.0%59.4%82.5%
olmocr2:7b73.6%71.6%56.7%79.1%
mistral-24b (weakest — swap target)73.2%69.8%47.8%78.4%
CURRENT union87.8%87.0%57.9%93.8%
Candidate results land here as the 2-hour run completes — each model's single-model score and its union-variant (add / swap) score, so the decision (keep three, swap one, or grow to four/five) is made on measured numbers, not on a vendor's brochure. (Batch is a 30-file stratified slice, so these subset numbers run a few points below the full-corpus 91.8%/73.2% headline — the comparison between models is what counts.)

10Why the number keeps moving

Accuracy is not frozen at deployment. Rows flagged for review — a low agreement, a weekday that disagrees with its date, a recomputed total that contradicts the written one — are the exact signal for the next improvement, and each human correction becomes a new labelled example. The metric is re-measured on the same frozen 6,406-row set after every change, and only a change that moves null-matched ships. The galleries are the audit trail: every extracted row is drawn on the sheet it came from, so any number here is traceable to its pixels.