Feraoun-36M

A 36.3M-parameter Vision-Encoder-Decoder that reads a line of printed Kabyle (Taqbaylit, kab) and writes it out as text — in the Latin orthography and in Neo-Tifinagh, from one model.

It exists because the Kabyle written record is on paper. Novels, grammars, periodicals and the archive scans that carry a century of prose are images, and no OCR system had been trained on the language: the emphatic consonants ḍ ḥ ṛ ṣ ṭ ẓ are a base letter plus a dot that general-purpose engines drop or normalise away, and dropping them changes the word.

Results

1,000 held-out lines, in text the model never read and typefaces it never saw, drawn with a fixed seed from beyond the 80,000-sentence prefix training consumed.

Character error rate 2.85%
Word error rate 13.20%
Line exact match 70.20%
Diacritic F1, positional 0.8538 (P 0.8416 / R 0.8664, over 1,355 gold glyphs)

Reproduce it without a GPU, in about six minutes on a laptop:

make ocr TASK=evaluate LINES=1000

Tifinagh is read as well as Latin. On a second 1,000-line draw, half of it Neo-Tifinagh from the agbalu/KabTifinagh test split, the model returns CER 1.64%, WER 7.12%, 79.50% line exact match (make ocr TASK=evaluate LINES=1000 RATIO=0.5).

Those are lower numbers and not a better result. Tifinagh writes no sub-dots and no capitals, so half of that draw cannot produce the two error classes that dominate the Latin condition, and its lines are shorter. Read it as evidence that the Tifinagh half works — ⵜⴰⵇⴱⴰⵢⵍⵉⵜ ⴷ ⵜⵓⵜⵍⴰⵢⵜ ⵜⴰⵢⴻⵎⵎⴰⵜ ⵏⵏⴻⵖ. comes back exact — not as a lower error rate on the same task. The Latin row is the headline.

Four things the headline table does not say on its own.

The diacritic score is positional, not a count. A hypothesis holding the right number of in the wrong places scores 1.0 under a bag-of-characters comparison and 0.0 here: the metric aligns hypothesis to reference by edit distance and credits a glyph only where it lands on its own position. The distinction is not academic — it is worth several points on this model, and the count-based figure should not be compared with this one.

Word error rate is 4.6× the character rate, and that ratio is the behaviour. Most failures are one character inside an otherwise correct word — a lost space, a capital the model supplies where the source has none — so a 2.85% character rate still fails 13.2% of words and 29.8% of lines. Azul, ansuf yis-m! comes back as Azul, Ansuf yi s-m!: two character edits, three word edits of three, one line failed.

Every typeface in the table is one the model has never seen. Training renders in DejaVu, FreeFont, Liberation and Noto; the evaluation renders in Times New Roman, Times, Arial, Helvetica, Georgia, Baskerville, Courier New and Didot. The numbers are therefore a generalisation result across letterforms, not a memorisation one — and they are conservative for it.

Sub-dot accuracy depends on which of those eight it is. In Times New Roman and Arial the model returns Aḍris n uḥric ɣef tɛeṛṛamt d uẓekka. exactly. In a high-contrast display face with hairline serifs the same line loses dots — tɛeṛṛamt becomes tzeṛṛamt, uḥric becomes uɣric. 0.8538 is the average over all eight. On a clean 300 DPI scan in a book face, expect better; on a photocopy of a display-set title page, expect worse.

The training log's 2.65% CER is a different statistic and is not comparable. It is measured over at most sixteen batches — about 512 lines — of a split held out from the same prefix the model trained on. It selects checkpoints; the table above is the benchmark.

Intended use

Turning printed Kabyle into text: scanned novels, grammars, periodicals and archive material, in the Latin orthography or in Neo-Tifinagh, from the same model and without telling it which it is looking at.

A line or a page, not a word. Every input is scaled to 52 px of usable height, so a single word blown up to that height is nothing the model has seen.

