--- license: apache-2.0 language: - hi - en pipeline_tag: translation tags: - transliteration - hindi - devanagari - romanized-hindi - on-device - browser - javascript - sentence-level - language-model - tiny datasets: - ai4bharat/IndicCorpV2 - ai4bharat/Aksharantar metrics: - accuracy - cer - wer model-index: - name: AksharaLP-Sentence-2.8M results: - task: type: translation name: Romanized Hindi → Devanagari transliteration (sentence level) dataset: type: google/dakshina name: Dakshina (Hindi romanized sentences) split: test metrics: - type: accuracy name: Word accuracy value: 89.44 - type: exact_match name: Sentence exact match value: 26.73 - type: cer name: CER value: 3.93 - type: wer name: WER value: 10.78 ---

AksharaLP Sentence — sentences, not words. 10.85 MB, on-device.

Live demo Complete download size Add-on download size Language model parameters Word model parameters On-device License

# AksharaLP-Sentence-2.8M

It doesn’t guess one word at a time. It fixes the whole sentence.

Romanized Hindi → Devanagari, decided sentence by sentence, in 10.85 MB, on the device.
A word model proposes the spellings; a 2.77M-parameter neural Hindi language model picks between them jointly.

--- **The one-line pitch.** Hindi typed in English letters usually goes wrong at the sentence level, not the word level: *hu* should be हूं here and हुआ there, *hai* should be है or हैं, *pata* should be पता or पाता. AksharaLP Sentence keeps the word model's proposals and adds a tiny neural language model that reads the whole sentence before choosing — and it is still small enough to ship inside a web page.

Live demo: the same sentence typed, then decoded by word mode and sentence mode

Recorded from the live demo: Boht ache h Bhai, mai ghr pe hu is typed, decoded by the word decoder, then re-decoded by the sentence decoder in a Web Worker in the browser tab. Both decoders are in this repository.

## The numbers, up front | | | | |:---:|:---:|:---:| | **26.73%**
whole sentences exactly right
Dakshina Hindi sentence test, 4,976 sentences | **+1.02 pts**
sentences right vs the word model
95% CI 0.40–1.65 · significant | **+0.42 pts**
words right vs the word model
95% CI 0.33–0.51 · significant | | **4.07 → 3.93**
CER on the same 4,976 sentences
WER 11.20 → 10.78 | **132,299**
output words audited
0 outside their own candidate list | **2.77M**
parameters in the sentence LM
+ 989K in the word model underneath |

Every number above was measured by us, in a single one-shot run of the shipped configuration. v2 and v3 were scored with the same harness on the same sentences; intervals are paired bootstrap over sentences (2,000 resamples, seed 0). Settings were chosen on validation and internal dev sets only.

## Same sentence, two decoders

Before and after: four sentences decoded by v2 word mode and v3 sentence mode

