jiteshkprasad commited on
Commit
4635aae
·
verified ·
1 Parent(s): c596162

1.2.0: fix words cut short (acha, achha -> अच्छा); Hinglish set 112 -> 113, Aksharantar test unchanged

Browse files
Files changed (2) hide show
  1. README.md +11 -7
  2. translit.js +26 -2
README.md CHANGED
@@ -62,9 +62,9 @@ console.log(tr.word("namaste").candidates.slice(0, 4).map((c) => c.text));
62
  |---|---|---|
63
  | `model.q4.gz` | the model: encoder-decoder transformer, 2 + 2 layers, width 128, 811,904 parameters at 4 bits | 432,512 bytes |
64
  | `words.bin.gz` | 200,000 common Hindi words in 8 frequency bands | 432,193 bytes |
65
- | `translit.js` | the runner (29.5 KB as written) | 10,308 bytes |
66
  | `model.json.gz`, `rerank.json` | settings and alphabet | 1,248 bytes |
67
- | **Total download** | | **876,261 bytes (0.88 MB)** |
68
 
69
  Speed: about 2 ms a word with WebAssembly SIMD, which current browsers have (1.9 ms a word in Node on a Mac mini over all
70
  9,991 Aksharantar test words; 2.4 to 2.8 ms on everyday words in Chrome and Firefox on an Intel laptop). Without SIMD it
@@ -78,7 +78,7 @@ paired for gaps. Measured by us on 28 October 2026.
78
 
79
  | System | What you download or hold | Aksharantar Hindi | Hinglish chat set (of 122) |
80
  |---|---|---|---|
81
- | **Lekhani** | **0.88 MB** | **65.9%** (64.9 to 66.8) | **112** |
82
  | IndicXlit, model on its own (beam 4, its default) | 11.5M parameters (46 MB fp32; a third-party 8-bit browser export is 14.4 MB) | 58.9% | 102 |
83
  | IndicXlit, full Python package (model + word list) | the model + a 4.2M-word list, 358 MB in memory | 68.7% | 112 |
84
 
@@ -86,7 +86,7 @@ paired for gaps. Measured by us on 28 October 2026.
86
  - **Against IndicXlit's full package: -2.8 points** (-3.6 to -2.1). The package is more accurate if you can afford its
87
  word list.
88
  - **Hinglish chat set:** 122 words people type in chat (everyday words, English words used in Hindi, brands, names),
89
- written by us and checked word by word by a native Hindi speaker. Lekhani gets 112, level with IndicXlit's package.
90
  The set is too small to call a few words' difference.
91
  - **Other open transliterators** we ran on the same test, word by word, scored far lower: IB-Emper
92
  hindi-translit-v3 (15M parameters, its model on its own) 48.6%, UnimeType Transliteration 4B 24.6% (v1.0) and
@@ -98,9 +98,8 @@ paired for gaps. Measured by us on 28 October 2026.
98
  ## Where it falls short
99
 
100
  - **About one word in three on this test is wrong at top-1.** The test is hard: many rare words and names. On
101
- everyday chat words it does much better (41 of 44 in our chat set), but it still makes mistakes there.
102
- - **Some common words come out wrong:** `acha` → अच् (should be अच्छा; `accha` works), `kal` → काल (कल is its second
103
- choice), `delhi` → देल्ही (should be दिल्ली).
104
  - **No sentence context.** It writes one word at a time, so `main` ("I") and `mein` ("in") both become में:
105
  `main ghar ja raha hoon` → में घर जा रहा हूं (should be मैं).
106
  - **IndicXlit's full package is 2.8 points more accurate** on the main test.
@@ -125,6 +124,11 @@ human check.
125
 
126
  ## Changes
127
 
 
 
 
 
 
128
  - **1.1.0 (2 Nov 2026):** the runner is about 10 times faster (batched beam search, WebAssembly SIMD kernels with a
129
  plain-JavaScript fallback, a GELU table, leaner beam search and a 2,000-word cache), contributed by Manit Roy
