ichkil 0.0.0-stage → 1.0.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maaouia BenHamed
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,3 +1,177 @@
1
- # Temporary Holding Version
1
+ # ichkil — Arabic Tashkeel for JavaScript / TypeScript
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > Vocalize bare Arabic text with a tiny, self-contained ONNX model (≈5 MB, CPU-only,
4
+ > no GPU, no tokenizer, no server). One call, fully offline after the first run.
5
+
6
+ [![CI](https://github.com/Ichkil/ichkil-js/actions/workflows/ci.yml/badge.svg)](https://github.com/Ichkil/ichkil-js/actions/workflows/ci.yml)
7
+ [![npm](https://img.shields.io/npm/v/ichkil.svg)](https://www.npmjs.com/package/ichkil)
8
+ [![npm - Node.js version](https://img.shields.io/badge/node-%3E%3D18-blue.svg)](https://nodejs.org)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
10
+ [![Hugging Face model](https://img.shields.io/badge/%F0%9F%A4%97_model-ichkil%2Fichkil-yellow)](https://huggingface.co/ichkil/ichkil)
11
+ [![Try it in your browser](https://img.shields.io/badge/%F0%9F%A4%97_try%20it-ichkil%2Fichkil%2Ddemo-blue)](https://huggingface.co/spaces/ichkil/ichkil-demo)
12
+
13
+ `ichkil` downloads its model from [Hugging Face](https://huggingface.co/ichkil/ichkil)
14
+ on first use, verifies its SHA-256, and runs inference with
15
+ [ONNX Runtime](https://onnxruntime.ai/). After that it is 100 % offline.
16
+
17
+ ```ts
18
+ import { diacritize } from "ichkil";
19
+
20
+ console.log(await diacritize("محمد قرأ الكتاب في المدرسة"));
21
+ // -> 'مُحَمَّدٌ قَرَأَ الْكِتَابَ فِي الْمَدْرَسَةِ'
22
+ ```
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npm install ichkil
28
+ # or
29
+ pnpm add ichkil
30
+ yarn add ichkil
31
+ ```
32
+
33
+ Requires Node.js ≥ 18 (tested on 18, 20 and 22). Dependencies: `onnxruntime-node`
34
+ (CPU). Dual ESM + CommonJS builds with TypeScript definitions — no build step
35
+ needed on the consumer side.
36
+
37
+ ## Quickstart
38
+
39
+ ```ts
40
+ // ESM (TypeScript or .mjs)
41
+ import { diacritize, predictLabels, Diacritizer } from "ichkil";
42
+
43
+ // one-liner (lazy model download on first call, then cached)
44
+ await diacritize("العلم نور والجهل ظلمة");
45
+ // -> 'الْعِلْمِ نُورٌ وَالْجَهْلُ ظُلْمَةُ'
46
+
47
+ ```
48
+
49
+ ```ts
50
+ import { Diacritizer } from "ichkil";
51
+
52
+ const d = await Diacritizer.create(); // downloads + verifies + caches the model
53
+ await d.diacritize("القهوة جميلة في الصباح");
54
+ // -> 'الْقَهْوَةِ جَمِيلَةٌ فِي الصَّبَاحِ'
55
+
56
+ const labels = await d.predictLabels("كتاب"); // [4, 2, 1, 4] (one label id per character, 0..12)
57
+
58
+ await d.release(); // when done (optional, idempotent)
59
+ ```
60
+
61
+ ```js
62
+ // CommonJS
63
+ const { diacritize } = require("ichkil");
64
+
65
+ (async () => {
66
+ console.log(await diacritize("محمد قرأ الكتاب"));
67
+ })();
68
+ ```
69
+
70
+ ```ts
71
+ // fully offline: point at local files
72
+ import { Diacritizer } from "ichkil";
73
+
74
+ const d = await Diacritizer.fromFile("./model.onnx");
75
+ ```
76
+
77
+ ### Command line
78
+
79
+ ```bash
80
+ npm install -g ichkil
81
+ ichkil "محمد قرأ الكتاب"
82
+ # مُحَمَّدٌ قَرَأَ الْكِتَابَ
83
+
84
+ ichkil --json "العلم نور"
85
+ # {"input": "العلم نور", "output": "الْعِلْمِ نُورٌ"}
86
+
87
+ cat text.txt | ichkil # read from stdin
88
+ ichkil --model ./model.onnx "محمد" # offline mode
89
+ ```
90
+
91
+ ## API
92
+
93
+ | Item | Description |
94
+ | --- | --- |
95
+ | `diacritize(text: string): Promise<string>` | Vocalize text (module-level convenience, lazy default model). |
96
+ | `predictLabels(text: string): Promise<number[]>` | Label id (0–12) per character. |
97
+ | `Diacritizer.create(options?): Promise<Diacritizer>` | `repoId`, `revision`, `cacheDir`, `token`, `verifyChecksum`, `numThreads`. |
98
+ | `Diacritizer.fromFile(modelPath, configPath?): Promise<Diacritizer>` | Build from local `model.onnx` (+ `config.json` beside it). |
99
+ | `d.diacritize(text)` / `d.predictLabels(text)` | Instance methods (same behavior). |
100
+ | `d.release()` | Release the ONNX session (idempotent). |
101
+ | Pipeline | Pure text helpers: `stripDiacritics`, `encode`, `decode`, `attach`, label tables. |
102
+ | Errors | `IchkilError` ← `ChecksumError`, `ModelLoadError`, `InferenceError`. |
103
+ | `resetDefault()` | Drop the lazily-created default instance (useful in tests). |
104
+
105
+ ### Label contract
106
+
107
+ The model predicts exactly one label per input character:
108
+
109
+ | id | meaning | id | meaning |
110
+ | --- | --- | --- | --- |
111
+ | 0 | non-letter token / padding | 7 | shadda + fatha (ّـَ) |
112
+ | 1 | letter, no diacritic | 8 | shadda + damma (ّـُ) |
113
+ | 2 | fatha (ـَ) | 9 | shadda + kasra (ّـِ) |
114
+ | 3 | damma (ـُ) | 10 | tanwin fatha (ـً) |
115
+ | 4 | kasra (ـِ) | 11 | tanwin damma (ـٌ) |
116
+ | 5 | sukun (ـْ) | 12 | tanwin kasra (ـٍ) |
117
+ | 6 | shadda (ـّ) | | |
118
+
119
+ Already-vocalized input is stripped first, so `diacritize` is idempotent.
120
+ Non-Arabic characters (Latin letters, digits, punctuation, spaces) pass through
121
+ unchanged.
122
+
123
+ ## Model provenance
124
+
125
+ - Repository: [`ichkil/ichkil`](https://huggingface.co/ichkil/ichkil)
126
+ (`model.onnx`, `config.json`, `SHA256SUMS`)
127
+ - SHA-256 of `model.onnx`: `9055816b214346b0a8044a4a8deceb36e818dffd0b04699715ea23883bf54744`
128
+ - Contract: `input_ids` int64 `[batch, seq]` → `logits` float32 `[batch, seq, 13]`;
129
+ sequence length fully dynamic (up to `max_seq_len = 1024` in the golden contract)
130
+ - Quality: DER 2.80 % on the reference evaluation set; ONNX output is bit-identical
131
+ to the PyTorch reference (equivalence verified)
132
+ - The cross-runtime golden vectors shipped in `test/golden.json` are a verbatim
133
+ copy of the core repository's `golden/vectors.json`
134
+
135
+ ## Model cache
136
+
137
+ The model is cached under `~/.cache/ichkil` (override with the
138
+ `ICHKIL_CACHE_DIR` environment variable or the `cacheDir` option). Only the
139
+ first call per revision touches the network; the SHA-256 is verified on every
140
+ resolve and a corrupt cache file is re-downloaded once before failing.
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ npm install
146
+ npm run lint # eslint
147
+ npm run typecheck # tsc --noEmit
148
+ npm run build # tsup → dist/ (ESM + CJS + d.ts)
149
+ npm run test:unit # pure pipeline tests (no network)
150
+ npm run test:integration # + model tests (downloads once, ~5 MB)
151
+ npm test # everything
152
+ ```
153
+
154
+ ## Publishing
155
+
156
+ `v*` tags run the full check suite and are published to npm with **provenance**
157
+ (attested by GitHub OIDC). One-time setup:
158
+
159
+ 1. Create the `ichkil` package on npm (first publish, or transfer an existing
160
+ one to the account you will use).
161
+ 2. Create an [automation token](https://docs.npmjs.com/creating-and-managing-auth-tokens)
162
+ with *Publish* access for that package (a regular token works too).
163
+ 3. In the GitHub repo: *Settings → Secrets and variables → Actions* → add
164
+ `NPM_TOKEN` with that token.
165
+
166
+ Then tag and push: `git tag v1.0.1 && git push origin v1.0.1`.
167
+
168
+ ## Sibling libraries
169
+
170
+ - [ichkil-python](https://github.com/Ichkil/ichkil-python) — PyPI package
171
+ - [ichkil-go](https://github.com/Ichkil/ichkil-go) — Go module
172
+ - [Core repo](https://github.com/Ichkil/ichkil) — training, evaluation, model spec,
173
+ golden vectors
174
+
175
+ ## License
176
+
177
+ [MIT](LICENSE) © 2026 Maaouia BenHamed
package/bin/ichkil.mjs ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `ichkil` CLI launcher (see `src/cli.ts`).
4
+ *
5
+ * The build output (`dist/cli.js`) is ESM; this shim exits with the code
6
+ * returned by `main()` (0 success, 1 runtime error, 2 usage error).
7
+ */
8
+ import { main } from "../dist/cli.js";
9
+
10
+ main(process.argv.slice(2))
11
+ .then((code) => {
12
+ process.exitCode = code;
13
+ })
14
+ .catch((err) => {
15
+ console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
16
+ process.exitCode = 1;
17
+ });
@@ -0,0 +1,381 @@
1
+ import { createHash } from 'crypto';
2
+ import { createReadStream, readFileSync, rmSync, existsSync, mkdirSync, createWriteStream, renameSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join, dirname, basename } from 'path';
5
+ import { Readable } from 'stream';
6
+ import { pipeline } from 'stream/promises';
7
+ import * as ort from 'onnxruntime-node';
8
+
9
+ // src/pipeline.ts
10
+ var AR_LETTER_RE = /[\u0621-\u064A]/;
11
+ var AR_DIACRITIC_RE = /[\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06ED]/g;
12
+ var TATWEEL = "\u0640";
13
+ var LABEL_NON_LETTER = 0;
14
+ var LABEL_NONE = 1;
15
+ var NUM_LABELS = 13;
16
+ var LABEL_TO_COMBO = [
17
+ "",
18
+ // 0 non-letter token
19
+ "",
20
+ // 1 letter carries no diacritic
21
+ "\u064E",
22
+ // 2 fatha
23
+ "\u064F",
24
+ // 3 damma
25
+ "\u0650",
26
+ // 4 kasra
27
+ "\u0652",
28
+ // 5 sukun
29
+ "\u0651",
30
+ // 6 shadda
31
+ "\u0651\u064E",
32
+ // 7 shadda + fatha
33
+ "\u0651\u064F",
34
+ // 8 shadda + damma
35
+ "\u0651\u0650",
36
+ // 9 shadda + kasra
37
+ "\u064B",
38
+ // 10 tanwin fatha
39
+ "\u064C",
40
+ // 11 tanwin damma
41
+ "\u064D"
42
+ // 12 tanwin kasra
43
+ ];
44
+ var comboEntries = [];
45
+ for (let i = 0; i < LABEL_TO_COMBO.length; i++) {
46
+ const combo = LABEL_TO_COMBO[i];
47
+ if (combo) comboEntries.push([combo, i]);
48
+ }
49
+ var COMBO_TO_LABEL = Object.freeze(Object.fromEntries(comboEntries));
50
+ function isArabicLetter(ch) {
51
+ return ch.length === 1 && AR_LETTER_RE.test(ch);
52
+ }
53
+ function labelToCombo(label) {
54
+ if (Number.isInteger(label) && label >= 0 && label < NUM_LABELS) {
55
+ return LABEL_TO_COMBO[label];
56
+ }
57
+ return "";
58
+ }
59
+ function comboToLabel(combo) {
60
+ return COMBO_TO_LABEL[combo] ?? LABEL_NON_LETTER;
61
+ }
62
+ function stripDiacritics(text) {
63
+ return String(text).replace(AR_DIACRITIC_RE, "").split(TATWEEL).join("");
64
+ }
65
+ function encode(text, sym2id, unkId) {
66
+ const out = [];
67
+ for (const ch of text) {
68
+ out.push(Object.hasOwn(sym2id, ch) ? sym2id[ch] : unkId);
69
+ }
70
+ return out;
71
+ }
72
+ function argmax(values) {
73
+ if (values.length === 0) return 0;
74
+ let best = 0;
75
+ let bestV = values[0];
76
+ for (let i = 1; i < values.length; i++) {
77
+ const v = values[i];
78
+ if (v > bestV) {
79
+ bestV = v;
80
+ best = i;
81
+ }
82
+ }
83
+ return best;
84
+ }
85
+ function decode(base, logits, numLabels) {
86
+ const chars = Array.from(base);
87
+ let out = "";
88
+ for (let i = 0; i < chars.length; i++) {
89
+ const ch = chars[i];
90
+ if (isArabicLetter(ch)) {
91
+ const off = i * numLabels;
92
+ let best = 0;
93
+ let bestV = -Infinity;
94
+ for (let k = 0; k < numLabels; k++) {
95
+ const v = logits[off + k];
96
+ if (v > bestV) {
97
+ bestV = v;
98
+ best = k;
99
+ }
100
+ }
101
+ out += ch + (LABEL_TO_COMBO[best] ?? "");
102
+ } else {
103
+ out += ch;
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+ function attach(base, labels) {
109
+ const chars = Array.from(base);
110
+ let out = "";
111
+ for (let i = 0; i < chars.length; i++) {
112
+ const ch = chars[i];
113
+ const lab = labels[i];
114
+ out += isArabicLetter(ch) ? ch + labelToCombo(lab) : ch;
115
+ }
116
+ return out;
117
+ }
118
+
119
+ // src/errors.ts
120
+ var IchkilError = class extends Error {
121
+ constructor(message) {
122
+ super(message);
123
+ this.name = new.target.name;
124
+ }
125
+ };
126
+ var ChecksumError = class extends IchkilError {
127
+ };
128
+ var ModelLoadError = class extends IchkilError {
129
+ };
130
+ var InferenceError = class extends IchkilError {
131
+ };
132
+ var DEFAULT_REPO_ID = "ichkil/ichkil";
133
+ var DEFAULT_REVISION = "main";
134
+ var HF_BASE_URL = "https://huggingface.co";
135
+ function defaultCacheDir() {
136
+ return process.env.ICHKIL_CACHE_DIR || join(homedir(), ".cache", "ichkil");
137
+ }
138
+ function msg(err) {
139
+ return err instanceof Error ? err.message : String(err);
140
+ }
141
+ async function sha256OfFile(path) {
142
+ const hash = createHash("sha256");
143
+ const stream = createReadStream(path);
144
+ for await (const chunk of stream) {
145
+ hash.update(chunk);
146
+ }
147
+ return hash.digest("hex").toLowerCase();
148
+ }
149
+ function parseSha256Sums(text, filename) {
150
+ for (const line of text.split(/\r?\n/)) {
151
+ const parts = line.trim().split(/\s+/);
152
+ if (parts.length >= 2 && (parts[parts.length - 1] ?? "").replace(/^\*/, "") === filename) {
153
+ return (parts[0] ?? "").toLowerCase();
154
+ }
155
+ }
156
+ return void 0;
157
+ }
158
+ function loadConfig(configPath) {
159
+ let config;
160
+ try {
161
+ config = JSON.parse(readFileSync(configPath, "utf8"));
162
+ } catch (err) {
163
+ throw new ModelLoadError(`failed to read config.json (${configPath}): ${msg(err)}`);
164
+ }
165
+ for (const key of ["sym2id", "num_tags"]) {
166
+ if (!(key in config)) {
167
+ throw new ModelLoadError(
168
+ `config.json is missing required key '${key}' (from ${configPath})`
169
+ );
170
+ }
171
+ }
172
+ return config;
173
+ }
174
+ async function fetchFile(url, dest, token, fetchImpl) {
175
+ const headers = { "user-agent": "ichkil-js/1.0" };
176
+ if (token) headers.authorization = `Bearer ${token}`;
177
+ let res;
178
+ try {
179
+ res = await fetchImpl(url, { headers, redirect: "follow" });
180
+ } catch (err) {
181
+ throw new ModelLoadError(`download failed for ${url}: ${msg(err)}`);
182
+ }
183
+ if (!res.ok) {
184
+ throw new ModelLoadError(
185
+ `download failed for ${url}: HTTP ${res.status} ${res.statusText} (check the repo id, revision and network access)`
186
+ );
187
+ }
188
+ if (!(res.body instanceof ReadableStream)) {
189
+ throw new ModelLoadError(`download failed for ${url}: empty response body`);
190
+ }
191
+ mkdirSync(dirname(dest), { recursive: true });
192
+ const tmp = `${dest}.part`;
193
+ try {
194
+ const nodeStream = Readable.fromWeb(
195
+ res.body
196
+ );
197
+ await pipeline(nodeStream, createWriteStream(tmp));
198
+ renameSync(tmp, dest);
199
+ } finally {
200
+ rmSync(tmp, { force: true });
201
+ }
202
+ }
203
+ async function ensureFile(path, url, token, fetchImpl) {
204
+ if (existsSync(path)) return;
205
+ await fetchFile(url, path, token, fetchImpl);
206
+ }
207
+ async function resolveModel(options = {}) {
208
+ const repoId = options.repoId ?? DEFAULT_REPO_ID;
209
+ const revision = options.revision ?? DEFAULT_REVISION;
210
+ const verify = options.verifyChecksum ?? true;
211
+ const base = (options.baseUrl ?? HF_BASE_URL).replace(/\/+$/, "");
212
+ const fetchImpl = options.fetchImpl ?? fetch;
213
+ const dir = join(
214
+ options.cacheDir ?? defaultCacheDir(),
215
+ `models--${repoId.replace(/\//g, "--")}`,
216
+ revision
217
+ );
218
+ const onnxPath = join(dir, "model.onnx");
219
+ const configPath = join(dir, "config.json");
220
+ const urlOf = (file) => `${base}/${repoId}/resolve/${revision}/${file}`;
221
+ await ensureFile(onnxPath, urlOf("model.onnx"), options.token, fetchImpl);
222
+ await ensureFile(configPath, urlOf("config.json"), options.token, fetchImpl);
223
+ const config = loadConfig(configPath);
224
+ if (verify) {
225
+ let expected;
226
+ try {
227
+ const sumsPath = join(dir, "SHA256SUMS");
228
+ await ensureFile(sumsPath, urlOf("SHA256SUMS"), options.token, fetchImpl);
229
+ expected = parseSha256Sums(readFileSync(sumsPath, "utf8"), "model.onnx");
230
+ } catch (err) {
231
+ if (err instanceof ModelLoadError || err instanceof ChecksumError) throw err;
232
+ expected = void 0;
233
+ }
234
+ expected ??= typeof config.sha256 === "string" ? config.sha256.toLowerCase() : void 0;
235
+ if (!expected) {
236
+ throw new ModelLoadError(
237
+ "cannot verify model checksum: the repository has neither SHA256SUMS nor config.sha256 (pass verifyChecksum: false to bypass)"
238
+ );
239
+ }
240
+ const actual = await sha256OfFile(onnxPath);
241
+ if (actual !== expected) {
242
+ rmSync(onnxPath, { force: true });
243
+ await ensureFile(onnxPath, urlOf("model.onnx"), options.token, fetchImpl);
244
+ const retry = await sha256OfFile(onnxPath);
245
+ if (retry !== expected) {
246
+ throw new ChecksumError(
247
+ `model checksum mismatch for ${onnxPath}: expected ${expected}, got ${retry}. Delete the local cache file or pass verifyChecksum: false to bypass.`
248
+ );
249
+ }
250
+ }
251
+ }
252
+ return { onnxPath, config, repoId, revision };
253
+ }
254
+ function msg2(err) {
255
+ return err instanceof Error ? err.message : String(err);
256
+ }
257
+ var Diacritizer = class _Diacritizer {
258
+ /** Hugging Face repo id of the loaded model (`<local>` for `fromFile`). */
259
+ repoId;
260
+ /** Revision (branch / tag / commit) of the loaded model. */
261
+ revision;
262
+ /** Number of labels (13). */
263
+ numLabels;
264
+ /** Maximum supported sequence length (0 = unknown). */
265
+ maxSeqLen;
266
+ sym2id;
267
+ unkId;
268
+ session;
269
+ constructor(session, config, repoId, revision) {
270
+ this.session = session;
271
+ this.sym2id = { ...config.sym2id };
272
+ this.numLabels = config.num_tags;
273
+ this.maxSeqLen = config.max_seq_len ?? 0;
274
+ this.unkId = this.sym2id["<unk>"] ?? 1;
275
+ this.repoId = repoId;
276
+ this.revision = revision;
277
+ }
278
+ /**
279
+ * Create a diacritizer, downloading the model from Hugging Face on first
280
+ * use (cached afterwards).
281
+ */
282
+ static async create(options = {}) {
283
+ const bundle = await resolveModel(options);
284
+ return _Diacritizer.fromBundle(bundle, options);
285
+ }
286
+ /**
287
+ * Build a diacritizer from local `model.onnx` + `config.json` files.
288
+ *
289
+ * `configPath` defaults to `config.json` next to the model.
290
+ */
291
+ static async fromFile(modelPath, configPath, options = {}) {
292
+ if (!existsSync(modelPath)) {
293
+ throw new ModelLoadError(`model file not found: ${modelPath}`);
294
+ }
295
+ const cfgPath = configPath ?? join(dirname(modelPath), "config.json");
296
+ if (!existsSync(cfgPath)) {
297
+ throw new ModelLoadError(`config file not found: ${cfgPath}`);
298
+ }
299
+ const config = loadConfig(cfgPath);
300
+ return _Diacritizer.fromBundle(
301
+ { onnxPath: modelPath, config, repoId: "<local>", revision: basename(modelPath) },
302
+ options
303
+ );
304
+ }
305
+ static async fromBundle(bundle, options) {
306
+ const sessionOptions = {
307
+ graphOptimizationLevel: "all"
308
+ };
309
+ if (options.numThreads) {
310
+ sessionOptions.intraOpNumThreads = options.numThreads;
311
+ }
312
+ let session;
313
+ try {
314
+ session = await ort.InferenceSession.create(bundle.onnxPath, sessionOptions);
315
+ } catch (err) {
316
+ throw new ModelLoadError(
317
+ `failed to load ONNX model ${bundle.onnxPath}: ${msg2(err)}`
318
+ );
319
+ }
320
+ return new _Diacritizer(session, bundle.config, bundle.repoId, bundle.revision);
321
+ }
322
+ /**
323
+ * Predicted label id (0..12) for every character of `text`.
324
+ *
325
+ * The input is stripped of diacritics and tatweel first, so the returned
326
+ * array is aligned with the stripped characters.
327
+ */
328
+ async predictLabels(text) {
329
+ if (typeof text !== "string") {
330
+ throw new TypeError("text must be a string");
331
+ }
332
+ const base = stripDiacritics(text);
333
+ if (base.length === 0) return [];
334
+ const ids = encode(base, this.sym2id, this.unkId);
335
+ const tensor = new ort.Tensor("int64", ids, [1, ids.length]);
336
+ let results;
337
+ try {
338
+ results = await this.session.run({ input_ids: tensor });
339
+ } catch (err) {
340
+ throw new InferenceError(`inference failed: ${msg2(err)}`);
341
+ }
342
+ const logits = results["logits"];
343
+ if (!logits) {
344
+ throw new InferenceError("model produced no 'logits' output");
345
+ }
346
+ const data = logits.data;
347
+ const n = base.length;
348
+ const labels = new Array(n);
349
+ for (let i = 0; i < n; i++) {
350
+ labels[i] = argmax(data.subarray(i * this.numLabels, (i + 1) * this.numLabels));
351
+ }
352
+ return labels;
353
+ }
354
+ /**
355
+ * Turn bare (or already vocalized) Arabic into fully-vocalized text.
356
+ *
357
+ * ```ts
358
+ * await diacritizer.diacritize("محمد قرأ الكتاب")
359
+ * // -> 'مُحَمَّدٌ قَرَأَ الْكِتَابَ'
360
+ * ```
361
+ */
362
+ async diacritize(text) {
363
+ let raw = text == null ? "" : String(text);
364
+ raw = raw.trim();
365
+ if (!raw) return "";
366
+ const base = stripDiacritics(raw);
367
+ if (!base) return raw;
368
+ return attach(base, await this.predictLabels(base));
369
+ }
370
+ /** Release the underlying ONNX Runtime session (idempotent). */
371
+ async release() {
372
+ await this.session.release();
373
+ }
374
+ };
375
+
376
+ // src/version.ts
377
+ var version = "1.0.0";
378
+
379
+ export { AR_DIACRITIC_RE, AR_LETTER_RE, COMBO_TO_LABEL, ChecksumError, DEFAULT_REPO_ID, DEFAULT_REVISION, Diacritizer, HF_BASE_URL, IchkilError, InferenceError, LABEL_NONE, LABEL_NON_LETTER, LABEL_TO_COMBO, ModelLoadError, NUM_LABELS, TATWEEL, argmax, attach, comboToLabel, decode, defaultCacheDir, encode, isArabicLetter, labelToCombo, loadConfig, parseSha256Sums, resolveModel, sha256OfFile, stripDiacritics, version };
380
+ //# sourceMappingURL=chunk-P3WX7WF5.js.map
381
+ //# sourceMappingURL=chunk-P3WX7WF5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pipeline.ts","../src/errors.ts","../src/download.ts","../src/diacritizer.ts","../src/version.ts"],"names":["msg","existsSync","join","dirname"],"mappings":"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","file":"chunk-P3WX7WF5.js","sourcesContent":["/**\n * Pure text pipeline for Arabic Tashkeel — no model, no third-party imports.\n *\n * The model emits exactly one label per input character:\n *\n * 0 non-letter token (space / punctuation) or padding\n * 1 letter carries no diacritic\n * 2 .. 12 the eleven harakat / shadda / tanwin combinations\n *\n * This module is the single source of truth shared by inference and the\n * tests, and it mirrors the reference implementation in the core repository\n * (`ichkil/constants.py`) and the JS pipeline used by the Hugging Face Space.\n * Keep the three in sync.\n */\n\n/** A single Arabic letter (the characters the model may decorate). */\nexport const AR_LETTER_RE = /[\\u0621-\\u064A]/;\n\n/** Any Arabic diacritic / harakat code point (stripped from input first). */\nexport const AR_DIACRITIC_RE = /[\\u0610-\\u061A\\u064B-\\u065F\\u0670\\u06D6-\\u06ED]/g;\n\n/** Tatweel (horizontal line), removed from input before encoding. */\nexport const TATWEEL = \"\\u0640\";\n\n/** Label id for a non-letter token (space / punctuation) or padding. */\nexport const LABEL_NON_LETTER = 0;\n\n/** Label id for a letter that carries no diacritic. */\nexport const LABEL_NONE = 1;\n\n/** Number of labels emitted by the model (13). */\nexport const NUM_LABELS = 13;\n\n/** Label id -> canonical diacritic combination (13 labels; 0/1 attach nothing). */\nexport const LABEL_TO_COMBO: readonly string[] = [\n \"\", // 0 non-letter token\n \"\", // 1 letter carries no diacritic\n \"\\u064E\", // 2 fatha\n \"\\u064F\", // 3 damma\n \"\\u0650\", // 4 kasra\n \"\\u0652\", // 5 sukun\n \"\\u0651\", // 6 shadda\n \"\\u0651\\u064E\", // 7 shadda + fatha\n \"\\u0651\\u064F\", // 8 shadda + damma\n \"\\u0651\\u0650\", // 9 shadda + kasra\n \"\\u064B\", // 10 tanwin fatha\n \"\\u064C\", // 11 tanwin damma\n \"\\u064D\", // 12 tanwin kasra\n];\n\nconst comboEntries: [string, number][] = [];\nfor (let i = 0; i < LABEL_TO_COMBO.length; i++) {\n const combo = LABEL_TO_COMBO[i] as string;\n if (combo) comboEntries.push([combo, i]);\n}\n\n/** Diacritic combination -> label id (only the non-empty combinations). */\nexport const COMBO_TO_LABEL: Readonly<Record<string, number>> =\n Object.freeze(Object.fromEntries(comboEntries));\n\n/** Return `true` if `ch` is a single Arabic letter (U+0621..U+064A). */\nexport function isArabicLetter(ch: string): boolean {\n return ch.length === 1 && AR_LETTER_RE.test(ch);\n}\n\n/** Map a label id to its diacritic combination (``\"\"`` for unknown ids). */\nexport function labelToCombo(label: number): string {\n if (Number.isInteger(label) && label >= 0 && label < NUM_LABELS) {\n return LABEL_TO_COMBO[label] as string;\n }\n return \"\";\n}\n\n/** Map a diacritic combination (or `\"\"`) to its label id (`0` if unknown). */\nexport function comboToLabel(combo: string): number {\n return COMBO_TO_LABEL[combo] ?? LABEL_NON_LETTER;\n}\n\n/**\n * Return `text` with all diacritics and tatweel removed.\n *\n * Letters, spaces and punctuation are preserved so the result stays aligned\n * with the original character positions.\n */\nexport function stripDiacritics(text: string): string {\n return String(text).replace(AR_DIACRITIC_RE, \"\").split(TATWEEL).join(\"\");\n}\n\n/** Map each character to its vocabulary id (out-of-vocabulary -> `unkId`). */\nexport function encode(\n text: string,\n sym2id: Readonly<Record<string, number>>,\n unkId: number\n): number[] {\n const out: number[] = [];\n for (const ch of text) {\n out.push(Object.hasOwn(sym2id, ch) ? (sym2id[ch] as number) : unkId);\n }\n return out;\n}\n\n/** Index of the maximum value (first one wins ties), matching ONNX Runtime. */\nexport function argmax(values: ArrayLike<number>): number {\n if (values.length === 0) return 0;\n let best = 0;\n let bestV = values[0] as number;\n for (let i = 1; i < values.length; i++) {\n const v = values[i] as number;\n if (v > bestV) {\n bestV = v;\n best = i;\n }\n }\n return best;\n}\n\n/**\n * Attach predicted diacritics onto `base` from flat logits.\n *\n * `logits` is the model output row for `base` in row-major order\n * (`logits.length === base.length * numLabels`). Non-letter characters pass\n * through unchanged.\n */\nexport function decode(base: string, logits: ArrayLike<number>, numLabels: number): string {\n const chars = Array.from(base);\n let out = \"\";\n for (let i = 0; i < chars.length; i++) {\n const ch = chars[i] as string;\n if (isArabicLetter(ch)) {\n const off = i * numLabels;\n let best = 0;\n let bestV = -Infinity;\n for (let k = 0; k < numLabels; k++) {\n const v = logits[off + k] as number;\n if (v > bestV) {\n bestV = v;\n best = k;\n }\n }\n out += ch + (LABEL_TO_COMBO[best] ?? \"\");\n } else {\n out += ch;\n }\n }\n return out;\n}\n\n/**\n * Attach predicted diacritics onto the bare letters of `base`.\n *\n * `labels` is one entry per character of `base`. Non-letter characters pass\n * through unchanged.\n */\nexport function attach(base: string, labels: ArrayLike<number>): string {\n const chars = Array.from(base);\n let out = \"\";\n for (let i = 0; i < chars.length; i++) {\n const ch = chars[i] as string;\n const lab = labels[i] as number;\n out += isArabicLetter(ch) ? ch + labelToCombo(lab) : ch;\n }\n return out;\n}\n","/** Base class for all ichkil errors. */\nexport class IchkilError extends Error {\n constructor(message: string) {\n super(message);\n this.name = new.target.name;\n }\n}\n\n/** The downloaded model file does not match its expected SHA-256. */\nexport class ChecksumError extends IchkilError {}\n\n/** The model artifact or its config is missing, invalid or unreadable. */\nexport class ModelLoadError extends IchkilError {}\n\n/** Inference failed (bad input shape, runtime error, ...). */\nexport class InferenceError extends IchkilError {}\n","/**\n * Model acquisition: download (once) from Hugging Face, then use the local\n * cache.\n *\n * Files fetched from the model repository (`ichkil/ichkil` by default):\n *\n * * `model.onnx` — the ONNX artifact (int64 `input_ids` -> float32 `logits`)\n * * `config.json` — vocabulary (`sym2id`), `num_tags`, `max_seq_len`\n * * `SHA256SUMS` — expected checksum of `model.onnx` (verified by default)\n *\n * Everything is cached under `~/.cache/ichkil` (override with the\n * `ICHKIL_CACHE_DIR` environment variable or the `cacheDir` option), so only\n * the first call per revision touches the network.\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n createReadStream,\n createWriteStream,\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n rmSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\n\nimport { ChecksumError, ModelLoadError } from \"./errors.js\";\nimport type { Config } from \"./diacritizer.js\";\n\n/** Default Hugging Face model repository. */\nexport const DEFAULT_REPO_ID = \"ichkil/ichkil\";\n\n/** Default branch / tag / commit to fetch. */\nexport const DEFAULT_REVISION = \"main\";\n\n/** Base URL of the Hugging Face Hub (overridable for tests / mirrors). */\nexport const HF_BASE_URL = \"https://huggingface.co\";\n\n/** A resolved, verified local model artifact plus its runtime config. */\nexport interface ModelBundle {\n onnxPath: string;\n config: Config;\n repoId: string;\n revision: string;\n}\n\n/** Options for {@link resolveModel}. */\nexport interface ResolveOptions {\n /** Hugging Face model repository (default `ichkil/ichkil`). */\n repoId?: string;\n /** Branch, tag or commit to fetch (default `main`). */\n revision?: string;\n /** Cache directory (default: `ICHKIL_CACHE_DIR` or `~/.cache/ichkil`). */\n cacheDir?: string;\n /** Optional Hugging Face token (only needed for gated / private repos). */\n token?: string;\n /** Verify `model.onnx` against `SHA256SUMS` (default `true`). */\n verifyChecksum?: boolean;\n /** Hub base URL (default `https://huggingface.co`). */\n baseUrl?: string;\n /** `fetch` implementation to use (default: the global one). */\n fetchImpl?: typeof fetch;\n}\n\n/** Default cache directory (`ICHKIL_CACHE_DIR` or `~/.cache/ichkil`). */\nexport function defaultCacheDir(): string {\n return process.env.ICHKIL_CACHE_DIR || join(homedir(), \".cache\", \"ichkil\");\n}\n\nfunction msg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Hex SHA-256 digest of a local file. */\nexport async function sha256OfFile(path: string): Promise<string> {\n const hash = createHash(\"sha256\");\n const stream = createReadStream(path);\n for await (const chunk of stream) {\n hash.update(chunk as Buffer);\n }\n return hash.digest(\"hex\").toLowerCase();\n}\n\n/**\n * Extract the expected SHA-256 for `filename` from a `SHA256SUMS` document,\n * or `undefined` when the file is not listed.\n */\nexport function parseSha256Sums(text: string, filename: string): string | undefined {\n for (const line of text.split(/\\r?\\n/)) {\n const parts = line.trim().split(/\\s+/);\n if (parts.length >= 2 && (parts[parts.length - 1] ?? \"\").replace(/^\\*/, \"\") === filename) {\n return (parts[0] ?? \"\").toLowerCase();\n }\n }\n return undefined;\n}\n\n/** Load and minimally validate a `config.json`. */\nexport function loadConfig(configPath: string): Config {\n let config: Config;\n try {\n config = JSON.parse(readFileSync(configPath, \"utf8\")) as Config;\n } catch (err) {\n throw new ModelLoadError(`failed to read config.json (${configPath}): ${msg(err)}`);\n }\n for (const key of [\"sym2id\", \"num_tags\"]) {\n if (!(key in config)) {\n throw new ModelLoadError(\n `config.json is missing required key '${key}' (from ${configPath})`\n );\n }\n }\n return config;\n}\n\nasync function fetchFile(\n url: string,\n dest: string,\n token: string | undefined,\n fetchImpl: typeof fetch\n): Promise<void> {\n const headers: Record<string, string> = { \"user-agent\": \"ichkil-js/1.0\" };\n if (token) headers.authorization = `Bearer ${token}`;\n let res: Response;\n try {\n res = await fetchImpl(url, { headers, redirect: \"follow\" });\n } catch (err) {\n throw new ModelLoadError(`download failed for ${url}: ${msg(err)}`);\n }\n if (!res.ok) {\n throw new ModelLoadError(\n `download failed for ${url}: HTTP ${res.status} ${res.statusText} ` +\n \"(check the repo id, revision and network access)\"\n );\n }\n if (!(res.body instanceof ReadableStream)) {\n throw new ModelLoadError(`download failed for ${url}: empty response body`);\n }\n mkdirSync(dirname(dest), { recursive: true });\n const tmp = `${dest}.part`;\n try {\n const nodeStream = Readable.fromWeb(\n res.body as unknown as import(\"node:stream/web\").ReadableStream\n );\n await pipeline(nodeStream, createWriteStream(tmp));\n renameSync(tmp, dest);\n } finally {\n rmSync(tmp, { force: true });\n }\n}\n\nasync function ensureFile(\n path: string,\n url: string,\n token: string | undefined,\n fetchImpl: typeof fetch\n): Promise<void> {\n if (existsSync(path)) return;\n await fetchFile(url, path, token, fetchImpl);\n}\n\n/**\n * Download (or reuse the cached) model files from Hugging Face.\n *\n * The first call per revision fetches `model.onnx`, `config.json` and\n * `SHA256SUMS`; every later call is served from the local cache.\n */\nexport async function resolveModel(options: ResolveOptions = {}): Promise<ModelBundle> {\n const repoId = options.repoId ?? DEFAULT_REPO_ID;\n const revision = options.revision ?? DEFAULT_REVISION;\n const verify = options.verifyChecksum ?? true;\n const base = (options.baseUrl ?? HF_BASE_URL).replace(/\\/+$/, \"\");\n const fetchImpl = options.fetchImpl ?? fetch;\n const dir = join(\n options.cacheDir ?? defaultCacheDir(),\n `models--${repoId.replace(/\\//g, \"--\")}`,\n revision\n );\n\n const onnxPath = join(dir, \"model.onnx\");\n const configPath = join(dir, \"config.json\");\n const urlOf = (file: string) => `${base}/${repoId}/resolve/${revision}/${file}`;\n\n await ensureFile(onnxPath, urlOf(\"model.onnx\"), options.token, fetchImpl);\n await ensureFile(configPath, urlOf(\"config.json\"), options.token, fetchImpl);\n\n const config = loadConfig(configPath);\n\n if (verify) {\n let expected: string | undefined;\n try {\n const sumsPath = join(dir, \"SHA256SUMS\");\n await ensureFile(sumsPath, urlOf(\"SHA256SUMS\"), options.token, fetchImpl);\n expected = parseSha256Sums(readFileSync(sumsPath, \"utf8\"), \"model.onnx\");\n } catch (err) {\n if (err instanceof ModelLoadError || err instanceof ChecksumError) throw err;\n expected = undefined; // SHA256SUMS may be absent on some revisions\n }\n expected ??= typeof config.sha256 === \"string\" ? config.sha256.toLowerCase() : undefined;\n if (!expected) {\n throw new ModelLoadError(\n \"cannot verify model checksum: the repository has neither SHA256SUMS \" +\n \"nor config.sha256 (pass verifyChecksum: false to bypass)\"\n );\n }\n\n const actual = await sha256OfFile(onnxPath);\n if (actual !== expected) {\n // Corrupt or tampered cache file: re-download once and re-verify.\n rmSync(onnxPath, { force: true });\n await ensureFile(onnxPath, urlOf(\"model.onnx\"), options.token, fetchImpl);\n const retry = await sha256OfFile(onnxPath);\n if (retry !== expected) {\n throw new ChecksumError(\n `model checksum mismatch for ${onnxPath}: expected ${expected}, got ${retry}. ` +\n \"Delete the local cache file or pass verifyChecksum: false to bypass.\"\n );\n }\n }\n }\n\n return { onnxPath, config, repoId, revision };\n}\n","/**\n * The {@link Diacritizer} — a ready-to-use Arabic diacritizer backed by ONNX.\n *\n * The model (`model.onnx` + `config.json`) is downloaded from Hugging Face\n * on first use and cached under `~/.cache/ichkil`; afterwards construction\n * is offline.\n *\n * ```ts\n * import { diacritize } from \"ichkil\";\n *\n * console.log(await diacritize(\"محمد قرأ الكتاب\"));\n * // -> 'مُحَمَّدٌ قَرَأَ الْكِتَابَ'\n * ```\n *\n * All inference entry points are asynchronous: the ONNX Runtime Node backend\n * executes `session.run` on a worker, and the first call may download the\n * model from Hugging Face.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport * as ort from \"onnxruntime-node\";\n\nimport {\n attach,\n encode,\n argmax,\n stripDiacritics,\n} from \"./pipeline.js\";\nimport { loadConfig, resolveModel } from \"./download.js\";\nimport type { ModelBundle, ResolveOptions } from \"./download.js\";\nimport { InferenceError, ModelLoadError } from \"./errors.js\";\n\n/** Runtime configuration loaded from the model repository `config.json`. */\nexport interface Config {\n /** Character -> vocabulary id mapping (includes `<pad>`/`<unk>`). */\n sym2id: Record<string, number>;\n /** Number of labels (13). */\n num_tags: number;\n /** Maximum supported sequence length (1024). */\n max_seq_len?: number;\n /** Optional SHA-256 of `model.onnx` (fallback when `SHA256SUMS` is absent). */\n sha256?: string;\n [key: string]: unknown;\n}\n\n/** Options for {@link Diacritizer.create}. */\nexport type DiacritizerOptions = ResolveOptions & {\n /** ONNX Runtime intra-op thread count (default: ORT heuristic). */\n numThreads?: number;\n};\n\nfunction msg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nexport class Diacritizer {\n /** Hugging Face repo id of the loaded model (`<local>` for `fromFile`). */\n readonly repoId: string;\n /** Revision (branch / tag / commit) of the loaded model. */\n readonly revision: string;\n /** Number of labels (13). */\n readonly numLabels: number;\n /** Maximum supported sequence length (0 = unknown). */\n readonly maxSeqLen: number;\n\n private readonly sym2id: Record<string, number>;\n private readonly unkId: number;\n private readonly session: ort.InferenceSession;\n\n private constructor(\n session: ort.InferenceSession,\n config: Config,\n repoId: string,\n revision: string\n ) {\n this.session = session;\n this.sym2id = { ...config.sym2id };\n this.numLabels = config.num_tags;\n this.maxSeqLen = config.max_seq_len ?? 0;\n this.unkId = this.sym2id[\"<unk>\"] ?? 1;\n this.repoId = repoId;\n this.revision = revision;\n }\n\n /**\n * Create a diacritizer, downloading the model from Hugging Face on first\n * use (cached afterwards).\n */\n static async create(options: DiacritizerOptions = {}): Promise<Diacritizer> {\n const bundle = await resolveModel(options);\n return Diacritizer.fromBundle(bundle, options);\n }\n\n /**\n * Build a diacritizer from local `model.onnx` + `config.json` files.\n *\n * `configPath` defaults to `config.json` next to the model.\n */\n static async fromFile(\n modelPath: string,\n configPath?: string,\n options: Pick<DiacritizerOptions, \"numThreads\"> = {}\n ): Promise<Diacritizer> {\n if (!existsSync(modelPath)) {\n throw new ModelLoadError(`model file not found: ${modelPath}`);\n }\n const cfgPath = configPath ?? join(dirname(modelPath), \"config.json\");\n if (!existsSync(cfgPath)) {\n throw new ModelLoadError(`config file not found: ${cfgPath}`);\n }\n const config = loadConfig(cfgPath);\n return Diacritizer.fromBundle(\n { onnxPath: modelPath, config, repoId: \"<local>\", revision: basename(modelPath) },\n options\n );\n }\n\n private static async fromBundle(\n bundle: ModelBundle,\n options: DiacritizerOptions\n ): Promise<Diacritizer> {\n const sessionOptions: ort.InferenceSession.SessionOptions = {\n graphOptimizationLevel: \"all\",\n };\n if (options.numThreads) {\n sessionOptions.intraOpNumThreads = options.numThreads;\n }\n let session: ort.InferenceSession;\n try {\n session = await ort.InferenceSession.create(bundle.onnxPath, sessionOptions);\n } catch (err) {\n throw new ModelLoadError(\n `failed to load ONNX model ${bundle.onnxPath}: ${msg(err)}`\n );\n }\n return new Diacritizer(session, bundle.config, bundle.repoId, bundle.revision);\n }\n\n /**\n * Predicted label id (0..12) for every character of `text`.\n *\n * The input is stripped of diacritics and tatweel first, so the returned\n * array is aligned with the stripped characters.\n */\n async predictLabels(text: string): Promise<number[]> {\n if (typeof text !== \"string\") {\n throw new TypeError(\"text must be a string\");\n }\n const base = stripDiacritics(text);\n if (base.length === 0) return [];\n const ids = encode(base, this.sym2id, this.unkId);\n const tensor = new ort.Tensor(\"int64\", ids, [1, ids.length]);\n let results: ort.InferenceSession.ReturnType;\n try {\n results = await this.session.run({ input_ids: tensor });\n } catch (err) {\n throw new InferenceError(`inference failed: ${msg(err)}`);\n }\n const logits = results[\"logits\"];\n if (!logits) {\n throw new InferenceError(\"model produced no 'logits' output\");\n }\n const data = logits.data as Float32Array;\n const n = base.length;\n const labels = new Array<number>(n);\n for (let i = 0; i < n; i++) {\n labels[i] = argmax(data.subarray(i * this.numLabels, (i + 1) * this.numLabels));\n }\n return labels;\n }\n\n /**\n * Turn bare (or already vocalized) Arabic into fully-vocalized text.\n *\n * ```ts\n * await diacritizer.diacritize(\"محمد قرأ الكتاب\")\n * // -> 'مُحَمَّدٌ قَرَأَ الْكِتَابَ'\n * ```\n */\n async diacritize(text: string): Promise<string> {\n let raw = text == null ? \"\" : String(text);\n raw = raw.trim();\n if (!raw) return \"\";\n const base = stripDiacritics(raw);\n if (!base) return raw;\n return attach(base, await this.predictLabels(base));\n }\n\n /** Release the underlying ONNX Runtime session (idempotent). */\n async release(): Promise<void> {\n await this.session.release();\n }\n}\n","/**\n * Package version.\n *\n * Kept in sync with `version` in package.json (bump both when releasing).\n */\nexport const version = \"1.0.0\";\n"]}