<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stephen Ohakanu</title>
    <description>The latest articles on DEV Community by Stephen Ohakanu (@sohakanu).</description>
    <link>https://dev.to/sohakanu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4126601%2F9b5e9289-d903-457f-ba1c-8b1fbc54236f.png</url>
      <title>DEV Community: Stephen Ohakanu</title>
      <link>https://dev.to/sohakanu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sohakanu"/>
    <language>en</language>
    <item>
      <title>One imperfect page per hundred: ambushing our pipeline with data nobody tuned</title>
      <dc:creator>Stephen Ohakanu</dc:creator>
      <pubDate>Tue, 15 Sep 2026 16:30:25 +0000</pubDate>
      <link>https://dev.to/sohakanu/one-imperfect-page-per-hundred-ambushing-our-pipeline-with-data-nobody-tuned-fh8</link>
      <guid>https://dev.to/sohakanu/one-imperfect-page-per-hundred-ambushing-our-pipeline-with-data-nobody-tuned-fh8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Brand-name reading accuracy fell from ~80% to 38% the moment we left our own benchmark. The flag rate rose from 28% to 97%. Exactly one imperfect page in a hundred reached the record unreviewed. This post is about why we consider that a pass.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Part 2 of a series. &lt;a href="https://dev.to/sohakanu/confidence-is-theater-benchmarking-nine-local-vlm-pipelines-on-handwritten-clinical-forms-38h6"&gt;Part 1&lt;/a&gt; benchmarked nine configurations of **Scribe&lt;/em&gt;* — a fully local pipeline that turns handwritten clinical forms into structured records, flags what it can't trust, and lets nothing unreviewed into a record unless two models independently agree. It ended with a caveat: every number was measured against gold we curated ourselves, on a schema we tuned, on public data our models may have seen. This post removes as much of that as public data allows.*&lt;/p&gt;

&lt;h2&gt;
  
  
  The ambush
&lt;/h2&gt;

&lt;p&gt;We took the two best configurations from part 1 — the same primary (&lt;a href="https://huggingface.co/mlx-community/Qwen3.8-27B-8bit" rel="noopener noreferrer"&gt;Qwen3.8-27B&lt;/a&gt;) with each of the two best verifiers — and ran them against &lt;a href="https://huggingface.co/datasets/chaithanyakota/100-handwritten-medical-records" rel="noopener noreferrer"&gt;100 handwritten Indian prescriptions&lt;/a&gt; (CC-BY-ND-4.0), whose ground truth was curated by the dataset author, not by us. The task: extract the medicines list. Nothing was tuned: the schema was written once from the task description, the prompts were frozen from part 1, and the gold conventions were the author's, held apart from ours.&lt;/p&gt;

&lt;p&gt;Two things make this set brutal. The handwriting is real doctors' cursive — not the legible volunteer writing of part 1's corpus — and the drug names are Indian brand names the models have little prior for. Which is exactly the point: a system built for clinics in one country will someday meet a register from another.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;config A (Qwen-8B verifier)&lt;/th&gt;
&lt;th&gt;config B (Gemma-3 verifier)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;strict page-level exact&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;medicine token-match F1&lt;/td&gt;
&lt;td&gt;0.27&lt;/td&gt;
&lt;td&gt;0.27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;brand-name recall&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;page flag rate&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;97%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both configurations read identically — same primary, so same extractions; the verifier only changes how much gets flagged. That is itself a finding, and it repeats part 1's: &lt;strong&gt;verification cannot fix reading. It can only catch it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading collapsed
&lt;/h2&gt;

&lt;p&gt;Brand names came back right 38% of the time. &lt;code&gt;CEPODEM&lt;/code&gt; became &lt;em&gt;Capelin&lt;/em&gt;; &lt;code&gt;ESOTAB&lt;/code&gt; became &lt;em&gt;Esotrib&lt;/em&gt;; &lt;code&gt;NIVEOLI&lt;/code&gt; became &lt;em&gt;Ulfah Niveoli&lt;/em&gt;. These are the same confident-cursive-misread failures from part 1's patient names, at triple the rate, because the handwriting is harder and the vocabulary is foreign. No prompt we own fixes this; it is the frontier of what current local VLMs can read, and it matches what the &lt;a href="https://arxiv.org/abs/2604.16504" rel="noopener noreferrer"&gt;South African maternity-record benchmark&lt;/a&gt; found on comparable material with frontier cloud models.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safety system did not
&lt;/h2&gt;

