The islands that vanish inside Hebrew.
A line of Hebrew with an English word in the middle is completely ordinary — an institution, a date, a catalogue number. Most OCR models hand that line back with the middle silently missing. We traced it to the decoder, fixed it, and released the whole stack under Apache 2.0.
A compression bias at the boundary
Hebrew documents are rarely purely Hebrew. An institution name, a gene symbol, a catalogue number, a citation, a date, a URL — real pages are full of short left-to-right runs sitting inside right-to-left text. We call them islands.
To understand why they disappear, it helps to know how a CTC decoder reads. It scans the image in vertical slices, left to right, and emits one character — or a blank — per slice. Built into that is a hard assumption: the order of the slices is the order of the characters.
Hebrew runs right to left, so its characters arrive in the reverse of the scanning order. A model trained on Hebrew learns to handle that consistently. But an embedded English word runs left to right inside that reversed line, so at each of its two edges the direction of the text and the direction of the scan disagree. There is no single alignment that satisfies both the island and the Hebrew around it.
CTC has an escape hatch for exactly this kind of conflict: the blank symbol. Emitting blanks across the whole island is cheaper than committing to an alignment that contradicts the rest of the line. So the island isn't misread — it is emitted as nothing at all.
What makes this diagnosable rather than mysterious is where it doesn't happen. Isolated Latin reads at 1.20% CER. Isolated digits at 1.41%. Pure Hebrew at 0.64%. The model can plainly see every glyph involved. The failure is specific to the mixed boundary, and it isn't an ordering bug either — the pure-reorder rate is near zero. It is a compression bias, and it is silent: you get back clean, fluent, confident Hebrew with a hole in it.
Per-slice micro-CER, SVTRv2 CTC head. Every glyph class is solved in isolation. Only the seam fails.
Swap the head, not the model
An attention decoder has no monotonic constraint. It attends over the whole encoded line and emits the next character from context, so an embedded island is just more context. We kept the SVTRv2 backbone exactly as it was and changed only the decoding head.
| Slice | CTC | Attention | Gain |
|---|---|---|---|
| Real Hebrew + Latin (n=233) | 12.63% | 2.33% | 5.4× |
| Hebrew + digits | 4.99% | 0.56% | 8.9× |
Attention is slower, so running it on every crop would be wasteful — roughly 90% of real crops are single-script and CTC handles them perfectly. So production runs a script-gated cascade: CTC first on everything, then fall back to attention only when the CTC output contains a Latin or digit character, or comes back low-confidence.
That buys the attention ceiling on mixed text at about 1.25–1.36× CTC latency, with no retraining and no second model to maintain.
Nothing in the mechanism is specific to Hebrew. Any bidirectional script that embeds LTR runs — Arabic, Farsi, Urdu, Hebrew — should exhibit the same CTC boundary collapse, and should respond to the same fix. If you are benchmarking a bidirectional OCR model, measure the mixed-script slice separately. A single aggregate CER will hide this completely.
A 52% plateau that turned out to be the wrong question
Long Hebrew lines — 61 to 80 characters — sat at 52–55% CER across every line-level model we trained. No amount of additional data moved it, and three independent levers (learning rate, input width, data up-weighting) all failed the same way.
Long-sequence degradation is not a Hebrew problem, though. It shows up in the PaddleOCR issue tracker independently of script: issue #11482 reports that recognition accuracy on long sequences "never matches the accuracy of single words" across CRNN, SAR, ABINET and SVTR, and issue #13938 documents a Chinese model stuck at ~50% accuracy on ~135-character lines. Different scripts, different metrics, same shape of failure — which was the clue that ours wasn't a Hebrew data problem either.
We first beat it with sliding-window inference: process the line in overlapping ~320px windows and stitch the seams by Levenshtein alignment. That was a genuine 45-point drop — 52% down to 6.93%. It was also a trap. It only worked on one exact model at one exact scale, cost 3–5× the recognition passes, and every seam was a fresh opportunity for duplication garbage. The next backbone couldn't slide at all.
The actual answer was to stop patching a short-crop model and use a reader built for the sequence length. SVTRv2 reads the whole line in a single pass, no windows, no seams — and drops 71–80 character Hebrew to 0.49% CER. The wall wasn't a data problem or an inference problem. It was an architecture mismatch.
Robustness is the edge, not pristine text
The obvious target — beat Tesseract on clean Hebrew — is the wrong one. On clean academic Hebrew, Tesseract is genuinely good, and our earlier word-level stack narrowly lost to it. Chasing that margin was low-value work.
What matters is what happens on the documents people actually scan. Tesseract collapses on those; we don't.
Every CER figure on this page comes from our own in-house evaluation harness, run on our own test sets. They are measured rather than estimated, and BiDi-normalised on both sides before the Levenshtein distance — but they are vendor-reported and have not been independently verified. The methodology, the caveats and the per-slice breakdowns are all in the repository, so you can disagree with them specifically.
| Domain | Tesseract | Ours |
|---|---|---|
| Thermal receipts | 26.32% | 2.09% |
| Menus | 12.41% | 2.80% |
| Invoices | 7.99% | 2.68% |
| Page pipeline | micro-CER |
|---|---|
| Word detection + SVTRv2 cascade | 7.56% |
| Hebrew-finetuned line detection | 10.36% |
| Stock line detection | 10.67% |
| Tesseract 5 | 14.20% |
Word-level detection beats line-level on Hebrew pages, and we tested that fairly: our first comparison was confounded because the word detector was Hebrew-finetuned while the line detector was stock. So we trained a Hebrew line detector on the same architecture and the same warm-start base, converged it to 0.913 val hmean, and re-ran. The finetune closed only 0.31pt. The gap is the word-vs-line difference itself, not detector tuning.
And we did close the academic gap too. On clean rendered public-domain Hebrew with perfect ground truth (n=600), the attention reader now beats Tesseract outright: 0.35% versus 1.34% micro-CER. But robustness is what makes the thing worth shipping.
Seven models, finetuned from PaddleOCR
Every model here is a finetune of a PaddleOCR model (PaddleOCR v3.7.0, Apache-2.0), released in the same licence. All of them ship as ONNX and share one 120-character charset.
| Model | Role | Finetuned from |
|---|---|---|
| server-svtrv2 | Flagship recogniser (CTC + attention) | SVTRv2, Chinese pretrain |
| server-v6 | Word-level server recogniser | PP-OCRv6, PPLCNetV4-medium |
| server-v5 | Word-level server recogniser | PP-OCRv5, PPHGNetV2-B4 |
| light-svtrv2small | Edge / CPU recogniser | SVTRv2-small, distilled from the flagship |
| mobile-word | Mobile recogniser (7.4 MB) | PP-OCRv5 mobile, PPLCNetV3 |
| word-det | Flagship word detector | PP-OCR mobile DBNet |
| line-det | Line detector (hard layouts) | PP-OCR mobile DBNet |
The flagship pairs word-det with server-svtrv2. Start there; the rest are alternatives for smaller footprints or harder layouts.
pip install -e . huggingface_hub
from ocr import HebrewOCR
ocr = HebrewOCR.word(models_dir) # flagship
result = ocr.read("page.pdf") # image or PDF
for line in result["lines"]:
print(line["text"]) # logical order
Apache-2.0, code and weights, finetuned from PaddleOCR models under the same licence. The training weights and configs are not published — the ONNX files are all you need to run the models. If you want to finetune on your own Hebrew data, adapt the charset, or collaborate, open an issue or write to ronen@rivoklabs.com.
Reading-order assembly uses paragraph-direction sorting rather than full UAX#9 BiDi. A Hebrew-dominant line containing a long embedded Latin run can therefore order the runs by position rather than by strict logical BiDi. This is fine for most documents and it is documented in the repository — but if your corpus is heavily bilingual at the line level, test it before you rely on it.