All projects

B.L.A.S.T.

Automatic document text-reader (OCR): PDFs, slides, and images in, clean text out.

The demo sleeps when nobody is using it, so the first load can take a minute.

Status
Live
Built with
Python, RapidOCR / ONNX, Streamlit, SQLite
A two-page book spread, pages 60 and 61 of Darwin's On the Origin of Species, typeset from the 1859 text and made to look like a scan, with a dark shadow down the gutter
A two-page spread, typeset from Darwin's 1859 text and roughed up to read like a scan. B.L.A.S.T. finds the gutter and splits the pages before OCR.
Problem
Extracting text from scanned book spreads, slide decks, and multi-column PDFs breaks naive OCR with interleaved lines and lost reading order.
What was built
A deterministic Python OCR pipeline using RapidOCR/ONNX, PyMuPDF, Streamlit, and SQLite with automated gutter detection and column ordering.
What it proves
Character error rate dropped from 0.499 (EasyOCR v1) to 0.108 on a 14-page gold set. Out-of-the-box Tesseract scored 0.177, won 9 of 14 pages and its OCR step is ~4.5x faster. Backed by 914 collected tests.
Status & context
Live demo running on Streamlit; public repository on GitHub. Tested on 4-core CPU.

Overview

Problem

Getting text out of scanned PDFs, slide decks, and photos is tedious and unreliable: existing tools break on messy real-world documents.

B.L.A.S.T. reads text out of documents automatically (a technique called OCR) and turns it into clean Markdown or Word files. Most of the pages it was built for are scanned book spreads: two pages in one image. It finds the gutter between them and reads each page on its own, then orders the columns within each page. It retries a page that fails and gives the same output for the same input. Try it in the live demo.

The B.L.A.S.T. OCR web app: counters for files completed, pages decoded and job time, the Book / Spread Dewarp preset selected, a scanned spread uploaded and an Execute OCR Engine button
The live app with a two-page spread uploaded and the book preset chosen, one click before a run.

Architecture

Key decision: Split the system into three clear layers (routing, validation, and extraction) so each part stays independently testable and failures are isolated.
  1. Input document (PDF/PPTX/Image)
  2. Navigator (blast_ocr/main.py)
  3. Preprocess + route by source type
  4. OCR text extraction
  5. Self-healing retries/backoff
  6. Markdown + DOCX output
  7. Job metrics in SQLite

Measured on 14 hand-transcribed pages

Every version was scored on the same 14 pages from one scanned 1959 book. The first version used EasyOCR; splitting spreads at the gutter halved the error, and switching to RapidOCR made it faster as well. The last row is out-of-the-box Tesseract on the same pages: it won 9 of the 14 and ran faster, and B.L.A.S.T. is ahead on average because Tesseract fails on two nearly empty pages.

VersionMean CERMean WERReading order τs / page
First version (EasyOCR)0.49920.72880.6770~60
+ preprocessing0.49440.72480.682233.4
+ gutter split and columns0.23380.49680.9641117.1
+ RapidOCR engine0.19160.47390.975815.6
Now, 2026-10-200.1080.4490.99316.5
Tesseract 4.1.1, default0.1770.2790.9953.7

All timings are on one 4-core laptop CPU, and the stop clock covers the OCR call, not full document processing. Full per-page results and the script are in the RapidOCR vs Tesseract write-up, and benchmark files are at commit 43590fd on GitHub.

Impact / Learnings

The biggest gain came from layout, not from the recognition engine: plain RapidOCR read straight across both pages of a spread and scored 0.458. The open problem is words: RapidOCR drops spaces after punctuation, which is why Tesseract still has the better word error rate.

RapidOCR on the whole spread

  1. 60Before entering on the subject of this
  2. 61thestructure ofthebeetlewhich dives
  3. 60chapter, I must make a few preliminary
  4. 61through the water; in the plumed seed which is
  5. 60remarks, to show how the struggle for
  6. 61wafted by the gentlest breeze; in short, we
  7. 60existencebearsonNatural Selection.It has
  8. 61see beautiful adaptations everywhere and in
  9. 60been seen in the last chapter that amongst

B.L.A.S.T., split at the gutter

  1. 60chapter, I must make a few preliminary
  2. 60remarks, to show how the struggle for
  3. 60existencebearsonNaturalSelection.Ithas
  4. 60beenseeninthelastchapterthatamongst
  5. 60organic beings in a state of nature there is
  6. 60some individual variability; indeed I am not
  7. 60aware that this has ever been disputed. It is
  8. 60immaterial for uswhether a multitude of
  9. 60doubtful forms be called species or sub-
Lines three to eleven of each read of the Darwin demo spread, with the page each line came from. Read whole, the spread alternates between pages 60 and 61; split first, page 60 reads in order. Tesseract read this spread better still (CER 0.0024 against 0.011).

914 collected tests (gathered by pytest at commit 43590fd on 2026-10-20) cover extraction, retry, cleanup, and concurrency paths. That is a count of collected tests, not a pass rate, and the live Streamlit deployment runs the same pipeline as the CLI.

Architecture

Layered Design

Layer 1 (Architect)
architecture/ + SOP docs
Layer 2 (Navigator)
blast_ocr/main.py routing and control flow
Layer 3 (Tool)
blast_ocr/core/* modules for extraction/healing/parallelism
Thread-safety
global OCR lock around thread-unsafe calls
Session isolation
scoped sessions per worker thread

Runtime Output

CLI
python run.py <file-or-dir> --out <dir>
GUI
streamlit run blast_ocr/ui/web_app.py
Artifacts
formatted markdown + docx
Storage
SQLite job history + timing metadata
Controls
env-based knobs for workers/confidence/backoff

When it goes wrong

What failsWhat the system does
Unsupported file type or missing input pathRejected before processing starts (see the Code tab), instead of silently producing garbage output.
A page fails to extract in a multi-page documentSelf-healing retry with backoff on that page; the rest of the document still completes.

Code

blast_ocr/main.py
# blast_ocr/main.py (simplified)
# Navigator layer routes source type -> extraction pipeline

from pathlib import Path
from blast_ocr.pipeline import run_pipeline

SUPPORTED = {".pdf", ".pptx", ".png", ".jpg", ".jpeg", ".bmp"}

def process(source: str, out_dir: str | None = None) -> dict:
    path = Path(source)
    if not path.exists():
        raise FileNotFoundError(f"Input not found: {path}")

    if path.suffix.lower() not in SUPPORTED:
        raise ValueError(f"Unsupported format: {path.suffix}")

    return run_pipeline(path=path, out_dir=out_dir)

if __name__ == "__main__":
    result = process("sample.pdf", out_dir="./outputs")
    print(result["status"], result["artifacts"])

Have documents or scans you need processed automatically?

Send a brief →

Need something like this? See Document OCR & Processing Pipelines.