130
  ([GitHub #1](https://github.com/udgamlabs/lekhani/pull/1)). Same model and word list: the same best word and the same 8
 
62
  |---|---|---|
63
  | `model.q4.gz` | the model: encoder-decoder transformer, 2 + 2 layers, width 128, 811,904 parameters at 4 bits | 432,512 bytes |
64
  | `words.bin.gz` | 200,000 common Hindi words in 8 frequency bands | 432,193 bytes |
65
+ | `translit.js` | the runner (30.9 KB as written) | 10,900 bytes |
66
  | `model.json.gz`, `rerank.json` | settings and alphabet | 1,248 bytes |
67
+ | **Total download** | | **876,853 bytes (0.88 MB)** |
68
 
69
  Speed: about 2 ms a word with WebAssembly SIMD, which current browsers have (1.9 ms a word in Node on a Mac mini over all
70
  9,991 Aksharantar test words; 2.4 to 2.8 ms on everyday words in Chrome and Firefox on an Intel laptop). Without SIMD it
 
78
 
79
  | System | What you download or hold | Aksharantar Hindi | Hinglish chat set (of 122) |
80
  |---|---|---|---|
81
+ | **Lekhani** | **0.88 MB** | **65.9%** (64.9 to 66.8) | **113** |
82
  | IndicXlit, model on its own (beam 4, its default) | 11.5M parameters (46 MB fp32; a third-party 8-bit browser export is 14.4 MB) | 58.9% | 102 |
83
  | IndicXlit, full Python package (model + word list) | the model + a 4.2M-word list, 358 MB in memory | 68.7% | 112 |
84
 
 
86
  - **Against IndicXlit's full package: -2.8 points** (-3.6 to -2.1). The package is more accurate if you can afford its
87
  word list.
88
  - **Hinglish chat set:** 122 words people type in chat (everyday words, English words used in Hindi, brands, names),
89
+ written by us and checked word by word by a native Hindi speaker. Lekhani gets 113, one more than IndicXlit's package (112).
90
  The set is too small to call a few words' difference.
91
  - **Other open transliterators** we ran on the same test, word by word, scored far lower: IB-Emper
92
  hindi-translit-v3 (15M parameters, its model on its own) 48.6%, UnimeType Transliteration 4B 24.6% (v1.0) and
 
98
  ## Where it falls short
99
 
100
  - **About one word in three on this test is wrong at top-1.** The test is hard: many rare words and names. On
101
+ everyday chat words it does much better (42 of 44 in our chat set), but it still makes mistakes there.
102
+ - **Some common words come out wrong:** `kal` → काल (कल is its second choice), `delhi` → देल्ही (should be दिल्ली).
 
103
  - **No sentence context.** It writes one word at a time, so `main` ("I") and `mein` ("in") both become में:
104
  `main ghar ja raha hoon` → में घर जा रहा हूं (should be मैं).
105
  - **IndicXlit's full package is 2.8 points more accurate** on the main test.
 
124
 
125
  ## Changes
126
 
127
+ - **1.2.0 (4 Nov 2026):** common words cut short are fixed. When the best spelling ends in a bare virama (्), which a
128
+ Hindi word almost never does, the runner now takes the best candidate without one, also trying a few spelling
129
+ variants of the input (for example `acha` → `accha`) and keeping only listed words. `acha` and `achha` now give
130
+ अच्छा. It changes about 0.1% of words: the Aksharantar Hindi test is unchanged at 65.9% (one word fixed, one broken),
131
+ and the Hinglish chat set rises from 112 to 113. Same model and word list; speed is unchanged.
132
  - **1.1.0 (2 Nov 2026):** the runner is about 10 times faster (batched beam search, WebAssembly SIMD kernels with a
133
  plain-JavaScript fallback, a GELU table, leaner beam search and a 2,000-word cache), contributed by Manit Roy
134
  ([GitHub #1](https://github.com/udgamlabs/lekhani/pull/1)). Same model and word list: the same best word and the same 8
translit.js CHANGED
@@ -521,7 +521,7 @@ export class Transliterator {
521
  return res;
522
  }
523
 
524
- rank(src) {
525
  const { lam = 0, off = 0, beam = 4 } = this.rr, nb = this.nbest(src, beam);
526
  for (const c of nb) {
527
  const lp = this.list ? this.list.logp(c.text) : undefined;
@@ -530,7 +530,18 @@ export class Transliterator {
530
  }
531
  let best = null;
532
  for (const c of nb) if (!best || c.final > best.final) best = c;
533
- return { best: best ? best.text : "", nbest: nb, candidates: [...nb].sort((a, b) => b.final - a.final) };
 
 
 
 
 
 
 
 
 
 
 
534
  }
535
 
536
  // a line of text: runs of Latin letters are transliterated, everything else is kept
@@ -539,6 +550,19 @@ export class Transliterator {
539
  }
540
  }
541
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
  function norm(s) {
543
  return s.normalize("NFC").replace(/[‌‍]/g, "");
544
  }
 
521
  return res;
522
  }
523
 
524
+ rank(src, noFix = false) {
525
  const { lam = 0, off = 0, beam = 4 } = this.rr, nb = this.nbest(src, beam);
526
  for (const c of nb) {
527
  const lp = this.list ? this.list.logp(c.text) : undefined;
 
530
  }
531
  let best = null;
532
  for (const c of nb) if (!best || c.final > best.final) best = c;
533
+ // A Hindi word almost never ends in a bare virama, so a best spelling that does was cut short (acha -> अच्).
534
+ // Then take the best candidate without one, from this beam plus the in-list candidates of a few spelling variants
535
+ // (acha -> accha, a final a added or lengthened). Fires on about 0.1% of words (Aksharantar Hindi, 4 Nov 2026).
536
+ let candidates = [...nb].sort((a, b) => b.final - a.final);
537
+ if (best && best.text.endsWith(VIRAMA) && !noFix) {
538
+ const pool = nb.filter((c) => !c.text.endsWith(VIRAMA));
539
+ for (const v of spellingVariants(src)) for (const c of this.rank(v, true).nbest) if (c.inList && !c.text.endsWith(VIRAMA)) pool.push(c);
540
+ for (const c of pool) if (best.text.endsWith(VIRAMA) || c.final > best.final) best = c;
541
+ // the chosen spelling leads the candidates, as it always does
542
+ candidates = [best, ...candidates.filter((c) => c.text !== best.text)];
543
+ }
544
+ return { best: best ? best.text : "", nbest: nb, candidates };
545
  }
546
 
547
  // a line of text: runs of Latin letters are transliterated, everything else is kept
 
550
  }
551
  }
552
 
553
+ const VIRAMA = "\u094d";
554
+
555
+ // romanised spellings to try when the model cuts a word short: doubled middle consonant (acha -> accha), a final "a"
556
+ // after a consonant, a final "a" lengthened
557
+ function spellingVariants(w) {
558
+ const out = new Set(), m = w.match(/^(.*[aeiou])([bcdfgjklmnpqrstvwxyz])(h?)([aeiou]+)$/);
559
+ if (m) out.add(m[1] + m[2] + m[2] + m[3] + m[4]);
560
+ if (/[bcdfghjklmnpqrstvwxyz]$/.test(w)) out.add(w + "a");
561
+ if (/[^a]a$/.test(w)) out.add(w + "a");
562
+ out.delete(w);
563
+ return [...out];
564
+ }
565
+
566
  function norm(s) {
567
  return s.normalize("NFC").replace(/[‌‍]/g, "");
568
  }