Home
Research
Blog
Portfolio
Resume

ME

HomeResearchBlogPortfolioResume

SITE

AboutContactPrivacy PolicySecurity Policy

SOCIALS

connect with me:EmailLinkedInGitHubGoogle ScholarORCIDItch.ioArtStationBehanceFacebookInstagram

© 2026 - All Rights Reserved

Building a Local LLM-Powered Hybrid OCR Engine

Turn scans, handwriting, and page photos into searchable, selectable documents on your own machine. Meet the local OCR engine I built with a browser interface, flexible vision-model support, and PDF, HTML, Markdown, and text export.

OCRLLMVision Language ModelsAI EngineeringPythonPDF
Ahnaf An Nafee
Ahnaf An Nafee
26 Apr 2026

Your scans should be as useful as the documents you type: searchable, selectable, and ready to work with. I built local-llm-pdf-ocr to bring that experience to scanned PDFs, handwritten notes, and page photos using vision models on your own machine. Drop a document into the browser, choose a local model, and download a searchable PDF that keeps the original page visible. The app also exports HTML, Markdown, and plain text, with dedicated processing for dense handwriting and models that return coordinates. Surya detection and a dynamic-programming aligner handle page geometry behind the interface, and a CLI supports scripted workflows.

Run Local LLM PDF OCR

Requires Git, Python 3.10 or newer, uv, and a running vision-capable model endpoint. After setup, use the browser interface.

GitHubSetup guide

In LM Studio, load a vision model such as allenai/olmocr-2-7b and start its local server at http://localhost:1234/v1. Install uv if needed, then run:

bash
git clone https://github.com/ahnafnafee/local-llm-pdf-ocr.git
cd local-llm-pdf-ocr
uv sync --extra web
uv run local-llm-pdf-ocr-server --port 8000

Open localhost:8000. Drop a document, select Hybrid and the loaded model, then choose Run OCR. Download the searchable PDF when it finishes. Change the model endpoint under Advanced if you use a different server.

Download the model weights before working offline. Keep the configured model endpoint local so document processing stays on your own machine.

Local LLM PDF OCR browser interface in light mode, with document upload, engine selection, a local model picker, and output formatsLocal LLM PDF OCR browser interface in dark mode with document upload and the same processing controls

The main workflow stays in the browser: upload, choose a model, run OCR, and download the result.

Jump to the browser workflow, engine choices, the architecture, or troubleshooting.

From a Scan to a Searchable Document

A scan preserves a document's appearance, but the words remain pixels. That becomes frustrating when I need a name in a stack of forms or a passage from handwritten notes. A transcript helps, but I also want to search and select text where it appears on the original page.

The browser app makes that the default deliverable. Drag in a PDF or image, choose Searchable PDF, and start processing. Per-page progress shows the work advancing, and View text exposes the raw extraction for inspection. Multiple-file selection handles a collection of scans in the same session, with a merged Markdown download to bring their extracted text together.

The output keeps the page image visible and adds an invisible text layer. In the form below, blue selection reveals the recognized printed labels and handwriting. The same page can be searched or copied in a PDF reader without replacing its layout with a recreated document.

A sample form containing printed labels and handwritten entries, with the searchable PDF text layer selected in blue

Printed text and handwritten entries become searchable over the original scan. The blue highlight reveals the normally invisible OCR layer.

Choose the Engine in the Browser

The interface offers Hybrid, Grounded, and Text only, with options that adapt to the selected engine. Hybrid is the starting point for searchable documents because it combines a text detector with a vision model that only needs to transcribe what it sees.

EngineWhat happens behind the interfaceChoose it for
HybridSurya locates text; the local model transcribes it; alignment connects words to regionsScans, forms, handwriting, and models that return ordinary text
GroundedA compatible model returns text and coordinates togetherModels with reliable structured coordinate output
Text onlyThe model transcribes each page without detection or alignmentText extraction for notes, search indexing, or RAG

Dense-page processing is part of Hybrid rather than another engine to install. It switches from full-page transcription to individual text crops when the page needs it. Grounded skips separate detection and alignment, while Text only skips geometry entirely.

Model choice remains flexible. LM Studio is the default endpoint, and Ollama, vLLM, SGLang, or another OpenAI-compatible server can be configured under Advanced. The model picker discovers available models, and preflight verification checks the requested model before processing. Use a vision-capable model: returning plain text is compatible with Hybrid, but the model still has to understand the page image.

Give Handwriting and Page Photos the Attention They Need

Dense handwriting makes full-page recognition harder because many similar lines compete within one image. The pipeline can instead send each detected region through its own recognition call, keeping the model's attention on a smaller part of the page. Automatic mode uses a default threshold of 60 detected boxes and can also retry per-box when full-page alignment matches fewer than half the regions.

For a page with repeated or missing lines, open Advanced, set Dense mode to Always, and try that page again. It makes more model requests, but it gives tightly packed notes a processing route built around their structure. Dense machine-print crops receive neighbor masking to reduce adjacent-line leakage; handwriting keeps its surrounding strokes because they can cross detection boundaries.

Photographed documents have a separate preparation step. Automatic preprocessing detects a tilted page outline and can correct perspective and illumination for recognition. Coordinates then map back onto the original photograph, so the output preserves the image you supplied. Compare automatic preprocessing with Never if the detected outline does not describe the page correctly.

Advanced browser options for the OCR endpoint, page range, image size, concurrency, dense mode, preprocessing, and refinement

Start with the defaults, then tune a difficult page through the same browser interface.

Export for the Work You Want to Do Next

The Output format control makes the tool useful beyond PDF conversion. Choose a searchable PDF for an archive, an HTML overlay for browser-based inspection, or Markdown and text for content that will move into other tools.

