Skip to content

feat: add PaddleOCR-VL 1.6 OCR server - #405

Open
acrosley wants to merge 1 commit into
run-llama:mainfrom
acrosley:add-paddleocr-vl-server
Open

feat: add PaddleOCR-VL 1.6 OCR server#405
acrosley wants to merge 1 commit into
run-llama:mainfrom
acrosley:add-paddleocr-vl-server

Conversation

@acrosley

@acrosley acrosley commented Aug 6, 2026

Copy link
Copy Markdown

What

Adds ocr/paddleocrvl/ — a FastAPI server wrapping PaddleOCR-VL 1.6 (Apache 2.0, 0.9B params) behind the LiteParse OCR API on port 8831, following the structure of the recent ocr/suryaocr/ contribution:

  • server.py — pipeline wrapper + POST /ocr / GET /health
  • test_server.py — 12 pytest cases against a mocked pipeline (no model download in CI)
  • Dockerfile + .dockerignore — CPU image; GPU serving documented via the official genai vLLM container
  • README.md — setup, backends, API, limitations
  • Registered in ocr/README.md, OCR_API_SPEC.md, and the ocr_servers.yml CI matrix

Why

PaddleOCR-VL 1.6 pairs a small layout detector with an ERNIE-based VLM and currently tops OmniDocBench v1.6 (96.33). It reads tables, formulas, and charts across 109 languages — a document class the existing servers (line-level OCR) handle poorly. In my benchmarks below it posted the best accuracy of every engine I tested on modern structured documents, in every category.

Design notes

  • Zero-setup default: everything runs locally on CPU through PaddlePaddle — works on Linux/macOS/Windows with uv run server.py, nothing else to install.
  • GPU paths: PADDLEOCR_VL_ENGINE=transformers runs both models through PyTorch/CUDA (this is also the only practical GPU route on Windows, where vLLM/SGLang can't run natively and the Paddle GPU wheels currently ship a broken cuDNN pin). PADDLEOCR_VL_SERVER_URL attaches VL recognition to a running vLLM/SGLang genai server for throughput serving.
  • Output mapping: results are layout blocks in reading order. bbox comes from the parser, confidence from the layout detector's region score (the VLM exposes no token confidence), and the optional polygon field is populated from the detector's 4-point polygons so LiteParse can recover rotated text. Table HTML is flattened to plain text with stdlib only; the VLM's LaTeX escapes (\%, \$) are unescaped.
  • The pipeline isn't thread-safe, so requests are serialized with a lock.

Benchmarks

Run locally (RTX 5090, PADDLEOCR_VL_ENGINE=transformers, all engines via their /ocr HTTP endpoints; CER/WER via jiwer on reading-order concatenation). Two datasets:

Real-world PDFs — 110 pages rendered at 200 dpi from 22 public documents (arXiv papers, IRS forms, Berkshire shareholder letters, NIST publications), scored against the PDFs' native text layer:

engine WER ↓ CER ↓ p50 latency errors
paddleocrvl 0.233 0.189 32.2 s 0/110
easyocr 0.306 0.223 4.3 s 0/110
tesseract 0.300 0.235 0/110
glmocr (0.9B VLM) 0.322 0.357 0/110

Per-category WER (paddleocrvl vs next-best): academic 0.275 vs 0.320, business 0.013 vs 0.084, forms 0.491 vs 0.528, technical 0.160 vs 0.185 — best in all four categories.

FUNSD (50 scanned 1990s forms, test split):

engine CER ↓ WER ↓ p50 latency errors
paddleocr (classic det+rec) 0.132 0.323 2.2 s 0/50
paddleocrvl 0.205 0.354 21.0 s 0/50
easyocr 0.257 0.625 4.2 s 0/50
glmocr (0.9B VLM) 0.588 0.506 3.5 s 0/50

Honest limitations

  • It's slow. A 0.9B VLM decoding structured output is ~7× slower per page than easyocr even on a fast GPU, and tens of seconds per page on CPU. The README says so. This engine trades latency for accuracy and structure.
  • Degraded scans are not its strength: classic PaddleOCR's specialized det+rec still wins on FUNSD's noisy scans. On one bordered form the layout detector classified the whole page as a single table and the VL decode collapsed (1 region, 169 s) — a known model-level failure mode on full-page bordered tables, documented in the server README.
  • Block-level boxes: like Surya, output is layout blocks, not words — token-level localization metrics are accordingly coarse.

Testing

  • uv run pytest — 12/12 pass (mocked pipeline; verified in a fresh Python 3.12 env, same as the CI job this PR adds)
  • Live end-to-end: POST /ocr verified against synthetic pages and both benchmark datasets above (160 pages, zero HTTP or empty-result failures)

🤖 Generated with Claude Code

Adds ocr/paddleocrvl/, a FastAPI server wrapping the PaddleOCR-VL 1.6
document parsing VLM (0.9B) behind the LiteParse OCR API on port 8831.

- Layout-aware block extraction (tables, formulas, charts) across 109
  languages; polygon output for rotated-text recovery
- Runs locally on CPU by default; PADDLEOCR_VL_ENGINE=transformers for
  the PyTorch GPU path, PADDLEOCR_VL_SERVER_URL to attach a vLLM/SGLang
  genai server
- Mocked pytest suite (no model download) wired into ocr_servers.yml CI
- Docs: comparison table, server entry, spec reference list

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f9c273ca2a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ocr/paddleocrvl/server.py
Comment on lines +230 to +231
with self._lock:
predictions = list(self.pipeline.predict(np.asarray(image)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid queuing concurrent pages behind the model lock

When LiteParse processes a multi-page scanned document with the documented CPU default, it sends up to num_workers pages concurrently (ocr_merge.rs:502-545, defaulting to CPU cores minus one in config.rs:247-251), but this lock serializes their tens-of-seconds inferences. The HTTP client times each request out after 60 seconds (http_simple.rs:143-152), so later queued pages can time out before acquiring the lock; their retries then add more work behind the still-running synchronous handlers. Configure LiteParse to use one worker for this server, reject busy requests so retries can back off, or otherwise align queueing with the client timeout.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant