Skip to content

Commit ab09dfd

Browse files
author
Ronald Tse
committed
feat: initial ml-models framework skeleton
Unified training framework for Interscript ML-powered maps. One repo trains every ML model (rababa_arabic, rababa_hebrew, secryst_thai_ipa, and future tasks). Each task is a config + data module; the framework is shared. Layout: - src/framework/: MECE abstractions (config, registry, data, model, trainer, evaluator, exporter, pipeline). Adding a new task = one directory under src/tasks/, zero framework edits. - src/tasks/{rababa_arabic,rababa_hebrew,secryst_thai_ipa}/: each has config.yaml + data.py + student.py + metrics.py. - src/cli.py: `interscript-ml {list,train,evaluate,export,publish}`. - tests/: 37 tests, all green (CPU-only; no torch required). - scripts/: setup_env.sh, fetch_data.sh, train.sh, export.sh, benchmark.sh, publish.sh. - docs/{architecture,model_card,training_log}.md. Design follows project conventions: OCP (registry plugins), MECE (each module owns one concern), DRY (epoch loop, edit-distance, ONNX export written once), no hand-rolled serialization, no require_relative equivalent (Python has no such concept; tasks register via decorator).
0 parents  commit ab09dfd

48 files changed

Lines changed: 3301 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitattributes‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*.onnx filter=lfs diff=lfs merge=lfs -text
2+
*.safetensors filter=lfs diff=lfs merge=lfs -text
3+
*.ckpt filter=lfs diff=lfs merge=lfs -text
4+
*.pt filter=lfs diff=lfs merge=lfs -text
5+
*.bin filter=lfs diff=lfs merge=lfs -text