&lt;p&gt;Here is what the pipeline did with input it could not read: the calibrated confidence (part 1, finding 1: "any ambiguous character → ≤0.7") dropped to 0.60–0.70 on nearly every page, and &lt;strong&gt;97–99% of pages routed to the human review queue.&lt;/strong&gt; The system's answer to "extract this" was overwhelmingly "a person must look at this" — which, for this input, is the correct answer.&lt;/p&gt;

&lt;p&gt;And the pages that skipped the human? We audited every one. Of the three auto-accepted pages in config A:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two were &lt;strong&gt;fully correct&lt;/strong&gt; — including an 11-medicine page transcribed almost perfectly.&lt;/li&gt;
&lt;li&gt;One was correct on every medicine it listed but &lt;strong&gt;dropped one item&lt;/strong&gt; (&lt;code&gt;BETNESOL INJ&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One imperfect page escaped per hundred, under total distribution shift, on the hardest input this pipeline has ever seen. The part-1 thesis — &lt;em&gt;report what escapes, and you're grading the system&lt;/em&gt; — is the reason we can say precisely that, rather than gesture at an accuracy number.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed because of this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A scoring lesson.&lt;/strong&gt; Our first pass at medicine-level scoring had a directional bug: extracted rows carrying dose schedules (&lt;code&gt;1-0-1&lt;/code&gt;) were penalized as spurious even when they named the right drug. External evaluation debugs your evaluator too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expectation-setting for sign-off.&lt;/strong&gt; Our production target (≥98% auto-accepted accuracy) was measured at 96% on the friendly corpus and is unmeasurable on this hostile one — the true number for our actual target forms will land between, and only the held-out set of real register pages will say where.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence in the gate, not the reader.&lt;/strong&gt; Every investment that generalized was in the &lt;em&gt;system&lt;/em&gt; — calibration, dual-read verification, the review queue. Every investment that didn't was in squeezing the reader.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The held-out evaluation: real target-form registers, hand-filled and photographed, never published, created after every model's training cutoff — the only data that can produce sign-off numbers. And one engineering item part 1 predicted and this post reinforces: explicit checkbox-state detection, likely powered by the one model family we found that refuses to invent.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Corpus: &lt;a href="https://huggingface.co/datasets/chaithanyakota/100-handwritten-medical-records" rel="noopener noreferrer"&gt;100-handwritten-medical-records&lt;/a&gt; (CC-BY-ND-4.0), evaluated as-is and not redistributed. Our own benchmark gold from part 1 is published at &lt;a href="https://huggingface.co/datasets/sohakanu/scribe-flow-gold" rel="noopener noreferrer"&gt;sohakanu/scribe-flow-gold&lt;/a&gt;. All extraction ran locally on one machine; no page left it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Confidence is theater: benchmarking nine local VLM pipelines on handwritten clinical forms</title>
      <dc:creator>Stephen Ohakanu</dc:creator>
      <pubDate>Tue, 15 Sep 2026 16:29:54 +0000</pubDate>
      <link>https://dev.to/sohakanu/confidence-is-theater-benchmarking-nine-local-vlm-pipelines-on-handwritten-clinical-forms-38h6</link>
      <guid>https://dev.to/sohakanu/confidence-is-theater-benchmarking-nine-local-vlm-pipelines-on-handwritten-clinical-forms-38h6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Median self-reported confidence was 0.95 when the model was right — and 0.95 when it was wrong. Everything useful we learned came from making models disagree with each other, not from asking one how sure it felt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Scribe&lt;/strong&gt; is a local pipeline that turns scanned or photographed handwritten clinical forms — vitals charts, prescriptions, lab requests, visit notes — into schema-conformant structured records. Every field carries provenance back to a region of the page, and anything uncertain is routed to a human instead of into the record. It is built for offline-first health settings, where the documents are paper, the connectivity is unreliable, and the data cannot leave the machine: &lt;code&gt;scribe run&lt;/code&gt; holds a process-wide socket guard that blocks any connection off loopback, so "local" is enforced rather than intended.&lt;/p&gt;

&lt;p&gt;This is the benchmark log from getting there: nine configurations of pipeline and model architecture, run on the same 23 handwritten pages against the same hand-curated gold set, including the experiments that failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One disclaimer before any numbers.&lt;/strong&gt; The corpus is public — pages from the &lt;a href="https://huggingface.co/datasets/Nigeria-Health-data-OCR-pipeline/African-Medical-Records" rel="noopener noreferrer"&gt;African Medical Records&lt;/a&gt; dataset (CC-BY-4.0), pinned at revision &lt;code&gt;fa27d29&lt;/code&gt;. Public data may overlap the training corpora of every model tested, and one of the models tested was released &lt;em&gt;after&lt;/em&gt; this dataset was published. So nothing here is a performance claim. These numbers compare configurations against each other under identical conditions — they measure the &lt;em&gt;pipeline&lt;/em&gt;, not the models' true reading ability. Sign-off numbers will come from a held-out set of homemade pages on real target forms, created after the models' training cutoffs, which never leaves the machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Host:&lt;/strong&gt; MacBook Pro, Apple M5 Max, 128 GB unified memory, &lt;a href="https://github.com/ml-explore/mlx" rel="noopener noreferrer"&gt;MLX&lt;/a&gt; via &lt;code&gt;mlx-vlm&lt;/code&gt;, models served OpenAI-compatible with grammar-constrained JSON decoding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corpus:&lt;/strong&gt; 23 pages — 20 from the &lt;a href="https://huggingface.co/datasets/Nigeria-Health-data-OCR-pipeline/African-Medical-Records" rel="noopener noreferrer"&gt;AMR dataset&lt;/a&gt; plus 3 synthetic pages built to carry specific traps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gold:&lt;/strong&gt; 221 hand-curated field values. 51 of them (23%) are legitimately &lt;strong&gt;null&lt;/strong&gt; — absent, illegible, or non-numeric where a number is required. Those 51 are the point of the set: each must reach the review queue rather than acquire a plausible value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The traps are the interesting part. Real forms from this setting write an adult's age as &lt;code&gt;"Ad"&lt;/code&gt; in an integer field; give an infant's age in months; use four different date formats; identify a patient only by hospital number and bed; and hand the model a tick-box grid with &lt;strong&gt;nothing ticked&lt;/strong&gt;. The gold answer for each is null-plus-flag. A model that guesses politely is worse than a model that abstains, because a guessed value in a clinical record looks exactly like a real one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fsohakanu%2Fscribe-article-assets%2Fresolve%2Fmain%2Fpipeline.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fsohakanu%2Fscribe-article-assets%2Fresolve%2Fmain%2Fpipeline.png" alt="Scribe pipeline: ingest, locate, read, validate, verify, then review queue or structured record" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Six stages. A grounding call localizes every field on the page at once; extraction is &lt;strong&gt;routed&lt;/strong&gt; — tables, dates, enums, and checkbox groups are read from margin-padded crops, scalars and prose from the whole page (finding #4 explains why); a validator enforces schema and types; then a &lt;strong&gt;verify pass&lt;/strong&gt; re-reads every auto-accepted field with a &lt;em&gt;different&lt;/em&gt; model, and disagreement flags the field for a human. The bench below is the story of how each of those stages earned its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoreboard
&lt;/h2&gt;

&lt;p&gt;Nine configurations, same 23 pages, same 221-value gold set. Column meanings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;s/field&lt;/strong&gt; — mean wall-clock seconds per field, including verify where present.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;exact&lt;/strong&gt; — fields exactly matching gold, flagged or not. How well the models &lt;em&gt;read&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;auto-acc&lt;/strong&gt; — accuracy of only the fields the pipeline accepted &lt;em&gt;without&lt;/em&gt; flagging. How much you can trust what skips human review. This is the number a production deployment lives on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;flags&lt;/strong&gt; — fraction of fields sent to the review queue (budget: ≤30%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FN flags&lt;/strong&gt; — wrong values that were &lt;em&gt;not&lt;/em&gt; flagged: silent errors, the worst category.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;halluc&lt;/strong&gt; — values invented for fields whose gold is null.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;halluc esc.&lt;/strong&gt; — inventions that were &lt;em&gt;not&lt;/em&gt; flagged, so they reach the record with no human look. The clinically meaningful one (finding 6).&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Primary&lt;/th&gt;
&lt;th&gt;Verifier&lt;/th&gt;
&lt;th&gt;s/field&lt;/th&gt;
&lt;th&gt;exact&lt;/th&gt;
&lt;th&gt;auto-acc&lt;/th&gt;
&lt;th&gt;flags&lt;/th&gt;
&lt;th&gt;FN flags&lt;/th&gt;
&lt;th&gt;halluc&lt;/th&gt;
&lt;th&gt;halluc esc.&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v1&lt;/td&gt;
&lt;td&gt;whole-page&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/mlx-community/Qwen3-VL-30B-A3B-Instruct-8bit" rel="noopener noreferrer"&gt;Qwen3-VL-30B-A3B&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;9.8&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v1c&lt;/td&gt;
&lt;td&gt;crop-all&lt;/td&gt;
&lt;td&gt;Qwen3-VL-30B-A3B&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;td&gt;71%&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;29%&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v1r&lt;/td&gt;
&lt;td&gt;routed&lt;/td&gt;
&lt;td&gt;Qwen3-VL-30B-A3B&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;6.4&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;td&gt;79%&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;td&gt;21%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2&lt;/td&gt;
&lt;td&gt;+ tuned prompts, same-model verify&lt;/td&gt;
&lt;td&gt;Qwen3-VL-30B-A3B&lt;/td&gt;
&lt;td&gt;itself&lt;/td&gt;
&lt;td&gt;9.9&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;td&gt;93%&lt;/td&gt;
&lt;td&gt;32%&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v3&lt;/td&gt;
&lt;td&gt;cross-model verify&lt;/td&gt;
&lt;td&gt;Qwen3-VL-30B-A3B&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/mlx-community/Qwen3-VL-8B-Instruct-8bit" rel="noopener noreferrer"&gt;Qwen3-VL-8B&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;34%&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v4&lt;/td&gt;
&lt;td&gt;cross-family verify&lt;/td&gt;
&lt;td&gt;Qwen3-VL-30B-A3B&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/mlx-community/GLM-4.5V-4bit" rel="noopener noreferrer"&gt;GLM-4.5V&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;11.9&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;td&gt;94%&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;td&gt;6%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v5&lt;/td&gt;
&lt;td&gt;new primary — &lt;strong&gt;shipping default&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/mlx-community/Qwen3.8-27B-8bit" rel="noopener noreferrer"&gt;Qwen3.8-27B&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Qwen3-VL-8B&lt;/td&gt;
&lt;td&gt;17.3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;81%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v7&lt;/td&gt;
&lt;td&gt;out-of-family verifier&lt;/td&gt;
&lt;td&gt;Qwen3.8-27B&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/mlx-community/gemma-3-12b-it-8bit" rel="noopener noreferrer"&gt;Gemma 3 12B&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;19.8&lt;/td&gt;
&lt;td&gt;81%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;97%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v8&lt;/td&gt;
&lt;td&gt;next-gen verifier&lt;/td&gt;
&lt;td&gt;Qwen3.8-27B&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/mlx-community/gemma-4-12B-it-qat-OptiQ-4bit" rel="noopener noreferrer"&gt;Gemma 4 12B&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;20.0&lt;/td&gt;
&lt;td&gt;81%&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;td&gt;29%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v9&lt;/td&gt;
&lt;td&gt;third-family primary&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/Shiftedx/Muse-Glimmer-30B-MXFP8-Vision-MLX" rel="noopener noreferrer"&gt;Muse Glimmer 30B&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Qwen3-VL-8B&lt;/td&gt;
&lt;td&gt;24.6&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;(v4 and v8 changed only the verifier, so their reading-accuracy columns track their primary's row, as expected.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The trajectory that matters is &lt;strong&gt;auto-acc&lt;/strong&gt;: 75% → 96%. Exact accuracy barely moved (75% → 81%). The models never learned to read much better — the pipeline learned to &lt;em&gt;know when they were wrong&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 1 — confidence is theater; agreement is evidence
&lt;/h2&gt;

&lt;p&gt;The original plan for the accept/flag gate was threshold tuning on the models' self-reported confidence. Offline analysis killed it in one table: the median composite confidence was &lt;strong&gt;0.95 for correct values and 0.95 for wrong ones&lt;/strong&gt;. Handwritten proper names misread with total confidence (Amaka → Anaelly, Okafor → Okaro) sailed straight into the record. There was no threshold to tune.&lt;/p&gt;

&lt;p&gt;What worked instead was disagreement. The verify pass re-reads every auto-accepted field from the complementary source (crop if the first read was whole-page, and vice versa). In v2 that one change took auto-accepted accuracy from 79% to 93% and cut silent errors from 21% to 7% — with the model's reading ability &lt;em&gt;unchanged&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 2 — the second reader must be a different model
&lt;/h2&gt;

&lt;p&gt;Same-model verification cannot catch a bias both reads share. Switching the verifier to a different model (&lt;a href="https://huggingface.co/mlx-community/Qwen3-VL-8B-Instruct-8bit" rel="noopener noreferrer"&gt;Qwen3-VL-8B&lt;/a&gt;, running alongside the primary in 128 GB) caught 52 disagreements and pushed auto-acc to 95%. The decorrelation was visible field by field: every confidently misread patient name was now caught before auto-acceptance (94% → 100%), and systematic medication-column-splitting differences surfaced as flags instead of passing silently. For a clinical pipeline, 79% → 95% trusted-output accuracy for ~1.6× latency is not a close call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 3 — four model families all tick the empty checkbox
&lt;/h2&gt;

&lt;p&gt;The most stubborn failure in the bench. Here is the trap, from one of our synthetic pages — a lab request form where nothing is ticked and the gold answer is null:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fsohakanu%2Fscribe-article-assets%2Fresolve%2Fmain%2Fcheckbox_trap_syn003.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fsohakanu%2Fscribe-article-assets%2Fresolve%2Fmain%2Fcheckbox_trap_syn003.png" alt="An untouched checkbox row: Specimen type — Blood, Urine, Stool, CSF, all boxes empty" width="800" height="75"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Qwen3-VL-30B says "blood". Qwen3-VL-8B says "blood". GLM-4.5V says "blood" on the real forms. Gemma 3 read the empty box glyph as a tick. &lt;strong&gt;Four model families, three generations, one shared conviction that somebody must have wanted a blood test.&lt;/strong&gt; Hallucination on this field stayed at 75% under every model pairing tried — cross-model verification can't catch a bias when both readers share it, and this one appears to be how current VLMs treat printed option rows as such. The fix is not a better prompt; it is explicit checkbox-state detection, which is now a scheduled pipeline stage rather than a hope.&lt;/p&gt;

&lt;p&gt;A coda earned after this section was first drafted: a fifth family finally broke the pattern. Meta's &lt;a href="https://huggingface.co/Shiftedx/Muse-Glimmer-30B-MXFP8-Vision-MLX" rel="noopener noreferrer"&gt;Muse Glimmer 30B&lt;/a&gt; (v9), tried as a primary, invented &lt;strong&gt;nothing&lt;/strong&gt; — 0% hallucination, on this field and every other — while losing badly everywhere else (74% exact, 42% flags). The bias is not universal to VLMs after all; it is absent in Meta's perception-encoder lineage. Muse doesn't get the primary seat, but its abstention discipline makes it the obvious candidate to &lt;em&gt;power&lt;/em&gt; the checkbox-detection stage.&lt;/p&gt;

&lt;p&gt;This generalizes: &lt;strong&gt;a verifier only buys you accuracy on failures the two models don't share.&lt;/strong&gt; Know which of your failure modes are idiosyncratic and which are universal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 4 — crops fix hallucination, whole pages fix context; route by field type
&lt;/h2&gt;

&lt;p&gt;Reading each field from a cropped region (one grounding call per page localizes everything at once) was 2.45× faster and fixed exactly what it was supposed to fix: &lt;code&gt;document_date&lt;/code&gt; hallucination fell from 67% to 17% — the model could no longer helpfully borrow a date from the observation table — and table extraction accuracy doubled. But crops &lt;em&gt;hurt&lt;/em&gt; header-labeled scalars and prose (hospital number 87% → 65%): mislocalize the box and the value is gone.&lt;/p&gt;

&lt;p&gt;So: route. Tables, dates, enums, and checkbox groups from crops; scalars and free text from the whole page. Routed mode beat both pure modes on every quality metric at 1.5× whole-page speed, which is why it's the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 5 — the verifier seat saturated; the negative results say where the ceiling is
&lt;/h2&gt;

&lt;p&gt;v4, v7, and v8 are three attempts to buy more accuracy with a better verifier — a different family (GLM-4.5V), a fully out-of-family lineage (Gemma 3), and a next-generation model (Gemma 4). All three landed within ~2 fields of the 8B verifier: statistical ties on 221 values. GLM even flagged 100% of medication rows — review burden without accuracy gain.&lt;/p&gt;

&lt;p&gt;The conclusion is worth keeping precisely &lt;em&gt;because&lt;/em&gt; the experiments "failed": with a competent primary and this gate design, &lt;strong&gt;any competent verifier lands at 94–97% auto-accepted accuracy.&lt;/strong&gt; Further gains live in the primary model, the gold conventions, and checkbox detection — not in verifier shopping. The 8B stays: smallest, fastest, same topline.&lt;/p&gt;

&lt;p&gt;(One genuine signal: Gemma 4 was the first model to pass the synthetic empty-checkbox trap. Noted, and waiting for held-out data.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 6 — count the hallucinations that &lt;em&gt;escape&lt;/em&gt;, not the ones that happen
&lt;/h2&gt;

&lt;p&gt;Late in the bench we split hallucination into &lt;strong&gt;invented&lt;/strong&gt; (the model produced a value for an empty field) and &lt;strong&gt;escaped&lt;/strong&gt; (the invention wasn't flagged, so it would reach the record with no human look). Only the second one matters clinically, and the pipeline had been quietly winning it all along:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;invented&lt;/th&gt;
&lt;th&gt;escaped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v1r routed, no verify&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2 same-model verify&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v3 cross-model verify&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v5 shipping default&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v7 Gemma 3 verifier&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v8 Gemma 4 verifier&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v9 Muse Glimmer primary&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Models keep inventing — the checkbox bias never went away — but from v3 onward the pipeline catches all but one or two inventions per run. If your eval only reports raw hallucination rate, you are grading the model. Report what escapes, and you're grading the system, which is the thing you actually ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  What no benchmark on public data can settle
&lt;/h2&gt;

&lt;p&gt;Three things this bench cannot honestly answer, and how each gets answered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Contamination.&lt;/strong&gt; The corpus is public and the current primary (&lt;a href="https://huggingface.co/mlx-community/Qwen3.8-27B-8bit" rel="noopener noreferrer"&gt;Qwen3.8-27B&lt;/a&gt;) was released &lt;em&gt;after&lt;/em&gt; the corpus was. Its v3 → v5 improvement could be reading or could be recall. A canary-based contamination check runs before any accuracy is reported per model revision, and the held-out set — created after training cutoffs, never published — is the only source of sign-off numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The production targets.&lt;/strong&gt; ≥98% auto-accepted accuracy and ≤2% escaped hallucination, held-out only. Flow data sits at 96% and 2%; the remaining distance is concentrated in checkbox detection and one gold-convention question about how to split medication dose/frequency columns — a judgment call two humans would also disagree on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generalization.&lt;/strong&gt; Every number above was measured against gold we curated ourselves, on a schema we tuned. The next post in this series takes the two best configurations and ambushes them with an external set nobody here tuned against — 100 handwritten prescription pages with the dataset author's own gold. The results surprised us in both directions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything that public flow data can teach has been extracted. That felt like the right moment to write it down.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;This is part 1 of a series.&lt;/strong&gt; Part 2: &lt;a href="https://dev.to/sohakanu/one-imperfect-page-per-hundred-ambushing-our-pipeline-with-data-nobody-tuned-fh8"&gt;&lt;em&gt;One imperfect page per hundred&lt;/em&gt;&lt;/a&gt; — the external evaluation on data nobody tuned against. Code and methodology: &lt;a href="https://github.com/kod201/scribe" rel="noopener noreferrer"&gt;github.com/kod201/scribe&lt;/a&gt;. The benchmark gold set from this post is available at &lt;a href="https://huggingface.co/datasets/sohakanu/scribe-flow-gold" rel="noopener noreferrer"&gt;sohakanu/scribe-flow-gold&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Scribe runs entirely on one machine; adding a new form type means writing a schema YAML, never editing code. The bench methodology — pinned model revisions, trap-carrying gold sets, invented-vs-escaped hallucination accounting — is documented alongside the pipeline. Corpus: &lt;a href="https://huggingface.co/datasets/Nigeria-Health-data-OCR-pipeline/African-Medical-Records" rel="noopener noreferrer"&gt;African Medical Records&lt;/a&gt; (CC-BY-4.0). The checkbox illustration above is from one of our synthetic pages; every name on every sample page shown or discussed is fictional.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
