MonOCR β line-level OCR for Mon (mnw)
A CRNN that reads one line of Mon text and returns a string. Mon is classified by UNESCO as a vulnerable language, and this is the model, the charset and the export artifacts around it.
In production. This revision ships on web (ocr.mondevhub.com), Android and iOS.
Held-out CER 0.0100 on 150 unseen lines in a typeface the model never trained on. That number, its confidence interval and the four things it does not cover are in Performance. Read them before quoting it.
What changed at v3.5, and what it breaks
This revision replaces the v2 network. It is not a drop-in. Four parts of the contract moved:
v2 (revision a51be11) |
v3.5 (this revision) | |
|---|---|---|
| Input height | 128 | 160 |
| Input width | dynamic β any width accepted | static 1024 |
| Batch axis | fixed at 1 | dynamic |
| Output classes | 316 | 277 |
| Charset | 315 characters | 276 characters |
| Parameters | 6,575,868 | 11,553,437 |
The width axis is the one that catches integrations. v2's graph accepted a crop
of any width; v3.5 accepts 1024 and nothing else. nn.MultiheadAttention fixes
the sequence length when the graph is traced, so a declaration of anything wider
would be a promise the artifact cannot keep. Resize to 1024 before calling.
If you are pinned to a51be11 you are unaffected β that revision still serves v2
and always will. Move deliberately.
Files
| Path | What |
|---|---|
onnx/monocr.onnx |
ONNX FP32, opset 17, 46,247,040 bytes |
onnx/monocr.json |
sidecar: charset, geometry, class count, normalization contract |
onnx/charset.txt |
the 276 characters, index 1..276; first character is U+0020 |
coreml/monocr.mlpackage |
Core ML, FP32 |
coreml/monocr.mlpackage.json |
its sidecar |
charset.txt, monocr.json |
copies at the root, for consumers that expect them there |
Only the deployment artifacts are published. There is no PyTorch checkpoint here. ONNX and Core ML are what the SDKs and the apps load, and they are what the export gates check; a training checkpoint would add weight that nothing in the toolchain reads.
Load the charset from the same revision as the weights. They are one contract:
a charset from another revision decodes every index to the wrong character and
raises nothing. Strip only \n and \r β the first class is a space, and a bare
.strip() removes it and shifts every index by one.
Input contract
- Grayscale, 1 channel, float32, shape
[batch, 1, 160, 1024] - Aspect-preserving resize to height 160, then pad the width to 1024
- Normalize
pixel / 127.5 - 1.0 - Pad with white, which is
+1.0after normalization, not0.0 - Greedy CTC decode; index 0 is the blank
Feeding raw uint8 in [0, 255] produces confident garbage with no error.
Architecture
MobileNetV3-Large + squeeze-excitation neck β band pooling β 2ΓBiLSTM(512) β bottleneck self-attention (256-dim, 4 heads) β Linear(1024 β 277) β CTC. 11,553,437 parameters, all trainable.
Performance
Held-out evaluation
Measured 2026-08-16 on the test split, which was held out from training and never scored before this run. Under the project's evaluation ledger a given (data, weights) pair may only be spent once, and this is that spend.
| CER | CI95 | sequence accuracy | |
|---|---|---|---|
| Overall | 0.0100 | [0.0056, 0.0147] | 85.33% |
| Mon (n=99) | 0.0113 | [0.0050, 0.0186] | 85.86% |
| Burmese (n=32) | 0.0027 | [0.0000, 0.0064] | 93.75% |
| English (n=14) | 0.0103 | [0.0009, 0.0226] | 71.43% |
| Mon + English (n=5) | 0.0144 | [0.0000, 0.0292] | 60.00% |
n = 150 lines. Macro-averaged CER 0.0047. Expected calibration error 0.0315. Greedy decoding.
Three baselines ran alongside it, and they are the reason the number is believable:
| Baseline | CER | What it rules out |
|---|---|---|
| empty prediction | 1.0000 | The metric itself. Anything other than exactly 1.0 means the scorer is broken, and the suite refuses to report |
| most-common grapheme | 1.0832 | That the charset's prior alone explains the result |
| 1-nearest-neighbour pixel retrieval over 3,000 training images | 1.3675 | Memorisation. Retrieving the visually closest training image scores worse than predicting nothing, so the test images are not near-duplicates of training ones |
Train/serve skew: none measured. The PyTorch checkpoint and the published
ONNX graph both scored 0.0100 (cer_delta 0.0) and produced
exactly the same string on 150 of 150 lines. The number above therefore
describes the artifact in this repository, not just the checkpoint it came from.
Per typeface: Pyidaungsu-Regular 0.0076 (n=56), Pyidaungsu-Numbers 0.0108 (n=51), Pyidaungsu-Bold 0.0124 (n=43).
Latency p50 134.8 ms/line, CPU, batched β a per-batch timing divided by batch size, so it is a mean and not a tail.
Four things this number does not cover
- n = 150. The interval is [0.0056, 0.0147]; treat the width as real.
- One typeface. All 150 lines are Pyidaungsu, held out from training. A
second held-out design,
yunghkio, is not represented: this split was generated before that design was set aside. - Unseen text, not an unseen renderer. The same generator and the same augmentation pipeline produced training and test images. A defect in the renderer is learned, validated and tested against identically. This is the single largest caveat on the card and only real photographed lines close it.
- Disjointness is argued, not directly verified. The training-side labels for this model were produced on a machine whose state was not fully retained. The partition function is byte-unchanged since generation and none of the 150 test labels appears in the local training or validation sets, but that is an argument from the stability of a hash rather than a direct comparison.
The selection metric, for contrast
val_cer 0.0210 at epoch 23 / step 93,645 β a periodic check over 4,096
lines during training, in a single typeface, on data that also drove checkpoint
selection. It is not an evaluation and it is not comparable to the 0.0100
above: different split, different typeface, different sample size, and one of
them chose the weights being scored.
Against v2, on identical images
The one comparison that is available and fair. Both generations ship an ONNX export, so the same rendered lines go through both graphs with the same preprocessing and the same greedy decode. Text is restricted to the 273 characters both charsets can emit, so neither model is charged for a character it has no class for. 600 lines per arm, rendered 2026-08-15.
| Rendered in | n | v2 CER | v3.5 CER | error reduced |
|---|---|---|---|---|
| the 32 trained designs | 600 | 0.1470 | 0.0396 | 73% |
namkhon, held out from v3.5 |
600 | 0.0521 | 0.0188 | 64% |
pyidaungsu, yunghkio, held out from v3.5 |
600 | 0.0342 | 0.0051 | 85% |
The bottom two rows are held out from v3.5 and from nothing for v2, which predates font-disjoint splits, so the comparison is tilted against v3.5 and it wins anyway. This is a preview, not an evaluation: one rendering pipeline reading its own output, synthetic for both models. The held-out number above is the evaluation; this table only says v3.5 reads rendered Mon better than v2 did.
Reading a page
Wide lines need care. Measured 2026-08-15 over 240 rendered lines wide enough to need the choice, median 3 model windows each: squeezing a whole line into the 1024px canvas scored 0.1434, cutting it into canvas-width tiles at whitespace columns and joining scored 0.0795. Tile wide lines for this model. The opposite is true of v2, where squeezing wins β the direction is a property of the network, so re-measure if you swap models.
Exports, and how they were checked
Both exports are gated against the PyTorch model on a seeded uniform [-1, 1]
input, and the gate fails the build:
- ONNX β logits within
rtol=atol=1e-3. - Core ML β logits within
1e-3and an identical CTC-decoded string.
The Core ML gate runs on CPU. The Neural Engine computes in fp16 and its compiler may reassociate, so what ships to a device is not what was verified. No device latency number exists for this model, on any platform.
Limitations
- Real photographs. Every training sample is synthetic, rendered by one generator. A camera photo of physical text is out of domain and fails confidently β measured on a whiteboard photo, the model returned fluent Mon at confidence 0.83 for text that appears nowhere on the page. Confidence is not a usable filter for this.
- Page layout. The input is a cropped line. There is no detector and no reading-order model.
- Handwriting, except Myanmar digits.
- Beam search. Greedy only in the exported graph.
- Latency. No per-line tail measured, and no on-device number at all.
- Typeface coverage. 35 distinct designs across 79 usable font files bound every generalisation claim above.
Training data
Rendered from a Mon text corpus. The corpus licensing is not fully settled: one source has no established licence, and CC BY-SA attribution for another cannot be reconstructed because per-article URLs were stripped at import. The MIT licence on this repository covers the model weights and the code that produced them; it does not resolve the provenance of the text they were rendered from.
Citation
@misc{monocr,
title = {MonOCR: line-level OCR for the Mon language},
author = {Zin Min},
year = {2026},
url = {https://huggingface.co/janakhpon/monocr}
}
- Downloads last month
- 22