Skip to main content

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).

Each worker loads its own full OCR model set (~2.4GB RSS measured) — page_workers is automatically capped against both available CPU cores (respecting taskset/cgroup/Docker --cpus limits, not just total system cores) and available RAM, so a request for more workers than the machine can actually hold gets a smaller worker count instead of an OOM kill.

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.py is dispatch only; business logic lives in pipeline/, 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.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for paradox2 0.2.2
File Size Uploaded
paradox2-0.2.2.tar.gz 282.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for paradox2 0.2.2
File Interpreter ABI Platform
paradox2-0.2.2-py3-none-any.whl Python 3 none any Details

Total release size: 434.6 kB

Release files / paradox2-0.2.2.tar.gz

Download URL paradox2-0.2.2.tar.gz
Size 282.2 kB
Tags Source
SHA-256 checksum
How to use checksums
5f2f77a45e1f3d0781f7b55c028e0eeed67ae60171f88b3a80ba1c3dd8e6a670
BLAKE2b-256 checksum
How to use checksums
f5b359c91540523361c719a1ae9edf58389b7d3a3a2135bfc613fd44169d4f2a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.18

Release files / paradox2-0.2.2-py3-none-any.whl

Download URL paradox2-0.2.2-py3-none-any.whl
Size 152.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
dd4ef6624629c5d193945abaf914f3d5f9637d74c3bc560ad0b75db7a0bd522e
BLAKE2b-256 checksum
How to use checksums
8d86482b81c9a0693916c52f07fdee0437d63d666a118815cd3645b4069471e4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.18

Release history Release notifications | RSS feed

0.2.13

2 release files

0.2.12

2 release files

0.2.11

2 release files

0.2.10

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

This release

0.2.2 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page