OutputWhat you receive
Searchable PDFThe visible page image with an invisible, selectable text layer
HTML overlayPage images with a selectable text overlay, optional hover inspection, and optional inline images
MarkdownExtracted text in a format suited to editing and downstream processing
Plain textTranscription without document-format markup

HTML can be packaged as one self-contained file with inline images. Its hover-text option exposes the recognized content over a region, which is useful when checking the overlay. Text-only mode is a direct route for retrieval preparation because it avoids geometry work when a selectable page is not part of the deliverable.

Inputs include PDF, JPEG, PNG, BMP, WebP, AVIF, and TIFF. A multi-frame TIFF becomes multiple output pages, so archives do not need a separate image-to-PDF conversion before OCR.

How the Hybrid Engine Connects Text to the Page

The architecture separates locating text from recognizing it. Surya's detection predictor supplies text regions without running its recognition model, and the configured vision model supplies transcription. A shared OCRPipeline orchestrates those stages for both the browser and CLI.

The remaining problem is assigning the model's lines to the right regions. A proportional split can move a caption into a neighboring column or combine unrelated form fields. I use a Needleman-Wunsch-style dynamic program to find a monotonic alignment between ordered boxes and text lines, with costs that favor plausible text-to-width fit while allowing skipped boxes and unmatched lines.

The aligner tries row-major and column-major box orderings and chooses the lower-cost result. Different models can therefore read a two-column page in different orders without requiring a separate model-specific alignment implementation. Unmatched model lines attach to nearby matched regions, and sizeable empty regions can receive a crop-recognition pass.

Loading diagram...

PyMuPDF writes a sandwich PDF with the rasterized page as the visible layer. Invisible text is scaled to its assigned region, giving selection a geometric connection to the underlying scan. A detected region and a recognized line remain estimates, so exact selection placement depends on both stages.

Grounded OCR Without Silent Empty Output

Grounded mode lets a compatible model return coordinates alongside the text. The project documents working Qwen2.5-VL and Qwen3-VL configurations; select Grounded and the appropriate loaded model in the interface. Models that return only prose or Markdown belong on Hybrid.

If a page returns no usable grounded blocks, processing stops before writing output and identifies the affected pages. This also applies to blank pages. That behavior prevents a completed-looking run from quietly producing an image-only PDF when the requested text layer is missing.

The distinction matters because a model's general ability to read an image does not establish its coordinate format. Keeping both engines available lets the user choose a suitable model without committing the rest of the workflow to that model's response structure.

Get a Difficult Page Working

The browser's Advanced controls expose page range, rendering DPI, image dimensions, concurrency, refinement, and preprocessing. Use a representative page while tuning, then apply the settings to the full document.

What you seeWhat to try in the interface
The requested model is unavailableSelect its exact loaded ID and check the endpoint
Repeated or missing linesHybrid with Dense mode set to Always
Grounded reports no usable blocksSelect Hybrid, or load a model that returns structured coordinates
GLM-OCR exceeds image capacity in OllamaUse the documented maximum image dimension of 640
A page photo is distorted after preparationCompare automatic preprocessing with Never
Higher concurrency slows progressLower concurrent requests and check the model server's memory and queueing

Concurrency limits in-flight model requests; it does not start extra copies of the model. Full-page and crop-heavy workloads have different costs, so increase it when the server has room rather than assuming a larger number always wins.

For a quick quality check, search a visible phrase, copy across a paragraph or column boundary, and compare handwritten identifiers with the scan. The confidence evaluator goes further by measuring geometry, character errors, word-level similarity, and structural coverage against project fixtures. Saved per-document baselines make changes traceable to the part of the pipeline they improve.

CLI Support for Automation

Everything above can be done in the browser. The CLI is also available when the same configuration needs to run from a script or batch workflow:

bash
uv run local-llm-pdf-ocr scan.pdf scan_ocr.pdf

Its options cover the same engine, model, endpoint, page selection, and tuning controls. The repository README documents the full command surface, example documents, and evaluation tools.

For a first run, use the web interface and a document you already know well. Search it, select a passage, and download the format your next task needs. The source is MIT-licensed and available to adapt to your own document workflow.

Edit on GitHub
PreviousFrom DOI to Markdown: A Two-Repo Pipeline for Faster ResearchNextBringing Real Tooling to Ollama Modelfiles

Related Posts

Feb 2026

From DOI to Markdown: A Two-Repo Pipeline for Faster Research

How DOI Paper Scraper and Papers-to-Markdown work together to convert academic papers into structured Markdown for better reading, search, and research productivity.

May 2026

Bringing Real Tooling to Ollama Modelfiles

How I built a VSCode-family extension that brings syntax highlighting, an 18-rule linter, hover documentation, autocomplete, and 26+ snippets to Ollama Modelfiles — so the next time you scaffold a custom GGUF runtime, your editor catches the foot-guns before `ollama create` does.

Dec 2025

Scaling Education: A Strategy Pattern Approach to Grading at Scale

Deep dive into building a scalable autograder using Python, Java, and the Strategy Pattern. Learn how we can automate grading for hundreds of students using "Frankenstein" code stitching, dynamic rubrics, and local LLMs.

flowchart TD B["Browser upload and options"] --> P["Shared OCR pipeline"] C["CLI for automation"] --> P P --> R["Render PDF or image pages"] R --> H["Hybrid: detect text regions"] R --> G["Grounded: text and coordinates"] R --> T["Text-only transcription"] H --> F["Full-page transcription and alignment"] H --> D["Dense pages: recognize crops"] F --> A["Aligned text regions and optional refinement"] D --> A G --> A A --> O["PDF, HTML, Markdown, or text"] T --> O