Arjuna-OCR [Hi & En] v1.1
v1.1.3: adds the row-structure fallback and mislabelled-panel re-typing; recogniser weights are unchanged, and these changes are not re-benchmarked.
What's new in v1.1.0 — layout v15
No recognised word is ever dropped from the page text. Measured on held-out pages of private documents:
every word the recogniser read also appears in doc["text"] — assemble.suppress_text_in is now empty, and
lines keep their in_seal / in_handwriting / in_figure flags so a consumer can filter without data loss.
The layout model gains six classes v1.0 could not express — section_header, list_item, caption,
footnote, formula and barcode. barcode matters most: without it a stamp panel could only be called
figure, and suppressing that figure removed real text. Three reading-order defects in the assembler are
also fixed (column ordering was silently disabled by any centred running header; the column-order routine was
unreachable; and a full-width block could absorb both columns of a two-column page).
Layout-aware OCR for printed Hindi (Devanagari) and English documents. Detector -> layout -> recognizer
-> assembler, shipped as ONNX with an optional TensorRT path. Sibling of
anandkaman/arjuna-ocr-kn-en, with which it shares the
detector and layout model.
- Reading order, paragraphs, titles, tables, seals, figures, ID panels and handwriting regions
- Output as JSON, Markdown, plain text, hOCR, ALTO or TSV
- Every line carries a confidence, its weakest character's confidence and review flags — nothing is silently rewritten
- 9.99 pages/s on one RTX 5060 Ti with TensorRT (54 dense pages, ~40 lines/page); CUDA and CPU fallbacks
Results
Public benchmark — Mozhi Hindi test, the dataset's own unmodified labels
| slice | n | CER | WER | word acc | exact |
|---|---|---|---|---|---|
[2000:] — clean | 7,544 | 0.65 | 2.23 | 97.77 % | 97.84 % |
[:2000] — dev | 2,000 | 0.84 | 2.75 | 97.25 % | 97.35 % |
| full split | 9,544 | 0.70 | 2.36 | 97.64 % | 97.72 % |
Read the split honestly. This test set doubles as our development set, and evaluation during training
scored only its first 2,000 lines, whose CER influenced checkpoint selection. Lines 2,000-9,543 were never
trained on and never used for selection, so [2000:] is the only clean claim; the full-split row is
given because published numbers are usually quoted on the whole split. The clean slice scores better than
the dev slice, so there is no sign of selection overfitting.
There is only one public Hindi benchmark here, and that is a limitation. himalaya-ai/indic-deva-ocr-eval
is in our training mix; vishwam-101/devanagari-ocr-dataset was rejected by our data audit (its rows are long
multi-line passages, not line crops).
Private validation — 234 frozen lines, registration and revenue records
Never trained on. Numbers only; the pages and lines are private and are not published.
| reference chars | n | CER | exact |
|---|---|---|---|
| 1-20 | 107 | 0.74 | 93.5 % |
| 21-45 | 46 | 1.32 | 78.3 % |
| 46-80 | 37 | 0.41 | 83.8 % |
| 81+ | 44 | 0.42 | 72.7 % |
| all | 234 | 0.58 | 85.0 % |
The 1-45 character band carries the form values (plot numbers, amounts, reference codes, dates).
Throughput — one RTX 5060 Ti, 4-core host
| configuration | pages/s | lines/s |
|---|---|---|
| CPU | 0.13 | — |
| CUDA, no TensorRT engines | 1.67 | 67 |
| TensorRT, two-fill layout, page_batch 16 | 8.9-10.0 | 354-390 |
TensorRT engines are built on first use. They are built in a background thread by default, so a
short-lived process exits before they are ready and silently runs on CUDA — set KANEN_BACKGROUND_TRT=0
to build them up front, or run prepare_engines.py once.
Try it
Live demo — choose Hindi (Devanagari) + English in the sidebar. CPU inference.
Quick start
pip install -r requirements.txt onnxruntime-gpu==1.29.0 # CPU: just requirements.txt
pip install -r requirements-trt.txt # optional TensorRT
python run_ocr.py --pages "scans/*.png" --out results.json --markdown out/
python kanen_ocr.py scans/page.png --format md # CLI
python serve.py # FastAPI on :8000
from kanen_infer.api import KanEnOCR
ocr = KanEnOCR() # reads arjuna_hi.yaml next to the package
doc = ocr.page("scans/page.png")
print(doc["text"])
Samples
samples/pages/hindi_sample_synthetic.jpg is a synthetic Devanagari page rendered by this
project's own generator from public Hindi text — the real documents this model was tuned on are private
registration and revenue records and are not published. samples/reference.json and samples/markdown/
hold its output.
Known limitations
- Text inside
handwriting,sealandfigureblocks is withheld from pagetext(assemble.suppress_text_in) — a print recognizer fed a signature emits confident nonsense. Guarded since v1.0: a block is only suppressed when the layout model is confident (>= 0.50), the block is small (<= 10 % of the page) and it holds at most 3 recognised lines. Without those guards, 119 of 160 withheld lines across 87 pages were real text (certificate and GRN numbers, dates, registrar seal text) — an e-stamp panel holds emblems, a barcode and a QR code, so the layout model boxed the whole panel asfigureat confidence 0.31-0.38. With the guards that falls to 28, andfigureno longer appears among suppressed blocks. What remains suppressed is mostly seal text, which the recognizer reads poorly on stamped and curved impressions. A block whose suppression was overridden carriessuppression_overriddenin the output. All lines stay inblocksregardless. - Layout sometimes cuts one paragraph into boxes that interleave vertically. Every line is then
recognised correctly but emitted in block order, which scrambles the page. Mitigated since v1.0.3:
assemble.merge_split_blocks_x/ymerges same-class blocks that overlap on both axes (x by 0.60 of the narrower box, y by 0.25 of the shorter), so the lines are re-sorted by geometry. Two-column pages, stacked paragraphs and nested seals are never merged. Setmerge_split_blocks_x: 0to disable. The layout model still draws the wrong boxes — this restores reading order, it does not fix the cause. - A leading capital can be dropped after a numbered clause (
11. That ...->11. hat ...), seen on numbered contract clauses. Under investigation for the next version. - Curly quotes are sometimes emitted as straight quotes.
- Combining marks are the dominant error class: over 9,544 held-out lines the model drops 75 marks and
invents 46, led by virama, nukta and the
i/umatras. This is a known hard class for Devanagari OCR generally, not specific to this model. - Handwriting is not recognised — handwritten regions are detected and flagged, not read.
- Anusvara recall is not quoted because part of our label review was done at a magnification we no longer consider adequate; the re-check is not finished.
Training data
Synthetic Hindi/English line images (600k) rendered from Hindi Wikipedia, a domain corpus and templates, plus
public Devanagari OCR corpora (darknight054/indic-mozhi-ocr train+validation, himalaya-ai/indic-deva-ocr-eval)
and a small set of our own hand-labelled lines from private registration and revenue records. Other-script
lines (Bengali, Tamil, Telugu, Malayalam, Gujarati, Gurmukhi, Odia, Kannada, Arabic) are trained with empty
targets so the model stays silent on scripts it does not read.
License and attribution
Apache-2.0. The layout model derives from DocLayout-YOLO and carries AGPL-3.0 — see
models/layout/LICENSE-AGPL-3.0.txt and NOTICE.md. Base models: PaddleOCR (Apache-2.0), OpenOCR SVTRv2
(Apache-2.0).
Author: anandkaman · kamananand98@gmail.com