**Real outputs**, produced by running both decoders in this repository on the inputs below (the 21-sentence casual set in `cases/`, plus a few more of the same kind). Nothing here is illustrative: these are the exact strings the runtime returns. | You type | Word mode (v2) | Sentence mode (v3) | | |---|---|---|---| | `mai ghr pe hu` | मैं घर पे हुआ | **मैं घर पे हूं** | fixed | | `mai school me hu` | मैं स्कूल में हुआ | **मैं स्कूल में हूं** | fixed | | `mujhe nahi pata` | मुझे नहीं पाता | **मुझे नहीं पता** | fixed | | `kal milte hai` | कल मिलते है | **कल मिलते हैं** | fixed | | `hum ghr ja rhe hai` | हम घर जा रहे है | **हम घर जा रहे हैं** | fixed | | `yaar mai thak gya hu` | यार मैं तक गया हूं | **यार मैं थक गया हूं** | fixed | | `Boht ache h Bhai` | बोहत अच्छे है भाई | बोहत अच्छे है भाई | **not fixed** (see below) | | `boht accha laga` | बोहत अच्छा लगा | बोहत अच्छा लगा | **not fixed** (see below) | | `mein bazaar ja rahi hu` | में बाजार जा रही हूं | में बाजार जा रही हूं | **not fixed** (see below) | | `nahi pata` | नहीं पाता | नहीं पाता | **not fixed** (see below) | | `main ghar ja raha hoon` | मैं घर जा रहा हूं | मैं घर जा रहा हूं | already right | | `wo ladki school gayi thi` | वो लड़की स्कूल गई थी | वो लड़की स्कूल गई थी | already right | | `tum kya kar rahe ho` | तुम क्या कर रहे हो | तुम क्या कर रहे हो | already right | | `ye kitab bahut acchi hai` | ये किताब बहुत अच्छी है | ये किताब बहुत अच्छी है | already right | | `wo ghar me hai` | वो घर में है | वो घर में है | already right | Word mode = the v2 decoder (word model + dictionary pack + bigram Viterbi). Sentence mode = the same candidates re-scored by the neural LM (`search3`, beam 8). Both ran on the same machine, same files, same harness. `Boht` → बोहत is a channel/candidate problem, not a sentence problem: standard spelling is बहुत. ### The four cases it does not fix, and why | Input | What comes out | Why the sentence LM cannot help | |---|---|---| | `Boht ache h Bhai` | बोहत अच्छे है भाई | **बहुत is candidate #11** for *boht* — channel score −5.83, below the top-6 the LM is allowed to see (`nlm_k 6`, `nlm_margin 2`). The LM can only reorder what it is given, so the channel, not the LM, has to be fixed. | | `mein bazaar ja rahi hu` | में बाजार जा रही हूं | **मैं is candidate #2 at −2.14**, just outside `nlm_margin 2` (the LM is skipped for a slot when the top candidate leads by more than 2). हूं is right; the pronoun stays में. | | `boht accha laga` | बोहत अच्छा लगा | Same बहुत/बोहत channel problem as the first row. अच्छा is already correct here, so there is nothing else for the LM to change. | | `nahi pata` | नहीं पाता | **No context.** Two words, no auxiliary, no agreement: पाता is a perfectly good sentence ending in a bigger corpus than the two-word one. `mujhe nahi pata` — three words — *is* fixed to मुझे नहीं पता, which is exactly the point: the LM needs a sentence to have a sentence to read. | **And one it does not get credit for:** `kal` → कल. The older bigram LM already prefers कल (−1.03) to काल (−0.70) in every sentence we tried, so the neural LM has nothing left to do there. ## Measured results

Sentence exact match, v2 vs v3, across five suites

| Set | Sentences | Word % v2 → v3 | Sentence % v2 → v3 | Δ sentence (95% CI) | |---|---|---|---|---| | **Dakshina Hindi sentence test** (held-out) | 4,976 | 89.02 → **89.44** | 25.70 → **26.73** | **+1.02 [0.40, 1.65]** ✓ | | Context set (ambiguous words in context) | 66 | 94.89 → **97.02** | 86.36 → **90.91** | +4.55 [0.00, 10.61] (words +2.13 ✓) | | val600 (selection) | 600 | 88.80 → **89.35** | 23.83 → **26.33** | **+2.50 [0.67, 4.33]** ✓ | | val2 (selection-side) | 1,000 | 89.33 → **89.91** | 25.60 → **26.60** | +1.00 [−0.50, 2.50] | | devA (casual chat, tuning) | 488 | 92.37 → **93.82** | 84.22 → **87.30** | **+3.07 [1.23, 4.92]** ✓ | | devB (casual chat, report-only) | 480 | 91.54 → **92.22** | 82.50 → **83.96** | +1.46 [−0.42, 3.54] **not significant** | Word-side intervals on the test set: **+0.42 [0.33, 0.51]**; sentence-exact: **+1.02 [0.40, 1.65]**. CER 4.07 → 3.93, WER 11.20 → 10.78. Word accuracy on the casual sets is token accuracy and sentence accuracy is whole-sentence exact match, both after Unicode NFC normalisation. The test set was run once, after all settings were fixed. val600, val2 and devA are selection/tuning sets and are shown for completeness — their gains are real but they are not held-out. devB is the honest one: the sentence model gains 1.46 points of sentence accuracy there and that gain is not significant, which is why the card leads with the test-set number and not with devB. ### Word mode is unchanged, by construction | Benchmark | v2 | v3 | |---|---|---| | Aksharantar Hindi test (9,991 words), top-1 | 70.04 | **70.04** | | top-3 | 88.31 | **88.31** | | top-8 | 93.63 | **93.63** | The sentence model overrides only the search over an existing candidate lattice; candidate generation is the v2 code path, unmodified. These are the v2 numbers, not a re-run — that is the point. (Not re-run for v3 because there is nothing different to run.) ## What it changes

