Hybrid multi-engine OCR for Arabic and English documents
Python
0
15 commits
updated Oct 16, 2026
A hybrid multi-engine OCR toolkit for Arabic and English documents. Routes between PaddleOCR (fast, strong on printed text and tables), QARI-OCR (state-of-the-art Arabic with diacritics), and Surya (Arabic and handwriting fallback) behind a single unified interface. Supports PDFs, images, and batch processing with output as plain text, JSON, Markdown, or HTML.
Built as a university project at the University of Sheffield.
src/qira_ocr/
__init__.py # Public OCR class — single entry point
loader.py # PDF text extraction, rasterization, image loading
router.py # Automatic engine selection via Arabic script detection
result.py # Structured result model (Page > Block > Line > Word)
structure.py # Table/layout analysis using PPStructure
cli.py # Click CLI (qira-ocr scan ...)
api.py # FastAPI REST API (/ocr, /health)
engines/
base.py # OCREngine protocol
paddle.py # PaddleOCR wrapper
qari.py # QARI-OCR wrapper (Arabic VLM)
surya.py # Surya OCR wrapper
tests/ # 73 tests covering all modules
Requires Python 3.12+. Install with uv:
uv sync # core OCR library
uv sync --extra cli # + command-line interface
uv sync --extra api # + REST API server
uv sync --extra qari # + QARI-OCR engine (Arabic VLM, needs GPU)
uv sync --extra all # everything
from qira_ocr import OCR
ocr = OCR()
# Basic — auto-detects engine, extracts text from PDF text layers directly
result = ocr.read("invoice.pdf")
print(result.to_text())
# Force a specific engine
result = ocr.read("arabic_note.jpg", engine="qari") # best Arabic quality
result = ocr.read("arabic_note.jpg", engine="surya") # lighter Arabic fallback
# Export as structured data
for page in result.pages:
for block in page.blocks:
print(block.text, block.bbox, block.confidence)
# Other export formats
result.to_dict() # nested dict with bounding boxes + confidence scores
result.to_markdown() # markdown with table formatting
result.to_html() # html with table formatting
qira-ocr scan document.pdf
qira-ocr scan photo.jpg --engine surya --format markdown
qira-ocr scan ./documents/ --output ./results/ --format json
uvicorn qira_ocr.api:app
POST /ocr file upload, optional engine/format params
GET /health engine availability status
When engine="auto" (default), qira-ocr inspects the document to pick the best engine:
| Condition | Engine | Why |
|---|---|---|
| PDF with text layer | Direct extraction | No OCR needed |
| Low Paddle confidence (non-Latin) | QARI-OCR | State-of-the-art Arabic, 0.061 CER |
| Low confidence, QARI not installed | Surya | Lighter Arabic/handwriting fallback |
| Everything else | PaddleOCR | Fastest, strong on printed English + tables |
Override with engine="paddle", engine="surya", or engine="qari".
Surya (0.20+) runs OCR on a vision-language model served by an external inference server. On first use it starts the server itself and downloads the model (about 1.47 GB GGUF from datalab-to/surya-ocr-2-gguf). You need one of these:
llama-server binary from llama.cpp (brew install llama.cpp, or a release build with LLAMA_CPP_BINARY=/path/to/llama-server)To use a server that is already running, set SURYA_INFERENCE_URL=http://host:port/v1. Without a server, the surya engine raises an error with install hints. The auto route falls back to surya when Paddle confidence is low and QARI isn't installed.
If surya fails on every block it attempted on a page (the backend is down or unreachable), recognize raises RuntimeError instead of returning an empty page. This is deliberate and aborts a multi-page document; partial failures only log a warning and the failed blocks' text is missing.
Surya bounding boxes are per layout block (a paragraph, table, header, and so on), not per text line: every line in a surya block shares that block's bbox and confidence.
uv sync --extra cli --extra api
uv run pytest -m "not requires_ocr_server" # what CI runs: no model downloads, no server
uv run pytest # full suite: downloads Paddle/surya models, needs llama-server or vllm
Hybrid multi-engine OCR for Arabic and English documents
Python
0
15 commits
updated Oct 16, 2026
A hybrid multi-engine OCR toolkit for Arabic and English documents. Routes between PaddleOCR (fast, strong on printed text and tables), QARI-OCR (state-of-the-art Arabic with diacritics), and Surya (Arabic and handwriting fallback) behind a single unified interface. Supports PDFs, images, and batch processing with output as plain text, JSON, Markdown, or HTML.
Built as a university project at the University of Sheffield.
src/qira_ocr/
__init__.py # Public OCR class — single entry point
loader.py # PDF text extraction, rasterization, image loading
router.py # Automatic engine selection via Arabic script detection
result.py # Structured result model (Page > Block > Line > Word)
structure.py # Table/layout analysis using PPStructure
cli.py # Click CLI (qira-ocr scan ...)
api.py # FastAPI REST API (/ocr, /health)
engines/
base.py # OCREngine protocol
paddle.py # PaddleOCR wrapper
qari.py # QARI-OCR wrapper (Arabic VLM)
surya.py # Surya OCR wrapper
tests/ # 73 tests covering all modules
Requires Python 3.12+. Install with uv:
uv sync # core OCR library
uv sync --extra cli # + command-line interface
uv sync --extra api # + REST API server
uv sync --extra qari # + QARI-OCR engine (Arabic VLM, needs GPU)
uv sync --extra all # everything
from qira_ocr import OCR
ocr = OCR()
# Basic — auto-detects engine, extracts text from PDF text layers directly
result = ocr.read("invoice.pdf")
print(result.to_text())
# Force a specific engine
result = ocr.read("arabic_note.jpg", engine="qari") # best Arabic quality
result = ocr.read("arabic_note.jpg", engine="surya") # lighter Arabic fallback
# Export as structured data
for page in result.pages:
for block in page.blocks:
print(block.text, block.bbox, block.confidence)
# Other export formats
result.to_dict() # nested dict with bounding boxes + confidence scores
result.to_markdown() # markdown with table formatting
result.to_html() # html with table formatting
qira-ocr scan document.pdf
qira-ocr scan photo.jpg --engine surya --format markdown
qira-ocr scan ./documents/ --output ./results/ --format json
uvicorn qira_ocr.api:app
POST /ocr file upload, optional engine/format params
GET /health engine availability status
When engine="auto" (default), qira-ocr inspects the document to pick the best engine:
| Condition | Engine | Why |
|---|---|---|
| PDF with text layer | Direct extraction | No OCR needed |
| Low Paddle confidence (non-Latin) | QARI-OCR | State-of-the-art Arabic, 0.061 CER |
| Low confidence, QARI not installed | Surya | Lighter Arabic/handwriting fallback |
| Everything else | PaddleOCR | Fastest, strong on printed English + tables |
Override with engine="paddle", engine="surya", or engine="qari".
Surya (0.20+) runs OCR on a vision-language model served by an external inference server. On first use it starts the server itself and downloads the model (about 1.47 GB GGUF from datalab-to/surya-ocr-2-gguf). You need one of these:
llama-server binary from llama.cpp (brew install llama.cpp, or a release build with LLAMA_CPP_BINARY=/path/to/llama-server)To use a server that is already running, set SURYA_INFERENCE_URL=http://host:port/v1. Without a server, the surya engine raises an error with install hints. The auto route falls back to surya when Paddle confidence is low and QARI isn't installed.
If surya fails on every block it attempted on a page (the backend is down or unreachable), recognize raises RuntimeError instead of returning an empty page. This is deliberate and aborts a multi-page document; partial failures only log a warning and the failed blocks' text is missing.
Surya bounding boxes are per layout block (a paragraph, table, header, and so on), not per text line: every line in a surya block shares that block's bbox and confidence.
uv sync --extra cli --extra api
uv run pytest -m "not requires_ocr_server" # what CI runs: no model downloads, no server
uv run pytest # full suite: downloads Paddle/surya models, needs llama-server or vllm