Not suitable for: handwriting, which is absent from the training data entirely; multi-column layout, which transcribe_page reads straight across; any decision about a person; or any language other than Kabyle. Tarifit, Tashelhit and Central Atlas Tamazight share part of this orthography and are not evaluated here.

Usage

transformers, torch, pillow and numpy, nothing else. The architecture is not one of the library's own, so the modelling code travels in this repository and trust_remote_code=True is what loads it.

from PIL import Image
from transformers import AutoModel, AutoTokenizer

REPO = "agbalu/Feraoun-36M"
tokenizer = AutoTokenizer.from_pretrained(REPO, trust_remote_code=True)
model = AutoModel.from_pretrained(REPO, trust_remote_code=True).eval()

model.transcribe([Image.open("line.png")], tokenizer)
# ['Aḍris n uḥric ɣef tɛeṛṛamt d uẓekka.']

print(model.transcribe_page(Image.open("page.png"), tokenizer))   # one line per text band

transcribe does the preprocessing as well as the decoding, and that matters: the model was fitted on lines scaled to 52 px of usable height on a 64 × 512 canvas, so a caller who builds the tensor themselves at another scale is measuring a different model. prepare_line_image and segment_page_into_lines are exported from the same module if you need the pieces.

Feed it a line, or a page — not a cropped word. A single word blown up to 52 px is nothing the model has seen.

Every example above was executed against the published directory before being written down, with this repository absent from the import path. Decoding is greedy and free-running: there is no beam search and no key-value cache, so every step re-reads the whole prefix, which is what the published evaluation did and what makes the two agree. A few hundred milliseconds per line on CPU; nothing here needs a GPU.

Architecture

Parameters 36,291,840
Encoder DeiT, 12 layers, 384 hidden, patch 16 over a 384 × 384 field
Encoder initialisation microsoft/trocr-small-stage1
Decoder 6 layers, 6 heads, 1,536 feed-forward, GELU, pre-norm
Positions sinusoidal, 256 maximum
Vocabulary 171 characters — Latin, Kabyle extended and sub-dot, accented Latin, digits, punctuation, 31 Tifinagh
Output projection 171 × 384, untied
Optimiser AdamW, weight decay 0.01 on non-norm parameters
Schedule cosine to 5% of peak, 5% warmup, peak 1.5e-4
Best checkpoint step 6,500 of epoch 3

A 64 × 512 line canvas, resampled to the encoder's square input. 52 px of usable height is what keeps a sub-dot at four to six distinct dark pixels instead of the one or two it occupies at 32 px. extract_features then interpolates that canvas to the backbone's own 384 × 384 — 576 patches plus the class and distillation tokens, 578 memory positions. The 8:1 line aspect ratio is lost at that interpolation, and it is the first thing to change if the decoder is ever memory-bound.

A character alphabet, not subwords. A subword tokenizer completes vocabulary words where the ink is broken, which is precisely the failure an OCR system must not have: it produces fluent Kabyle that is not what the page says. 171 classes put the logit surface at 0.25 MB and make the character error rate a count of this table's own symbols.

Training data

Lines are rendered, not scanned. There is no page of real Kabyle print with character-level ground truth to train against, so the corpus is text set in the serif and sans faces of DejaVu, FreeFont, Liberation and Noto — plus Noto Sans Tifinagh, which no distribution font package carries and which ships in the training repository — and put through a degradation pipeline: skew up to ±2°, Gaussian defocus, sensor grain, ink bleed and ribbon fade, photocopier exposure jitter, aged-paper tint, spine-gutter shadow, and bleed-through from the reverse side.

The Latin text is one source. Training read the first 80,000 sentences of AƔBALU-Text v1, and that prefix is 100% hf.abdelhaqueidali.kab-latn-tfng, CC-BY-2.0 — a single sentence-level dataset, not the 42-source corpus. The Tifinagh half is the agbalu/KabTifinagh train split. The held-out lines in the results table come from the same source, further down the same file: they are text the model has not read, in a register it has. Nothing here measures generalisation to a different kind of Kabyle prose, and a novel's long sentences and a newspaper's headlines are both outside what was tested.

Training recipe