Per-pattern word accuracy change, v2 to v3, on devB

Gains and regressions on the 480-sentence casual-chat set (word accuracy per spelling pattern): | Improves | v2 → v3 | | Regresses | v2 → v3 | |---|---|---|---|---| | Word-final y / h (`y_end`) | 82.98 → **89.36** (+6.4) | | Extra h (`asp_add`) | 85.45 → 81.82 (−3.6) | | Missing nasal (`nasal`) | 86.00 → **90.00** (+4.0) | | Stretched letters (`repeat`) | 82.14 → 78.57 (−3.6) | | Missing h in kh/th/bh (`asp_drop`) | 71.19 → **74.58** (+3.4) | | Dropped or added a (`schwa`) | 97.44 → 94.87 (−2.6) | | Chat abbreviations (`abbr`) | 91.67 → **93.75** (+2.1) | | Long / short vowel spelling (`vlen`) | 96.43 → 94.64 (−1.8) | | Extra doubled letters (`gem_add`) | 98.39 → **100.00** (+1.6) | | English words in chat (`english`) | 98.39 → 96.77 (−1.6) | | Ambiguous short words (`homograph`) | 93.59 → **94.87** (+1.3) | | | | The LM is good exactly where a bigram cannot be: verb endings, auxiliaries and agreement across the whole sentence. It is slightly too eager to normalise chat spellings toward dictionary forms — an extra *h*, stretched letters, a dropped or added *a* — and those rows are the cost. They are shown because they are real. ## The guardrail > ### ✓ It reorders candidates. It never invents words. > Every Devanagari word in the output is a candidate the word model already produced **for that typed word**. The sentence search can only choose *between* those candidates. Punctuation, digits and kept-as-Latin tokens pass through untouched. Each result carries `guard: {ok, words, outside}`, and the demo re-checks it on every keystroke. > > Across every evaluation set — **132,299 output words — 0 fell outside their slot's candidate list.** That property is what makes this safe to ship: the sentence LM cannot hallucinate a spelling, drop a word, or reorder the sentence. Worst case, it picks a different candidate from the same short list the word model already offered. ## Size

Download size: 7.92 MB word-only, 10.85 MB with the sentence LM, 9.46 MB with the 4-bit LM

| Part | On disk | gzip -9 | |---|---|---| | Word model package (v2: `xlit.q4` + `params.json` + dictionary pack + runtime) | 16,259,506 B | **7,919,816 B (7.92 MB)** | | `nlm.q8` — sentence LM, 2,770,752 parameters, tokenizer embedded | 3,239,640 B | 2,921,848 B | | `nlm.mjs` — LM loader + inference | 8,430 B | 3,132 B | | `v3.mjs` — lattice beam search | 4,942 B | 2,115 B | | **Add-on total** | **3,253,012 B** | **2,927,095 B (2.93 MB)** | | **Complete sentence package** | **19,512,518 B** | **10,846,911 B (10.85 MB)** | | *Same system with the 4-bit LM* (`nlm.q4`) | 1,862,360 B | 1,537,849 B → **9.46 MB complete** | So: **10.85 MB complete with the 8-bit LM**, of which the word-only model is **7.92 MB**; **9.46 MB** if you take the 4-bit LM. (`nlm.q4e8` sits between the two at 2,184,860 B gz.) The 4-bit LM is within tuning noise and saves 1.39 MB: on the same tuning row (`w_nlm 0.3`, `margin 4`) it scores 89.45 val / 93.73 devA / 90.01 val2 against 89.44 / 93.82 / 89.95 for the 8-bit one, i.e. everything inside ±0.1 pt. The 8-bit LM ships because it is the file the final one-shot evaluation was run with. Every byte count above was measured with `du -b` and `gzip -9`. ## Why the LM is this big (and not bigger) The shipped LM is the Kaggle **S** model: 2,770,752 parameters, 21k steps, valid cross-entropy **4.188 nats/token** (4.758 nats/word). A larger **M** model (3,766,080 parameters, 4.063 nats/token, 4.648 nats/word) was trained and evaluated too, and **it was no better** at the sentence task while being 1.3× bigger — so S ships. The LM is deliberately only asked to *rank about six spellings per word*, not to generate text, which is why 2.77M parameters are enough. - **Data:** IndicCorp v2 Hindi, **CC0**. No scraped chat logs, no non-commercial corpora. - **Tokenizer:** our own 8,000-piece unigram set, Viterbi segmentation reimplemented so Python and JavaScript tokenize identically. - **Model:** 2-layer LSTMP (512 hidden, 160-dim embedding, tied input/output embedding, dropout 0.05), 8-bit quantised to 3.1 MB. - **Scoring:** trained with a self-normalisation penalty, so the runtime can skip the softmax normaliser (`norm: "self"`) — that is the fast setting used here (95 ms/sentence in JS vs 193 ms for exact normalisation). - **Dependency-free inference:** `nlm.mjs` is plain JavaScript on `Float32Array`, no ONNX, no WASM, no GPU.

