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.