Faithful, fully-local OCR for Chrome. Grab text from anything on screen — a region, the viewport, or a whole scrolling page — code in a paused video, a paragraph in a PDF, a formula, a table. Or turn an entire page into clean Markdown for an LLM. No server. No image ever leaves your machine. No hallucinated text.
🌐 ocr-buddy.com · 🧩 Chrome extension (Manifest V3) · 🔓 Free & open source (MIT) · 🛡️ 100% local, privacy-first


Silent autoplay loops. ▶ Watch in High Quality Video: demo 1 · demo 2.
Modern OCR is dominated by large autoregressive vision-language models. They top the benchmarks — and they invent fluent, plausible, wrong text the moment the pixels get unclear. For most uses that's an annoyance. For code, numbers, prices, IDs, or anything you intend to trust, a confidently-wrong transcription is worse than no transcription at all. Those models are also far too heavy to run in a browser tab.
OCR Buddy is built on the opposite bet: faithfulness over fluency, and the whole pipeline on your device. The interesting part is that those two goals don't fight — they point at the same engineering choices.
Hallucination in OCR is largely architectural. A generative model predicts the next likely token, so when the image is ambiguous it falls back on its language prior and writes something that reads well but isn't there. The classic OCR family — detection + CTC recognition — has no such prior. It transcribes the glyphs that are actually present and, when it can't, it fails to blanks or low-confidence output. It never makes up a sentence.
That family is also small, fast, and runs comfortably in WebAssembly/WebGPU. So:
In-browser and no-hallucination are not a tradeoff. Both constraints select the same stack: PaddleOCR's PP-OCRv5 (Apache-2.0) on ONNX Runtime Web.
Everything below follows from that one decision.
content overlay (drag-select a region)
│ rect + devicePixelRatio
▼
service worker (coordinator only — no DOM, no model, no inference)
│ captureVisibleTab → crop on an OffscreenCanvas → PNG data URL
▼
offscreen document (cross-origin isolated, WebGPU-capable, long-lived)
└─ PP-OCRv5 (+ pix2text-mfr for formulas) on ONNX Runtime Web
▼
side panel (crop shown beside the result; low-confidence words flagged)
A few choices worth calling out, because each solved a concrete problem:
SharedArrayBuffer for multi-threaded WASM, with WebGPU as the primary backend.chrome.tabs.captureVisibleTab, not <video> frame-grabbing.
Grabbing a frame off a cross-origin video taints the canvas and the read fails.
captureVisibleTab returns clean, composited pixels — so OCR-ing code from a
paused YouTube video Just Works.Built with Vite + CRXJS. Requires Chrome 124+ (WebGPU in workers).
Region-select is precise, but dragging a box is overkill when you just want everything on screen — or everything on a long page. So there are three ways to choose what gets OCR'd, all sharing the selected read mode and language:
A separate one-click action turns the current page into faithful Markdown, shown
in a preview you can copy or download as a .md. Unlike capture, this reads the page's
DOM structure (headings, lists, links, tables, code blocks) — not pixels — so the
output is real Markdown, not OCR'd text, and it's clean enough to hand straight to an
LLM. It's fully local (reads the DOM, no network), keeps the full page (nav/header/
footer included), and resolves relative links to absolute.
It's also hybrid: text baked into readable images is OCR'd and inserted right
after the image as a clearly-labelled blockquote (> **Text extracted from image
(OCR):** …), so it's never silently merged into the prose — you always know which
text came from a picture. Cross-origin images can't be read (browser canvas taint),
so those keep just their alt text.
You pick how a region should be read — and you can change your mind after capturing, with the "Read as" switcher in the result view, which re-runs a different mode on the same crop without re-selecting.
Plain OCR for code, prose, or any text. The journey here was mostly about faithfully reconstructing layout from geometry, because the recognizer only emits glyphs:
o wedged between digits back to 0 (4o0 → 400)
— without touching code identifiers like arg0 or octal 0o755. (Both folds
apply only to Latin-script packs — for Cyrillic or Greek packs those glyphs are
the real text.)The one place I had to use a generative model — there's no CTC equivalent that emits structured LaTeX. That reopens the hallucination risk the whole project avoids, so the design is built around containing it.
A single table → a Markdown grid, reconstructed by pure geometry from the OCR word boxes: rows by vertical position, columns from an x-coverage profile, each word placed in its nearest column. No extra model. Because it keys off column alignment rather than ruled lines, it handles borderless tables — which a layout model reads as figures.
The thought-flow wasn't a straight line. Two experiments shipped and were then removed, on purpose:
\| → I/l confusion. A lone vertical bar is
visually identical to capital-I, lowercase-L and the digit 1; the recognizer
(favouring letters) usually picks one of those, so pipes in code and tables read as
I. The tempting fix — fold a standalone I/l back to \| — was dropped because
the glyphs are genuinely ambiguous (especially in monospace, where their boxes are
identical width), so any rule that catches real pipes also corrupts real I/l/1.
Turning someone's variable l into \| is exactly the silent corruption this tool
refuses to make. A visible, correctable I beats an invented \|.Keeping these out is part of the design: a small, honest tool beats a broad, flaky one.
One thing that did ship from this round of testing: coloured text on light
backgrounds (a red form error, a blue link) used to be dropped at detection — the
detector is tuned for dark, high-contrast glyphs. A background-adaptive contrast
boost (collapse each pixel to its darkest channel on light backgrounds) pulls weak
colour contrast up to strong luminance contrast, with no effect on ordinary
dark-on-light text or on dark mode. Emoji remain best-effort: an undetected one is
marked with a □ placeholder, but one the recognizer boxes anyway can still come out
as garbage — a Latin model has no glyph for it.
Anti-hallucination isn't a tagline here, it's the feature set:
Measured with scripts/ocr-image-test.mjs (Node, the exact PP-OCRv5 config the
extension uses) against ground truth on real academic pages:
[22], tokens like RoPE-2D, all correct.In short: on the content each mode is meant for, accuracy is essentially perfect. I don't claim "100% OCR of anything" — that would be the kind of overstatement the project is a reaction against.
$ claude mcp add OCR-buddy \
-- python -m otcore.mcp_server <graph>