paradox2
Fast, simple document extraction — a from-scratch rewrite of paradox-pdf, built around one idea: most PDFs are digital and don't need a GPU.
import paradox2 as pdx
pages = pdx.extract("invoice.pdf")
Why this rewrite exists
paradox-pdf grew into a 2,783-line facade doing routing, backend resolution, and overlay rendering all in one file. paradox2 starts over with a hard rule: digital pages never pay for OCR or vision models. Every PDF page is classified independently — a digital page (has a text layer) goes through a zero-ML fast path (PyMuPDF text + vector-line table detection); a scanned page routes to the GPU OCR pipeline. A single mixed document is handled correctly per-page, automatically.
Install
pip install paradox2
Everything above works with just that — digital PDFs, text, vector-line tables, key-value fields. Heavier features are opt-in extras so the base install stays small:
| Extra | Adds | When you need it |
|---|---|---|
paradox2[gpu] |
PaddleOCR + PP-DocLayoutV2 | Scanned/photographed pages |
paradox2[rapidai] |
TableStructureRec (RapidAI), ONNX-only | Mid-tier table structure for scanned pages the OCR-grid heuristic rejects — no torch/torchvision, CPU-only, ~5.9s/table |
paradox2[yolo] |
YOLO26-document-layout | Independent table-bbox detector — crops the page before RapidAI/VLM instead of feeding them the whole page. Needed for the router's full accuracy; see below |
paradox2[formats] |
docx/xlsx/pptx/msg/rtf/7z/rar/odf readers | Non-PDF documents |
paradox2[specialists] |
torch + transformers (handwriting, signatures, formulas) | Handwriting (TrOCR), signature detection (Conditional-DETR), formula-to-LaTeX (pix2tex) |
First-time setup (scanned/OCR support)
For scanned or photographed pages, run paradox2-setup right after
installing the base package — it detects whether the machine has an NVIDIA
GPU and installs the matching extras and the correct pinned paddlepaddle
build for you:
pip install paradox2
paradox2-setup
This installs paradox2[gpu,rapidai,yolo,formats] plus paddlepaddle
pinned to 3.2.1 (GPU build from PaddlePaddle's own index if a GPU is
detected, plain CPU build otherwise). Pinning matters: paddlepaddle 3.3.1 — pip's unpinned default — has a real PIR/oneDNN inference bug;
3.2.1 is the verified-good version.
If you'd rather install extras manually instead of running the script:
pip install "paradox2[gpu,rapidai,formats]"
Use a clean virtual environment for [gpu]. Installing into a shared/base
environment (e.g. Anaconda's base) that already has an older paddleocr
or paddlepaddle from a prior project can leave an incompatible version in
place — pip does not always resolve this cleanly against pre-existing
packages in a polluted environment. python -m venv .venv && source .venv/bin/activate (or conda create -n paradox2 python=3.12) before
installing avoids this.
What it does
import paradox2 as pdx
from dataclasses import asdict
# Every page, as a list of PageResult dataclasses (NOT dicts — a page
# doesn't support page["blocks"]; use page.blocks, or asdict(page) /
# the CLI's --format json for a plain-dict/JSON form) — digital pages
# via the fast path, scanned pages via GPU OCR, decided per page.
pages = pdx.extract("document.pdf")
page_dicts = [asdict(p) for p in pages] # if you want plain dicts
# Just the text
text = pdx.extract_text("document.pdf")
# Just the tables (vector-line detection on digital pages,
# OCR-grid heuristic + optional RapidAI/VLM fallback on scanned pages)
tables = pdx.extract_tables("document.pdf")
# Key-value pairs from an invoice/form-like page (digital only)
fields = pdx.extract_kie("invoice.pdf")
# Any non-PDF format too — same call, same output shape
data = pdx.extract("spreadsheet.xlsx")
data = pdx.extract("scan.docx")
# Before processing a scanned document: check whether it's actually
# processable with what's currently installed, without running OCR.
report = pdx.can_process("document.pdf")
# {"n_pages": 12, "n_scanned_pages": 12, "needs_ocr": True,
# "can_process": False, "problems": [...]}
page_workers > 1 (multi-page process-pool parallelism) uses
multiprocessing's spawn start method — a caller script MUST guard its
top-level code with if __name__ == "__main__":, or the worker processes
die on import and every page silently falls back to sequential processing
(you'll see a RuntimeWarning when this happens).
Selecting pages, features, and output format
pdx.extract("report.pdf", pages="1-3,7") # specific pages
pdx.extract("report.pdf", feature="tables") # just one feature, still full extract() under the hood
pdx.extract("report.pdf", output_format="markdown") # rendered markdown instead of the raw IR
pdx.extract("report.pdf", fields=True) # turn on key-value extraction inline
Optional specialists (all off by default, all lazy-loaded)
pdx.extract("form.pdf", handwriting=True) # TrOCR on handwritten blocks
pdx.extract("contract.pdf", signatures=True) # Conditional-DETR signature boxes
pdx.extract("paper.pdf", formulas=True) # inline math -> LaTeX
None of these import their heavy dependencies unless the flag is set —
import paradox2 alone never touches torch, paddle, or transformers.
The scanned-table router
Scanned pages route tables through three tiers, each opt-in past the first:
OCR-grid heuristic (free, always on)
| rejects merged/borderless/dense tables by design,
| or under-reads a real table (low_confidence flag)
v
YOLO26 bbox (PARADOX2_YOLO_DETECT=1) -- crop the page to the detected
| table region before handing it to the tiers below. A
| borderline-confidence detection (<0.85) skips RapidAI
| entirely and goes straight to the VLM tier - VLM tolerates
| a loose/imprecise crop better than RapidAI's structure model
v
RapidAI mid-tier (PARADOX2_RAPIDAI_TABLES=1)
| ONNX wired/wireless classifier + structure model. Its own
| result is cross-checked against OCR line density in the same
| crop (table_rapidai.py's low_confidence) - implausibly few
| OR implausibly many rows both re-route to the VLM tier
v
VLM fallback (PARADOX2_VLM_TABLES=1)
| PaddleOCR-VL single-pass, reserved for tables that look
| genuinely complex - not run on everything RapidAI touches
v
best available result
Every tier is measured end-to-end (paradox2.extract(), not the isolated
engine wrapper) on the same 20 real full-page tables (OmniDocBench), not
simulated:
| config | exact row match | within +/-3 rows | mean time/page |
|---|---|---|---|
| heuristic only (pre-router) | 5% | 15% | 0.8s |
| + RapidAI, full page (bug, fixed) | 15% | 50% | 6.0s |
| + RapidAI, cropped to heuristic's own bbox (bug, fixed - made 3/20 pages worse) | 15% | 40% | 4.3s |
| + RapidAI, cropped to YOLO26 bbox | 25% | 75% | 5.5s |
| + selective VLM escalation (low YOLO confidence or implausible RapidAI row count) | 35% | 80% | 6.4s |
The isolated RapidAI engine alone scores 65% exact on tight ground-truth crops — the gap to the router's end-to-end number is the YOLO26 crop's margin versus a perfect crop, not a RapidAI accuracy problem. Cropping to the heuristic's own bbox instead of an independent detector actively hurts: that bbox only spans the rows the heuristic already found, so it bakes its own under-read into the pixels before the next tier ever sees them. A same-crop self-consistency check (RapidAI's row count vs. OCR line density) can't catch a loose crop itself, since both numbers inflate together on the same imprecise crop — that's what the YOLO-confidence trigger (<0.85) is for instead.
The heuristic never gets replaced by a worse result — a lower tier's output is kept unless a higher tier actually produces something.
export PARADOX2_YOLO_DETECT=1
export PARADOX2_RAPIDAI_TABLES=1
export PARADOX2_VLM_TABLES=1
Health check
from paradox2 import doctor
doctor() # {"cuda": True/False, "pymupdf": "1.24.x", "ok": True/False, "problems": [...], ...}
paradox2 doctor # full report
paradox2 doctor --fix # prints only the pip install command(s) needed, exit 1 if any
First OCR call downloads model weights (PP-OCRv5, no progress bar) —
a first extract() on a scanned page that appears to hang for 10-30s on a
slow connection is this, not a crash. Backend stderr noise (onnxruntime/
TensorFlow/cuDNN registration warnings) is upstream noise from paddleocr's
own dependencies, not paradox2 — there is currently no flag to suppress it.
Design principles
- Digital-first: a PDF with a text layer never pays for OCR, vision layout models, or GPU inference — verified per-page, not per-document.
- Facade stays thin:
paradox2/api.pyis dispatch only; business logic lives inpipeline/,tables/,engines/,formats/. If the facade creeps toward 100+ lines, that's a signal something leaked out of place. - Specialists are lazy and swallow their own failures: an optional
engine (handwriting, signatures, formulas, RapidAI, VLM) that fails to
load or errors mid-call returns
[]/None— it never takes down the base extraction path. - No simulated benchmarks: every accuracy/speed number in this README and in the codebase's docstrings comes from a real run against real documents, with the script path noted alongside it.
Status
Early-stage rewrite, private while the API and table router stabilize. See
docs/PLAN.md for the phase breakdown and docs/TASKS.md for current work.
Release files for paradox2 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| paradox2-0.2.1.tar.gz | 279.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| paradox2-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 428.4 kB
Release files / paradox2-0.2.1.tar.gz
| Download URL | paradox2-0.2.1.tar.gz |
|---|---|
| Size | 279.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1b6ea2553a5f447629360c28f2b16395ad164de3387830449cf8fd446e40752f
|
|
BLAKE2b-256 checksum How to use checksums |
446fc8bfc689d1377ab089015be05d443027f87a7cd71da205e196116fdbe611
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.18
|
Release files / paradox2-0.2.1-py3-none-any.whl
| Download URL | paradox2-0.2.1-py3-none-any.whl |
|---|---|
| Size | 149.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
addf75ff0d18f7d2c357763b10ffd4f4198b8c82e5cb6fe7729734835c87d696
|
|
BLAKE2b-256 checksum How to use checksums |
f1516bf84522704f09b02362689e2dd508089884ede3ca32a13302f52fabbe3a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.18
|