‎.gitignore‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.egg-info/
5+
.pytest_cache/
6+
.mypy_cache/
7+
.ruff_cache/
8+
.venv/
9+
venv/
10+
build/
11+
dist/
12+
13+
# Data (fetched via scripts/fetch_data.sh)
14+
data/raw/
15+
data/processed/
16+
data/augmented/
17+
18+
# Model artifacts (Git-LFS tracked if committed at all)
19+
models/*.onnx
20+
models/*.safetensors
21+
models/*.ckpt
22+
*.tmp
23+
24+
# Notebooks
25+
.ipynb_checkpoints/
26+
27+
# OS
28+
.DS_Store
29+
Thumbs.db
30+
31+
# Secrets
32+
.env
33+
.env.local
34+
hf_token.txt

‎README.md‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# interscript/ml-models
2+
3+
Unified training framework for Interscript ML-powered maps.
4+
5+
**Status:** skeleton (P0 in TODO.rababa/10). Framework abstractions
6+
implemented and tested. Task data modules + configs ready; full
7+
training requires GPU + dataset fetch.
8+
9+
## What this is
10+
11+
One training repo for every ML map in Interscript:
12+
13+
- **rababa_arabic** — Arabic diacritization (adds harakat)
14+
- **rababa_hebrew** — Hebrew diacritization (adds nikud)
15+
- **secryst_thai_ipa** — Thai → IPA transliteration
16+
17+
Each task is a **config + data module**. The training framework is
18+
shared. Adding a new transliteration pair (Khmer → IPA, Japanese →
19+
Romaji) is one new directory under `src/tasks/` — zero edits to
20+
framework code.
21+
22+
## Architecture
23+
24+
```
25+
src/
26+
├── framework/ # SHARED abstractions (MECE)
27+
│ ├── config.py # TaskConfig loaded from YAML
28+
│ ├── registry.py # Plugin registry (OCP)
29+
│ ├── data.py # DataModule ABC
30+
│ ├── model.py # ModelModule ABC (teacher + student)
31+
│ ├── trainer.py # BaseTrainer + FineTune + Distill (DRY)
32+
│ ├── evaluator.py # BaseEvaluator + edit_distance + DER/PER utils
33+
│ ├── exporter.py # OnnxExporter ABC
34+
│ └── pipeline.py # TrainingPipeline orchestrator
35+
├── tasks/
36+
│ ├── rababa_arabic/ # config.yaml + data.py + student.py + metrics.py
37+
│ ├── rababa_hebrew/
38+
│ └── secryst_thai_ipa/
39+
└── cli.py # python -m src.cli train --task rababa_arabic
40+
```
41+
42+
## Design principles (project conventions)
43+
44+
- **OCP** — adding a task = one new directory. Adding a metric, model
45+
architecture, or data source = one new subclass + `@register_*`
46+
decorator. Framework code is never edited.
47+
- **MECE** — each module owns one concern. Data has no knowledge of
48+
model architecture. Model has no knowledge of trainer. Trainer has
49+
no knowledge of evaluator.
50+
- **DRY** — the epoch loop, edit-distance math, and ONNX export
51+
wrapper are written once.
52+
- **Model-driven, semantically-driven** — class names mirror domain
53+
concepts (`RababaArabicData`, `DEREvaluator`, `SecrystThaiIpaStudent`).
54+
- **Performance** — frozen dataclasses for config; lazy imports for
55+
torch so framework tests run without GPU deps.
56+
57+
## Quick start
58+
59+
```bash
60+
scripts/setup_env.sh # creates .venv, installs deps
61+
scripts/fetch_data.sh # fetch raw datasets (set env vars first)
62+
scripts/train.sh rababa_arabic # full training pipeline
63+
scripts/export.sh rababa_arabic # export student to ONNX
64+
scripts/publish.sh rababa_arabic # upload to HuggingFace Hub
65+
```
66+
67+
Or via the CLI directly:
68+
69+
```bash
70+
python -m src.cli list
71+
python -m src.cli train --task rababa_arabic --data-root data --out-root models
72+
python -m src.cli evaluate --task secryst_thai_ipa
73+
python -m src.cli export --task rababa_hebrew
74+
```
75+
76+
## Adding a new task
77+
78+
1. Create `src/tasks/<name>/config.yaml` (copy from an existing task).
79+
2. Create `src/tasks/<name>/data.py` extending `DataModule`, decorated
80+
with `@register_data_module("<name>_data")`.
81+
3. Create `src/tasks/<name>/student.py` extending `ModelModule`,
82+
decorated with `@register_model_module("<name>_student")`.
83+
4. Create `src/tasks/<name>/metrics.py` extending `BaseEvaluator`,
84+
decorated with `@register_evaluator("<metric>")`.
85+
5. Run `python -m src.cli train --task <name>`.
86+
87+
That's it. No framework edits.
88+
89+
## Tests
90+
91+
```bash
92+
pytest -v
93+
```
94+
95+
Framework tests run without torch (CPU-only, fast). Training and ONNX
96+
export tests are gated behind `@pytest.mark.gpu` and require the
97+
`[train]` and `[export]` extras.
98+
99+
## License
100+
101+
MIT. Model weights are released under their own licenses (see
102+
`docs/model_card.md` per task).

‎configs/defaults.yaml‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Default training hyperparameters. Override per-task in config.yaml.
2+
3+
default:
4+
optimizer: adamw
5+
scheduler: cosine
6+
warmup_ratio: 0.03
7+
fp16: true
8+
seed: 42
9+
log_every: 50
10+
save_every: 1000
11+
grad_clip: 1.0
12+
13+
distillation:
14+
# Mix of cross-entropy (gold labels) and KL divergence (teacher logits)
15+
alpha: 0.5 # weight on CE; (1-alpha) on KL
16+
temperature: 4.0 # softmax temperature for KL
17+
18+
finetune:
19+
# LoRA config for teacher fine-tune
20+
lora_r: 16
21+
lora_alpha: 32
22+
lora_dropout: 0.05
23+
target_modules:
24+
- q_proj
25+
- v_proj
26+
- k_proj
27+
- o_proj

‎docs/architecture.md‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Architecture decision record: LLM teacher + distilled student
2+
3+
## Context
4+
5+
The legacy rababa model used a Tacotron CBHG encoder-decoder with
6+
attention. Tacotron is a speech synthesis architecture repurposed for
7+
text diacritization. Quantizing it (the original "make it small"
8+
proposal) makes a bad architecture smaller but not better.
9+
10+
## Decision
11+
12+
Replace Tacotron with a two-stage training pipeline:
13+
14+
1. **Teacher**: Qwen3.5-4B-Instruct fine-tuned via LoRA on Tashkeela++.
15+
The LLM already has strong Arabic language understanding from
16+
pretraining; fine-tuning teaches it the diacritization task.
17+
2. **Student**: A 6M-parameter character-level transformer distilled
18+
from the teacher. Small enough to ship in the browser (~6MB ONNX).
19+
20+
## Why this works
21+
22+
- LLMs already model Arabic morphotactics — diacritization is a
23+
downstream capability, not a from-scratch learning problem.
24+
- Distillation transfers the teacher's softened output distribution
25+
to the student. The student learns richer signal than gold labels
26+
alone (the "dark knowledge" of near-misses).
27+
- Character-level (not subword) is the right granularity for
28+
diacritization: harakat attach to single characters, and a 6MB model
29+
is fast enough for in-browser inference.
30+
31+
## Alternatives considered
32+
33+
- **Quantized Tacotron.** Rejected: makes a bad model smaller, not
34+
better. DER barely improves; latency barely drops.
35+
- **ByT5-small fine-tune as teacher.** Plausible fallback if Qwen3.5
36+
proves too large. ByT5 is character-native, smaller, and already
37+
proven for text-infilling tasks. Kept as a backup in `models/`.
38+
- **Distillation from a speech model (Nemotron, Whisper).** Rejected:
39+
those models output speech features, not text. We'd be converting
40+
between modalities unnecessarily.
41+
42+
## Risks
43+
44+
- LLM teacher may hallucinate plausible-but-wrong diacritization on
45+
out-of-domain text. Mitigated by training-data augmentation with
46+
adversarial examples.
47+
- Student may underfit if teacher logits collapse to one-hot.
48+
Mitigated by KL temperature = 4.0 + alpha = 0.5 (equal CE and KL).

‎docs/model_card.md‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Model Card: rababa_arabic
2+
3+
## Model details
4+
5+
- **Name:** `interscript/rababa_arabic`
6+
- **Type:** Character-level transformer (4 layers, 256 dim, 4 heads)
7+
- **Trained from:** distilled from Qwen3.5-4B-Instruct teacher
8+
- **License:** MIT (code), CC-BY-4.0 (model weights)
9+
10+
## Training data
11+
12+
- Tashkeela++ (~2M verses of diacritized Arabic)
13+
- OpenDiacritizer (augmentation set)
14+
- Synthetic labels generated by fine-tuned Qwen3.5-4B teacher
15+
16+
## Intended use
17+
18+
- Diacritization of Modern Standard Arabic text
19+
- Inference in browser via ONNX runtime (~6MB)
20+
- Powering the `var-ara-Arab-Arab-rababa` map in interscript.org
21+
22+
## Out of scope
23+
24+
- Dialectal Arabic (Egyptian, Gulf, Levantine) — not in training data
25+
- Classical Arabic poetry — different conventions
26+
27+
## Metrics
28+
29+
- DER (Diacritic Error Rate) on held-out set
30+
- Target: ≤ 5%
31+
- p95 inference latency: ≤ 30ms per word
32+
33+
## Ethical considerations
34+
35+
This model reproduces biases in its training data. Tashkeela++ is
36+
heavily Quranic + religious text; diacritization choices on secular
37+
text may differ from speaker expectations.
38+
39+
## How to cite
40+
41+
```
42+
@software{interscript_rababa_arabic,
43+
title = {Interscript rababa_arabic diacritization model},
44+
url = {https://huggingface.co/interscript/rababa_arabic}
45+
}
46+
```

‎docs/training_log.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Training log
2+
3+
Append new entries at the bottom. Format:
4+
5+
```
6+
## YYYY-MM-DD — task_name — notes
7+
- params: {key: value}
8+
- DER/PER: x.xx
9+
- wall time: Xh
10+
```
11+
12+
## 2026-08-01 — framework skeleton committed
13+
14+
- Initial framework: config, registry, data, model, trainer,
15+
evaluator, exporter, pipeline.
16+
- Task packages: rababa_arabic, rababa_hebrew, secryst_thai_ipa.
17+
- CPU-only unit tests pass. No GPU training yet.

‎pyproject.toml‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
[build-system]
2+
requires = ["setuptools>=68", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "interscript-ml-models"
7+
version = "0.1.0"
8+
description = "Unified training framework for Interscript ML models (rababa + secryst)"
9+
readme = "README.md"
10+
license = { text = "MIT" }
11+
requires-python = ">=3.10"
12+
authors = [{ name = "Interscript Project" }]
13+
keywords = ["nlp", "transliteration", "diacritization", "onnx", "distillation"]
14+
15+
dependencies = [
16+
"numpy>=1.26",
17+
"pyyaml>=6.0",
18+
"tqdm>=4.66",
19+
"pydantic>=2.5",
20+
]
21+
22+
[project.optional-dependencies]
23+
train = [
24+
"torch>=2.2",
25+
"transformers>=4.42",
26+
"datasets>=2.18",
27+
"accelerate>=0.27",
28+
"peft>=0.10",
29+
"sentencepiece>=0.2",
30+
]
31+
export = [
32+
"onnx>=1.16",
33+
"onnxruntime>=1.17",
34+
]
35+
publish = [
36+
"huggingface_hub>=0.23",
37+
"omegaconf>=2.3",
38+
]
39+
dev = [
40+
"pytest>=8.0",
41+
"pytest-cov>=5.0",
42+
"ruff>=0.4",
43+
"mypy>=1.10",
44+
]
45+
46+
[project.scripts]
47+
interscript-ml = "src.cli:main"
48+
49+
[tool.setuptools.packages.find]
50+
where = ["src"]
51+
include = ["framework*", "tasks*"]
52+
53+
[tool.pytest.ini_options]
54+
testpaths = ["tests"]
55+
addopts = "-ra -q --strict-markers"
56+
markers = [
57+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
58+
"gpu: marks tests requiring a GPU",
59+
]
60+
61+
[tool.ruff]
62+
line-length = 100
63+
target-version = "py310"
64+
65+
[tool.ruff.lint]
66+
select = ["E", "F", "I", "B", "UP", "SIM", "C4"]
67+
68+
[tool.mypy]
69+
python_version = "3.10"
70+
strict = true
71+
warn_unused_configs = true

‎scripts/benchmark.sh‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bash
2+
# Run all benchmarks on a trained model.
3+
set -euo pipefail
4+
5+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
6+
cd "$ROOT"
7+
8+
TASK="${1:-}"
9+
if [[ -z "$TASK" ]]; then
10+
echo "Usage: $0 <task_name>" >&2
11+
exit 1
12+
fi
13+
14+
# shellcheck disable=SC1091
15+
source .venv/bin/activate 2>/dev/null || true
16+
17+
python -m src.cli evaluate --task "$TASK" --data-root data --out-root "models/$TASK"

‎scripts/export.sh‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bash
2+
# Export a trained task to ONNX. Usage: scripts/export.sh <task_name>
3+
set -euo pipefail
4+
5+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
6+
cd "$ROOT"
7+
8+
TASK="${1:-}"
9+
if [[ -z "$TASK" ]]; then
10+
echo "Usage: $0 <task_name>" >&2
11+
exit 1
12+
fi
13+
14+
# shellcheck disable=SC1091
15+
source .venv/bin/activate 2>/dev/null || true
16+
17+
python -m src.cli export --task "$TASK" --data-root data --out-root "models/$TASK"

0 commit comments

Comments
 (0)