T-RSI Fast Reflex OCR
A 1.58-bit ternary BitNet text-line recognizer in 380 KB
[!WARNING]
Experimental research release. Every number below was measured on one
Apple Silicon machine against ground truth this project generated. It has not
been independently replicated. Treat the figures as reproducible-by-you, not
as verified.
Every weight is -1, 0 or +1, packed four to a byte. The model reads a
32-pixel-tall text strip and emits characters through CTC, decoded with a prefix
beam search over a 27 KB character language model.
It is more accurate than Tesseract 5 on degraded text and on fonts, ties on
clean documents, and returns noticeably fewer perfect lines. Full numbers and
the reasons are below, including where it loses.
Measured results
Reproduce with python3 benchmark_vs_tesseract.py. Ground truth needs no hand
labelling: a digital PDF already contains its own exact text, so rasterizing a
page and cropping each line at the bbox the PDF reports gives pixels whose
correct answer is known. Training and test documents are split by file and
deduplicated by content fingerprint.
Character accuracy
| scenario |
T-RSI |
Tesseract 5 |
|
| fonts, clean |
99.14% |
97.77% |
+1.37 |
| fonts, degraded |
99.36% |
96.69% |
+2.67 |
| documents, clean |
96.82% |
96.85% |
−0.03 |
| documents, degraded 0.5 |
96.36% |
96.37% |
−0.01 |
| documents, degraded 1.0 |
93.75% |
91.98% |
+1.77 |
Exact-line match — where it loses
| scenario |
T-RSI |
Tesseract 5 |
| fonts, clean |
89.3% |
72.7% |
| fonts, degraded |
92.7% |
70.7% |
| documents, clean |
73.7% |
85.7% |
Tesseract's errors cluster onto a few hard lines and leave the rest clean. This
model's scatter — single characters across many otherwise-good lines — and one
stray character ruins a line as thoroughly as ten. That is the documented
weakness of CTC, which treats every timestep as conditionally independent given
the image and so cannot notice that the letters it just emitted do not form a
word.
Multi-hypothesis voting closes most of that gap at the cost of extra passes:
| decoding |
char accuracy |
WER |
exact line |
ms/line |
| single pass |
97.90% |
0.1004 |
76.2% |
19.3 |
| 3 stretches + vote |
98.21% |
0.0827 |
84.5% |
62.4 |
| 5 stretches + vote |
98.29% |
0.0786 |
86.5% |
108.8 |
| Tesseract 5 |
97.86% |
0.0548 |
87.9% |
105.0 |
By content type (real documents)
| content |
T-RSI |
Tesseract 5 |
| prose |
98.82% |
98.96% |
| symbol-heavy |
96.01% |
94.68% |
| numeric / amounts |
98.20% |
100.00% |
| alphanumeric IDs |
80.00% |
83.10% |
| ALL CAPS headers |
99.60% |
100.00% |
Alphanumeric identifiers are the weakest content for both engines. Random
codes carry no language structure, so no language model can rescue a misread
glyph.
Size, memory and speed
|
T-RSI |
Tesseract 5 |
| Packed ternary weights |
312.0 KB |
— |
| Float32 tensors (LayerNorm, biases, scales) |
27.4 KB |
— |
| Character 3-gram LM |
27.4 KB |
— |
| Inference code |
32.2 KB |
— |
| Install total |
380 KB |
48.9 MB |
| Peak process RAM |
41.5 MB |
35.4 MB |
| Cold start to first result |
77 ms |
395 ms |
| Steady-state per line |
19.3 ms |
105 ms |
129× smaller on disk, 5× faster, 5× faster to cold start — and it uses more
total memory than Tesseract. The model is only ~9 MB of that; Python with
numpy and pillow costs 31.8 MB before a single weight loads. A C or Rust port
would remove that, but no such port has been measured, so it is not a claim.
Position encoding is fixed sinusoidal, computed at load and stored as zero
bytes. A learned embedding at 1024×192 float32 would cost 768 KB — more than
twice all the ternary weights combined.
Architecture
|
|
| Layers |
4 |
| Hidden size |
192 |
| Attention heads |
6 (head dim 32) |
| FFN intermediate |
384 |
| Patch grid |
32 × 8, one token per 8-pixel column |
| Ternary weights |
1,277,952 |
| Vocabulary |
256 slots, 95 printable ASCII |
| Weight quantization |
ternary {-1, 0, +1}, STE, per-tensor mean|w| scale |
| Activation quantization |
8-bit absmax, dequantized |
| Decoder |
CTC prefix beam search, width 4 |
Inference is engine.py, NumPy only, no framework. Weights are ternary, so an
integer SIMD or microcontroller port needs only additions and subtractions. On a
CPU the same arithmetic runs through BLAS, which is not literally
multiplication-free — the property belongs to the weights, not to this runtime.
engine.py is checked against the PyTorch training module on shared weights and
must agree to float32 precision. That test exists because the two once silently
computed different functions.
Quickstart
from PIL import Image
from engine import FastReflexOCREngine
engine = FastReflexOCREngine.from_pretrained(
"psikosen/t-rsi-ocr",
lm_name="char_lm_3gram.npz", # omit for greedy CTC
beam_width=4,
)
text, latency_us, ram_kb = engine.recognize(Image.open("line.png"))
print(f"{text!r} ({latency_us:.0f} us)")
Higher accuracy, more passes:
from data_generator_v2 import stretch_width
from vote import rover
engine.input_stretch = 1.0 # variants carry their own stretch
factors = [1.05, 1.15, 1.25, 1.35, 1.45]
weights = [0.8, 1.2, 1.6, 1.4, 0.8]
hyps = [engine.recognize(stretch_width(img, f))[0] for f in factors]
text = rover(hyps, weights=weights, null_penalty=0.85)
Two things that matter more than the model
1. Most documents need no OCR at all. DOCX files and digital PDFs already
contain their exact text. Reading it takes 4.8 ms per page against
Tesseract's 2,038 ms for the same page — 425× faster and exact rather than 97%
accurate. Rasterizing a text layer and guessing it back loses table structure and
turns 1999.00 into 999.00. Check for a text layer before reaching for any
recognizer.
2. This is a line recognizer, not a page reader. It needs pre-cropped lines.
Tesseract does layout analysis and recognition in one pass; this does not, and no
line detector ships with it. A per-line speed comparison assumes the crops
already exist.
Training
python3 train_v2.py --epochs 70 --pool 50000 --real 25000 \
--layers 4 --hidden 192 --heads 6 --guide-weight 0.5 --out model_v4
49,022 synthetic lines across 111 screened system fonts, mixed with 19,816 real
lines cropped from PDFs. Both matter: a synthetic-only model scored 81% on
held-out fonts and 16% on real documents — it had learned the renderer.
Augmentation covers blur, sensor speckle, non-uniform lighting, ±5° skew,
exposure drift, and horizontal scale jitter (1.0–1.4×). That last one is what
makes input_stretch work at inference: CTC cannot emit more characters than it
has frames, real lines average 1.29 frames per character, and stretching to 1.69
is worth +14.9 points of exact-line match. Applied to a model trained without the
jitter, the same trick is a net loss.
Training uses an attention decoder as an auxiliary teacher (GTC,
arXiv:2002.01276), discarded at export — it
costs zero bytes and zero milliseconds at inference.
Limitations
- Line recognizer only. No page layout, no line segmentation, no table
structure.
- 95 printable ASCII characters. No accents, no CJK, no mathematical
notation, no typographic quotes.
- Exact-line match trails Tesseract on clean documents, 73.7% against 85.7%
single-pass. Voting narrows it to 86.5% against 87.9% at equal speed.
- Uses more total RAM than Tesseract (41.5 MB against 35.4 MB), because of
the Python runtime rather than the model.
- Alphanumeric identifiers are weak (80.0%), as they are for every engine
measured.
- Evaluated on one machine. Independent replication welcome.
What was tried and did not work
Recorded so it is not rebuilt from the same reasoning.
| approach |
result |
| Patch width 4 (more CTC frames) |
92.2% against patch-8's 97.8%; each token sees too little context |
| Dictionary word repair |
built three times; detection works (greedy-vs-beam disagreement is an 11.6× error signal), correction does not |
| Larger LM corpus |
saturated at 4,933 of 9,025 contexts |
| Caching ternary weights as float32 |
1.13× faster end to end, +4 MB RAM |
| Per-head attention |
no memory saved; CPython does not return freed memory to the OS |
Team C · experimental research release