Recover a garbled scan layer
Goal: rescue scanned PDFs that ship a present but garbled native text layer —
the kind a scanner or an old “Paper Capture” pass leaves behind, where a title block
reads 0RlGlt IAt lJn tbe @nitp! btutts instead of “In the United States Court of
Federal Claims”. An empty-layer check misses these (there is text), so the garbage
flows straight into search and extraction.
kaos-pdf carries a cheap, dependency-free legibility signal — an English
dictionary hit-rate scored on the worst line of a page — and
parse_pdf(path, ocr="auto") uses it to re-OCR a scanned page whose native layer is
garbled, not just the empty ones.
uv run examples/recover-garbled-ocr.pygarbled line legibility: 0.10clean line legibility: 1.00
page worst-line score: 0.10worst line: '0RlGlt IAt lJn tbe @nitp! btutts ourt of trs lsims'
garbled layer -> needs re-OCRclean layer -> kept as-is#!/usr/bin/env -S uv run --script# /// script# requires-python = ">=3.13"# dependencies = ["kaos-pdf>=0.1.6,<0.2"]# ///"""Detect a garbled OCR layer so `ocr="auto"` re-extracts it.
Scanners and old "Paper Capture" passes often leave a PDF with a *present butgarbled* text layer — the page reads ``0RlGlt IAt lJn tbe @nitp! btutts`` whereit should say "In the United States Court of Federal Claims". An empty-layercheck misses this (there IS text), so the garbage flows downstream.
`kaos-pdf` ships a cheap, dependency-free legibility signal (an Englishdictionary hit-rate scored on the *worst* line of a page) that flags theselayers. `parse_pdf(..., ocr="auto")` uses exactly this signal to automaticallyre-OCR a scanned page whose native text is garbled — not just empty ones.
This example scores a garbled layer against a clean one — fully offline anddeterministic, no OCR engine required.
Run it:
uv run examples/recover-garbled-ocr.py"""
from __future__ import annotations
from kaos_pdf import assess_text_quality, is_low_quality_layer, line_legibility
# The garbled string is the real native text layer of a Canon-scanned court# order; the clean string is what the page actually says.GARBLED = "0RlGlt IAt lJn tbe @nitp! btutts ourt of trs lsims"CLEAN = "In the United States Court of Federal Claims"
def main() -> tuple[bool, bool]: print(f"garbled line legibility: {line_legibility(GARBLED):.2f}") # ~0.10 print(f"clean line legibility: {line_legibility(CLEAN):.2f}\n") # 1.00
# A real page has clean body text plus one garbled title line. The page # score is the *worst* substantial line, so localized garbage still trips it. page = f"{CLEAN} filed July 17, 2015\n{GARBLED}\nNot for publication" quality = assess_text_quality(page) print(f"page worst-line score: {quality.score:.2f}") print(f"worst line: {quality.worst_line!r}\n")
garbled_low = is_low_quality_layer(GARBLED) clean_low = is_low_quality_layer(CLEAN) print(f"garbled layer -> {'needs re-OCR' if garbled_low else 'kept as-is'}") print(f"clean layer -> {'needs re-OCR' if clean_low else 'kept as-is'}") print('\nparse_pdf(path, ocr="auto") applies this automatically: it re-OCRs') print("scanned pages whose native layer is garbled, and leaves the rest alone.") return garbled_low, clean_low
if __name__ == "__main__": garbled_low, clean_low = main() assert garbled_low is True # garbled layer is flagged for re-OCR assert clean_low is False # clean text is kept as-isWhat to notice
ocr="auto"now re-OCRs a structurally-scanned page when its native text is empty OR garbled — the old behavior only caught empty layers. Born-digital text pages are never touched. Tune the cut-off withocr_quality_threshold(default0.35; set0.0to restore the empty-layer-only behavior).- The signal is public, so you can measure layer quality yourself:
line_legibility(text),assess_text_quality(text)(worst substantial line), andis_low_quality_layer(text). It needs no OCR engine and no network.
Pick a local OCR engine
ocr="auto"/"always" need an OCR engine. Two local options, both Apache-2.0:
- Tesseract (default) —
pip install 'kaos-pdf[ocr]'plus the systemtesseractbinary. Zero-config and fast. - RapidOCR / PP-OCRv5 on ONNX Runtime —
pip install 'kaos-pdf[onnx]'. Higher accuracy on degraded and multi-column scans, no system binary, no PyTorch and no Hugging Facetransformersruntime. Pass it explicitly:
from kaos_pdf import parse_pdffrom kaos_pdf.ocr import RapidOcrEngine
doc = parse_pdf("scan.pdf", ocr="auto", ocr_engine=RapidOcrEngine())From the CLI, reach OCR with kaos-pdf extract scan.pdf --ocr auto (or --ocr always,
--ocr-dpi 400).
Escalate the hardest pages to a vision model
When a deterministic engine still struggles — handwriting, stamps, mixed scripts — a
KAOS agent can escalate just those pages to a vision-language
model. kaos-agents runs Tesseract first and sends only the low-confidence or garbled
pages (the same legibility signal) to kaos_llm_core.vision.ocr_page, bounded by a
per-document budget. Turn it on with KAOS_AGENT_OCR_VLM_ESCALATION=1 (off by default —
it makes live, billed calls). The vision programs are also
MCP tools: kaos-llm-core-vision-ocr, -describe, and
-classify.
- The whole chain produces the same document AST with provenance — page, bounding box, and confidence — whether a page came from the native text layer, Tesseract, RapidOCR, or a vision model. Downstream search and grounded extraction don’t care which path it took.
- Pair this with page-level routing to OCR only the pages that need it, then re-OCR only the ones whose layer is garbled.