Tibetan Script Classification — Model Training

This project sorts Tibetan script styles from page images. Instead of training one big model to tell all the styles apart at once, I built a step-by-step pipeline: a simple model handles the easy cases, and the hard, look-alike cases get passed to smaller models that focus on just two styles each. Each step was added because the step before it ran into a wall. The base model for everything is DINOv3, which I picked after testing it against other models.

The Data

The training images come from the script classification tools we built ourselves. The tools pull manuscript and printed pages from the Buddhist Digital Resource Center (BDRC), and our annotation team then labelled each page by hand with its script style. So every label is human-checked, not auto-generated. I took that labelled data, split it into training and validation sets, and trained the models below on it.

Evaluation Benchmark

Every number in this post is measured on one fixed test set, so all the models can be compared fairly. The benchmark has 360 real BDRC pages — 60 pages for each of the six script families. It is also balanced at the subclass level, meaning each style’s sub-styles get an equal share of those 60 pages instead of one common sub-style taking over. This set is only used for testing, never for training, so the scores show how well the models handle pages they have never seen.

Class Images
Danyig 60
Druma 60
Gyuyig 60
Pedri 60
Tsugdri 60
Uchen 60

Parquet schema

Column Type Description
id string BDRC page id
image_bytes binary JPEG/PNG page image
script_type string One of the six script families (parent)
script_name string Sub-script name (e.g. Uchen Sugthung)
source string bdrc (real scans only)
font_name string Empty for BDRC scans

Benchmark: https://huggingface.co/datasets/BDRC/script-classification-benchmark-balance-subclass

Picking the Backbone

A backbone is the part of the model that turns an image into numbers the classifier can read. To find the best one, I froze each candidate (kept its weights fixed), ran all the images through it, and trained a simple classifier on top. This is a cheap, fair way to compare them on the same data. DINOv3 came out on top, so I used it for everything after this.

Model Params Accuracy Macro-F1
DINOv3 ViT-S/14 21M 47.2% 0.412
DINOv2 ViT-B/14 86M 45.7% 0.392
SigLIP 2 Base 86M ~46% ~0.40
DINOv2 ViT-S/14 21M 44.3% 0.385
ConvNeXt-Base 88M 43.2% 0.374
EfficientNet-B0 5.3M 42.1% 0.353
ResNet-50 25M 41.7% 0.347
CLIP ViT-B/32 63M 39.3% 0.283

Step 1 — 18 styles at once (didn’t work)

My first try was to tell all 18 styles apart directly. I dropped the rest of the styles because we didn’t have enough labelled data for them yet. It still came out weak (~57–60% accuracy). That failure pointed me toward the main idea of the project: cut the problem down to the styles that have enough data and are actually different enough to tell apart.
Model: openpecha/tibetan-script-classifier · Hugging Face

Step 2 — A simple test: Uchen vs Umê

Before narrowing the full task, I wanted to check something basic: could the model do well on an easy version of the problem at all? So I trained it on the simplest split there is — Uchen (printed, with head strokes) versus Umê (cursive, no head strokes). The big lesson here was that the image processing at test time has to match the processing used during training. When it matched, the model reached 99.3% accuracy; when it didn’t, accuracy dropped to 80.7%.

Variant Test acc Test macro-F1
center_crop_all 99.3% 0.983
without_preprocess 80.7% 0.708

This worked really well, which told me the DINOv3 setup was solid. So I kept the same approach and went back to the harder, multi-style task — just with a smaller, cleaner set of styles.
Model: openpecha/uchen-ume-classifier · Hugging Face

Step 3 — 6 styles (~81%)

For the six-style model, the tricky part was that some styles had far more pages than others. I tried three fixes — resampling the data, adding synthetic pages, and keeping the real (uneven) data but telling the model to pay more attention to the rare styles. The first one one worked best. The winning model,hit 80.4% accuracy.

Checkpoint Accuracy Macro-F1
Final Model 80.0% 0.792
imbalanced Model 78.3% 0.772
balanced final_model.pt 71.7% 0.647
bclass final_model.pt 64.2% 0.617

Model: https://huggingface.co/BDRC/6-class-tibetan-script-classifier

Step 4 — Merge the look-alikes (~94%)

The six-style model kept mixing up two pairs of styles: Danyig with Pedri, and Tsugdri with Gyuyig. Instead of forcing it to separate them, I merged each pair into one group and kept Druma and Uchen on their own. Now the model only had four things to choose between, and mixing up styles inside the same group no longer counted as a mistake. I started from the six-style model and retrained it. This jumped accuracy way up.

Dataset Accuracy Macro-F1 N
Validation (real) 94.6% 0.944 2,581
Benchmark (real BDRC) 92.1% 0.921 240

Model:https://huggingface.co/BDRC/4-class-balanced-script-classifier

Step 5 — Split the merged groups back apart

Merging the look-alikes made the main model strong, but I still needed the exact style. So I trained two small models, each focused on just one pair: Danyig vs Pedri, and Gyuyig vs Tsugdri. When the main model says a page is in one of the merged groups, I pass it to the matching small model to get the final answer.

Classifier n Accuracy Macro-F1 AUC-ROC
Danyig vs Pedri 120 85% 0.849 0.914
Gyuyig vs Tsugdri 120 85% 0.848 0.930

Model1: BDRC/danyig-pedri-binary-script-classifier
Model2: BDRC/gyuyig-tsugdri-binary-script-classifier · Hugging Face

Credits

Developed by Dharmaduta based on specifications from the Buddhist Digital Resource Center for the project The BDRC Etext Corpus, funded by the Khyentse Foundation.