Pipeline: propose candidates, re-score whole sentences, pick one per slot under the guardrail

## Speed | Path | Per sentence | Notes | |---|---|---| | Python reference, search only | **13.6 ms** | lattice given; on a shared box | | JavaScript (Node), margin 2, norm self — **the shipped setting** | **95 ms** | 200 sentences, box shared with other jobs | | JavaScript (Node), margin 2, exact normalisation | 193 ms | 2× slower, ~+0.1 pt on tuning | | JavaScript (Node), margin 4, norm self | 406 ms | 4.3× slower for −0.02 val / −0.10 devA tokens | These JavaScript numbers are deliberately pessimistic: they were measured on a shared machine while other research jobs were running, one sentence at a time, cold caches. A quiet machine, a warm candidate cache and a shorter sentence are all faster. Browser latency in the demo is shown live per sentence, in your tab, on your hardware — trust that number over this table. ## Usage ### Browser (ES modules, no build step) Download the files and serve them yourself — **the Hub returns `.mjs` as `text/plain`**, so a cross-origin `import` straight from `huggingface.co/.../resolve/main/` is rejected by the browser's MIME check. Any static server that sends `text/javascript` for `.mjs` works (`python3 -m http.server` in the folder below does). ```bash huggingface-cli download sourojitdotcom/AksharaLP-Sentence-2.8M --local-dir aklp-sent cd aklp-sent && python3 -m http.server 8000 # then open http://localhost:8000/ ``` ```html ``` `sent.mjs` is six dozen lines: it builds the v3 lattice from the word model's candidates and calls `search3(lat, p, coreLm, nlm)` from `v3.mjs`. Read it before you change it — the guardrail lives there. Run it in a Web Worker, as the demo does, so the page stays at 60 fps. If you only want to try it in a browser, use the [demo Space](https://huggingface.co/spaces/sourojitdotcom/AksharaLP-Sentence-demo) — it is this exact bundle, already served with the right MIME types. ### Node.js (≥ 18) ```bash huggingface-cli download sourojitdotcom/AksharaLP-Sentence-2.8M --local-dir aksharalp-sent ``` ```js import fs from "node:fs"; import { loadFromDir } from "./aksharalp-sent/aksharalp.mjs"; import { attachSentence } from "./aksharalp-sent/sent.mjs"; const aklp = await loadFromDir("./aksharalp-sent/"); attachSentence(aklp, fs.readFileSync("./aksharalp-sent/nlm.q8")); console.log(aklp.transliterate("kal milte hai").output); // कल मिलते हैं ``` ### Python (reference implementation) ```python from huggingface_hub import snapshot_download import sys path = snapshot_download("sourojitdotcom/AksharaLP-Sentence-2.8M") sys.path.insert(0, f"{path}/python") # needs numpy + torch (CPU) from bhc.translit3 import BhcTranslit3 from bhc.nlm import NLM nlm = NLM.load(f"{path}/nlm.q8", norm="self") xl = BhcTranslit3(path, nlm=nlm, w_nlm=0.4, w_lm=0.25, w_ch_s=1.5, w_agn=2, w_agr=0, nlm_margin=2, nlm_k=6, beam_s=8, nlm_norm="self") print(xl.convert("mai ghr pe hu")) # मैं घर पे हूं ``` The Python package is the reference used for training and evaluation; the JavaScript runtime reproduces its decisions exactly (verified sentence by sentence on the casual set and on the evaluation sets: identical v2 and v3 outputs, identical accuracies). ### API at a glance | Call | Returns | |---|---| | `attachSentence(engine, nlmBytes)` | attaches sentence mode; returns `{nlm, info}` where `info` has the decoding params, the parameter count (2,770,752) and the LM config | | `engine.sentenceMode` | `true` (sentence) or `false` (word mode) — toggles the decoder without reloading anything | | `engine.transliterate(text, {mode, context, k, overrides})` | same result object as the word model, plus `guard: {ok, words, outside}` | | `search3(lattice, params, coreLm, nlm)` | the beam search itself, if you build your own lattice (format documented at the top of `v3.mjs`) | ## Files | File | What it is | Size (gz) | |---|---|---| | `nlm.q8` | **the sentence LM** — 2,770,752 params, 8-bit, tokenizer embedded | 2.92 MB | | `v3.mjs` | lattice beam search (`search3`) | 2.1 KB | | `nlm.mjs` | LM file reader + inference | 3.1 KB | | `sent.mjs` | glue: builds the lattice from the word model, toggles word/sentence mode, audits the guardrail | 1.8 KB | | `xlit.q4`, `params.json`, `dict/` | the **v2 word model** this builds on (unchanged) | 7.92 MB total | | `aksharalp.mjs`, `aksharalp.min.mjs` | the word-model runtime | 20 KB | | `manifest.json` | file list, sizes, checksums | — | | `python/` | reference Python implementation (numpy + PyTorch) | — | | `cases/` | the 21-sentence casual set used for the tables above | — | | `assets/` | banner, charts, screenshots (`assets/screenshots/`) | — | | `LICENSE`, `NOTICE` | Apache-2.0, third-party attributions | — | Decode settings (all fixed before the test run): `w_nlm 0.4`, `w_lm 0.25`, `w_ch_s 1.5`, `w_agn 2`, `w_agr 0`, `nlm_margin 2`, `nlm_k 6`, `beam_s 8`, `lat_k 12`, `rec_n 3`, `nlm_norm self`. ## Limitations - **Hindi only.** One script pair: romanized Hindi → Devanagari. No other Indic language, no reverse direction. - **It cannot add a candidate it was not given.** If the word model's top-6 misses the right spelling (see `Boht` → बोहत), no amount of sentence context finds it. That is a channel/recall problem and it is the single biggest remaining error source. - **Two-word inputs gain nothing.** Short fragments with no verb or auxiliary have no sentence signal (`nahi pata` → नहीं पाता). Add the subject and it works (`mujhe nahi pata` → मुझे नहीं पता). - **Chat normalisation cuts both ways.** The LM sometimes rewrites deliberate chat spellings into dictionary forms (extra *h*, doubled letters, stretched vowels): see the regressions table. - **Casual-chat gains are not significant.** On our 480-sentence casual set the sentence-accuracy gain is +1.46 points with a 95% CI of [−0.42, 3.54]. The significant wins are on the held-out Dakshina sentence test (sentence accuracy and word accuracy) and on the context set in word accuracy (+2.13 points, CI [0.42, 4.64]); the context set has only 66 sentences, so its sentence-level +4.55 has a CI of [0.00, 10.61] and is not flagged. - **Names, rare words and code-mixing** remain the weak spot, the same as the word model: proper nouns, brand-new slang, heavy English mixing. - **Speed is CPU JavaScript.** 95 ms per sentence at the shipped setting on a shared box; a long sentence, a cold cache or an old phone is slower. It is not a keystroke-latency budget at full sentence length — the demo debounces. - **One decoding setting.** These weights were tuned for `nlm_norm: "self"`. Changing to `exact` normalisation needs a re-tune to recover the 0.1–0.3 points it is worth on dev data. - **Not a translator.** It changes the script, not the language. ## How it fits the family | Model | What it is | Size | Best at | |---|---|---|---| | **[AksharaLP-1M](https://huggingface.co/sourojitdotcom/AksharaLP-1M)** | the first tiny word model, core only | 4.53 MB | smallest footprint, standalone word conversion | | **[AksharaLP-1M-v2](https://huggingface.co/sourojitdotcom/AksharaLP-1M-v2)** | the word model + dictionary pack this repo builds on | 7.92 MB | **word-mode accuracy** — 70.04 top-1 / 88.31 top-3 / 93.63 top-8 on Aksharantar Hindi test; best top-3 and top-8 in the family and best on chat spellings (v1 edges it on top-1 by 0.33 pts, CIs overlap) | | **[AksharaLP-Kosh-1.2M](https://huggingface.co/sourojitdotcom/AksharaLP-Kosh-1.2M)** | the sub-megabyte word model (its own card is CC-BY-SA-4.0) | 0.78 MB | the smallest possible download at 68.09% top-1 | | **AksharaLP-Sentence-2.8M** (this) | v2 word model + a 2.77M neural sentence LM | 10.85 MB | **sentence accuracy** — 26.73% sentences exact and 89.44% words on the Dakshina sentence test, the best in the family | Kosh is the word model for when one megabyte is the whole budget — 0.78 MB, 68.09% top-1, with its own licence. This is the sentence-accurate one: same word-mode candidates, plus a language model that reads the sentence. **v2 remains the best word-mode accuracy in the family on top-3/top-8 and on casual chat** (AksharaLP-1M is 0.33 points ahead on top-1 alone, inside overlapping intervals) — this repo does not claim otherwise, and does not change word mode at all. ## Licence & attribution **Commercial use is allowed.** Code and model weights ship under the **Apache License 2.0** (see `LICENSE`), Copyright 2026 Sourojit Dhua. **The sentence LM is commercially clean, and that is the important part.** It was trained only on **IndicCorp v2 Hindi (CC0)** with our own tokenizer, our own training code and our own runtime. No non-commercial corpus, no scraped chat data, no third-party model weights. The full system still inherits the v2 word package's terms (Apache-2.0, with the data attributions below listed in `NOTICE`). **Attribution is required when you redistribute it.** Apache-2.0 §4(d): include `NOTICE` in every redistribution or derivative, and keep the runtime's `/*! … */` header (including in `aksharalp.min.mjs`). ```text Transliteration by AksharaLP (Sourojit Dhua) — https://huggingface.co/sourojitdotcom/AksharaLP-Sentence-2.8M ``` | Source | Licence | Used for | |---|---|---| | [IndicCorp v2](https://github.com/AI4Bharat/IndicBERT/tree/main#indiccorp-v2), AI4Bharat | **CC0** | sentence LM training data; word-model lexicon and bigram LM | | [Aksharantar](https://huggingface.co/datasets/ai4bharat/Aksharantar), AI4Bharat | CC0 (mined) / CC-BY (manually collected) | word-model training pairs | | [IndicXlit](https://github.com/AI4Bharat/IndicXlit), AI4Bharat | MIT | teacher model for word-model distillation | | [CMUdict](https://github.com/cmusphinx/cmudict), Carnegie Mellon University | BSD-2-Clause-style | English word cues in the word model | Fonts in the demo Space (Inter, Noto Sans Devanagari) are under the SIL Open Font License 1.1. ## Citation ```bibtex @software{dhua_aksharalp_sentence_2026, author = {Dhua, Sourojit}, title = {{AksharaLP-Sentence}: Tiny, Sentence-Level Hindi Transliteration for the Browser}, year = {2026}, version = {3.0.0}, url = {https://huggingface.co/sourojitdotcom/AksharaLP-Sentence-2.8M}, note = {Word model: AksharaLP-1M-v2 (989K parameters). Sentence LM: 2.77M parameters, trained on IndicCorp v2 Hindi (CC0).}, license = {Apache-2.0} } ``` AksharaLP · sentence model v3.0.0 · built 2026-11-04 · Questions and feedback: open a discussion on this repository.