OCR comparisons are easy to make look scientific: send a few pages to several models, eyeball the Markdown, and turn the outputs into a leaderboard. The difficult part is making every page, corruption, prompt, output contract, timing, failure, and retry mean the same thing. OCR Model Arena is the evaluation system I built to make that comparison reproducible. This is a report on the working platform and its first measured baseline, not a claim that all of its model adapters have already been benchmarked.
- What is real
- Architecture
- Dataset
- Corruptions
- Scoring
- Exact tests
- Results
- Model comparison
- Limitations
- Next benchmark
Start with the evidence boundary
The most important result is not a score. It is a precise boundary around what the repository proves. A registry entry proves that an adapter can be configured. An injected runtime test proves that the adapter understands an upstream response shape. Neither proves that the heavyweight package, checkpoint, CUDA kernel, prompt, or serving stack works in the target environment. I keep those levels separate:
| System | Evidence produced here | Real inference? |
|---|---|---|
| Tesseract | Native binary smoke, text/TSV/hOCR parsing tests, and the complete 280-evaluation robustness run. | Yes |
| Fixture model | Registry → CLI → backend → metrics → storage → report, including resume. It deliberately does not inspect the image. | No; test instrument |
| PP-OCR | Lifecycle, dependency failure, and documented response-field parsing through an injected Paddle-style pipeline. | No |
| PaddleOCR-VL page pipeline | Injected page parser and Markdown/plain-text separation. | No |
| PaddleOCR-VL element model | Registry construction; its generic Hugging Face parent runs against fake Torch/Transformers objects. | No |
| GLM-OCR page pipeline | Injected parse() response with structured JSON and Markdown. | No |
| GLM-OCR element model | Registry construction; generic Hugging Face parent contract test. | No |
| DeepSeek-OCR | Constructor and registry mapping only. Its dedicated CUDA infer() path remains unexecuted. | No |
| olmOCR | Injected OpenAI-compatible transport, actual image resize, and YAML/front-matter parsing. | No |
| Generic HF / endpoint VLM | Fake native generation objects and injected endpoint responses, including seeds and usage metadata. | No |
| Modal / RunPod | Submission, batching, handle recovery, timeout, stale-job, authentication, and routing behavior against fake providers. | No live cloud run |
The code repository is private during this first integration pass, so the article ships a public, machine-readable benchmark summary and robustness table, but not a public source link. The status should change only after the real checkpoint and cloud rows exist.
The system: page in, auditable experiment out
The core interfaces know nothing about Modal or RunPod. A model loads and predicts; a dataset yields samples; a metric compares typed truth with a typed prediction; and a backend submits jobs. Provider code only transports the same portable job envelope. This keeps a model usable on a laptop, in a persistent service, or in a cloud worker without copying its inference logic into three places.
dataset adapter
image + SHA-256 + MIME + plain truth + Markdown + ordered blocks
│
▼
seeded corruption engine ──▶ transformed PNG + complete provenance
│
▼
deterministic OCR job id = hash(dataset, truth, model, backend,
corruption, metric contract, seed, repeat)
│
├──────── local batch ────────▶ reused in-process model
├──────── Modal batch ────────▶ versioned deployed worker
└──────── RunPod batch ───────▶ queue endpoint / persistent worker
│
▼
raw response + plain text + optional Markdown + timings + model metadata
│
▼
normalization ──▶ metrics ──▶ deterministic failure flags
│
▼
atomic page JSON + separate raw output + rebuildable Parquet
│
▼
leaderboard + robustness + failures + latency/cost Pareto reports
A benchmark expands the Cartesian product of pages, models, corruption configurations, inference settings, and repeats. Existing records are parsed and identity-checked before they count as complete. Local workers reuse a loaded model. Remote workers accept batches so one page does not pay for one GPU container startup. Cloud submissions persist an uncertainty intent and then the provider handle before polling; if the process dies in the dangerous window where a provider may have accepted paid work but no handle was committed, the runner stops for reconciliation instead of silently submitting it again.
Storage treats individual JSON records as the transactional source of truth. Raw outputs live beside them, and Parquet is regenerated as an analytical view. Writes use a temporary file, file synchronization, atomic replacement, and directory synchronization. A filesystem lease prevents two local processes from driving the same run at once.
A tiny dataset that makes the whole repository runnable
The first dataset is synthetic on purpose. It avoids a multi-gigabyte download and gives the evaluator exact text, Markdown, block type, order, and bounding boxes. The canonical corpus contains eight 1275 × 1650 RGB pages at 300 DPI, rendered with DejaVu Sans, Bold, and Mono. Every page has a fixed research header and a page footer.
| Template | What it tests |
|---|---|
| Paragraphs | Headings, prose, punctuation, numbers, currency, and line wrapping. |
| Heading/list | Heading levels and four ordered list items. |
| Table | A 4 × 4 visual grid, cell text, and a separate Markdown-table reference. |
| Code/equation | Monospaced Python, operators, indentation, and a CER equation. |
| Columns | Two independently positioned text columns and global reading order. |
| Invoice | Dates, account and tax identifiers, line items, and amounts. |
| Formulas | Four plain-text/Unicode mathematical expressions. |
| Mixed | A heading, incident paragraph, unordered list, header, and footer. |
Generation uses a dedicated deterministic random stream derived from the global seed and template index. Tests generate the seed-42 dataset twice in separate directories and compare complete truth objects and image hashes. Prepared images are still the reproducible artifact: font files, Pillow, and PNG encoders can change bytes across environments even when the pseudo-random sequence is identical.
Robustness is a controlled experiment, not an “ugly scan” folder
Every transformation validates and stores its resolved parameters. A typo fails instead of quietly falling back to a default. Each step records its own seed and input/output pixel hashes, which means a failure can be replayed exactly from the original page. The measured sweep used one corruption family at a time:
| Family | Parameter points | Configurations |
|---|---|---|
| Gaussian blur | σ = 0.5, 1, 2 | 3 |
| Motion blur | size 3, 7 × angle 0°, 30° | 4 |
| Gaussian noise | standard deviation 5, 15, 30 | 3 |
| JPEG | quality 80, 60, 40, 20 | 4 |
| Downsample | factor 0.75, 0.5, 0.25 | 3 |
| Rotation | 1°, 2°, 5° | 3 |
| Skew | 1°, 3° | 2 |
| Perspective | magnitude 0.02, 0.06 | 2 |
| Uneven illumination | strength 0.25, 0.5 | 2 |
| Contrast reduction | factor 0.75, 0.5 | 2 |
| Partial occlusion | 10% width × 5%, 10% height | 2 |
| Crop | 2%, 5% on each edge | 2 |
| Scanner artifacts | 3, 8 lines at speckle rate 0.0003 | 2 |
| Total | 34 corrupted + 1 clean | 35 |
Within one family, parameter lists form a Cartesian product; across families, this run uses one_at_a_time. That isolates a blur curve from a rotation curve. A separate cartesian mode can compose families for stress tests, but its scores answer a different question.
Crop and occlusion preserve the original full-page reference. I treat those as document recovery tasks—“recover the page despite missing pixels”—rather than visible-only OCR. The report labels this policy, and 32 records in the run changed visibility. A later visible-only track should derive its reference from block bounding boxes and a visibility mask.
Scoring without letting output formatting choose the winner
Text normalization uses Unicode NFKC, removes invisible formatting characters, and collapses whitespace and line breaks. It preserves case and punctuation. Line-wrap differences should not decide a model comparison; changing Case to case should.
CER = Levenshtein(reference characters, prediction characters)
─────────────────────────────────────────────────────────
number of reference characters
WER uses whitespace-delimited words instead of characters.
Normalized similarity = 1 - edit_distance / max(reference_len, prediction_len)
The default suite includes CER, WER, normalized edit similarity, exact match, reading order, heading/list signature, table shape, and Markdown block-sequence similarity. CER and WER are reported both as page-level macro means and as micro scores that sum edit counts and reference lengths. Structure metrics mark whether the reference makes them applicable; a page with no table cannot inflate table accuracy merely because the prediction also contains no table.
Structure scoring is deliberately described as heuristic. Reading order fuzzy-matches ordered reference blocks to locations in the output and measures the longest increasing subsequence. Table similarity currently compares table count and row/column shape, not cell transcription. Markdown similarity compares block-type order, not semantic content. There is not yet a geometric bounding-box IoU score or a specialized formula-recognition score.
Failure flags are also deterministic diagnostics rather than a learned hallucination oracle. They cover empty and excessive output, repeated sequences, duplicate paragraphs, missing headings, unexpected tokens, broken order, malformed tables/Markdown, and complete generation collapse. Raw output is always retained so a flag can be audited.
Exactly how I tested it
The repository is pinned to Python 3.12 through uv. The local benchmark ran on CPython 3.12.12, Linux x86-64, Tesseract 4.1.1 with Leptonica 1.82.0, English, OEM 1 (LSTM-only), PSM 3 (automatic page segmentation), plain-text output, and a 120-second timeout. It used a local CPU, batch size one, one in-flight batch, seed 42, and one repeat.
uv sync --locked --extra dev
uv run ocr-arena datasets prepare synthetic-v1 \
--root data/synthetic-v1 \
--count 8 \
--seed 42
uv run ocr-arena sweep \
--config configs/robustness.yaml \
--artifacts artifacts
The sweep expansion was checked before inference:
8 unique pages × 1 model × (34 corruptions + clean) × 1 repeat
= 280 evaluations
Every request rereads and verifies the expected SHA-256 and MIME type before execution and rejects images above 50 megapixels. Tesseract starts one subprocess per page, so its measured latency includes process startup. The report distinguishes warm model inference from driver end-to-end timing where an adapter can measure both; this baseline has no persistent model server, network, queue, cold-start, token, GPU, or VRAM component.
The automated suite
The final checkout passed 111 tests with 82% statement coverage, plus Ruff formatting/linting, strict mypy over 52 source files, a frozen dependency-lock check, package build, Docker Compose validation, two Docker image builds, and a one-page Docker Tesseract smoke. GitHub Actions repeats the locked Python checks and a two-page fixture CLI benchmark on every push.
| Test area | Tests | What is exercised |
|---|---|---|
| Backends | 28 | Local lifecycle/batches; Modal call IDs, versions, failures; RunPod submit/poll, payload limits, expiration, concurrency. |
| Models | 15 | Registry, lifecycle, parsing contracts, seeds, token metadata, optional dependency errors, native Tesseract. |
| Failure analysis | 12 | All deterministic flags plus ranked failure bundles. |
| CLI and configuration | 12 | Strict sweep schemas, model revisions, paid-cloud confirmation, routing, end-to-end CLI output. |
| Metrics and normalization | 15 | Known CER/WER cases, empty strings, Unicode, Markdown conversion, structure applicability. |
| Corruptions | 7 | 13 registrations, seed replay, canonical parameters, typo rejection, sweep modes. |
| Evaluation, IDs, storage, reports | 18 | Experiment identity, serialization, atomic records, resume, Parquet, aggregation, Pareto behavior. |
| Synthetic data and training schemas | 4 | Byte/truth reproducibility, all templates, and holdout-safe training configuration boundaries. |
| Total | 111 | All passed; no skipped tests in the recorded final run. |
Cloud tests use fake provider transports. The environment had no Modal or RunPod credentials, and the local NVIDIA installation had a driver/library mismatch, so I did not run a GPU model and do not report cloud latency, peak VRAM, or cost.
What the first 280 OCR evaluations said
The run completed all 280 jobs with zero execution errors in 163.75 seconds wall time. A resume immediately afterward skipped all 280 OCR jobs, though it still regenerated the corruptions before discovering the durable records, taking 46.78 seconds. That inefficiency is visible and should be fixed by checking experiment identity before materializing variants.
| Measurement | All 280 conditions | Interpretation |
|---|---|---|
| Macro CER | 0.10177 | Unweighted mean across evaluations. |
| Micro CER | 0.12381 | Total character edits / total reference characters. |
| Macro WER | 0.16920 | Unweighted evaluation mean. |
| Micro WER | 0.18248 | Total word edits / total reference words. |
| Normalized similarity | 0.89834 | One minus edit distance normalized by the longer side. |
| Reading order | 0.84756 | Heuristic ordered-block score. |
| p50 / p95 / p99 | 290 / 405 / 440 ms | Includes Tesseract subprocess startup. |
| Batch-service rate | 3.09 pages/s | Deduplicated model-service durations. |
| Wall throughput | ≈ 1.71 pages/s | Includes planning, corruptions, persistence, and reporting. |
| Failure flags | 5 / 280 | All five were missing_sections; none were empty. |
I exclude the report's legacy gpu_seconds_per_page field here. In this CPU-only run it contains mean inference seconds, not GPU utilization. Cost and VRAM were null.
Clean documents: one layout failure dominates the average
On the eight clean pages, macro CER was 0.06586 and normalized similarity was 0.93414. Seven templates were transcribed with a combined macro CER of 0.00776. The two-column page alone had CER 0.47253.
Clean-page CER; bars are scaled to the worst page, not to 1.0.
Tesseract did not mainly misread the characters in the column page. It interleaved the left and right lines, while the reference reads the complete left column and then the complete right. The reference and prediction both contain 455 characters, but their global order differs:
Column one describes acquisition. Every source page receives a content hash before any transformation is ... Column two describes evaluation. Text accuracy, document structure, latency, memory, and cost are ...
Column one describes acquisition. Column two describes evaluation. Every source page receives a content Text accuracy, document structure, hash before any transformation is latency, memory, and cost are ...
The reading-order score for that clean page was 0.6. This is the concrete reason the arena keeps transcription and order metrics separate: a recognizer can see nearly every glyph and still produce a document that is wrong to read.
Corruption response
Large clean synthetic text is unsurprisingly tolerant of JPEG compression and random pixel noise. Strong geometric and low-frequency image changes hurt much more:
| Condition | Mean CER | Change from clean |
|---|---|---|
| Clean | 0.06586 | Baseline |
| JPEG quality 20 | 0.06631 | Essentially flat on these large glyphs. |
| Gaussian noise σ 30 | 0.06825 | Small change. |
| Downsample to 25% | 0.13211 | 2.0× clean CER. |
| Uneven illumination 0.5 | 0.15701 | 2.4× clean CER. |
| Gaussian blur σ 2 | 0.21502 | 3.3× clean CER. |
| Rotation 5° | 0.25366 | 3.9× clean CER. |
| Motion blur size 7, angle 0° | 0.26817 | 4.1× clean CER. |
| Perspective magnitude 0.06 | 0.29143 | 4.4× clean CER. |
| Crop 5% per edge | 0.32792 | 5.0×; original full reference retained. |
The full 34-condition table is available as CSV. Do not over-read monotonicity: there is one page per type, one seed, and one repeat. Occlusion location changes with its seed, so a larger occluder can happen to cover less important text. Document content and corruption are also crossed only eight times, making these curves useful diagnostics rather than confident causal estimates.
How the model families differ—and what that might change
A fair arena cannot pretend all registry names describe the same kind of system. The bare PaddleOCR-VL or GLM visual-language checkpoint is not the same product as its full page pipeline, which also detects regions, crops them, recognizes elements, and reconstructs reading order. The registry gives those variants different identities.
| Family | System boundary | Hypothesis to test |
|---|---|---|
| Tesseract 4.1.1 | Classic OCR engine with an LSTM line recognizer and built-in page segmentation; no autoregressive language decoder. | Low compute and little generative hallucination, but global columns and reconstructed structure can fail. |
| PP-OCRv6 | Compact text detector plus recognizer; upstream offers 1.5M–34.5M parameter tiers and 50-language models. | Fast, predictable transcription and strong region OCR; less native ability to emit global Markdown or infer document semantics. |
| PaddleOCR-VL 1.6 | 0.9B document VLM; the page pipeline continues to use PP-DocLayoutV3 for layout analysis. | Better tables, formulas, charts, languages, and reading order are plausible; extra layout latency and cascading region errors are possible. |
| GLM-OCR | 0.9B OCR VLM; the recommended parsing SDK adds PP-DocLayoutV3 and structured reconstruction. | Specialized document context at modest model size; bare-model and full-pipeline quality/cost must be reported separately. |
| DeepSeek-OCR | Trusted custom inference code with native and dynamic resolutions; the adapter uses 1024 base, 640 crops, and crop mode. | Crops may preserve tiny glyphs and compress visual context, while fragmenting global order or adding preprocessing cost. |
| olmOCR 2 | 7B-class VLM pipeline with an official prompt/output contract; the adapter targets 1288 pixels on the longest edge and an OpenAI-compatible server. | Global document linearization and structured output may improve; GPU memory, autoregressive decode, retries, and repetition can cost more. |
| Generic SmolVLM 256M control | Small general-purpose image-to-text model through the generic Hugging Face adapter. | Useful low-cost control, but without the document-specific layout, table, and tiny-text training of specialized models. |
Upstream descriptions and links were verified Aug 12, 2026. Upstream benchmark claims are not reproduced here and are intentionally not mixed into this project's result table.
Six mechanisms that could explain a future difference
- Segmentation boundary. A detector/recognizer works region by region; a page VLM can condition on the whole document; a page pipeline combines both. Missed regions, crop order, and reconstruction can dominate the recognizer itself.
- Visual resolution. A 1288-pixel page render, dynamic high-resolution crops, and a downsampled whole page expose very different numbers of pixels per glyph. Tiny text may improve exactly where compute and context fragmentation increase.
- Decoder behavior. Classical and discriminative recognizers do not generate a long response token by token. VLMs can use language context to repair ambiguous text and express Markdown, but can also invent plausible content, repeat, or stop early. Latency scales with output length.
- Task-specific training. OCR VLMs are trained for documents, tables, formulas, and reading order. A general VLM may understand broader semantics but have weaker glyph-level and layout priors. Model size alone does not settle this.
- Pre/postprocessing. Orientation classification, dewarping, retry policies, header removal, prompt construction, and Markdown cleanup can move the score while the checkpoint stays fixed. They belong in the named model identity.
- Serving system. A local subprocess, direct CUDA generation, and a persistent vLLM/SGLang endpoint have different cold starts, batching, queues, network overhead, and memory accounting. Warm inference and end-to-end latency must remain separate.
What these results do not prove
- There is no empirical Tesseract-versus-VLM comparison yet and no meaningful multi-model Pareto frontier.
- The corpus has eight unique synthetic pages, one page per document type, one repeat, one language, and no handwriting or photographed real document.
- OmniDocBench, OCRBench, and other external benchmarks were not downloaded or run.
- Tesseract 4.1.1 was the installed binary; upstream identifies 5.x as the current stable major series.
- No GPU, Modal, RunPod, quantization, dtype, VRAM, generated-token, or dollar measurement exists.
- The structure scores are heuristic and do not yet include bounding-box IoU or a dedicated formula metric.
- Crop and occlusion use the full original truth, which measures recovery rather than visible-only OCR.
- The result uses one seed, so it has no repeat variance, confidence interval, or paired bootstrap uncertainty.
The source-snapshot caveat
The saved run is complete and its per-page images, hashes, transformation provenance, outputs, and records agree internally. However, it was produced during development before the source was committed. Its recorded arena-source digest does not match the current private repository commit, and no commit in the short Git history matches that digest. The English Tesseract trained-data hash was also not persisted. The identical command resumed all 280 evaluations at the time, but running it now would create a new run identity.
I therefore call this a real development benchmark, not an exact release benchmark reproducible from a public commit. The correct fix is to rerun from a tagged source revision and pinned container, capture the language-data hash, and publish the manifest and result bundle together.
The next benchmark I would trust
- Tag the evaluator, freeze the dependency lock, and record the CPU/GPU worker image digest and immutable checkpoint revision.
- Rerun the Tesseract baseline on that tag, using current Tesseract 5.x as a separately named row rather than overwriting 4.1.1.
- Add a larger synthetic corpus and a license-reviewed, held-out real-document set. Keep benchmark test examples out of any future training data.
- Deploy one dependency-isolated worker each for PP-OCR, PaddleOCR-VL, GLM-OCR, DeepSeek-OCR, olmOCR, and the generic VLM control.
- Run a plain-transcription track and a structured-parsing track with identical fixed images and explicit prompts.
- Use deterministic decoding where supported and at least three repeats for generative systems; report page-paired bootstrap confidence intervals.
- Warm each runtime, then record cold-start, warm model, batch-service, and driver end-to-end latency separately on one fixed GPU SKU.
- Attach a dated price source, token counts, VRAM scope, and cost coverage before drawing a three-axis accuracy/latency/cost frontier.
- Mine the worst paired disagreements, inspect the raw outputs, and only then form claims about why one architecture wins.
A standalone site becomes valuable at that point. The article is the durable narrative and methods record; the separate benchmark explorer should let a reader choose model revision, dataset, document type, corruption, metric, hardware, and pricing snapshot, then inspect individual page failures. The public summary JSON is deliberately shaped as the first static data source for that future explorer.
References
- Tesseract OCR upstream repository and version documentation.
- PP-OCRv6 official architecture documentation.
- PaddleOCR-VL 1.6 official documentation.
- GLM-OCR official model card.
- DeepSeek-OCR official repository.
- olmOCR official toolkit and model documentation.
- Hugging Face's SmolVLM 256M/500M announcement and model overview.