On-device OCR for the Mon language (mnw), running on ONNX Runtime. Images never leave the machine.
Note
Mon is classified as vulnerable in UNESCO's Atlas of the World's Languages in Danger.
This project digitises the Mon script, so that later work has something to build on: system integrations, corpora, language models.
This repository is the engine and its four bindings: Python, JavaScript (Node.js), Go, and Rust. Each loads the same ONNX graph and the same 276-character charset, pinned to one Hugging Face revision, and each decodes CTC output the same way.
They are checked against each other rather than assumed to agree, and the current result is not close to full agreement.
Re-measured 2026-09-04 on the page path — read_image / predict, which is
what the quick-start examples call — over the seven images in data/images/:
Python, JavaScript and Go return identical text on 1 of 7. Pairwise, Python
matches JavaScript on 1, Python matches Go on 1, and JavaScript matches Go on 3.
The widest gap is 000028.jpg, where Python returns 154 characters and
JavaScript and Go return 55 and 61.
docs/CROSS_BINDING_PARITY.md reports 5 of 7. It measured the line path on pre-cropped files, which skips segmentation entirely; that number was never a statement about reading a page. The two figures are not in conflict, they answer different questions, and only the page one describes what the documented API does.
The cause is two things compounding: four different image-resampling kernels, two of them the wrong family, and four line segmenters whose density threshold has four live values across the ports under two different formulas.
That is agreement, not accuracy. No binding has been scored against ground truth here, and four implementations reading the same wrong thing would agree perfectly.
Tip
The web and mobile apps cap uploads at 50 MB and 20 MB. This SDK has no such limit; use it directly for larger files.
Image (File/Buffer)
ModelManager → fetch + cache monocr.onnx at the pinned revision
LineSegmenter → horizontal projection profile → line boxes
MonOCR.predict → crop + scale + normalize to [-1.0, 1.0]
→ ONNX Runtime session → greedy CTC decode → String
ModelManager, LineSegmenter and MonOCR are the names in this repository, in
each of the four languages. The block above previously named ImagePreprocessor,
MonOcrEngine and CtcDecoder, which are classes in the Android and iOS apps and
appear nowhere here.
| Attribute | Specification |
|---|---|
| Architecture | MobileNetV3-Large + SE + 2×BiLSTM-512 + attention + CTC |
| Precision | FP32 (ONNX) |
| Parameters | 11.55M |
| Input | 160 × 1024 (H × W), both static |
| Charset | 276 characters, 277 classes |
| Asset Size | 46.2 MB |
The model is pinned to revision d3d9d5e of
janakhpon/monocr, not to main. Each
binding ships the matching charset and refuses to decode if the two disagree: CTC
reserves index 0 for the blank, so a model over N characters must emit N + 1
classes. Without that check a mismatch returns well-formed Mon text that is wrong,
with no error and no lookup miss.
| SDK | Directory | Registry/Source | Published | In this tree |
|---|---|---|---|---|
| JavaScript / Node.js | js/ |
npm: monocr | 0.4.0 | 0.4.0 |
| Python | python/ |
PyPI: monocr-onnx | 0.4.0 | 0.4.0 |
| Go | go/ |
pkg.go.dev: monocr-onnx/go | v0.4.0 | 0.4.0 |
| Rust | rust/ |
crates.io: monocr | 0.3.1 | 0.4.0 |
Python, JavaScript and Go are published at 0.4.0. Rust is at 0.3.1, which
differs only in the crate description and a doc comment, so cargo add monocr
gives you the same library.
Important
Upgrade the JavaScript package. Every npm release before 0.4.0 returned
noise rather than text: the preprocessing step read a three-channel buffer as
if it were one channel, so it sampled the wrong bytes. On a typeset page
monocr@0.3.2 returned 168 characters of garbage where 0.4.0 returns 1,178 of
Mon. The Python, Go and Rust bindings were never affected.
Rust is also the odd one out in naming: named monocr on
crates.io rather than monocr-onnx like the repository and the other three
registries — chosen once monocr was confirmed unclaimed there, before the
first publish. [lib] name in rust/Cargo.toml stays monocr_onnx, so
nothing importing the crate needed to change.
Registry state re-verified 2026-09-03 against pypi.org, registry.npmjs.org, crates.io and pkg.go.dev — each package's own API, not this repository's own claim about itself. The "Published" column is the registry's answer; those are different questions, and conflating them is what let 0.2.0 and 0.2.1 sit tagged-but-unpublished for months.
The >=0.3.0 bounds below are load-bearing: 0.1.x carries a 225-character
charset against a 277-class graph and returns wrong characters, not merely worse
ones.
pip install "monocr-onnx>=0.3.0"
# or
uv add "monocr-onnx>=0.3.0"npm install monocr@^0.3.0go get github.com/MonDevHub/monocr-onnx/goThe crate is monocr; the library it exposes is monocr_onnx.
cargo add monocrfrom monocr_onnx import MonOCR
# Downloads the pinned model on first run and caches it by revision.
engine = MonOCR()
# Page-level: segments into lines, then reads each one.
text = engine.predict("scanned_document.jpg")
print(text)
# Line-level: skips segmentation, for a crop you have already cut.
line_text = engine.predict_line("single_line_crop.png")monocr-onnx/
├── python/ # Python package & CLI
├── js/ # Node.js package (uses sharp)
├── go/ # Go module
├── rust/ # Rust crate
├── scripts/ # download_models.sh — fetch the pinned model
├── examples/ # runnable examples: python, js, go (no rust yet)
├── data/ # test images and fixtures
└── docs/ # parity results and design notes
All three apps now live in one repository, MonDevHub/monocr:
apps/web— in-browser OCR, SvelteKitapps/android— Jetpack Composeapps/ios— SwiftUI
The former MonDevHub/ocr-android and MonDevHub/ocr-ios links were listed here
until 2026-08-15 and both return 404; MonDevHub/monocr-web still resolves but
has not been pushed to since the web app moved into the monorepo.
- Hugging Face model — ONNX and Core ML.
The TFLite export was removed at revision
a51be11(2026-03-19) — a v2-era revision, not the current pin. Do not bump this hash with the pin:8da6178swepta51be11->d3d9d5eacross fourteen sites on 2026-08-15 and caught this sentence too, but it is provenance, not a pin. The model repo's own history is unambiguous:a51be11isDelete tfliteand deletes all three.tflitefiles;d3d9d5edeletespytorch/monocr.ptand nothing else, five months later. The three monorepo app READMEs kept the right value.
MIT