gpu-sankhya 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +320 -307
  3. package/package.json +59 -59
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Atharva Kusumbia
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Atharva Kusumbia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,307 +1,320 @@
1
- # gpu-sankhya
2
-
3
- Parses Indian informal number/currency shorthand — Hinglish (romanised
4
- Hindi) and Indian-English amount phrases like `sava lakh`, `dedh crore`,
5
- `2.5L`, `20k`, `2-3 lakh` — into a clean numeric value, with the span,
6
- unit, currency, and confidence that produced it.
7
-
8
- A small char-level CNN tags each character of the input with a BIO span
9
- label and a semantic token class (digit, prefix word like "sava"/"dedh",
10
- cardinal number word, scale unit like lakh/crore/hazaar). A deterministic
11
- arithmetic core then turns that class sequence into a number — the model
12
- never predicts the value directly, so the arithmetic can't drift from the
13
- grammar (prefix semantics, additive descending units, multiplicative
14
- ascending units like `das hazaar crore` = 10,000 × 1 crore = 1e11, etc).
15
-
16
- Ships with a small (27 KB gzipped) int8-quantized default model inlined
17
- in the package — `import { parse } from "gpu-sankhya"` works with no
18
- network fetch. Zero runtime dependencies.
19
-
20
- Model training lives in `python/` (a separate, actively-trained
21
- component); this package is the runtime that loads its exported weights.
22
- See `python/README.md` if you want to train your own weights and load
23
- them via `createParser({ weights })` instead of the bundled default.
24
-
25
- ## Install
26
-
27
- ```bash
28
- npm install gpu-sankhya
29
- ```
30
-
31
- ## Usage
32
-
33
- ```ts
34
- import { parse, parseBatch, createParser } from "gpu-sankhya";
35
-
36
- parse("sava lakh");
37
- // [{
38
- // span: "sava lakh", start: 0, end: 9,
39
- // value: 125000, unit: "lakh", currency: null,
40
- // confidence: 0.95, classes: ["PFX_SAVA", "SEP", "UNIT_LAKH"]
41
- // }]
42
-
43
- parse("mera budget paune do lakh tak ka hai");
44
- // [{ span: "paune do lakh", value: 175000, unit: "lakh", ... }]
45
-
46
- parse("2.5L");
47
- // [{ span: "2.5L", value: 250000, unit: "lakh", ... }]
48
-
49
- parse("20k logon ne attend kiya");
50
- // [{ span: "20k", value: 20000, unit: "hazaar", ... }]
51
-
52
- parse("2-3 lakh");
53
- // [{ span: "2-3 lakh", value: 200000, range: [200000, 300000], unit: "lakh", ... }]
54
-
55
- parse("das hazaar crore");
56
- // [{ span: "das hazaar crore", value: 100000000000, unit: "crore", ... }]
57
-
58
- parse("sawaa laakh ka budget hai");
59
- // [{ span: "sawaa laakh", value: 125000, unit: "lakh", ... }] -- spelling
60
- // variance ("sawaa"/"sava", "laakh"/"lakh") is part of the training data,
61
- // not special-cased.
62
-
63
- // batch (uses WebGPU automatically for large batches in a browser, else CPU)
64
- const results = await parseBatch(["sava lakh", "dedh crore", "..."]);
65
-
66
- // custom / newer trained weights (see python/README.md to train your own)
67
- const parser = createParser({ weights: myWeightsJson, backend: "cpu" });
68
- parser.parse("paune do lakh");
69
- ```
70
-
71
- ### Output shape
72
-
73
- ```ts
74
- interface Sankhya {
75
- span: string; // exact source substring
76
- start: number; end: number;
77
- value: number; // resolved value; low end for ranges
78
- range?: [number, number];// present only for ranges
79
- unit: "sau"|"hazaar"|"lakh"|"crore"|"million"|"billion"|"arab"|"kharab"|null;
80
- currency: "INR"|null; // adjacent marker detected outside the span
81
- confidence: number; // mean of span-tag softmax probs over the span
82
- classes: string[]; // normalised token classes, e.g. ["PFX_DHAI","UNIT_LAKH"]
83
- }
84
- ```
85
-
86
- ### API
87
-
88
- - **`parse(text, opts?) => Sankhya[]`** — synchronous, always CPU. Use
89
- this for one-off strings; there's no async overhead.
90
- - **`parseBatch(texts, opts?) => Promise<Sankhya[][]>`** — batched parse.
91
- Runs on CPU by default. Pass `{ backend: "webgpu" }` to force WebGPU, or
92
- `{ backend: "auto" }` to use WebGPU automatically when it's available
93
- *and* the batch has at least 32 texts (otherwise CPU, since GPU
94
- dispatch overhead dominates for small batches).
95
- - **`createParser({ weights?, backend? })`** — build a `Parser` instance
96
- around a custom weights JSON (float or int8 form, as written by
97
- `python/sankhya/export.py`), instead of the bundled default model. See
98
- `python/README.md` for how to train and export your own weights.
99
- - **`isWebGPUAvailable()`** — true if `navigator.gpu` exists in the
100
- current environment. Cheap and synchronous, but doesn't guarantee a
101
- usable adapter (e.g. headless browsers without GPU access).
102
- - **`probeWebGPU() => Promise<boolean>`** — authoritative async check:
103
- resolves false immediately if `navigator.gpu` is missing, otherwise
104
- awaits `requestAdapter()` and resolves to whether an adapter was
105
- actually obtained. Result is cached, so repeated calls only probe once.
106
- `parseBatch`'s `"auto"` backend uses this (not `isWebGPUAvailable()`)
107
- to decide whether to try the GPU path.
108
-
109
- ## Backends
110
-
111
- - **CPU** (default everywhere): a plain typed-array forward pass mirroring
112
- the Python reference implementation exactly, tuned for throughput
113
- (preallocated buffers, channels-outer loop order, no per-character
114
- allocation). Runs in Node and any browser.
115
- - **WebGPU**: the same forward pass as WGSL compute shaders (embedding
116
- gather, one dispatch per conv1d+ReLU layer, two head matmuls), used only
117
- through `parseBatch` for large batches. Device/shader setup is lazy —
118
- importing the package never touches `navigator.gpu`, so it's safe to
119
- import in Node or SSR. Falls back to CPU per-text for inputs longer than
120
- the model's 128-char window (the sliding-window path used by `parse` is
121
- CPU-only for now).
122
-
123
- ### Why CPU by default
124
-
125
- One inference is small — roughly 0.8M multiply-adds through a 4-layer,
126
- 32-channel char CNN — and runs in about 1.5 ms in plain JS. A WebGPU
127
- dispatch has fixed overhead of a few milliseconds (device/pipeline setup,
128
- buffer upload, queue submit, readback), which dwarfs that per-string cost.
129
- So the GPU only pays off once you're amortizing that overhead over a
130
- batch — hundreds of strings at once — which is exactly what `parseBatch`
131
- does with `backend: "auto"`/`"webgpu"`; `parse()` stays CPU-only and
132
- synchronous on purpose.
133
-
134
- The WebGPU path is implemented and type-checks, and has been exercised in
135
- Node (where it falls back to CPU since there's no GPU) — it has **not**
136
- yet been verified against a real GPU in a browser. Treat it as
137
- implemented-but-unverified until that happens (see Roadmap).
138
-
139
- ## Accuracy
140
-
141
- The bundled default model: 17,883 parameters, 4 conv layers (kernel sizes
142
- 3/5/3/3, dilations 1/1/2/4, 32 channels), 16-dim char embeddings over a
143
- 56-character vocab. Trained 20 epochs (~9 minutes on 4 CPU cores) on
144
- 150,000 synthetic examples generated from the `hi_latn` language pack's
145
- grammar (see `python/README.md`).
146
-
147
- On synthetic validation data (drawn from the same generator/templates as
148
- training): 0.97 value accuracy. This number is optimistic — it's testing
149
- the model on its own distribution.
150
-
151
- On a hand-written gold set of 180 sentences / 161 spans
152
- (`python/tests/gold.jsonl`), written independently of the generator:
153
-
154
- | metric | value |
155
- | --- | --- |
156
- | value accuracy | 0.95 (153/161) |
157
- | span precision | 0.90 |
158
- | span recall | 0.96 |
159
- | span F1 | 0.93 |
160
-
161
- **The gold number is the one to trust.** Known miss categories, in rough
162
- order of frequency:
163
-
164
- - unusual typos the noise model doesn't cover (e.g. "croer" for "crore")
165
- - possessive apostrophes ("do lakh's")
166
- - long multi-term ranges ("paanch se sadhe saat lakh")
167
- - occasional spurious spans triggered by unfamiliar words near number-ish
168
- context
169
-
170
- Reproduce these numbers yourself with
171
- `python -m sankhya.eval_gold --gold tests/gold.jsonl --weights-json src/data/default-weights.json --int8`
172
- from `python/` (see `python/README.md`).
173
-
174
- ## How it works
175
-
176
- 1. The input string is lowercased and char-encoded against the model's
177
- vocab (unknown chars map to `<unk>`).
178
- 2. A 4-layer dilated conv1d stack (see Accuracy above) produces, per
179
- character, a 3-way BIO logit (O/B/I) and a class logit over the
180
- semantic token vocabulary (prefix words, cardinals, units, digits,
181
- separators, misc).
182
- 3. Decoding turns those per-character predictions into spans and tokens
183
- (see Decoding below).
184
- 4. The deterministic arithmetic core (`src/core.ts`, mirrored 1:1 from
185
- `python/sankhya/core.py` and unit-tested directly in
186
- `test/core.test.ts`) evaluates each span's token sequence into a
187
- value: prefix semantics (sava = ×1.25, dedh = ×1.5, paune = subtract
188
- 1/4 from the next cardinal, ...), additive combination of descending
189
- units, multiplicative combination of ascending units, and range
190
- handling for `X-Y unit` / `X se Y unit` phrases.
191
-
192
- ### Decoding
193
-
194
- Raw per-character BIO/class predictions are cleaned up before evaluation:
195
-
196
- - **Strict BIO decode**: a span starts only at a `B` tag; an `I` that
197
- isn't preceded by an open span is treated as `O`.
198
- - **BIO bridging**: a single-character `O` gap inside what's otherwise a
199
- contiguous span is closed (handles a stray misclassified character
200
- without splitting the span in two).
201
- - **Digit-run extension**: a span is extended forward through a trailing
202
- run of digit characters it was cut short of.
203
- - **Class repair**: within a span, per-character classes are smoothed by
204
- majority vote over character-type sub-runs (fixes a stray misclassified
205
- character inside an otherwise-consistent digit or letter run), plus a
206
- few punctuation-specific rules.
207
- - **Confidence filter**: a span's confidence is the mean of the max BIO
208
- softmax probability per character; spans below 0.5 are dropped.
209
- - Only after all of the above does the deterministic arithmetic core run
210
- on the resulting token sequence.
211
-
212
- One more detail that matters more than it looks like it should: the
213
- runtime right-pads the character-id array with 16 pad tokens before
214
- running the forward pass (mirroring `python/sankhya/np_infer.py`'s
215
- `pad_ids`/`PAD_TAIL=16`), because training always right-pads every
216
- example to the model's max length the same way. Running a short, tightly
217
- cropped input (e.g. the bare 4 characters of `"2.5L"`) without that
218
- padding measurably corrupts predictions — the model was never trained on
219
- inputs that end at the literal edge of the array. The padded tail's
220
- outputs are discarded; only the real characters' predictions are used.
221
-
222
- ## Limitations
223
-
224
- - **Latin-script Hindi only.** Only Hinglish / romanised Hindi and Indian
225
- English amount phrases are supported today. Devanagari script and other
226
- Indian languages are planned via additional language packs (the
227
- arithmetic core is already language-independent; only the class
228
- vocabulary and currency-marker lists are per-language) — see Roadmap.
229
- - Text longer than 128 characters is processed with a sliding window
230
- (128-char windows, 16-char overlap) and results are merged/deduplicated
231
- by span; extremely long inputs may still miss a span that straddles a
232
- window boundary in an unlucky way.
233
- - Model quality: see Accuracy above. The known miss categories there
234
- (unusual typos, possessive apostrophes, long multi-term ranges,
235
- occasional spurious spans) are model-quality issues, not bugs in the
236
- arithmetic core, which is unit-tested directly and independently of the
237
- model in `test/core.test.ts`.
238
-
239
- ## Repository layout
240
-
241
- - `src/` — the JS/TS runtime: char encoding, CPU forward pass
242
- (`infer-cpu.ts`), WebGPU forward pass, decode, the arithmetic core
243
- (`core.ts`), the public API (`index.ts`), and the bundled default
244
- weights (`src/data/default-weights.json`).
245
- - `test/` — Node test files (`node --test`), including parity fixtures
246
- generated from the Python reference implementation
247
- (`test/fixtures/parity.jsonl`, `decoded.jsonl` — see
248
- `python/README.md`'s "Ship to the npm package" section).
249
- - `bench/` — `parse()`/`parseBatch()` latency benchmarks.
250
- - `demo/` — a minimal textarea + live-results HTML demo.
251
- - `python/sankhya/` — `classes.py` (shared class vocabulary), `core.py`
252
- (the deterministic arithmetic core), `langs/` (language packs, e.g.
253
- `hi_latn.py`), `noise_latn.py` (typo/spelling-variance injection),
254
- `generator.py` (synthetic labelled-data generator), `model.py` (the
255
- char CNN), `train.py`, `export.py`, `np_infer.py` (numpy reference
256
- forward pass, what the JS port mirrors), `decode.py`, `eval_gold.py`,
257
- `make_fixtures.py`.
258
- - `python/tests/` — `test_core.py`, `test_decode.py`, `test_generator.py`,
259
- and `gold.jsonl` (the hand-written gold set).
260
- - `docs/DATA_GRAMMAR.md` — the data/grammar spec the generator and
261
- language packs implement.
262
-
263
- ## Roadmap
264
-
265
- Everything here is scoped to Indian languages — there's no plan to
266
- support non-Indian numbering/currency shorthand.
267
-
268
- 1. **Devanagari Hindi pack** (डेढ़ लाख). A new language pack plus a new
269
- noise module for Devanagari-specific variance (matra/nukta elision or
270
- substitution) and a charset rebuild — no changes needed to `core.ts`/
271
- `core.py` or the runtime, since the arithmetic core and BIO/class
272
- architecture are already language-independent.
273
- 2. **Other Indian languages as packs**: Marathi (साडे, सव्वा), Gujarati
274
- (સવા, દોઢ), Bengali (দেড়, আড়াই), and Tamil/Telugu/Kannada number
275
- words. Same shape as (1) — a new pack, a new noise function, a charset
276
- rebuild.
277
- 3. **WebGPU browser verification.** The WebGPU path type-checks and has
278
- been exercised in Node (falling back to CPU there), but has not yet
279
- been run against a real GPU in a browser — needs that verification
280
- pass before it should be relied on.
281
- 4. **A WASM SIMD kernel**, if sub-millisecond latency is ever needed
282
- beyond what the plain-JS CPU path already gives.
283
-
284
- ## Development
285
-
286
- ```bash
287
- npm install
288
- npm run build # esbuild -> dist/index.js (ESM), tsc -> dist/*.d.ts
289
- npm test # node --test over test/*.test.ts
290
- npm run bench # parse() and parseBatch() latency
291
- npm run size # gzipped dist/index.js size
292
- ```
293
-
294
- See `demo/index.html` for a minimal textarea + live-results demo that
295
- imports `dist/index.js` directly (no build step needed beyond `npm run
296
- build`).
297
-
298
- ## Releasing
299
-
300
- ```bash
301
- npm version patch|minor|major # bumps package.json and creates a git tag
302
- git push --follow-tags
303
- ```
304
-
305
- Then create a GitHub release from that tag — the `publish` workflow
306
- builds, tests, and publishes to npm (with provenance) automatically.
307
- The first release was published manually with `npm publish`.
1
+ # gpu-sankhya
2
+
3
+ Parses Indian informal number/currency shorthand — Hinglish (romanised
4
+ Hindi) and Indian-English amount phrases like `sava lakh`, `dedh crore`,
5
+ `2.5L`, `20k`, `2-3 lakh` — into a clean numeric value, with the span,
6
+ unit, currency, and confidence that produced it.
7
+
8
+ A small char-level CNN tags each character of the input with a BIO span
9
+ label and a semantic token class (digit, prefix word like "sava"/"dedh",
10
+ cardinal number word, scale unit like lakh/crore/hazaar). A deterministic
11
+ arithmetic core then turns that class sequence into a number — the model
12
+ never predicts the value directly, so the arithmetic can't drift from the
13
+ grammar (prefix semantics, additive descending units, multiplicative
14
+ ascending units like `das hazaar crore` = 10,000 × 1 crore = 1e11, etc).
15
+
16
+ Ships with a small (27 KB gzipped) int8-quantized default model inlined
17
+ in the package — `import { parse } from "gpu-sankhya"` works with no
18
+ network fetch. Zero runtime dependencies.
19
+
20
+ Model training lives in `python/` (a separate, actively-trained
21
+ component); this package is the runtime that loads its exported weights.
22
+ See `python/README.md` if you want to train your own weights and load
23
+ them via `createParser({ weights })` instead of the bundled default.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install gpu-sankhya
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```ts
34
+ import { parse, parseBatch, createParser } from "gpu-sankhya";
35
+
36
+ parse("sava lakh");
37
+ // [{
38
+ // span: "sava lakh", start: 0, end: 9,
39
+ // value: 125000, unit: "lakh", currency: null,
40
+ // confidence: 0.95, classes: ["PFX_SAVA", "SEP", "UNIT_LAKH"]
41
+ // }]
42
+
43
+ parse("mera budget paune do lakh tak ka hai");
44
+ // [{ span: "paune do lakh", value: 175000, unit: "lakh", ... }]
45
+
46
+ parse("2.5L");
47
+ // [{ span: "2.5L", value: 250000, unit: "lakh", ... }]
48
+
49
+ parse("20k logon ne attend kiya");
50
+ // [{ span: "20k", value: 20000, unit: "hazaar", ... }]
51
+
52
+ parse("2-3 lakh");
53
+ // [{ span: "2-3 lakh", value: 200000, range: [200000, 300000], unit: "lakh", ... }]
54
+
55
+ parse("das hazaar crore");
56
+ // [{ span: "das hazaar crore", value: 100000000000, unit: "crore", ... }]
57
+
58
+ parse("sawaa laakh ka budget hai");
59
+ // [{ span: "sawaa laakh", value: 125000, unit: "lakh", ... }] -- spelling
60
+ // variance ("sawaa"/"sava", "laakh"/"lakh") is part of the training data,
61
+ // not special-cased.
62
+
63
+ // batch (uses WebGPU automatically for large batches in a browser, else CPU)
64
+ const results = await parseBatch(["sava lakh", "dedh crore", "..."]);
65
+
66
+ // custom / newer trained weights (see python/README.md to train your own)
67
+ const parser = createParser({ weights: myWeightsJson, backend: "cpu" });
68
+ parser.parse("paune do lakh");
69
+ ```
70
+
71
+ ### Output shape
72
+
73
+ ```ts
74
+ interface Sankhya {
75
+ span: string; // exact source substring
76
+ start: number; end: number;
77
+ value: number; // resolved value; low end for ranges
78
+ range?: [number, number];// present only for ranges
79
+ unit: "sau"|"hazaar"|"lakh"|"crore"|"million"|"billion"|"arab"|"kharab"|null;
80
+ currency: "INR"|null; // adjacent marker detected outside the span
81
+ confidence: number; // mean of span-tag softmax probs over the span
82
+ classes: string[]; // normalised token classes, e.g. ["PFX_DHAI","UNIT_LAKH"]
83
+ }
84
+ ```
85
+
86
+ ### API
87
+
88
+ - **`parse(text, opts?) => Sankhya[]`** — synchronous, always CPU. Use
89
+ this for one-off strings; there's no async overhead.
90
+ - **`parseBatch(texts, opts?) => Promise<Sankhya[][]>`** — batched parse.
91
+ Runs on CPU by default. Pass `{ backend: "webgpu" }` to force WebGPU, or
92
+ `{ backend: "auto" }` to use WebGPU automatically when it's available
93
+ *and* the batch has at least 32 texts (otherwise CPU, since GPU
94
+ dispatch overhead dominates for small batches).
95
+ - **`createParser({ weights?, backend? })`** — build a `Parser` instance
96
+ around a custom weights JSON (float or int8 form, as written by
97
+ `python/sankhya/export.py`), instead of the bundled default model. See
98
+ `python/README.md` for how to train and export your own weights.
99
+ - **`isWebGPUAvailable()`** — true if `navigator.gpu` exists in the
100
+ current environment. Cheap and synchronous, but doesn't guarantee a
101
+ usable adapter (e.g. headless browsers without GPU access).
102
+ - **`probeWebGPU() => Promise<boolean>`** — authoritative async check:
103
+ resolves false immediately if `navigator.gpu` is missing, otherwise
104
+ awaits `requestAdapter()` and resolves to whether an adapter was
105
+ actually obtained. Result is cached, so repeated calls only probe once.
106
+ `parseBatch`'s `"auto"` backend uses this (not `isWebGPUAvailable()`)
107
+ to decide whether to try the GPU path.
108
+
109
+ ## Backends
110
+
111
+ - **CPU** (default everywhere): a plain typed-array forward pass mirroring
112
+ the Python reference implementation exactly, tuned for throughput
113
+ (preallocated buffers, channels-outer loop order, no per-character
114
+ allocation). Runs in Node and any browser.
115
+ - **WebGPU**: the same forward pass as WGSL compute shaders (embedding
116
+ gather, one dispatch per conv1d+ReLU layer, two head matmuls), used only
117
+ through `parseBatch` for large batches. Device/shader setup is lazy —
118
+ importing the package never touches `navigator.gpu`, so it's safe to
119
+ import in Node or SSR. Falls back to CPU per-text for inputs longer than
120
+ the model's 128-char window (the sliding-window path used by `parse` is
121
+ CPU-only for now).
122
+
123
+ ### Why CPU by default
124
+
125
+ One inference is small — roughly 0.8M multiply-adds through a 4-layer,
126
+ 32-channel char CNN — and runs in about 1.5 ms in plain JS. A WebGPU
127
+ dispatch has fixed overhead of a few milliseconds (device/pipeline setup,
128
+ buffer upload, queue submit, readback), which dwarfs that per-string cost.
129
+ So the GPU only pays off once you're amortizing that overhead over a
130
+ batch — hundreds of strings at once — which is exactly what `parseBatch`
131
+ does with `backend: "auto"`/`"webgpu"`; `parse()` stays CPU-only and
132
+ synchronous on purpose.
133
+
134
+ The WebGPU path is implemented and type-checks, and has been exercised in
135
+ Node (where it falls back to CPU since there's no GPU) — it has **not**
136
+ yet been verified against a real GPU in a browser. Treat it as
137
+ implemented-but-unverified until that happens (see Roadmap).
138
+
139
+ ## Accuracy
140
+
141
+ The bundled default model: 17,883 parameters, 4 conv layers (kernel sizes
142
+ 3/5/3/3, dilations 1/1/2/4, 32 channels), 16-dim char embeddings over a
143
+ 56-character vocab. Trained 20 epochs (~9 minutes on 4 CPU cores) on
144
+ 150,000 synthetic examples generated from the `hi_latn` language pack's
145
+ grammar (see `python/README.md`).
146
+
147
+ On synthetic validation data (drawn from the same generator/templates as
148
+ training): 0.97 value accuracy. This number is optimistic — it's testing
149
+ the model on its own distribution.
150
+
151
+ On a hand-written gold set of 180 sentences / 161 spans
152
+ (`python/tests/gold.jsonl`), written independently of the generator:
153
+
154
+ | metric | value |
155
+ | --- | --- |
156
+ | value accuracy | 0.95 (153/161) |
157
+ | span precision | 0.90 |
158
+ | span recall | 0.96 |
159
+ | span F1 | 0.93 |
160
+
161
+ **The gold number is the one to trust.** Known miss categories, in rough
162
+ order of frequency:
163
+
164
+ - unusual typos the noise model doesn't cover (e.g. "croer" for "crore")
165
+ - possessive apostrophes ("do lakh's")
166
+ - long multi-term ranges ("paanch se sadhe saat lakh")
167
+ - occasional spurious spans triggered by unfamiliar words near number-ish
168
+ context
169
+
170
+ Reproduce these numbers yourself with
171
+ `python -m sankhya.eval_gold --gold tests/gold.jsonl --weights-json src/data/default-weights.json --int8`
172
+ from `python/` (see `python/README.md`).
173
+
174
+ ## How it works
175
+
176
+ 1. The input string is lowercased and char-encoded against the model's
177
+ vocab (unknown chars map to `<unk>`).
178
+ 2. A 4-layer dilated conv1d stack (see Accuracy above) produces, per
179
+ character, a 3-way BIO logit (O/B/I) and a class logit over the
180
+ semantic token vocabulary (prefix words, cardinals, units, digits,
181
+ separators, misc).
182
+ 3. Decoding turns those per-character predictions into spans and tokens
183
+ (see Decoding below).
184
+ 4. The deterministic arithmetic core (`src/core.ts`, mirrored 1:1 from
185
+ `python/sankhya/core.py` and unit-tested directly in
186
+ `test/core.test.ts`) evaluates each span's token sequence into a
187
+ value: prefix semantics (sava = ×1.25, dedh = ×1.5, paune = subtract
188
+ 1/4 from the next cardinal, ...), additive combination of descending
189
+ units, multiplicative combination of ascending units, and range
190
+ handling for `X-Y unit` / `X se Y unit` phrases.
191
+
192
+ ### Decoding
193
+
194
+ Raw per-character BIO/class predictions are cleaned up before evaluation:
195
+
196
+ - **Strict BIO decode**: a span starts only at a `B` tag; an `I` that
197
+ isn't preceded by an open span is treated as `O`.
198
+ - **BIO bridging**: a single-character `O` gap inside what's otherwise a
199
+ contiguous span is closed (handles a stray misclassified character
200
+ without splitting the span in two).
201
+ - **Digit-run extension**: a span is extended forward through a trailing
202
+ run of digit characters it was cut short of.
203
+ - **Class repair**: within a span, per-character classes are smoothed by
204
+ majority vote over character-type sub-runs (fixes a stray misclassified
205
+ character inside an otherwise-consistent digit or letter run), plus a
206
+ few punctuation-specific rules.
207
+ - **Confidence filter**: a span's confidence is the mean of the max BIO
208
+ softmax probability per character; spans below 0.5 are dropped.
209
+ - Only after all of the above does the deterministic arithmetic core run
210
+ on the resulting token sequence.
211
+
212
+ One more detail that matters more than it looks like it should: the
213
+ runtime right-pads the character-id array with 16 pad tokens before
214
+ running the forward pass (mirroring `python/sankhya/np_infer.py`'s
215
+ `pad_ids`/`PAD_TAIL=16`), because training always right-pads every
216
+ example to the model's max length the same way. Running a short, tightly
217
+ cropped input (e.g. the bare 4 characters of `"2.5L"`) without that
218
+ padding measurably corrupts predictions — the model was never trained on
219
+ inputs that end at the literal edge of the array. The padded tail's
220
+ outputs are discarded; only the real characters' predictions are used.
221
+
222
+ ## Limitations
223
+
224
+ - **Latin-script Hindi only.** Only Hinglish / romanised Hindi and Indian
225
+ English amount phrases are supported today. Devanagari script and other
226
+ Indian languages are planned via additional language packs (the
227
+ arithmetic core is already language-independent; only the class
228
+ vocabulary and currency-marker lists are per-language) — see Roadmap.
229
+ - Text longer than 128 characters is processed with a sliding window
230
+ (128-char windows, 16-char overlap) and results are merged/deduplicated
231
+ by span; extremely long inputs may still miss a span that straddles a
232
+ window boundary in an unlucky way.
233
+ - Model quality: see Accuracy above. The known miss categories there
234
+ (unusual typos, possessive apostrophes, long multi-term ranges,
235
+ occasional spurious spans) are model-quality issues, not bugs in the
236
+ arithmetic core, which is unit-tested directly and independently of the
237
+ model in `test/core.test.ts`.
238
+
239
+ ## Repository layout
240
+
241
+ - `src/` — the JS/TS runtime: char encoding, CPU forward pass
242
+ (`infer-cpu.ts`), WebGPU forward pass, decode, the arithmetic core
243
+ (`core.ts`), the public API (`index.ts`), and the bundled default
244
+ weights (`src/data/default-weights.json`).
245
+ - `test/` — Node test files (`node --test`), including parity fixtures
246
+ generated from the Python reference implementation
247
+ (`test/fixtures/parity.jsonl`, `decoded.jsonl` — see
248
+ `python/README.md`'s "Ship to the npm package" section).
249
+ - `bench/` — `parse()`/`parseBatch()` latency benchmarks.
250
+ - `demo/` — a minimal textarea + live-results HTML demo.
251
+ - `python/sankhya/` — `classes.py` (shared class vocabulary), `core.py`
252
+ (the deterministic arithmetic core), `langs/` (language packs, e.g.
253
+ `hi_latn.py`), `noise_latn.py` (typo/spelling-variance injection),
254
+ `generator.py` (synthetic labelled-data generator), `model.py` (the
255
+ char CNN), `train.py`, `export.py`, `np_infer.py` (numpy reference
256
+ forward pass, what the JS port mirrors), `decode.py`, `eval_gold.py`,
257
+ `make_fixtures.py`.
258
+ - `python/tests/` — `test_core.py`, `test_decode.py`, `test_generator.py`,
259
+ and `gold.jsonl` (the hand-written gold set).
260
+ - `docs/DATA_GRAMMAR.md` — the data/grammar spec the generator and
261
+ language packs implement.
262
+
263
+ ## Roadmap
264
+
265
+ Everything here is scoped to Indian languages — there's no plan to
266
+ support non-Indian numbering/currency shorthand.
267
+
268
+ 1. **Devanagari Hindi pack** (डेढ़ लाख). A new language pack plus a new
269
+ noise module for Devanagari-specific variance (matra/nukta elision or
270
+ substitution) and a charset rebuild — no changes needed to `core.ts`/
271
+ `core.py` or the runtime, since the arithmetic core and BIO/class
272
+ architecture are already language-independent.
273
+ 2. **Other Indian languages as packs**: Marathi (साडे, सव्वा), Gujarati
274
+ (સવા, દોઢ), Bengali (দেড়, আড়াই), and Tamil/Telugu/Kannada number
275
+ words. Same shape as (1) — a new pack, a new noise function, a charset
276
+ rebuild.
277
+ 3. **WebGPU browser verification.** The WebGPU path type-checks and has
278
+ been exercised in Node (falling back to CPU there), but has not yet
279
+ been run against a real GPU in a browser — needs that verification
280
+ pass before it should be relied on.
281
+ 4. **A WASM SIMD kernel**, if sub-millisecond latency is ever needed
282
+ beyond what the plain-JS CPU path already gives.
283
+
284
+ ## Development
285
+
286
+ ```bash
287
+ npm install
288
+ npm run build # esbuild -> dist/index.js (ESM), tsc -> dist/*.d.ts
289
+ npm test # node --test over test/*.test.ts
290
+ npm run bench # parse() and parseBatch() latency
291
+ npm run size # gzipped dist/index.js size
292
+ ```
293
+
294
+ See `demo/index.html` for a minimal textarea + live-results demo that
295
+ imports `dist/index.js` directly (no build step needed beyond `npm run
296
+ build`).
297
+
298
+ ## Releasing
299
+
300
+ The normal flow: bump the version and push to master.
301
+
302
+ ```bash
303
+ npm version patch|minor|major # bumps package.json and commits + tags locally
304
+ git push --follow-tags origin master
305
+ ```
306
+
307
+ You can also just edit the `version` field in `package.json` in a PR — no
308
+ need to run `npm version` or create a tag yourself. Either way, once the
309
+ new version lands on master, the `publish` workflow detects that
310
+ `package.json`'s version isn't on npm yet, builds, tests, publishes (with
311
+ provenance), and creates the matching git tag and GitHub release for you.
312
+
313
+ Publishing a GitHub release directly, or running the workflow manually via
314
+ `workflow_dispatch`, also triggers a publish.
315
+
316
+ Trusted publishing (OIDC, no `NPM_TOKEN`) must be configured once on
317
+ npmjs.com for this to work: package page -> Settings -> Trusted publisher,
318
+ with Organization/user `athrvk`, Repository `gpu-sankhya`, Workflow
319
+ filename `publish.yml`. The first release was published manually with
320
+ `npm publish`.
package/package.json CHANGED
@@ -1,59 +1,59 @@
1
- {
2
- "name": "gpu-sankhya",
3
- "version": "0.1.0",
4
- "type": "module",
5
- "description": "Fast char-CNN extraction of Hinglish (Latin-script Hindi) numeric quantities, with CPU and WebGPU backends.",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/athrvk/gpu-sankhya.git"
10
- },
11
- "homepage": "https://athrvk.github.io/gpu-sankhya/",
12
- "bugs": {
13
- "url": "https://github.com/athrvk/gpu-sankhya/issues"
14
- },
15
- "keywords": [
16
- "hindi",
17
- "hinglish",
18
- "lakh",
19
- "crore",
20
- "number-parsing",
21
- "nlp",
22
- "webgpu",
23
- "indian-numbering",
24
- "char-cnn",
25
- "onnx"
26
- ],
27
- "author": "Atharva Kusumbia <atharvakusumbia@gmail.com>",
28
- "engines": {
29
- "node": ">=20"
30
- },
31
- "publishConfig": {
32
- "access": "public"
33
- },
34
- "exports": {
35
- ".": {
36
- "types": "./dist/index.d.ts",
37
- "import": "./dist/index.js"
38
- }
39
- },
40
- "types": "./dist/index.d.ts",
41
- "files": [
42
- "dist"
43
- ],
44
- "sideEffects": false,
45
- "scripts": {
46
- "build": "node scripts/build.mjs && tsc -p tsconfig.json --emitDeclarationOnly && node scripts/fix-dts-ext.mjs",
47
- "test": "node --test --experimental-strip-types test/*.test.ts",
48
- "bench": "node --experimental-strip-types bench/bench.mjs",
49
- "size": "node scripts/size.mjs",
50
- "site": "node scripts/build.mjs && node scripts/site.mjs",
51
- "prepublishOnly": "npm run build && npm test"
52
- },
53
- "devDependencies": {
54
- "@webgpu/types": "^0.1.72",
55
- "esbuild": "^0.28.2",
56
- "playwright-core": "^1.63.0",
57
- "typescript": "^5.9.3"
58
- }
59
- }
1
+ {
2
+ "name": "gpu-sankhya",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "Fast char-CNN extraction of Hinglish (Latin-script Hindi) numeric quantities, with CPU and WebGPU backends.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/athrvk/gpu-sankhya.git"
10
+ },
11
+ "homepage": "https://athrvk.github.io/gpu-sankhya/",
12
+ "bugs": {
13
+ "url": "https://github.com/athrvk/gpu-sankhya/issues"
14
+ },
15
+ "keywords": [
16
+ "hindi",
17
+ "hinglish",
18
+ "lakh",
19
+ "crore",
20
+ "number-parsing",
21
+ "nlp",
22
+ "webgpu",
23
+ "indian-numbering",
24
+ "char-cnn",
25
+ "onnx"
26
+ ],
27
+ "author": "Atharva Kusumbia <atharvakusumbia@gmail.com>",
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js"
38
+ }
39
+ },
40
+ "types": "./dist/index.d.ts",
41
+ "files": [
42
+ "dist"
43
+ ],
44
+ "sideEffects": false,
45
+ "scripts": {
46
+ "build": "node scripts/build.mjs && tsc -p tsconfig.json --emitDeclarationOnly && node scripts/fix-dts-ext.mjs",
47
+ "test": "node --test --experimental-strip-types test/*.test.ts",
48
+ "bench": "node --experimental-strip-types bench/bench.mjs",
49
+ "size": "node scripts/size.mjs",
50
+ "site": "node scripts/build.mjs && node scripts/site.mjs",
51
+ "prepublishOnly": "npm run build && npm test"
52
+ },
53
+ "devDependencies": {
54
+ "@webgpu/types": "^0.1.72",
55
+ "esbuild": "^0.28.2",
56
+ "playwright-core": "^1.63.0",
57
+ "typescript": "^5.9.3"
58
+ }
59
+ }