Corpus 80,000 rendered lines, 50/50 Latin and Neo-Tifinagh
Split 95/5, drawn at a fixed seed over the whole set rather than off the end
Epochs 3
Batch 32 lines
Optimiser AdamW, weight decay 0.01 on non-norm parameters
Schedule cosine to 5% of peak, 5% warmup, peak 1.5e-4
Gradient clipping 1.0, checked before the step rather than after
Selection best character error rate on the held-out split
Hardware one A10G

Limitations

ţ (U+0163) has no slot in the vocabulary, and it is real Kabyle. docs/orthography.md attests it 21,058 times in this project's own corpus. It encodes as <unk> and cannot be produced: Aţan yeţţaḍsa, ţ-ţaqbaylit. comes back as Aṭan yeaḍsa, -aqbaylit. in every typeface tested. Adding the letter changes the width of the output projection, so it costs a retrain, and until then any page using that convention is unrecoverable at those positions.

Casing and spacing are the dominant error class, not the diacritics. Word-initial capitals are inserted where the source has none, and word boundaries are split or merged.

Page layout is a projection profile, nothing more. transcribe_page finds horizontal ink bands and crops them. There is no column detection, no reading-order model and no table or figure handling, so a two-column periodical is read straight across.

Files

file contents
model.safetensors 314 tensors, 36,291,840 parameters
config.json the architecture, and the auto_map that points at the code below
configuration_feraoun.py, modeling_feraoun.py the architecture and the preprocessing in code, importing only torch, transformers, pillow and numpy
tokenizer.json, tokenizer_config.json the 171-symbol table, id for id with the one the model was trained on
feraoun-v1-heldout.json the evaluation behind the results table: every hypothesis's conditions, the seed, and the typefaces it was rendered in
export.stats.json what the source checkpoint held, and what the export dropped

pos_encoder.pe is in model.safetensors although it is derived. It is a registered buffer, so from_pretrained allocates it empty and fills it from the file; dropped as derived it would come back as uninitialised memory and rotate every position by a garbage angle without raising.

The source checkpoint holds the weights, the optimizer, the scheduler, the step, the epoch and the best CER. export.stats.json records that, and records the optimizer and scheduler as dropped — 435 MB down to 146 MB. Training cannot be resumed from the published files.

Reproduction

make modal-upload TASK=ocr             # corpus and Tifinagh split onto the data volume
make modal-ocr TASK=smoke              # 20 steps on a real GPU before anything is paid for
make modal-ocr TASK=train RATIO=0.5 LINES=80000 EPOCHS=3 LR=1.5e-4
make modal-ocr-pull                    # the checkpoint back into artifacts/runs/
make ocr TASK=evaluate LINES=1000      # the results table, on CPU

The name

Mouloud Feraoun (1913–1962), the schoolteacher from Tizi Hibel who wrote Le Fils du pauvre and put Si Mohand's oral poetry onto the printed page — the direction this model reverses. He was assassinated by the OAS on 15 March 1962, three days before the Évian Accords were signed. The naming is homage and carries no endorsement.

Citation

@software{agbalu_feraoun_2026,
  title  = {Feraoun-36M: dual-script document OCR for Kabyle},
  author = {AƔBALU},
  year   = {2026},
  url    = {https://huggingface.co/agbalu/Feraoun-36M},
  note   = {36,291,840 parameters; CER 2.85% over 1,000 held-out rendered lines}
}

Licence

Apache-2.0 on the weights and the code. The training text is CC-BY-2.0 and a permissive grant on weights makes no claim about the text behind them, so the composition is stated above rather than assumed. The Tifinagh half of the training corpus is agbalu/KabTifinagh; read its licence before redistributing derivatives.

Downloads last month
17
Safetensors
Model size
36.4M params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Evaluation results

  • Character error rate on 1,000 held-out lines, rendered
    self-reported
    0.029
  • Word error rate on 1,000 held-out lines, rendered
    self-reported
    0.132
  • Line exact match on 1,000 held-out lines, rendered
    self-reported
    0.702