Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Unlimited-OCR engine adapter** — wraps Baidu's open-weight [`Unlimited-OCR`](https://github.com/baidu/Unlimited-OCR) document-parsing VLM (loaded from HuggingFace via `trust_remote_code`). Builds on DeepSeek-OCR with Reference Sliding Window Attention (R-SWA) for one-shot long-horizon parsing. Supports `gundam` and `base` modes, images, and PDFs (rendered to images via PyMuPDF), emitting Markdown/HTML/JSON. Registered in the router for PDF and image inputs. Install: `pip install docfold[unlimited-ocr]` (requires a CUDA GPU).
- **MarkItDown engine adapter** — wraps Microsoft's [`markitdown`](https://github.com/microsoft/markitdown) pure-Python library that converts Office files, PDFs, HTML, images, CSV/JSON/XML, ePub, audio, and ZIP archives into LLM-friendly Markdown. Added to the `benchmark.py` harness alongside the other local engines. Install: `pip install docfold[markitdown]`.
- **Non-PDF benchmark fixtures** — `benchmark.py` now also generates synthetic DOCX (built with stdlib `zipfile` + minimal Office Open XML, no extra deps), HTML, and CSV documents, and filters engines per-doc by `supported_extensions` so PyMuPDF / OCR engines no longer log spurious errors on Office or web fixtures.
- **OpenDataLoader PDF engine adapter** — wraps the Java-based [`opendataloader-pdf`](https://github.com/opendataloader-project/opendataloader-pdf) tool (via its bundled-JAR Python wheel). Local, deterministic extraction with typed structural elements (heading, paragraph, table, list, header, footer) and per-element bounding boxes. Install: `pip install docfold[opendataloader]` (also requires Java 11+).
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Docfold is the open-source extraction engine from [Datatera.ai](https://datatera
| [**Azure Doc Intel**](https://azure.microsoft.com/en-us/products/ai-services/ai-document-intelligence) | ✅ | SaaS | Paid | ★★★ | ★★★ | ★★★ | ✅ | ✅ | Fast | $$ |
| [**Nougat**](https://github.com/facebookresearch/nougat) | ✅ | Local | MIT | ★★★ | ★★☆ | ★★☆ | — | — | Slow | Free |
| [**Surya**](https://github.com/VikParuchuri/surya) | ✅ | Local | GPL | ★★☆ | ★★★ | ★★☆ | ✅ | ✅ | Medium | Free |
| [**Unlimited-OCR**](https://github.com/baidu/Unlimited-OCR) | ✅ | Local/VLM | MIT | ★★★ | ★★★ | ★★★ | — | — | Slow | Free |
| [**MarkItDown**](https://github.com/microsoft/markitdown) | ✅ | Local | MIT | ★★☆ | ★☆☆ | ★★☆ | — | — | Fast | Free |

**★★★** Excellent **★★☆** Good **★☆☆** Basic **☆☆☆** Not supported — **$$** ~$1-3/1K pages **$$$** ~$5-15/1K pages — **BBox** Bounding boxes — **Conf** Confidence scores
Expand Down Expand Up @@ -109,6 +110,7 @@ for name, res in results.items():
| [**Azure Doc Intel**](https://azure.microsoft.com/en-us/products/ai-services/ai-document-intelligence) | SaaS | Paid | PDF, Office, HTML, images | N/A | `pip install docfold[azure-docint]` |
| [**Nougat**](https://github.com/facebookresearch/nougat) | Local | MIT (code) | PDF | Recommended | `pip install docfold[nougat]` |
| [**Surya**](https://github.com/VikParuchuri/surya) | Local | GPL-3.0 | PDF, images | Optional | `pip install docfold[surya]` |
| [**Unlimited-OCR**](https://github.com/baidu/Unlimited-OCR) | Local/VLM | MIT | PDF, images | Required | `pip install docfold[unlimited-ocr]` |
| [**MarkItDown**](https://github.com/microsoft/markitdown) | Local | MIT | PDF, Office, HTML, images, CSV/JSON/XML, ePub, audio, ZIP | No | `pip install docfold[markitdown]` |

> **Adding your own engine?** Implement the `DocumentEngine` interface — see [Adding a Custom Engine](#adding-a-custom-engine) below.
Expand Down Expand Up @@ -335,6 +337,7 @@ Docfold builds on and integrates with these excellent projects:
| [Zerox](https://github.com/getomni-ai/zerox) | Model-agnostic Vision LLM OCR |
| [Nougat](https://github.com/facebookresearch/nougat) | Meta's academic PDF to Markdown model |
| [Surya](https://github.com/VikParuchuri/surya) | Multilingual OCR + layout analysis |
| [Unlimited-OCR](https://github.com/baidu/Unlimited-OCR) | Baidu's one-shot long-horizon document-parsing VLM |

### Built by

Expand Down
2 changes: 2 additions & 0 deletions benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ async def main():
from docfold.engines.pymupdf_engine import PyMuPDFEngine
from docfold.engines.surya_engine import SuryaEngine
from docfold.engines.tesseract_engine import TesseractEngine
from docfold.engines.unlimited_ocr_engine import UnlimitedOCREngine
from docfold.engines.unstructured_engine import UnstructuredEngine

# All local/open-source engines to benchmark
Expand All @@ -496,6 +497,7 @@ async def main():
(MinerUEngine(), "pip install docfold[mineru]"),
(MarkerLocalEngine(), "pip install marker-pdf"),
(SuryaEngine(), "pip install surya-ocr"),
(UnlimitedOCREngine(), "pip install docfold[unlimited-ocr] (needs CUDA GPU)"),
(DoclingEngine(), "pip install docling"),
(EasyOCREngine(gpu=False), "pip install easyocr"),
(NougatEngine(), "pip install nougat-ocr"),
Expand Down
11 changes: 11 additions & 0 deletions docs/benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This guide helps you choose the right document processing engine for your use ca
| **Zerox** | VLM | MIT | ★★★ | ★★★ | ★★☆ | ★★☆ | ★★★ | Slow | VLM API cost |
| **Nougat** | Local | MIT | ★★★ | ★★☆ | ★★☆ | ★★★ | ★☆☆ | Slow | Free |
| **Surya** | Local | GPL-3.0 | ★★☆ | ★★★ | ★★☆ | ★☆☆ | ★★★ (90+) | Medium | Free |
| **Unlimited-OCR** | Local/VLM | MIT | ★★★ | ★★★ | ★★★ | ★★☆ | ★★☆ | Slow | Free |
| AWS Textract | SaaS | Paid | ★★★ | ★★★ | ★★★ | ★☆☆ | ★★☆ | Fast | ~$1.50/1K pages |
| Google Document AI | SaaS | Paid | ★★★ | ★★★ | ★★★ | ★★☆ | ★★★ | Fast | ~$1.50/1K pages |
| Azure Document Intelligence | SaaS | Paid | ★★★ | ★★★ | ★★★ | ★★☆ | ★★★ | Fast | ~$1.50/1K pages |
Expand Down Expand Up @@ -238,6 +239,16 @@ This guide helps you choose the right document processing engine for your use ca
- **Install:** `pip install docfold[surya]`
- **Links:** [GitHub](https://github.com/VikParuchuri/surya)

### Unlimited-OCR (Baidu)

**Best for:** Long, multi-page documents parsed in one shot — the headline "long-horizon" use case.

- **Strengths:** Open-weight VLM (MIT) released June 2026. Builds on DeepSeek-OCR and replaces decoder attention with **Reference Sliding Window Attention (R-SWA)** for a *constant* KV cache, so it can transcribe dozens of pages in a single 32K-context forward pass. Strong Markdown output with headings, tables, and reading order. Two modes: `gundam` (dynamic crop, single images) and `base` (full resolution, multi-page).
- **Weaknesses:** Requires a CUDA GPU and heavy deps (torch, transformers). Loaded via `trust_remote_code` from HuggingFace. Newer project — API may evolve. The docfold adapter processes page-by-page; native single-pass `infer_multi` long-horizon batching is a future enhancement.
- **GPU:** Required (CUDA), bfloat16.
- **Install:** `pip install docfold[unlimited-ocr]`
- **Links:** [GitHub](https://github.com/baidu/Unlimited-OCR) | [HuggingFace](https://huggingface.co/baidu/Unlimited-OCR) | [Paper](https://arxiv.org/abs/2606.23050)

---

## Feature Coverage Matrix
Expand Down
98 changes: 98 additions & 0 deletions docs/tasks/UNLIMITED_OCR_ENGINE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
purpose: "Add Baidu Unlimited-OCR as a local VLM document-parsing engine"
status: "OPEN"
priority: "P2"
created: "2026-06-27"
---

# Feature: Unlimited-OCR Engine

## Problem
[Baidu Unlimited-OCR](https://github.com/baidu/Unlimited-OCR) (released June 2026,
10.7k★, MIT-licensed code) is a new open-weight document-parsing VLM. It takes
DeepSeek-OCR as a baseline and replaces the decoder attention with **Reference
Sliding Window Attention (R-SWA)**, giving a *constant* KV cache across decoding.
Combined with DeepSeek-OCR's high-compression encoder, it can transcribe **dozens
of pages in a single forward pass** under a 32K context — the headline "one-shot
long-horizon parsing" capability. It outputs structured Markdown (headings,
tables, reading order) and is competitive on document-parsing benchmarks.

docfold already ships 20+ engine adapters (Chandra, Surya, MinerU, …) behind a
unified `DocumentEngine` interface. Adding Unlimited-OCR gives users a fresh,
free, locally-runnable VLM option — particularly for long, multi-page documents
where its constant-KV-cache design is an advantage. "Now" because the model just
shipped and there is clear user interest.

## Proposed Solution
Add a new `UnlimitedOCREngine` adapter that wraps the upstream HuggingFace model
(`baidu/Unlimited-OCR`, loaded with `trust_remote_code=True`), mirroring the
existing `ChandraEngine`/`SuryaEngine` adapters:

- Lazy-load the model + tokenizer on first `process()` call (never at import,
construction, `is_available()`, or router-registration time).
- Run blocking inference inside `loop.run_in_executor` like the other local
engines.
- Support the upstream **`gundam`** (base_size=1024, image_size=640,
crop_mode=True) and **`base`** (base_size=1024, image_size=1024,
crop_mode=False) modes via a `mode` constructor parameter.
- Accept images directly and render PDF pages to images (via PyMuPDF/`fitz`)
before inference, like `ChandraEngine`.
- Emit Markdown / HTML / JSON / text from the unified `EngineResult`.

## Affected Files
- `src/docfold/engines/unlimited_ocr_engine.py` — NEW adapter.
- `src/docfold/engines/router.py` — add `unlimited_ocr` to PDF + image priority
lists and the default fallback.
- `src/docfold/cli.py` — register the engine in `_build_router()`.
- `pyproject.toml` — add `unlimited-ocr` optional-dependency group; include in
`all`.
- `tests/engines/test_adapters.py` — `TestUnlimitedOCREngine` + add to the
`TestAllEnginesImplementInterface` parametrize list.
- `README.md`, `docs/benchmarks.md`, `CHANGELOG.md` — document the engine.

## Test Plan

### Unit / Functional Tests
- [ ] `test_name` — `name == "unlimited_ocr"`.
- [ ] `test_supported_extensions` — pdf + common image extensions present.
- [ ] `test_is_available_when_missing` — returns `bool` (False) when `torch`
is unavailable.
- [ ] `test_config_stored` — constructor params stored (`_mode`, `_model`,
`_max_length`, `_prompt`, `_device`).
- [ ] `test_default_mode_is_gundam` — `_mode` defaults to `"gundam"`.
- [ ] `test_mode_params` — `gundam` → (1024, 640, True); `base` → (1024, 1024, False).
- [ ] `test_capabilities` — table_structure / heading_detection / reading_order
True; bounding_boxes / confidence False.
- [ ] `test_process_returns_engine_result` — mocked model produces a valid
`EngineResult` (markdown).
- [ ] `test_process_json_output` — JSON output format wraps per-page text.
- [ ] Added to `TestAllEnginesImplementInterface` parametrize list.

### Integration / E2E Tests
- [ ] (manual, GPU) Run a real image and a multi-page PDF through the engine and
verify Markdown output and page count.

### Test Commands
```bash
# Run the new engine tests
pytest tests/engines/test_adapters.py -k UnlimitedOCR -v

# Full suite (no regressions)
pytest tests/ -m "not slow"
```

## Edge Cases
- `torch` / `transformers` not installed → `is_available()` returns `False`,
engine is simply not registered.
- PDF input with no PyMuPDF → raise a clear `ImportError` from the render step.
- Empty / unreadable page → model returns empty string; adapter keeps the page
slot so page numbering stays correct.
- Model emits text only when `save_results=False`; adapter relies on the return
value and uses a temp dir for any side-effect output.

## Out of Scope
- Native multi-page `infer_multi` long-horizon batching (single forward pass over
many pages). The first cut processes page-by-page for determinism and
testability; long-horizon batching is a future enhancement.
- Bounding-box extraction (the simple parse path returns Markdown only).
- vLLM serving backend.
11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@ surya = [
"torch>=2.0",
"Pillow>=10.0",
]
unlimited-ocr = [
# Model is loaded from HuggingFace via trust_remote_code — no dedicated package.
"torch>=2.0",
"torchvision>=0.15",
"transformers>=4.57",
"Pillow>=10.0",
"einops>=0.8",
"PyMuPDF>=1.23", # render PDF pages to images
]
firecrawl = [
"firecrawl-py>=1.0",
]
Expand All @@ -119,7 +128,7 @@ evaluation = [
"psutil>=5.9", # Memory measurement
]
all = [
"docfold[docling,mineru,marker,pymupdf,paddleocr,tesseract,easyocr,unstructured,llamaparse,liteparse,opendataloader,mistral-ocr,textract,google-docai,azure-docint,nougat,chandra,surya,firecrawl,markitdown,evaluation]",
"docfold[docling,mineru,marker,pymupdf,paddleocr,tesseract,easyocr,unstructured,llamaparse,liteparse,opendataloader,mistral-ocr,textract,google-docai,azure-docint,nougat,chandra,surya,unlimited-ocr,firecrawl,markitdown,evaluation]",
# Note: zerox excluded from [all] — py-zerox requires Python 3.11+
# Install separately: pip install docfold[zerox]
]
Expand Down
6 changes: 6 additions & 0 deletions src/docfold/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,12 @@ def _build_router():
except Exception:
pass

try:
from docfold.engines.unlimited_ocr_engine import UnlimitedOCREngine
router.register(UnlimitedOCREngine())
except Exception:
pass

try:
from docfold.engines.firecrawl_engine import FirecrawlEngine
router.register(FirecrawlEngine())
Expand Down
10 changes: 5 additions & 5 deletions src/docfold/engines/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@
# the first *available* engine that supports the extension.

_IMAGE_PRIORITY = [
"chandra", "surya", "paddleocr", "tesseract", "easyocr", "docling", "liteparse",
"mistral_ocr", "google_docai", "textract", "azure_docint", "zerox", "marker",
"markitdown",
"chandra", "unlimited_ocr", "surya", "paddleocr", "tesseract", "easyocr",
"docling", "liteparse", "mistral_ocr", "google_docai", "textract",
"azure_docint", "zerox", "marker", "markitdown",
]

_EXTENSION_PRIORITY: dict[str, list[str]] = {
# --- PDF ---
"pdf": [
"docling", "mineru", "chandra", "unstructured", "marker",
"docling", "mineru", "chandra", "unlimited_ocr", "unstructured", "marker",
"llamaparse", "liteparse", "mistral_ocr", "firecrawl", "google_docai",
"azure_docint", "textract", "zerox", "nougat", "surya", "pymupdf",
"paddleocr", "tesseract", "easyocr", "markitdown",
Expand Down Expand Up @@ -93,7 +93,7 @@

# Ultimate fallback when extension is unknown or missing from the map.
_DEFAULT_FALLBACK = [
"docling", "mineru", "chandra", "unstructured", "marker",
"docling", "mineru", "chandra", "unlimited_ocr", "unstructured", "marker",
"llamaparse", "liteparse", "mistral_ocr", "google_docai", "azure_docint",
"textract", "zerox", "nougat", "surya", "pymupdf", "paddleocr", "tesseract",
"easyocr", "markitdown",
Expand Down
Loading
Loading