shreyask commited on
Commit
2bb8806
·
verified ·
1 Parent(s): 8ec6b18

release: webgpu demo + source

Browse files

- 6 voicepacks (added Priya + Arjun from SPRINGLab)
- voicepacks.json: single source of truth for voice list + per-gender chips
- dynamic voice dropdown + chips (replaces hardcoded HTML)
- ship full source (src/, package.json, tsconfig.json, vite.config.ts) so
forks can rebuild without de-minifying
- LFS: add glob assets/index-*.js + espeakng worker

.gitattributes CHANGED
@@ -49,3 +49,8 @@ assets/index-BDXCV8Ud.js filter=lfs diff=lfs merge=lfs -text
49
  assets/index-HaFA3KMR.js filter=lfs diff=lfs merge=lfs -text
50
  assets/index-B0gBa3_t.js filter=lfs diff=lfs merge=lfs -text
51
  assets/index-C6vp9g9z.js filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
49
  assets/index-HaFA3KMR.js filter=lfs diff=lfs merge=lfs -text
50
  assets/index-B0gBa3_t.js filter=lfs diff=lfs merge=lfs -text
51
  assets/index-C6vp9g9z.js filter=lfs diff=lfs merge=lfs -text
52
+
53
+ # Vite-built hashed bundles (replaces per-file rules — covers all current and future builds)
54
+ assets/index-*.js filter=lfs diff=lfs merge=lfs -text
55
+ # espeak-ng-wasm worker (~18 MB; bundled engine + JS shim)
56
+ src/espeakng/espeakng.worker.js filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -14,21 +14,58 @@ tags:
14
  - marathi
15
  - kokoro
16
  - webgpu
17
- - indic
18
  ---
19
 
20
- # 🪔 Bol TTS Marathi WebGPU demo
21
 
22
- Marathi (मराठी) text-to-speech running entirely in your browser. Loads [shreyask/bol-tts-marathi-onnx](https://huggingface.co/shreyask/bol-tts-marathi-onnx) via transformers.js on WebGPU (falls back to WASM on Safari).
23
 
24
- Features:
25
- - Four voices: Asha (आशा), Vivek (विवेक), Mukta (मुक्ता), Dnyanesh (ज्ञानेश)
26
- - In-browser Marathi G2P via espeak-ng WASM (from hexgrad/kokoro's patched worker)
27
- - Per-word timestamps + live highlighting synchronized to audio
28
- - Click any word to seek
29
- - Indic-English-style phoneme remap toggle for Minglish (Marathi + English)
30
- - Warli-inspired UI (tribal art from Maharashtra)
31
 
32
- Full training recipe + reproducibility: [github.com/shreyaskarnik/bol-tts-marathi](https://github.com/shreyaskarnik/bol-tts-marathi)
33
 
34
- Model card + weights: [huggingface.co/shreyask/bol-tts-marathi](https://huggingface.co/shreyask/bol-tts-marathi)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  - marathi
15
  - kokoro
16
  - webgpu
17
+ - on-device
18
  ---
19
 
20
+ # bol-tts-marathi Marathi TTS via WebGPU
21
 
22
+ In-browser Marathi (मराठी) text-to-speech, running real-time on WebGPU. Six voices, code-switch (Minglish) support, no server round-trip.
23
 
24
+ This Space ships **both the built demo** (`index.html` + `assets/` served directly) **and the full source** (`src/`, `package.json`, `tsconfig.json`, `vite.config.ts`) so anyone can fork the Space and rebuild without de-minifying the bundle.
 
 
 
 
 
 
25
 
26
+ ## What's in this Space
27
 
28
+ | Path | Role |
29
+ |---|---|
30
+ | `index.html`, `assets/*` | built demo, served via static SDK |
31
+ | `voices/*.bin` | 6 voicepacks (style vectors, 522 KB each) |
32
+ | `voicepacks.json` | source-of-truth for voice list + per-gender test phrases |
33
+ | `src/` | TypeScript source (phonemizer, tokenizer, model wrapper, UI wiring) |
34
+ | `public/` | static assets fed to the build |
35
+ | `package.json`, `tsconfig.json`, `vite.config.ts` | reproducible build config |
36
+ | `index.template.html` | the source-side index.html that Vite injects modules into |
37
+
38
+ ## Reproduce locally
39
+
40
+ ```bash
41
+ git clone https://huggingface.co/spaces/<USER>/<THIS-SPACE>
42
+ cd <THIS-SPACE>
43
+ npm install
44
+ npm run dev # local dev server at http://localhost:5173
45
+ npm run build # produces dist/ — same artifacts shipped here
46
+ ```
47
+
48
+ ## How it works
49
+
50
+ - 82M-parameter Kokoro fine-tune for Marathi (model: [`shreyask/bol-tts-marathi-onnx`](https://huggingface.co/shreyask/bol-tts-marathi-onnx))
51
+ - `@huggingface/transformers` loads the ONNX model into WebGPU (or falls back to WASM)
52
+ - `espeak-ng-wasm` phonemizes Marathi + Indian English; we parse `(en)/(mr)` tags for per-segment Indian-accent rules on English chunks
53
+ - 6 voices via single shared model + per-voice 256-D style vectors (`voices/*.bin`, [510, 1, 256] float32)
54
+ - Test phrases + voice metadata in `voicepacks.json` — edit to add voices, no code changes needed
55
+
56
+ Recipe + training scripts: [github.com/shreyaskarnik/bol-tts-marathi](https://github.com/shreyaskarnik/bol-tts-marathi)
57
+
58
+ ## Voices
59
+
60
+ | ID | Name | Gender | Source | Status |
61
+ |---|---|---|---|---|
62
+ | `mf_asha` | Asha (आशा) | female | Rasa Marathi | stable |
63
+ | `mm_vivek` | Vivek (विवेक) | male | Rasa Marathi | stable |
64
+ | `mf_mukta` | Mukta (मुक्ता) | female | IndicVoices-R | stable |
65
+ | `mm_dnyanesh` | Dnyanesh (ज्ञानेश) | male | IndicVoices-R | stable |
66
+ | `mf_priya` | Priya (प्रिया) | female | SPRINGLab IndicTTS | v0.2 preview |
67
+ | `mm_arjun` | Arjun (अर्जुन) | male | SPRINGLab IndicTTS | v0.2 preview |
68
+
69
+ ## License
70
+
71
+ Apache 2.0. See LICENSE.
assets/index-BZq4ShBi.js ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bf6bfe9f328713c187f76ce5514e829945d5c9337022587cdca68885f6040469
3
+ size 19064726
index.html CHANGED
@@ -17,7 +17,7 @@
17
  rel="stylesheet"
18
  />
19
 
20
- <script type="module" crossorigin src="/assets/index-C6vp9g9z.js"></script>
21
  <link rel="stylesheet" crossorigin href="/assets/index-BdhRcMoO.css">
22
  </head>
23
  <body>
@@ -76,89 +76,19 @@
76
  पावसाच्या संध्याकाळी शनिवारवाड्याजवळ गरम कटिंग चहा घेत मी मित्रांना म्हणालो, “आज आकाशातला प्रत्येक ढग एखादी नवी गोष्ट सांगतोय.”</textarea
77
  >
78
 
79
- <div class="examples">
 
80
  <span class="examples-label">मराठी:</span>
81
- <button
82
- class="chip"
83
- data-text="आज हवामान खूप छान आहे, पाऊस पडतो आहे."
84
- >
85
- हवामान
86
- </button>
87
- <button
88
- class="chip"
89
- data-text="महाराष्ट्र हे भारतातील एक महत्त्वाचे राज्य आहे."
90
- >
91
- महाराष्ट्र
92
- </button>
93
- <button
94
- class="chip"
95
- data-text="मुंबई ही महाराष्ट्राची राजधानी आणि भारताची आर्थिक राजधानी आहे."
96
- >
97
- मुंबई
98
- </button>
99
- <button
100
- class="chip"
101
- data-text="पुणे ही महाराष्ट्राची सांस्कृतिक राजधानी मानली जाते."
102
- >
103
- पुणे
104
- </button>
105
- <button class="chip" data-text="एक दोन तीन चार पाच सहा सात आठ.">
106
- आकडे
107
- </button>
108
- <button
109
- class="chip"
110
- data-text="पूर्वी एका छोट्या गावात एक शहाणा शेतकरी राहत होता."
111
- >
112
- कथा
113
- </button>
114
- <button
115
- class="chip"
116
- data-text="गुढी पाडवा हा मराठी नववर्षाचा सण आहे."
117
- >
118
- गुढी पाडवा
119
- </button>
120
- <button
121
- class="chip"
122
- data-text="पावसाळी संध्याकाळी गरम भजी आणि कटिंग चहा म्हणजे स्वर्गच!"
123
- >
124
- भजी
125
- </button>
126
- <button
127
- class="chip"
128
- data-text="मुंबईचा वडापाव आणि पुण्याचा चहा — आपली खरी ओळख."
129
- >
130
- वडापाव
131
- </button>
132
  </div>
133
- <div class="examples">
134
  <span class="examples-label">Minglish:</span>
135
- <button class="chip" data-text="मी Google मध्ये काम करतो.">
136
- Google job
137
- </button>
138
- <button class="chip" data-text="आज मीटिंग ला late झालो.">
139
- late meeting
140
- </button>
141
- <button class="chip" data-text="Coffee पिऊया का?">coffee?</button>
142
- <button
143
- class="chip"
144
- data-text="Artificial intelligence ही technology खूप amazing आहे."
145
- >
146
- AI tech
147
- </button>
148
- <button class="chip" data-text="Weekend ला movie बघायचा plan आहे का?">
149
- weekend plan
150
- </button>
151
  </div>
152
 
153
  <div class="controls">
154
  <label
155
  >आवाज · Voice
156
- <select id="voice">
157
- <option value="mf_asha">Asha (आशा) — female</option>
158
- <option value="mm_vivek">Vivek (विवेक) — male</option>
159
- <option value="mf_mukta">Mukta (मुक्ता) — female</option>
160
- <option value="mm_dnyanesh">Dnyanesh (ज्ञानेश) — male</option>
161
- </select>
162
  </label>
163
  <label class="speed-control">
164
  <span class="speed-control-row">
 
17
  rel="stylesheet"
18
  />
19
 
20
+ <script type="module" crossorigin src="/assets/index-BZq4ShBi.js"></script>
21
  <link rel="stylesheet" crossorigin href="/assets/index-BdhRcMoO.css">
22
  </head>
23
  <body>
 
76
  पावसाच्या संध्याकाळी शनिवारवाड्याजवळ गरम कटिंग चहा घेत मी मित्रांना म्हणालो, “आज आकाशातला प्रत्येक ढग एखादी नवी गोष्ट सांगतोय.”</textarea
77
  >
78
 
79
+ <!-- chips populated dynamically from public/voicepacks.json (test_phrases.*) -->
80
+ <div class="examples" id="chips-marathi">
81
  <span class="examples-label">मराठी:</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  </div>
83
+ <div class="examples" id="chips-minglish">
84
  <span class="examples-label">Minglish:</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  </div>
86
 
87
  <div class="controls">
88
  <label
89
  >आवाज · Voice
90
+ <!-- options populated dynamically from public/voicepacks.json (voices[]) -->
91
+ <select id="voice"></select>
 
 
 
 
92
  </label>
93
  <label class="speed-control">
94
  <span class="speed-control-row">
index.template.html ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="mr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link
7
+ rel="icon"
8
+ type="image/svg+xml"
9
+ href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Ctext y='48' font-size='48'%3E%F0%9F%AA%94%3C/text%3E%3C/svg%3E"
10
+ />
11
+ <title>बोल — Marathi TTS · WebGPU</title>
12
+
13
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
14
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
15
+ <link
16
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Tiro+Devanagari+Marathi&family=Yatra+One&display=swap"
17
+ rel="stylesheet"
18
+ />
19
+
20
+ <link rel="stylesheet" href="/src/styles.css" />
21
+ </head>
22
+ <body>
23
+ <!-- Warli tribal art strip: dancers, hut, deer, microphones, sun motifs.
24
+ Custom-illustrated band in the style of traditional Warli wall paintings
25
+ from Maharashtra. Microphones are a contemporary addition reflecting
26
+ the TTS theme. -->
27
+ <div class="warli-strip" aria-hidden="true">
28
+ <img src="/warli-strip.svg" alt="" />
29
+ </div>
30
+
31
+ <main>
32
+ <header>
33
+ <h1>बोल</h1>
34
+ <span class="namaste">Marathi Text-to-Speech</span>
35
+ <p>
36
+ Kokoro-82M fine-tuned for
37
+ <a
38
+ href="https://en.wikipedia.org/wiki/Marathi_language"
39
+ target="_blank"
40
+ rel="noopener"
41
+ >मराठी</a
42
+ >
43
+ · runs in your browser via WebGPU
44
+ </p>
45
+ </header>
46
+
47
+ <section id="status" class="card status">initializing…</section>
48
+
49
+ <section class="card model-gate" id="model-gate" hidden>
50
+ <div class="model-gate-text">
51
+ <strong>Tap to load the TTS model</strong> (~325 MB fp32 ONNX ·
52
+ one-time download, browser-cached)<br />
53
+ <span class="model-gate-sub">
54
+ fp32 is the recommended precision for Kokoro on WebGPU (matches
55
+ kokoro.js + HeadTTS defaults; int8 breaks ISTFTNet decoder audio
56
+ quality). Plus ~40 MB for onnxruntime-web WASM + espeak-ng worker.
57
+ </span>
58
+ </div>
59
+ <button id="load-model" class="primary">Load model</button>
60
+ </section>
61
+
62
+ <section class="card">
63
+ <div class="text-label-row">
64
+ <label for="text">मराठी मजकूर — Marathi text (Devanagari)</label>
65
+ <button
66
+ id="transliterate"
67
+ class="text-tool"
68
+ title="Transliterate Latin → Devanagari using ITRANS scheme. e.g. 'namaskaar majhe naav amit aahe' → 'नमस्कार माझे नाव अमित आहे'. Edit the result before synthesizing if needed."
69
+ type="button"
70
+ >
71
+ A·a → अ
72
+ </button>
73
+ </div>
74
+ <textarea id="text" rows="3">
75
+ पावसाच्या संध्याकाळी शनिवारवाड्याजवळ गरम कटिंग चहा घेत मी मित्रांना म्हणालो, “आज आकाशातला प्रत्येक ढग एखादी नवी गोष्ट सांगतोय.”</textarea
76
+ >
77
+
78
+ <!-- chips populated dynamically from public/voicepacks.json (test_phrases.*) -->
79
+ <div class="examples" id="chips-marathi">
80
+ <span class="examples-label">मराठी:</span>
81
+ </div>
82
+ <div class="examples" id="chips-minglish">
83
+ <span class="examples-label">Minglish:</span>
84
+ </div>
85
+
86
+ <div class="controls">
87
+ <label
88
+ >आवाज · Voice
89
+ <!-- options populated dynamically from public/voicepacks.json (voices[]) -->
90
+ <select id="voice"></select>
91
+ </label>
92
+ <label class="speed-control">
93
+ <span class="speed-control-row">
94
+ <span>गती · Speed</span>
95
+ <span id="speed-val">1.00×</span>
96
+ </span>
97
+ <input
98
+ id="speed"
99
+ type="range"
100
+ min="0.5"
101
+ max="1.5"
102
+ step="0.05"
103
+ value="1.0"
104
+ />
105
+ </label>
106
+ <button id="synth" class="primary">Synthesize</button>
107
+ </div>
108
+ </section>
109
+
110
+ <section class="card" id="output-section" hidden>
111
+ <audio id="audio" hidden></audio>
112
+ <div class="player">
113
+ <button
114
+ id="playpause"
115
+ class="playpause"
116
+ title="Play / Pause (space)"
117
+ aria-label="Play / Pause"
118
+ >
119
+
120
+ </button>
121
+ <div class="player-main">
122
+ <canvas
123
+ id="waveform"
124
+ class="waveform"
125
+ tabindex="0"
126
+ aria-label="Click to seek"
127
+ ></canvas>
128
+ <div class="player-time">
129
+ <span id="time-cur">0:00</span>
130
+ <span id="time-total">0:00</span>
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <!-- Primary transcript: each word highlighted as audio plays.
136
+ Click a word to seek there. Expand the summary below for IPA + timing. -->
137
+ <div
138
+ class="transcript-compact"
139
+ id="transcript-compact"
140
+ title="Click any word to seek playback to it"
141
+ ></div>
142
+
143
+ <details class="transcript-details" id="transcript-details">
144
+ <summary>Per-word IPA + timing</summary>
145
+ <div class="sidebyside" id="transcript">
146
+ <div
147
+ class="col-head"
148
+ title="Devanagari script — Marathi text as written. Each row is one word from your input."
149
+ >
150
+ देवनागरी
151
+ </div>
152
+ <div
153
+ class="col-head"
154
+ title="International Phonetic Alphabet — how each word is pronounced. These are the phonemes fed to the model (via the espeak-ng WASM worker running in your browser)."
155
+ >
156
+ IPA
157
+ </div>
158
+ <div
159
+ class="col-head"
160
+ title="Start and end time of each word in the synthesized audio, predicted by Kokoro's prosody model (pred_dur). The bar fills progressively as the word is playing."
161
+ >
162
+ timing
163
+ </div>
164
+ <!-- rows injected by main.ts -->
165
+ </div>
166
+ </details>
167
+
168
+ <aside class="facts">
169
+ 🪔 <strong>मराठी phoneme fact:</strong> Marathi's retroflex lateral
170
+ <span class="ipa-inline">ɭ</span> (ळ) is unusual among Indic languages
171
+ — Hindi doesn't have it. We added <span class="ipa-inline">ɭ</span> to
172
+ Kokoro's vocabulary at slot 144 (replacing an unused placeholder) so
173
+ the model could learn ळ cleanly during fine-tuning. Listen for it in
174
+ "केळी" (banana) or "सकाळ" (morning).
175
+ </aside>
176
+
177
+ <details class="details">
178
+ <summary>All phonemes + raw timings (debug)</summary>
179
+ <pre id="phonemes" class="phonemes"></pre>
180
+ <pre id="word-timings" class="word-timings"></pre>
181
+ </details>
182
+ </section>
183
+
184
+ <footer>
185
+ <p>
186
+ Model
187
+ <a
188
+ href="https://huggingface.co/shreyask/bol-tts-marathi-onnx"
189
+ target="_blank"
190
+ rel="noopener"
191
+ >shreyask/bol-tts-marathi-onnx</a
192
+ >
193
+ · recipe
194
+ <a
195
+ href="https://github.com/shreyaskarnik/bol-tts-marathi"
196
+ target="_blank"
197
+ rel="noopener"
198
+ >github.com/shreyaskarnik/bol-tts-marathi</a
199
+ >
200
+ · built on
201
+ <a
202
+ href="https://github.com/hexgrad/kokoro"
203
+ target="_blank"
204
+ rel="noopener"
205
+ >Kokoro-82M</a
206
+ >
207
+ via
208
+ <a
209
+ href="https://github.com/semidark/kokoro-deutsch"
210
+ target="_blank"
211
+ rel="noopener"
212
+ >kokoro-deutsch</a
213
+ >
214
+ </p>
215
+ </footer>
216
+ </main>
217
+
218
+ <div class="warli-strip warli-strip--bottom" aria-hidden="true">
219
+ <img src="/warli-strip.svg" alt="" />
220
+ </div>
221
+
222
+ <script type="module" src="/src/main.ts"></script>
223
+ </body>
224
+ </html>
package-lock.json ADDED
@@ -0,0 +1,2006 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "bol-tts-marathi-webgpu-demo",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "bol-tts-marathi-webgpu-demo",
9
+ "version": "0.1.0",
10
+ "license": "Apache-2.0",
11
+ "dependencies": {
12
+ "@huggingface/transformers": "4.2.0",
13
+ "@indic-transliteration/sanscript": "^1.3.3"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.6.3",
17
+ "vite": "^5.4.11"
18
+ }
19
+ },
20
+ "node_modules/@emnapi/runtime": {
21
+ "version": "1.10.0",
22
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
23
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
24
+ "license": "MIT",
25
+ "optional": true,
26
+ "dependencies": {
27
+ "tslib": "^2.4.0"
28
+ }
29
+ },
30
+ "node_modules/@esbuild/aix-ppc64": {
31
+ "version": "0.21.5",
32
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
33
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
34
+ "cpu": [
35
+ "ppc64"
36
+ ],
37
+ "dev": true,
38
+ "license": "MIT",
39
+ "optional": true,
40
+ "os": [
41
+ "aix"
42
+ ],
43
+ "engines": {
44
+ "node": ">=12"
45
+ }
46
+ },
47
+ "node_modules/@esbuild/android-arm": {
48
+ "version": "0.21.5",
49
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
50
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
51
+ "cpu": [
52
+ "arm"
53
+ ],
54
+ "dev": true,
55
+ "license": "MIT",
56
+ "optional": true,
57
+ "os": [
58
+ "android"
59
+ ],
60
+ "engines": {
61
+ "node": ">=12"
62
+ }
63
+ },
64
+ "node_modules/@esbuild/android-arm64": {
65
+ "version": "0.21.5",
66
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
67
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
68
+ "cpu": [
69
+ "arm64"
70
+ ],
71
+ "dev": true,
72
+ "license": "MIT",
73
+ "optional": true,
74
+ "os": [
75
+ "android"
76
+ ],
77
+ "engines": {
78
+ "node": ">=12"
79
+ }
80
+ },
81
+ "node_modules/@esbuild/android-x64": {
82
+ "version": "0.21.5",
83
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
84
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
85
+ "cpu": [
86
+ "x64"
87
+ ],
88
+ "dev": true,
89
+ "license": "MIT",
90
+ "optional": true,
91
+ "os": [
92
+ "android"
93
+ ],
94
+ "engines": {
95
+ "node": ">=12"
96
+ }
97
+ },
98
+ "node_modules/@esbuild/darwin-arm64": {
99
+ "version": "0.21.5",
100
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
101
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
102
+ "cpu": [
103
+ "arm64"
104
+ ],
105
+ "dev": true,
106
+ "license": "MIT",
107
+ "optional": true,
108
+ "os": [
109
+ "darwin"
110
+ ],
111
+ "engines": {
112
+ "node": ">=12"
113
+ }
114
+ },
115
+ "node_modules/@esbuild/darwin-x64": {
116
+ "version": "0.21.5",
117
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
118
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
119
+ "cpu": [
120
+ "x64"
121
+ ],
122
+ "dev": true,
123
+ "license": "MIT",
124
+ "optional": true,
125
+ "os": [
126
+ "darwin"
127
+ ],
128
+ "engines": {
129
+ "node": ">=12"
130
+ }
131
+ },
132
+ "node_modules/@esbuild/freebsd-arm64": {
133
+ "version": "0.21.5",
134
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
135
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
136
+ "cpu": [
137
+ "arm64"
138
+ ],
139
+ "dev": true,
140
+ "license": "MIT",
141
+ "optional": true,
142
+ "os": [
143
+ "freebsd"
144
+ ],
145
+ "engines": {
146
+ "node": ">=12"
147
+ }
148
+ },
149
+ "node_modules/@esbuild/freebsd-x64": {
150
+ "version": "0.21.5",
151
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
152
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
153
+ "cpu": [
154
+ "x64"
155
+ ],
156
+ "dev": true,
157
+ "license": "MIT",
158
+ "optional": true,
159
+ "os": [
160
+ "freebsd"
161
+ ],
162
+ "engines": {
163
+ "node": ">=12"
164
+ }
165
+ },
166
+ "node_modules/@esbuild/linux-arm": {
167
+ "version": "0.21.5",
168
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
169
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
170
+ "cpu": [
171
+ "arm"
172
+ ],
173
+ "dev": true,
174
+ "license": "MIT",
175
+ "optional": true,
176
+ "os": [
177
+ "linux"
178
+ ],
179
+ "engines": {
180
+ "node": ">=12"
181
+ }
182
+ },
183
+ "node_modules/@esbuild/linux-arm64": {
184
+ "version": "0.21.5",
185
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
186
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
187
+ "cpu": [
188
+ "arm64"
189
+ ],
190
+ "dev": true,
191
+ "license": "MIT",
192
+ "optional": true,
193
+ "os": [
194
+ "linux"
195
+ ],
196
+ "engines": {
197
+ "node": ">=12"
198
+ }
199
+ },
200
+ "node_modules/@esbuild/linux-ia32": {
201
+ "version": "0.21.5",
202
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
203
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
204
+ "cpu": [
205
+ "ia32"
206
+ ],
207
+ "dev": true,
208
+ "license": "MIT",
209
+ "optional": true,
210
+ "os": [
211
+ "linux"
212
+ ],
213
+ "engines": {
214
+ "node": ">=12"
215
+ }
216
+ },
217
+ "node_modules/@esbuild/linux-loong64": {
218
+ "version": "0.21.5",
219
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
220
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
221
+ "cpu": [
222
+ "loong64"
223
+ ],
224
+ "dev": true,
225
+ "license": "MIT",
226
+ "optional": true,
227
+ "os": [
228
+ "linux"
229
+ ],
230
+ "engines": {
231
+ "node": ">=12"
232
+ }
233
+ },
234
+ "node_modules/@esbuild/linux-mips64el": {
235
+ "version": "0.21.5",
236
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
237
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
238
+ "cpu": [
239
+ "mips64el"
240
+ ],
241
+ "dev": true,
242
+ "license": "MIT",
243
+ "optional": true,
244
+ "os": [
245
+ "linux"
246
+ ],
247
+ "engines": {
248
+ "node": ">=12"
249
+ }
250
+ },
251
+ "node_modules/@esbuild/linux-ppc64": {
252
+ "version": "0.21.5",
253
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
254
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
255
+ "cpu": [
256
+ "ppc64"
257
+ ],
258
+ "dev": true,
259
+ "license": "MIT",
260
+ "optional": true,
261
+ "os": [
262
+ "linux"
263
+ ],
264
+ "engines": {
265
+ "node": ">=12"
266
+ }
267
+ },
268
+ "node_modules/@esbuild/linux-riscv64": {
269
+ "version": "0.21.5",
270
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
271
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
272
+ "cpu": [
273
+ "riscv64"
274
+ ],
275
+ "dev": true,
276
+ "license": "MIT",
277
+ "optional": true,
278
+ "os": [
279
+ "linux"
280
+ ],
281
+ "engines": {
282
+ "node": ">=12"
283
+ }
284
+ },
285
+ "node_modules/@esbuild/linux-s390x": {
286
+ "version": "0.21.5",
287
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
288
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
289
+ "cpu": [
290
+ "s390x"
291
+ ],
292
+ "dev": true,
293
+ "license": "MIT",
294
+ "optional": true,
295
+ "os": [
296
+ "linux"
297
+ ],
298
+ "engines": {
299
+ "node": ">=12"
300
+ }
301
+ },
302
+ "node_modules/@esbuild/linux-x64": {
303
+ "version": "0.21.5",
304
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
305
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
306
+ "cpu": [
307
+ "x64"
308
+ ],
309
+ "dev": true,
310
+ "license": "MIT",
311
+ "optional": true,
312
+ "os": [
313
+ "linux"
314
+ ],
315
+ "engines": {
316
+ "node": ">=12"
317
+ }
318
+ },
319
+ "node_modules/@esbuild/netbsd-x64": {
320
+ "version": "0.21.5",
321
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
322
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
323
+ "cpu": [
324
+ "x64"
325
+ ],
326
+ "dev": true,
327
+ "license": "MIT",
328
+ "optional": true,
329
+ "os": [
330
+ "netbsd"
331
+ ],
332
+ "engines": {
333
+ "node": ">=12"
334
+ }
335
+ },
336
+ "node_modules/@esbuild/openbsd-x64": {
337
+ "version": "0.21.5",
338
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
339
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
340
+ "cpu": [
341
+ "x64"
342
+ ],
343
+ "dev": true,
344
+ "license": "MIT",
345
+ "optional": true,
346
+ "os": [
347
+ "openbsd"
348
+ ],
349
+ "engines": {
350
+ "node": ">=12"
351
+ }
352
+ },
353
+ "node_modules/@esbuild/sunos-x64": {
354
+ "version": "0.21.5",
355
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
356
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
357
+ "cpu": [
358
+ "x64"
359
+ ],
360
+ "dev": true,
361
+ "license": "MIT",
362
+ "optional": true,
363
+ "os": [
364
+ "sunos"
365
+ ],
366
+ "engines": {
367
+ "node": ">=12"
368
+ }
369
+ },
370
+ "node_modules/@esbuild/win32-arm64": {
371
+ "version": "0.21.5",
372
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
373
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
374
+ "cpu": [
375
+ "arm64"
376
+ ],
377
+ "dev": true,
378
+ "license": "MIT",
379
+ "optional": true,
380
+ "os": [
381
+ "win32"
382
+ ],
383
+ "engines": {
384
+ "node": ">=12"
385
+ }
386
+ },
387
+ "node_modules/@esbuild/win32-ia32": {
388
+ "version": "0.21.5",
389
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
390
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
391
+ "cpu": [
392
+ "ia32"
393
+ ],
394
+ "dev": true,
395
+ "license": "MIT",
396
+ "optional": true,
397
+ "os": [
398
+ "win32"
399
+ ],
400
+ "engines": {
401
+ "node": ">=12"
402
+ }
403
+ },
404
+ "node_modules/@esbuild/win32-x64": {
405
+ "version": "0.21.5",
406
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
407
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
408
+ "cpu": [
409
+ "x64"
410
+ ],
411
+ "dev": true,
412
+ "license": "MIT",
413
+ "optional": true,
414
+ "os": [
415
+ "win32"
416
+ ],
417
+ "engines": {
418
+ "node": ">=12"
419
+ }
420
+ },
421
+ "node_modules/@huggingface/jinja": {
422
+ "version": "0.5.8",
423
+ "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.8.tgz",
424
+ "integrity": "sha512-ZdElB7DPS7QQS8ZnFc5RPPtkg+eN11z8AmIZWAyes6pSbwXqiFB/POVevvm01begdSX1ho9Gxln/F6qlQMsuaA==",
425
+ "license": "MIT",
426
+ "engines": {
427
+ "node": ">=18"
428
+ }
429
+ },
430
+ "node_modules/@huggingface/tokenizers": {
431
+ "version": "0.1.3",
432
+ "resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.1.3.tgz",
433
+ "integrity": "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA==",
434
+ "license": "Apache-2.0"
435
+ },
436
+ "node_modules/@huggingface/transformers": {
437
+ "version": "4.2.0",
438
+ "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.2.0.tgz",
439
+ "integrity": "sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ==",
440
+ "license": "Apache-2.0",
441
+ "dependencies": {
442
+ "@huggingface/jinja": "^0.5.6",
443
+ "@huggingface/tokenizers": "^0.1.3",
444
+ "onnxruntime-node": "1.24.3",
445
+ "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c",
446
+ "sharp": "^0.34.5"
447
+ }
448
+ },
449
+ "node_modules/@img/colour": {
450
+ "version": "1.1.0",
451
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
452
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
453
+ "license": "MIT",
454
+ "engines": {
455
+ "node": ">=18"
456
+ }
457
+ },
458
+ "node_modules/@img/sharp-darwin-arm64": {
459
+ "version": "0.34.5",
460
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
461
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
462
+ "cpu": [
463
+ "arm64"
464
+ ],
465
+ "license": "Apache-2.0",
466
+ "optional": true,
467
+ "os": [
468
+ "darwin"
469
+ ],
470
+ "engines": {
471
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
472
+ },
473
+ "funding": {
474
+ "url": "https://opencollective.com/libvips"
475
+ },
476
+ "optionalDependencies": {
477
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
478
+ }
479
+ },
480
+ "node_modules/@img/sharp-darwin-x64": {
481
+ "version": "0.34.5",
482
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
483
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
484
+ "cpu": [
485
+ "x64"
486
+ ],
487
+ "license": "Apache-2.0",
488
+ "optional": true,
489
+ "os": [
490
+ "darwin"
491
+ ],
492
+ "engines": {
493
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
494
+ },
495
+ "funding": {
496
+ "url": "https://opencollective.com/libvips"
497
+ },
498
+ "optionalDependencies": {
499
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
500
+ }
501
+ },
502
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
503
+ "version": "1.2.4",
504
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
505
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
506
+ "cpu": [
507
+ "arm64"
508
+ ],
509
+ "license": "LGPL-3.0-or-later",
510
+ "optional": true,
511
+ "os": [
512
+ "darwin"
513
+ ],
514
+ "funding": {
515
+ "url": "https://opencollective.com/libvips"
516
+ }
517
+ },
518
+ "node_modules/@img/sharp-libvips-darwin-x64": {
519
+ "version": "1.2.4",
520
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
521
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
522
+ "cpu": [
523
+ "x64"
524
+ ],
525
+ "license": "LGPL-3.0-or-later",
526
+ "optional": true,
527
+ "os": [
528
+ "darwin"
529
+ ],
530
+ "funding": {
531
+ "url": "https://opencollective.com/libvips"
532
+ }
533
+ },
534
+ "node_modules/@img/sharp-libvips-linux-arm": {
535
+ "version": "1.2.4",
536
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
537
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
538
+ "cpu": [
539
+ "arm"
540
+ ],
541
+ "license": "LGPL-3.0-or-later",
542
+ "optional": true,
543
+ "os": [
544
+ "linux"
545
+ ],
546
+ "funding": {
547
+ "url": "https://opencollective.com/libvips"
548
+ }
549
+ },
550
+ "node_modules/@img/sharp-libvips-linux-arm64": {
551
+ "version": "1.2.4",
552
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
553
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
554
+ "cpu": [
555
+ "arm64"
556
+ ],
557
+ "license": "LGPL-3.0-or-later",
558
+ "optional": true,
559
+ "os": [
560
+ "linux"
561
+ ],
562
+ "funding": {
563
+ "url": "https://opencollective.com/libvips"
564
+ }
565
+ },
566
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
567
+ "version": "1.2.4",
568
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
569
+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
570
+ "cpu": [
571
+ "ppc64"
572
+ ],
573
+ "license": "LGPL-3.0-or-later",
574
+ "optional": true,
575
+ "os": [
576
+ "linux"
577
+ ],
578
+ "funding": {
579
+ "url": "https://opencollective.com/libvips"
580
+ }
581
+ },
582
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
583
+ "version": "1.2.4",
584
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
585
+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
586
+ "cpu": [
587
+ "riscv64"
588
+ ],
589
+ "license": "LGPL-3.0-or-later",
590
+ "optional": true,
591
+ "os": [
592
+ "linux"
593
+ ],
594
+ "funding": {
595
+ "url": "https://opencollective.com/libvips"
596
+ }
597
+ },
598
+ "node_modules/@img/sharp-libvips-linux-s390x": {
599
+ "version": "1.2.4",
600
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
601
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
602
+ "cpu": [
603
+ "s390x"
604
+ ],
605
+ "license": "LGPL-3.0-or-later",
606
+ "optional": true,
607
+ "os": [
608
+ "linux"
609
+ ],
610
+ "funding": {
611
+ "url": "https://opencollective.com/libvips"
612
+ }
613
+ },
614
+ "node_modules/@img/sharp-libvips-linux-x64": {
615
+ "version": "1.2.4",
616
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
617
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
618
+ "cpu": [
619
+ "x64"
620
+ ],
621
+ "license": "LGPL-3.0-or-later",
622
+ "optional": true,
623
+ "os": [
624
+ "linux"
625
+ ],
626
+ "funding": {
627
+ "url": "https://opencollective.com/libvips"
628
+ }
629
+ },
630
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
631
+ "version": "1.2.4",
632
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
633
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
634
+ "cpu": [
635
+ "arm64"
636
+ ],
637
+ "license": "LGPL-3.0-or-later",
638
+ "optional": true,
639
+ "os": [
640
+ "linux"
641
+ ],
642
+ "funding": {
643
+ "url": "https://opencollective.com/libvips"
644
+ }
645
+ },
646
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
647
+ "version": "1.2.4",
648
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
649
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
650
+ "cpu": [
651
+ "x64"
652
+ ],
653
+ "license": "LGPL-3.0-or-later",
654
+ "optional": true,
655
+ "os": [
656
+ "linux"
657
+ ],
658
+ "funding": {
659
+ "url": "https://opencollective.com/libvips"
660
+ }
661
+ },
662
+ "node_modules/@img/sharp-linux-arm": {
663
+ "version": "0.34.5",
664
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
665
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
666
+ "cpu": [
667
+ "arm"
668
+ ],
669
+ "license": "Apache-2.0",
670
+ "optional": true,
671
+ "os": [
672
+ "linux"
673
+ ],
674
+ "engines": {
675
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
676
+ },
677
+ "funding": {
678
+ "url": "https://opencollective.com/libvips"
679
+ },
680
+ "optionalDependencies": {
681
+ "@img/sharp-libvips-linux-arm": "1.2.4"
682
+ }
683
+ },
684
+ "node_modules/@img/sharp-linux-arm64": {
685
+ "version": "0.34.5",
686
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
687
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
688
+ "cpu": [
689
+ "arm64"
690
+ ],
691
+ "license": "Apache-2.0",
692
+ "optional": true,
693
+ "os": [
694
+ "linux"
695
+ ],
696
+ "engines": {
697
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
698
+ },
699
+ "funding": {
700
+ "url": "https://opencollective.com/libvips"
701
+ },
702
+ "optionalDependencies": {
703
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
704
+ }
705
+ },
706
+ "node_modules/@img/sharp-linux-ppc64": {
707
+ "version": "0.34.5",
708
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
709
+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
710
+ "cpu": [
711
+ "ppc64"
712
+ ],
713
+ "license": "Apache-2.0",
714
+ "optional": true,
715
+ "os": [
716
+ "linux"
717
+ ],
718
+ "engines": {
719
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
720
+ },
721
+ "funding": {
722
+ "url": "https://opencollective.com/libvips"
723
+ },
724
+ "optionalDependencies": {
725
+ "@img/sharp-libvips-linux-ppc64": "1.2.4"
726
+ }
727
+ },
728
+ "node_modules/@img/sharp-linux-riscv64": {
729
+ "version": "0.34.5",
730
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
731
+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
732
+ "cpu": [
733
+ "riscv64"
734
+ ],
735
+ "license": "Apache-2.0",
736
+ "optional": true,
737
+ "os": [
738
+ "linux"
739
+ ],
740
+ "engines": {
741
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
742
+ },
743
+ "funding": {
744
+ "url": "https://opencollective.com/libvips"
745
+ },
746
+ "optionalDependencies": {
747
+ "@img/sharp-libvips-linux-riscv64": "1.2.4"
748
+ }
749
+ },
750
+ "node_modules/@img/sharp-linux-s390x": {
751
+ "version": "0.34.5",
752
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
753
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
754
+ "cpu": [
755
+ "s390x"
756
+ ],
757
+ "license": "Apache-2.0",
758
+ "optional": true,
759
+ "os": [
760
+ "linux"
761
+ ],
762
+ "engines": {
763
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
764
+ },
765
+ "funding": {
766
+ "url": "https://opencollective.com/libvips"
767
+ },
768
+ "optionalDependencies": {
769
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
770
+ }
771
+ },
772
+ "node_modules/@img/sharp-linux-x64": {
773
+ "version": "0.34.5",
774
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
775
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
776
+ "cpu": [
777
+ "x64"
778
+ ],
779
+ "license": "Apache-2.0",
780
+ "optional": true,
781
+ "os": [
782
+ "linux"
783
+ ],
784
+ "engines": {
785
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
786
+ },
787
+ "funding": {
788
+ "url": "https://opencollective.com/libvips"
789
+ },
790
+ "optionalDependencies": {
791
+ "@img/sharp-libvips-linux-x64": "1.2.4"
792
+ }
793
+ },
794
+ "node_modules/@img/sharp-linuxmusl-arm64": {
795
+ "version": "0.34.5",
796
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
797
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
798
+ "cpu": [
799
+ "arm64"
800
+ ],
801
+ "license": "Apache-2.0",
802
+ "optional": true,
803
+ "os": [
804
+ "linux"
805
+ ],
806
+ "engines": {
807
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
808
+ },
809
+ "funding": {
810
+ "url": "https://opencollective.com/libvips"
811
+ },
812
+ "optionalDependencies": {
813
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
814
+ }
815
+ },
816
+ "node_modules/@img/sharp-linuxmusl-x64": {
817
+ "version": "0.34.5",
818
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
819
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
820
+ "cpu": [
821
+ "x64"
822
+ ],
823
+ "license": "Apache-2.0",
824
+ "optional": true,
825
+ "os": [
826
+ "linux"
827
+ ],
828
+ "engines": {
829
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
830
+ },
831
+ "funding": {
832
+ "url": "https://opencollective.com/libvips"
833
+ },
834
+ "optionalDependencies": {
835
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
836
+ }
837
+ },
838
+ "node_modules/@img/sharp-wasm32": {
839
+ "version": "0.34.5",
840
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
841
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
842
+ "cpu": [
843
+ "wasm32"
844
+ ],
845
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
846
+ "optional": true,
847
+ "dependencies": {
848
+ "@emnapi/runtime": "^1.7.0"
849
+ },
850
+ "engines": {
851
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
852
+ },
853
+ "funding": {
854
+ "url": "https://opencollective.com/libvips"
855
+ }
856
+ },
857
+ "node_modules/@img/sharp-win32-arm64": {
858
+ "version": "0.34.5",
859
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
860
+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
861
+ "cpu": [
862
+ "arm64"
863
+ ],
864
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
865
+ "optional": true,
866
+ "os": [
867
+ "win32"
868
+ ],
869
+ "engines": {
870
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
871
+ },
872
+ "funding": {
873
+ "url": "https://opencollective.com/libvips"
874
+ }
875
+ },
876
+ "node_modules/@img/sharp-win32-ia32": {
877
+ "version": "0.34.5",
878
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
879
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
880
+ "cpu": [
881
+ "ia32"
882
+ ],
883
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
884
+ "optional": true,
885
+ "os": [
886
+ "win32"
887
+ ],
888
+ "engines": {
889
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
890
+ },
891
+ "funding": {
892
+ "url": "https://opencollective.com/libvips"
893
+ }
894
+ },
895
+ "node_modules/@img/sharp-win32-x64": {
896
+ "version": "0.34.5",
897
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
898
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
899
+ "cpu": [
900
+ "x64"
901
+ ],
902
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
903
+ "optional": true,
904
+ "os": [
905
+ "win32"
906
+ ],
907
+ "engines": {
908
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
909
+ },
910
+ "funding": {
911
+ "url": "https://opencollective.com/libvips"
912
+ }
913
+ },
914
+ "node_modules/@indic-transliteration/common_maps": {
915
+ "version": "1.0.6",
916
+ "resolved": "https://registry.npmjs.org/@indic-transliteration/common_maps/-/common_maps-1.0.6.tgz",
917
+ "integrity": "sha512-OYX+SrpK26FjVZNphOdsQc9Ph0yFDtoWsU7INjhEhhWvGqzxF3Mqi0fF0buUsB4jpRWJe+7U+u9PO4fsTP/lfg==",
918
+ "license": "MIT"
919
+ },
920
+ "node_modules/@indic-transliteration/sanscript": {
921
+ "version": "1.3.3",
922
+ "resolved": "https://registry.npmjs.org/@indic-transliteration/sanscript/-/sanscript-1.3.3.tgz",
923
+ "integrity": "sha512-zNGeARmQTPIlubwgEhl/JumpwTPHrdT/cNsQeCL+G67SQmjJe3qRnMIYghXiVt7+KDso/pU1Ky2ZfD/RBISfJQ==",
924
+ "license": "MIT",
925
+ "dependencies": {
926
+ "@indic-transliteration/common_maps": "^1.0.2",
927
+ "toml": "^2.3.6"
928
+ }
929
+ },
930
+ "node_modules/@protobufjs/aspromise": {
931
+ "version": "1.1.2",
932
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
933
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
934
+ "license": "BSD-3-Clause"
935
+ },
936
+ "node_modules/@protobufjs/base64": {
937
+ "version": "1.1.2",
938
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
939
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
940
+ "license": "BSD-3-Clause"
941
+ },
942
+ "node_modules/@protobufjs/codegen": {
943
+ "version": "2.0.4",
944
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
945
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
946
+ "license": "BSD-3-Clause"
947
+ },
948
+ "node_modules/@protobufjs/eventemitter": {
949
+ "version": "1.1.0",
950
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
951
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
952
+ "license": "BSD-3-Clause"
953
+ },
954
+ "node_modules/@protobufjs/fetch": {
955
+ "version": "1.1.0",
956
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
957
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
958
+ "license": "BSD-3-Clause",
959
+ "dependencies": {
960
+ "@protobufjs/aspromise": "^1.1.1",
961
+ "@protobufjs/inquire": "^1.1.0"
962
+ }
963
+ },
964
+ "node_modules/@protobufjs/float": {
965
+ "version": "1.0.2",
966
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
967
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
968
+ "license": "BSD-3-Clause"
969
+ },
970
+ "node_modules/@protobufjs/inquire": {
971
+ "version": "1.1.0",
972
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
973
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
974
+ "license": "BSD-3-Clause"
975
+ },
976
+ "node_modules/@protobufjs/path": {
977
+ "version": "1.1.2",
978
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
979
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
980
+ "license": "BSD-3-Clause"
981
+ },
982
+ "node_modules/@protobufjs/pool": {
983
+ "version": "1.1.0",
984
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
985
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
986
+ "license": "BSD-3-Clause"
987
+ },
988
+ "node_modules/@protobufjs/utf8": {
989
+ "version": "1.1.0",
990
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
991
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
992
+ "license": "BSD-3-Clause"
993
+ },
994
+ "node_modules/@rollup/rollup-android-arm-eabi": {
995
+ "version": "4.60.2",
996
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
997
+ "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
998
+ "cpu": [
999
+ "arm"
1000
+ ],
1001
+ "dev": true,
1002
+ "license": "MIT",
1003
+ "optional": true,
1004
+ "os": [
1005
+ "android"
1006
+ ]
1007
+ },
1008
+ "node_modules/@rollup/rollup-android-arm64": {
1009
+ "version": "4.60.2",
1010
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
1011
+ "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
1012
+ "cpu": [
1013
+ "arm64"
1014
+ ],
1015
+ "dev": true,
1016
+ "license": "MIT",
1017
+ "optional": true,
1018
+ "os": [
1019
+ "android"
1020
+ ]
1021
+ },
1022
+ "node_modules/@rollup/rollup-darwin-arm64": {
1023
+ "version": "4.60.2",
1024
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
1025
+ "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
1026
+ "cpu": [
1027
+ "arm64"
1028
+ ],
1029
+ "dev": true,
1030
+ "license": "MIT",
1031
+ "optional": true,
1032
+ "os": [
1033
+ "darwin"
1034
+ ]
1035
+ },
1036
+ "node_modules/@rollup/rollup-darwin-x64": {
1037
+ "version": "4.60.2",
1038
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
1039
+ "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
1040
+ "cpu": [
1041
+ "x64"
1042
+ ],
1043
+ "dev": true,
1044
+ "license": "MIT",
1045
+ "optional": true,
1046
+ "os": [
1047
+ "darwin"
1048
+ ]
1049
+ },
1050
+ "node_modules/@rollup/rollup-freebsd-arm64": {
1051
+ "version": "4.60.2",
1052
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
1053
+ "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
1054
+ "cpu": [
1055
+ "arm64"
1056
+ ],
1057
+ "dev": true,
1058
+ "license": "MIT",
1059
+ "optional": true,
1060
+ "os": [
1061
+ "freebsd"
1062
+ ]
1063
+ },
1064
+ "node_modules/@rollup/rollup-freebsd-x64": {
1065
+ "version": "4.60.2",
1066
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
1067
+ "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
1068
+ "cpu": [
1069
+ "x64"
1070
+ ],
1071
+ "dev": true,
1072
+ "license": "MIT",
1073
+ "optional": true,
1074
+ "os": [
1075
+ "freebsd"
1076
+ ]
1077
+ },
1078
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
1079
+ "version": "4.60.2",
1080
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
1081
+ "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
1082
+ "cpu": [
1083
+ "arm"
1084
+ ],
1085
+ "dev": true,
1086
+ "license": "MIT",
1087
+ "optional": true,
1088
+ "os": [
1089
+ "linux"
1090
+ ]
1091
+ },
1092
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
1093
+ "version": "4.60.2",
1094
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
1095
+ "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
1096
+ "cpu": [
1097
+ "arm"
1098
+ ],
1099
+ "dev": true,
1100
+ "license": "MIT",
1101
+ "optional": true,
1102
+ "os": [
1103
+ "linux"
1104
+ ]
1105
+ },
1106
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
1107
+ "version": "4.60.2",
1108
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
1109
+ "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
1110
+ "cpu": [
1111
+ "arm64"
1112
+ ],
1113
+ "dev": true,
1114
+ "license": "MIT",
1115
+ "optional": true,
1116
+ "os": [
1117
+ "linux"
1118
+ ]
1119
+ },
1120
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
1121
+ "version": "4.60.2",
1122
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
1123
+ "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
1124
+ "cpu": [
1125
+ "arm64"
1126
+ ],
1127
+ "dev": true,
1128
+ "license": "MIT",
1129
+ "optional": true,
1130
+ "os": [
1131
+ "linux"
1132
+ ]
1133
+ },
1134
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
1135
+ "version": "4.60.2",
1136
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
1137
+ "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
1138
+ "cpu": [
1139
+ "loong64"
1140
+ ],
1141
+ "dev": true,
1142
+ "license": "MIT",
1143
+ "optional": true,
1144
+ "os": [
1145
+ "linux"
1146
+ ]
1147
+ },
1148
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
1149
+ "version": "4.60.2",
1150
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
1151
+ "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
1152
+ "cpu": [
1153
+ "loong64"
1154
+ ],
1155
+ "dev": true,
1156
+ "license": "MIT",
1157
+ "optional": true,
1158
+ "os": [
1159
+ "linux"
1160
+ ]
1161
+ },
1162
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
1163
+ "version": "4.60.2",
1164
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
1165
+ "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
1166
+ "cpu": [
1167
+ "ppc64"
1168
+ ],
1169
+ "dev": true,
1170
+ "license": "MIT",
1171
+ "optional": true,
1172
+ "os": [
1173
+ "linux"
1174
+ ]
1175
+ },
1176
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
1177
+ "version": "4.60.2",
1178
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
1179
+ "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
1180
+ "cpu": [
1181
+ "ppc64"
1182
+ ],
1183
+ "dev": true,
1184
+ "license": "MIT",
1185
+ "optional": true,
1186
+ "os": [
1187
+ "linux"
1188
+ ]
1189
+ },
1190
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1191
+ "version": "4.60.2",
1192
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
1193
+ "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
1194
+ "cpu": [
1195
+ "riscv64"
1196
+ ],
1197
+ "dev": true,
1198
+ "license": "MIT",
1199
+ "optional": true,
1200
+ "os": [
1201
+ "linux"
1202
+ ]
1203
+ },
1204
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
1205
+ "version": "4.60.2",
1206
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
1207
+ "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
1208
+ "cpu": [
1209
+ "riscv64"
1210
+ ],
1211
+ "dev": true,
1212
+ "license": "MIT",
1213
+ "optional": true,
1214
+ "os": [
1215
+ "linux"
1216
+ ]
1217
+ },
1218
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
1219
+ "version": "4.60.2",
1220
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
1221
+ "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
1222
+ "cpu": [
1223
+ "s390x"
1224
+ ],
1225
+ "dev": true,
1226
+ "license": "MIT",
1227
+ "optional": true,
1228
+ "os": [
1229
+ "linux"
1230
+ ]
1231
+ },
1232
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
1233
+ "version": "4.60.2",
1234
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
1235
+ "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
1236
+ "cpu": [
1237
+ "x64"
1238
+ ],
1239
+ "dev": true,
1240
+ "license": "MIT",
1241
+ "optional": true,
1242
+ "os": [
1243
+ "linux"
1244
+ ]
1245
+ },
1246
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1247
+ "version": "4.60.2",
1248
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
1249
+ "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
1250
+ "cpu": [
1251
+ "x64"
1252
+ ],
1253
+ "dev": true,
1254
+ "license": "MIT",
1255
+ "optional": true,
1256
+ "os": [
1257
+ "linux"
1258
+ ]
1259
+ },
1260
+ "node_modules/@rollup/rollup-openbsd-x64": {
1261
+ "version": "4.60.2",
1262
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
1263
+ "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
1264
+ "cpu": [
1265
+ "x64"
1266
+ ],
1267
+ "dev": true,
1268
+ "license": "MIT",
1269
+ "optional": true,
1270
+ "os": [
1271
+ "openbsd"
1272
+ ]
1273
+ },
1274
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1275
+ "version": "4.60.2",
1276
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
1277
+ "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
1278
+ "cpu": [
1279
+ "arm64"
1280
+ ],
1281
+ "dev": true,
1282
+ "license": "MIT",
1283
+ "optional": true,
1284
+ "os": [
1285
+ "openharmony"
1286
+ ]
1287
+ },
1288
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1289
+ "version": "4.60.2",
1290
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
1291
+ "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
1292
+ "cpu": [
1293
+ "arm64"
1294
+ ],
1295
+ "dev": true,
1296
+ "license": "MIT",
1297
+ "optional": true,
1298
+ "os": [
1299
+ "win32"
1300
+ ]
1301
+ },
1302
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1303
+ "version": "4.60.2",
1304
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
1305
+ "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
1306
+ "cpu": [
1307
+ "ia32"
1308
+ ],
1309
+ "dev": true,
1310
+ "license": "MIT",
1311
+ "optional": true,
1312
+ "os": [
1313
+ "win32"
1314
+ ]
1315
+ },
1316
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1317
+ "version": "4.60.2",
1318
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
1319
+ "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
1320
+ "cpu": [
1321
+ "x64"
1322
+ ],
1323
+ "dev": true,
1324
+ "license": "MIT",
1325
+ "optional": true,
1326
+ "os": [
1327
+ "win32"
1328
+ ]
1329
+ },
1330
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1331
+ "version": "4.60.2",
1332
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
1333
+ "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
1334
+ "cpu": [
1335
+ "x64"
1336
+ ],
1337
+ "dev": true,
1338
+ "license": "MIT",
1339
+ "optional": true,
1340
+ "os": [
1341
+ "win32"
1342
+ ]
1343
+ },
1344
+ "node_modules/@types/estree": {
1345
+ "version": "1.0.8",
1346
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1347
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1348
+ "dev": true,
1349
+ "license": "MIT"
1350
+ },
1351
+ "node_modules/@types/node": {
1352
+ "version": "25.6.0",
1353
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
1354
+ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
1355
+ "license": "MIT",
1356
+ "dependencies": {
1357
+ "undici-types": "~7.19.0"
1358
+ }
1359
+ },
1360
+ "node_modules/adm-zip": {
1361
+ "version": "0.5.17",
1362
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz",
1363
+ "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==",
1364
+ "license": "MIT",
1365
+ "engines": {
1366
+ "node": ">=12.0"
1367
+ }
1368
+ },
1369
+ "node_modules/boolean": {
1370
+ "version": "3.2.0",
1371
+ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
1372
+ "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
1373
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
1374
+ "license": "MIT"
1375
+ },
1376
+ "node_modules/define-data-property": {
1377
+ "version": "1.1.4",
1378
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
1379
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
1380
+ "license": "MIT",
1381
+ "dependencies": {
1382
+ "es-define-property": "^1.0.0",
1383
+ "es-errors": "^1.3.0",
1384
+ "gopd": "^1.0.1"
1385
+ },
1386
+ "engines": {
1387
+ "node": ">= 0.4"
1388
+ },
1389
+ "funding": {
1390
+ "url": "https://github.com/sponsors/ljharb"
1391
+ }
1392
+ },
1393
+ "node_modules/define-properties": {
1394
+ "version": "1.2.1",
1395
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
1396
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
1397
+ "license": "MIT",
1398
+ "dependencies": {
1399
+ "define-data-property": "^1.0.1",
1400
+ "has-property-descriptors": "^1.0.0",
1401
+ "object-keys": "^1.1.1"
1402
+ },
1403
+ "engines": {
1404
+ "node": ">= 0.4"
1405
+ },
1406
+ "funding": {
1407
+ "url": "https://github.com/sponsors/ljharb"
1408
+ }
1409
+ },
1410
+ "node_modules/detect-libc": {
1411
+ "version": "2.1.2",
1412
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1413
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1414
+ "license": "Apache-2.0",
1415
+ "engines": {
1416
+ "node": ">=8"
1417
+ }
1418
+ },
1419
+ "node_modules/detect-node": {
1420
+ "version": "2.1.0",
1421
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
1422
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
1423
+ "license": "MIT"
1424
+ },
1425
+ "node_modules/es-define-property": {
1426
+ "version": "1.0.1",
1427
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
1428
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
1429
+ "license": "MIT",
1430
+ "engines": {
1431
+ "node": ">= 0.4"
1432
+ }
1433
+ },
1434
+ "node_modules/es-errors": {
1435
+ "version": "1.3.0",
1436
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1437
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1438
+ "license": "MIT",
1439
+ "engines": {
1440
+ "node": ">= 0.4"
1441
+ }
1442
+ },
1443
+ "node_modules/es6-error": {
1444
+ "version": "4.1.1",
1445
+ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
1446
+ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
1447
+ "license": "MIT"
1448
+ },
1449
+ "node_modules/esbuild": {
1450
+ "version": "0.21.5",
1451
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1452
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1453
+ "dev": true,
1454
+ "hasInstallScript": true,
1455
+ "license": "MIT",
1456
+ "bin": {
1457
+ "esbuild": "bin/esbuild"
1458
+ },
1459
+ "engines": {
1460
+ "node": ">=12"
1461
+ },
1462
+ "optionalDependencies": {
1463
+ "@esbuild/aix-ppc64": "0.21.5",
1464
+ "@esbuild/android-arm": "0.21.5",
1465
+ "@esbuild/android-arm64": "0.21.5",
1466
+ "@esbuild/android-x64": "0.21.5",
1467
+ "@esbuild/darwin-arm64": "0.21.5",
1468
+ "@esbuild/darwin-x64": "0.21.5",
1469
+ "@esbuild/freebsd-arm64": "0.21.5",
1470
+ "@esbuild/freebsd-x64": "0.21.5",
1471
+ "@esbuild/linux-arm": "0.21.5",
1472
+ "@esbuild/linux-arm64": "0.21.5",
1473
+ "@esbuild/linux-ia32": "0.21.5",
1474
+ "@esbuild/linux-loong64": "0.21.5",
1475
+ "@esbuild/linux-mips64el": "0.21.5",
1476
+ "@esbuild/linux-ppc64": "0.21.5",
1477
+ "@esbuild/linux-riscv64": "0.21.5",
1478
+ "@esbuild/linux-s390x": "0.21.5",
1479
+ "@esbuild/linux-x64": "0.21.5",
1480
+ "@esbuild/netbsd-x64": "0.21.5",
1481
+ "@esbuild/openbsd-x64": "0.21.5",
1482
+ "@esbuild/sunos-x64": "0.21.5",
1483
+ "@esbuild/win32-arm64": "0.21.5",
1484
+ "@esbuild/win32-ia32": "0.21.5",
1485
+ "@esbuild/win32-x64": "0.21.5"
1486
+ }
1487
+ },
1488
+ "node_modules/escape-string-regexp": {
1489
+ "version": "4.0.0",
1490
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
1491
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
1492
+ "license": "MIT",
1493
+ "engines": {
1494
+ "node": ">=10"
1495
+ },
1496
+ "funding": {
1497
+ "url": "https://github.com/sponsors/sindresorhus"
1498
+ }
1499
+ },
1500
+ "node_modules/flatbuffers": {
1501
+ "version": "25.9.23",
1502
+ "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz",
1503
+ "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==",
1504
+ "license": "Apache-2.0"
1505
+ },
1506
+ "node_modules/fsevents": {
1507
+ "version": "2.3.3",
1508
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1509
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1510
+ "dev": true,
1511
+ "hasInstallScript": true,
1512
+ "license": "MIT",
1513
+ "optional": true,
1514
+ "os": [
1515
+ "darwin"
1516
+ ],
1517
+ "engines": {
1518
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1519
+ }
1520
+ },
1521
+ "node_modules/global-agent": {
1522
+ "version": "3.0.0",
1523
+ "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
1524
+ "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
1525
+ "license": "BSD-3-Clause",
1526
+ "dependencies": {
1527
+ "boolean": "^3.0.1",
1528
+ "es6-error": "^4.1.1",
1529
+ "matcher": "^3.0.0",
1530
+ "roarr": "^2.15.3",
1531
+ "semver": "^7.3.2",
1532
+ "serialize-error": "^7.0.1"
1533
+ },
1534
+ "engines": {
1535
+ "node": ">=10.0"
1536
+ }
1537
+ },
1538
+ "node_modules/globalthis": {
1539
+ "version": "1.0.4",
1540
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
1541
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
1542
+ "license": "MIT",
1543
+ "dependencies": {
1544
+ "define-properties": "^1.2.1",
1545
+ "gopd": "^1.0.1"
1546
+ },
1547
+ "engines": {
1548
+ "node": ">= 0.4"
1549
+ },
1550
+ "funding": {
1551
+ "url": "https://github.com/sponsors/ljharb"
1552
+ }
1553
+ },
1554
+ "node_modules/gopd": {
1555
+ "version": "1.2.0",
1556
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1557
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1558
+ "license": "MIT",
1559
+ "engines": {
1560
+ "node": ">= 0.4"
1561
+ },
1562
+ "funding": {
1563
+ "url": "https://github.com/sponsors/ljharb"
1564
+ }
1565
+ },
1566
+ "node_modules/guid-typescript": {
1567
+ "version": "1.0.9",
1568
+ "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
1569
+ "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
1570
+ "license": "ISC"
1571
+ },
1572
+ "node_modules/has-property-descriptors": {
1573
+ "version": "1.0.2",
1574
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
1575
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
1576
+ "license": "MIT",
1577
+ "dependencies": {
1578
+ "es-define-property": "^1.0.0"
1579
+ },
1580
+ "funding": {
1581
+ "url": "https://github.com/sponsors/ljharb"
1582
+ }
1583
+ },
1584
+ "node_modules/json-stringify-safe": {
1585
+ "version": "5.0.1",
1586
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
1587
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
1588
+ "license": "ISC"
1589
+ },
1590
+ "node_modules/long": {
1591
+ "version": "5.3.2",
1592
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
1593
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
1594
+ "license": "Apache-2.0"
1595
+ },
1596
+ "node_modules/matcher": {
1597
+ "version": "3.0.0",
1598
+ "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
1599
+ "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
1600
+ "license": "MIT",
1601
+ "dependencies": {
1602
+ "escape-string-regexp": "^4.0.0"
1603
+ },
1604
+ "engines": {
1605
+ "node": ">=10"
1606
+ }
1607
+ },
1608
+ "node_modules/nanoid": {
1609
+ "version": "3.3.11",
1610
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1611
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1612
+ "dev": true,
1613
+ "funding": [
1614
+ {
1615
+ "type": "github",
1616
+ "url": "https://github.com/sponsors/ai"
1617
+ }
1618
+ ],
1619
+ "license": "MIT",
1620
+ "bin": {
1621
+ "nanoid": "bin/nanoid.cjs"
1622
+ },
1623
+ "engines": {
1624
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1625
+ }
1626
+ },
1627
+ "node_modules/object-keys": {
1628
+ "version": "1.1.1",
1629
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
1630
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
1631
+ "license": "MIT",
1632
+ "engines": {
1633
+ "node": ">= 0.4"
1634
+ }
1635
+ },
1636
+ "node_modules/onnxruntime-common": {
1637
+ "version": "1.24.3",
1638
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz",
1639
+ "integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==",
1640
+ "license": "MIT"
1641
+ },
1642
+ "node_modules/onnxruntime-node": {
1643
+ "version": "1.24.3",
1644
+ "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.24.3.tgz",
1645
+ "integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==",
1646
+ "hasInstallScript": true,
1647
+ "license": "MIT",
1648
+ "os": [
1649
+ "win32",
1650
+ "darwin",
1651
+ "linux"
1652
+ ],
1653
+ "dependencies": {
1654
+ "adm-zip": "^0.5.16",
1655
+ "global-agent": "^3.0.0",
1656
+ "onnxruntime-common": "1.24.3"
1657
+ }
1658
+ },
1659
+ "node_modules/onnxruntime-web": {
1660
+ "version": "1.26.0-dev.20260416-b7804b056c",
1661
+ "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.26.0-dev.20260416-b7804b056c.tgz",
1662
+ "integrity": "sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw==",
1663
+ "license": "MIT",
1664
+ "dependencies": {
1665
+ "flatbuffers": "^25.1.24",
1666
+ "guid-typescript": "^1.0.9",
1667
+ "long": "^5.2.3",
1668
+ "onnxruntime-common": "1.24.0-dev.20251116-b39e144322",
1669
+ "platform": "^1.3.6",
1670
+ "protobufjs": "^7.2.4"
1671
+ }
1672
+ },
1673
+ "node_modules/onnxruntime-web/node_modules/onnxruntime-common": {
1674
+ "version": "1.24.0-dev.20251116-b39e144322",
1675
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.0-dev.20251116-b39e144322.tgz",
1676
+ "integrity": "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==",
1677
+ "license": "MIT"
1678
+ },
1679
+ "node_modules/picocolors": {
1680
+ "version": "1.1.1",
1681
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1682
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1683
+ "dev": true,
1684
+ "license": "ISC"
1685
+ },
1686
+ "node_modules/platform": {
1687
+ "version": "1.3.6",
1688
+ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
1689
+ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
1690
+ "license": "MIT"
1691
+ },
1692
+ "node_modules/postcss": {
1693
+ "version": "8.5.10",
1694
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
1695
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
1696
+ "dev": true,
1697
+ "funding": [
1698
+ {
1699
+ "type": "opencollective",
1700
+ "url": "https://opencollective.com/postcss/"
1701
+ },
1702
+ {
1703
+ "type": "tidelift",
1704
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1705
+ },
1706
+ {
1707
+ "type": "github",
1708
+ "url": "https://github.com/sponsors/ai"
1709
+ }
1710
+ ],
1711
+ "license": "MIT",
1712
+ "dependencies": {
1713
+ "nanoid": "^3.3.11",
1714
+ "picocolors": "^1.1.1",
1715
+ "source-map-js": "^1.2.1"
1716
+ },
1717
+ "engines": {
1718
+ "node": "^10 || ^12 || >=14"
1719
+ }
1720
+ },
1721
+ "node_modules/protobufjs": {
1722
+ "version": "7.5.5",
1723
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz",
1724
+ "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==",
1725
+ "hasInstallScript": true,
1726
+ "license": "BSD-3-Clause",
1727
+ "dependencies": {
1728
+ "@protobufjs/aspromise": "^1.1.2",
1729
+ "@protobufjs/base64": "^1.1.2",
1730
+ "@protobufjs/codegen": "^2.0.4",
1731
+ "@protobufjs/eventemitter": "^1.1.0",
1732
+ "@protobufjs/fetch": "^1.1.0",
1733
+ "@protobufjs/float": "^1.0.2",
1734
+ "@protobufjs/inquire": "^1.1.0",
1735
+ "@protobufjs/path": "^1.1.2",
1736
+ "@protobufjs/pool": "^1.1.0",
1737
+ "@protobufjs/utf8": "^1.1.0",
1738
+ "@types/node": ">=13.7.0",
1739
+ "long": "^5.0.0"
1740
+ },
1741
+ "engines": {
1742
+ "node": ">=12.0.0"
1743
+ }
1744
+ },
1745
+ "node_modules/roarr": {
1746
+ "version": "2.15.4",
1747
+ "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
1748
+ "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
1749
+ "license": "BSD-3-Clause",
1750
+ "dependencies": {
1751
+ "boolean": "^3.0.1",
1752
+ "detect-node": "^2.0.4",
1753
+ "globalthis": "^1.0.1",
1754
+ "json-stringify-safe": "^5.0.1",
1755
+ "semver-compare": "^1.0.0",
1756
+ "sprintf-js": "^1.1.2"
1757
+ },
1758
+ "engines": {
1759
+ "node": ">=8.0"
1760
+ }
1761
+ },
1762
+ "node_modules/rollup": {
1763
+ "version": "4.60.2",
1764
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
1765
+ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
1766
+ "dev": true,
1767
+ "license": "MIT",
1768
+ "dependencies": {
1769
+ "@types/estree": "1.0.8"
1770
+ },
1771
+ "bin": {
1772
+ "rollup": "dist/bin/rollup"
1773
+ },
1774
+ "engines": {
1775
+ "node": ">=18.0.0",
1776
+ "npm": ">=8.0.0"
1777
+ },
1778
+ "optionalDependencies": {
1779
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
1780
+ "@rollup/rollup-android-arm64": "4.60.2",
1781
+ "@rollup/rollup-darwin-arm64": "4.60.2",
1782
+ "@rollup/rollup-darwin-x64": "4.60.2",
1783
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
1784
+ "@rollup/rollup-freebsd-x64": "4.60.2",
1785
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
1786
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
1787
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
1788
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
1789
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
1790
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
1791
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
1792
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
1793
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
1794
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
1795
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
1796
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
1797
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
1798
+ "@rollup/rollup-openbsd-x64": "4.60.2",
1799
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
1800
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
1801
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
1802
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
1803
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
1804
+ "fsevents": "~2.3.2"
1805
+ }
1806
+ },
1807
+ "node_modules/semver": {
1808
+ "version": "7.7.4",
1809
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
1810
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
1811
+ "license": "ISC",
1812
+ "bin": {
1813
+ "semver": "bin/semver.js"
1814
+ },
1815
+ "engines": {
1816
+ "node": ">=10"
1817
+ }
1818
+ },
1819
+ "node_modules/semver-compare": {
1820
+ "version": "1.0.0",
1821
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
1822
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
1823
+ "license": "MIT"
1824
+ },
1825
+ "node_modules/serialize-error": {
1826
+ "version": "7.0.1",
1827
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
1828
+ "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
1829
+ "license": "MIT",
1830
+ "dependencies": {
1831
+ "type-fest": "^0.13.1"
1832
+ },
1833
+ "engines": {
1834
+ "node": ">=10"
1835
+ },
1836
+ "funding": {
1837
+ "url": "https://github.com/sponsors/sindresorhus"
1838
+ }
1839
+ },
1840
+ "node_modules/sharp": {
1841
+ "version": "0.34.5",
1842
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
1843
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
1844
+ "hasInstallScript": true,
1845
+ "license": "Apache-2.0",
1846
+ "dependencies": {
1847
+ "@img/colour": "^1.0.0",
1848
+ "detect-libc": "^2.1.2",
1849
+ "semver": "^7.7.3"
1850
+ },
1851
+ "engines": {
1852
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1853
+ },
1854
+ "funding": {
1855
+ "url": "https://opencollective.com/libvips"
1856
+ },
1857
+ "optionalDependencies": {
1858
+ "@img/sharp-darwin-arm64": "0.34.5",
1859
+ "@img/sharp-darwin-x64": "0.34.5",
1860
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
1861
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
1862
+ "@img/sharp-libvips-linux-arm": "1.2.4",
1863
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
1864
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
1865
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
1866
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
1867
+ "@img/sharp-libvips-linux-x64": "1.2.4",
1868
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
1869
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
1870
+ "@img/sharp-linux-arm": "0.34.5",
1871
+ "@img/sharp-linux-arm64": "0.34.5",
1872
+ "@img/sharp-linux-ppc64": "0.34.5",
1873
+ "@img/sharp-linux-riscv64": "0.34.5",
1874
+ "@img/sharp-linux-s390x": "0.34.5",
1875
+ "@img/sharp-linux-x64": "0.34.5",
1876
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
1877
+ "@img/sharp-linuxmusl-x64": "0.34.5",
1878
+ "@img/sharp-wasm32": "0.34.5",
1879
+ "@img/sharp-win32-arm64": "0.34.5",
1880
+ "@img/sharp-win32-ia32": "0.34.5",
1881
+ "@img/sharp-win32-x64": "0.34.5"
1882
+ }
1883
+ },
1884
+ "node_modules/source-map-js": {
1885
+ "version": "1.2.1",
1886
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1887
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1888
+ "dev": true,
1889
+ "license": "BSD-3-Clause",
1890
+ "engines": {
1891
+ "node": ">=0.10.0"
1892
+ }
1893
+ },
1894
+ "node_modules/sprintf-js": {
1895
+ "version": "1.1.3",
1896
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
1897
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
1898
+ "license": "BSD-3-Clause"
1899
+ },
1900
+ "node_modules/toml": {
1901
+ "version": "2.3.6",
1902
+ "resolved": "https://registry.npmjs.org/toml/-/toml-2.3.6.tgz",
1903
+ "integrity": "sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ==",
1904
+ "license": "MIT"
1905
+ },
1906
+ "node_modules/tslib": {
1907
+ "version": "2.8.1",
1908
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1909
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
1910
+ "license": "0BSD",
1911
+ "optional": true
1912
+ },
1913
+ "node_modules/type-fest": {
1914
+ "version": "0.13.1",
1915
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
1916
+ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
1917
+ "license": "(MIT OR CC0-1.0)",
1918
+ "engines": {
1919
+ "node": ">=10"
1920
+ },
1921
+ "funding": {
1922
+ "url": "https://github.com/sponsors/sindresorhus"
1923
+ }
1924
+ },
1925
+ "node_modules/typescript": {
1926
+ "version": "5.9.3",
1927
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1928
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1929
+ "dev": true,
1930
+ "license": "Apache-2.0",
1931
+ "bin": {
1932
+ "tsc": "bin/tsc",
1933
+ "tsserver": "bin/tsserver"
1934
+ },
1935
+ "engines": {
1936
+ "node": ">=14.17"
1937
+ }
1938
+ },
1939
+ "node_modules/undici-types": {
1940
+ "version": "7.19.2",
1941
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
1942
+ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
1943
+ "license": "MIT"
1944
+ },
1945
+ "node_modules/vite": {
1946
+ "version": "5.4.21",
1947
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
1948
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
1949
+ "dev": true,
1950
+ "license": "MIT",
1951
+ "dependencies": {
1952
+ "esbuild": "^0.21.3",
1953
+ "postcss": "^8.4.43",
1954
+ "rollup": "^4.20.0"
1955
+ },
1956
+ "bin": {
1957
+ "vite": "bin/vite.js"
1958
+ },
1959
+ "engines": {
1960
+ "node": "^18.0.0 || >=20.0.0"
1961
+ },
1962
+ "funding": {
1963
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1964
+ },
1965
+ "optionalDependencies": {
1966
+ "fsevents": "~2.3.3"
1967
+ },
1968
+ "peerDependencies": {
1969
+ "@types/node": "^18.0.0 || >=20.0.0",
1970
+ "less": "*",
1971
+ "lightningcss": "^1.21.0",
1972
+ "sass": "*",
1973
+ "sass-embedded": "*",
1974
+ "stylus": "*",
1975
+ "sugarss": "*",
1976
+ "terser": "^5.4.0"
1977
+ },
1978
+ "peerDependenciesMeta": {
1979
+ "@types/node": {
1980
+ "optional": true
1981
+ },
1982
+ "less": {
1983
+ "optional": true
1984
+ },
1985
+ "lightningcss": {
1986
+ "optional": true
1987
+ },
1988
+ "sass": {
1989
+ "optional": true
1990
+ },
1991
+ "sass-embedded": {
1992
+ "optional": true
1993
+ },
1994
+ "stylus": {
1995
+ "optional": true
1996
+ },
1997
+ "sugarss": {
1998
+ "optional": true
1999
+ },
2000
+ "terser": {
2001
+ "optional": true
2002
+ }
2003
+ }
2004
+ }
2005
+ }
2006
+ }
package.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "bol-tts-marathi-webgpu-demo",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "In-browser Marathi TTS demo for shreyask/bol-tts-marathi-onnx",
7
+ "license": "Apache-2.0",
8
+ "scripts": {
9
+ "dev": "vite",
10
+ "build": "tsc && vite build",
11
+ "preview": "vite preview"
12
+ },
13
+ "dependencies": {
14
+ "@huggingface/transformers": "4.2.0",
15
+ "@indic-transliteration/sanscript": "^1.3.3"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.6.3",
19
+ "vite": "^5.4.11"
20
+ }
21
+ }
public/config.json ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "istftnet": {
3
+ "upsample_kernel_sizes": [
4
+ 20,
5
+ 12
6
+ ],
7
+ "upsample_rates": [
8
+ 10,
9
+ 6
10
+ ],
11
+ "gen_istft_hop_size": 5,
12
+ "gen_istft_n_fft": 20,
13
+ "resblock_dilation_sizes": [
14
+ [
15
+ 1,
16
+ 3,
17
+ 5
18
+ ],
19
+ [
20
+ 1,
21
+ 3,
22
+ 5
23
+ ],
24
+ [
25
+ 1,
26
+ 3,
27
+ 5
28
+ ]
29
+ ],
30
+ "resblock_kernel_sizes": [
31
+ 3,
32
+ 7,
33
+ 11
34
+ ],
35
+ "upsample_initial_channel": 512
36
+ },
37
+ "dim_in": 64,
38
+ "dropout": 0.2,
39
+ "hidden_dim": 512,
40
+ "max_conv_dim": 512,
41
+ "max_dur": 50,
42
+ "multispeaker": true,
43
+ "n_layer": 3,
44
+ "n_mels": 80,
45
+ "n_token": 178,
46
+ "style_dim": 128,
47
+ "text_encoder_kernel_size": 5,
48
+ "plbert": {
49
+ "hidden_size": 768,
50
+ "num_attention_heads": 12,
51
+ "intermediate_size": 2048,
52
+ "max_position_embeddings": 512,
53
+ "num_hidden_layers": 12,
54
+ "dropout": 0.1
55
+ },
56
+ "vocab": {
57
+ ";": 1,
58
+ ":": 2,
59
+ ",": 3,
60
+ ".": 4,
61
+ "!": 5,
62
+ "?": 6,
63
+ "—": 9,
64
+ "…": 10,
65
+ "\"": 11,
66
+ "(": 12,
67
+ ")": 13,
68
+ "“": 14,
69
+ "”": 15,
70
+ " ": 16,
71
+ "̃": 17,
72
+ "ʣ": 18,
73
+ "ʥ": 19,
74
+ "ʦ": 20,
75
+ "ʨ": 21,
76
+ "ᵝ": 22,
77
+ "ꭧ": 23,
78
+ "A": 24,
79
+ "I": 25,
80
+ "O": 31,
81
+ "Q": 33,
82
+ "S": 35,
83
+ "T": 36,
84
+ "W": 39,
85
+ "Y": 41,
86
+ "ᵊ": 42,
87
+ "a": 43,
88
+ "b": 44,
89
+ "c": 45,
90
+ "d": 46,
91
+ "e": 47,
92
+ "f": 48,
93
+ "h": 50,
94
+ "i": 51,
95
+ "j": 52,
96
+ "k": 53,
97
+ "l": 54,
98
+ "m": 55,
99
+ "n": 56,
100
+ "o": 57,
101
+ "p": 58,
102
+ "q": 59,
103
+ "r": 60,
104
+ "s": 61,
105
+ "t": 62,
106
+ "u": 63,
107
+ "v": 64,
108
+ "w": 65,
109
+ "x": 66,
110
+ "y": 67,
111
+ "z": 68,
112
+ "ɑ": 69,
113
+ "ɐ": 70,
114
+ "ɒ": 71,
115
+ "æ": 72,
116
+ "β": 75,
117
+ "ɔ": 76,
118
+ "ɕ": 77,
119
+ "ç": 78,
120
+ "ɖ": 80,
121
+ "ð": 81,
122
+ "ʤ": 82,
123
+ "ə": 83,
124
+ "ɚ": 85,
125
+ "ɛ": 86,
126
+ "ɜ": 87,
127
+ "ɟ": 90,
128
+ "ɡ": 92,
129
+ "ɥ": 99,
130
+ "ɨ": 101,
131
+ "ɪ": 102,
132
+ "ʝ": 103,
133
+ "ɯ": 110,
134
+ "ɰ": 111,
135
+ "ŋ": 112,
136
+ "ɳ": 113,
137
+ "ɲ": 114,
138
+ "ɴ": 115,
139
+ "ø": 116,
140
+ "ɸ": 118,
141
+ "θ": 119,
142
+ "œ": 120,
143
+ "ɹ": 123,
144
+ "ɾ": 125,
145
+ "ɻ": 126,
146
+ "ʁ": 128,
147
+ "ɽ": 129,
148
+ "ʂ": 130,
149
+ "ʃ": 131,
150
+ "ʈ": 132,
151
+ "ʧ": 133,
152
+ "ʊ": 135,
153
+ "ʋ": 136,
154
+ "ʌ": 138,
155
+ "ɣ": 139,
156
+ "ɤ": 140,
157
+ "χ": 142,
158
+ "ʎ": 143,
159
+ "ʒ": 147,
160
+ "ʔ": 148,
161
+ "ˈ": 156,
162
+ "ˌ": 157,
163
+ "ː": 158,
164
+ "ʰ": 162,
165
+ "ʲ": 164,
166
+ "↓": 169,
167
+ "→": 171,
168
+ "↗": 172,
169
+ "↘": 173,
170
+ "ᵻ": 177,
171
+ "ɭ": 144
172
+ },
173
+ "model_type": "style_text_to_speech_2",
174
+ "architectures": [
175
+ "StyleTextToSpeech2Model"
176
+ ]
177
+ }
public/voice_speeds.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_description": "Per-voice optimal speed for Kokoro Marathi. Tuned on Stage 2 epoch 2 checkpoint; may shift after full Stage 2 run.",
3
+ "_reference_sentence": "\u0928\u092e\u0938\u094d\u0915\u093e\u0930! \u092e\u0932\u093e \u0915\u0947\u0933\u0940 \u0906\u0923\u093f \u0906\u0902\u092c\u093e \u0906\u0935\u0921\u0924\u094b.",
4
+ "mf_asha": {
5
+ "speed": 0.85,
6
+ "notes": "User-validated"
7
+ },
8
+ "mm_vivek": {
9
+ "speed": null,
10
+ "notes": "TBD \u2014 likely similar to Asha (0.85-1.0) since both Rasa"
11
+ },
12
+ "mf_mukta": {
13
+ "speed": 1.0,
14
+ "notes": "IndicTTS female"
15
+ },
16
+ "mm_dnyanesh": {
17
+ "speed": 1.0,
18
+ "notes": "IndicTTS male"
19
+ }
20
+ }
public/voicepacks.json ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema_version": 1,
3
+ "_comment": "Source of truth for the bol-tts-marathi WebGPU demo's voice list + per-voice metadata + test phrases. Edit this to add/remove voices or update sample chips — main.ts consumes it dynamically on page load.",
4
+ "voices": [
5
+ {
6
+ "id": "mf_asha",
7
+ "name": "Asha",
8
+ "name_native": "आशा",
9
+ "gender": "female",
10
+ "source": "rasa",
11
+ "default_speed": 1.0,
12
+ "ship_status": "stable"
13
+ },
14
+ {
15
+ "id": "mm_vivek",
16
+ "name": "Vivek",
17
+ "name_native": "विवेक",
18
+ "gender": "male",
19
+ "source": "rasa",
20
+ "default_speed": 1.0,
21
+ "ship_status": "stable"
22
+ },
23
+ {
24
+ "id": "mf_mukta",
25
+ "name": "Mukta",
26
+ "name_native": "मुक्ता",
27
+ "gender": "female",
28
+ "source": "indicvoices_r",
29
+ "default_speed": 1.0,
30
+ "ship_status": "stable"
31
+ },
32
+ {
33
+ "id": "mm_dnyanesh",
34
+ "name": "Dnyanesh",
35
+ "name_native": "ज्ञानेश",
36
+ "gender": "male",
37
+ "source": "indicvoices_r",
38
+ "default_speed": 1.0,
39
+ "ship_status": "stable"
40
+ },
41
+ {
42
+ "id": "mf_priya",
43
+ "name": "Priya",
44
+ "name_native": "प्रिया",
45
+ "gender": "female",
46
+ "source": "springlab",
47
+ "default_speed": 1.0,
48
+ "ship_status": "v0.2_preview"
49
+ },
50
+ {
51
+ "id": "mm_arjun",
52
+ "name": "Arjun",
53
+ "name_native": "अर्जुन",
54
+ "gender": "male",
55
+ "source": "springlab",
56
+ "default_speed": 1.0,
57
+ "ship_status": "v0.2_preview"
58
+ }
59
+ ],
60
+ "test_phrases": {
61
+ "marathi_neutral": [
62
+ { "label": "हवामान", "text": "आज हवामान खूप छान आहे, पाऊस पडतो आहे." },
63
+ { "label": "महाराष्ट्र", "text": "महाराष्ट्र हे भारतातील एक महत्त्वाचे राज्य आहे." },
64
+ { "label": "मुंबई", "text": "मुंबई ही महाराष्ट्राची राजधानी आणि भारताची आर्थिक राजधानी आहे." },
65
+ { "label": "पुणे", "text": "पुणे ही महाराष्ट्राची सांस्कृतिक राजधानी मानली जाते." },
66
+ { "label": "आकडे", "text": "एक दोन तीन चार पाच सहा सात आठ." },
67
+ { "label": "कथा", "text": "पूर्वी एका छोट्या गावात एक शहाणा शेतकरी राहत होता." },
68
+ { "label": "गुढी पाडवा", "text": "गुढी पाडवा हा मराठी नववर्षाचा सण आहे." },
69
+ { "label": "भजी", "text": "पावसाळी संध्याकाळी गरम भजी आणि कटिंग चहा म्हणजे स्वर्गच!" },
70
+ { "label": "वडापाव", "text": "मुंबईचा वडापाव आणि पुण्याचा चहा — आपली खरी ओळख." }
71
+ ],
72
+ "marathi_female": [
73
+ { "label": "थकले", "text": "आज खूप काम केले, मी थकले आहे." }
74
+ ],
75
+ "marathi_male": [
76
+ { "label": "थकलो", "text": "आज खूप काम केले, मी थकलो आहे." }
77
+ ],
78
+ "minglish_neutral": [
79
+ { "label": "coffee?", "text": "Coffee पिऊया का?" },
80
+ { "label": "AI tech", "text": "Artificial intelligence ही technology खूप amazing आहे." },
81
+ { "label": "weekend plan", "text": "Weekend ला movie बघायचा plan आहे का?" }
82
+ ],
83
+ "minglish_female": [
84
+ { "label": "Google job", "text": "मी Google मध्ये काम करते." },
85
+ { "label": "late meeting", "text": "आज meeting ला late झाले." }
86
+ ],
87
+ "minglish_male": [
88
+ { "label": "Google job", "text": "मी Google मध्ये काम करतो." },
89
+ { "label": "late meeting", "text": "आज meeting ला late झालो." }
90
+ ]
91
+ }
92
+ }
public/voices/mf_asha.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:57784e5f6853a66605c897d9d4489e509e5c686f5cd95027f6d4ef9119115bea
3
+ size 522240
public/voices/mf_mukta.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4fbc32a8b5a067a828a1a43c360d8d0576873f69019b4c60cd9efa45c226412e
3
+ size 522240
public/voices/mf_priya.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c09561d3fda1dfddb88c461a79ff5d518198035a8906feece9fe38d6973225a1
3
+ size 522240
public/voices/mm_arjun.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d2eb6136af364a684e0504a77817e54ce6a96771284dee629c0d938e76b334b0
3
+ size 522240
public/voices/mm_dnyanesh.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5582a30e89860d4999ac1e1a82bc410c5e8401ea2cea0d377bdfeeaeba72ddff
3
+ size 522240
public/voices/mm_vivek.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:31668f284533bc72637781214b143fbfcef8519964067d40438df8d9cb66821e
3
+ size 522240
public/warli-strip.svg ADDED
src/espeakng/espeakng.worker.js ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ab1eaf96aa5d5b5fb76e264d440e9cb7672b7a2becead3fc0b1f75956d607398
3
+ size 18528868
src/espeakng/phonemizer.ts ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * In-browser Marathi G2P via espeak-ng WASM.
3
+ *
4
+ * Uses the patched espeakng.worker.js from hexgrad/kokoro (same Emscripten
5
+ * build as @echogarden/espeak-ng-emscripten, but the kokoro PR #262 worker
6
+ * ships cleanly for direct ESM import). ~18.5 MB; loaded lazily on first call.
7
+ *
8
+ * Produces IPA identical to the training-side misaki+espeak-ng pipeline.
9
+ */
10
+
11
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
12
+ // @ts-ignore — the worker.js doesn't ship types
13
+ import Module from "./espeakng.worker.js";
14
+
15
+ const SUPPORTED_LANGUAGES = ["en", "mr", "hi"];
16
+
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ let workerPromise: Promise<any> | null = null;
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ let voicesCache: Promise<{ identifiers: Set<string> }> | null = null;
21
+
22
+ function getWorker() {
23
+ if (!workerPromise) {
24
+ workerPromise = new Promise((resolve) => {
25
+ if (Module.calledRun) resolve(new Module.eSpeakNGWorker());
26
+ else Module.onRuntimeInitialized = () => resolve(new Module.eSpeakNGWorker());
27
+ });
28
+ }
29
+ return workerPromise;
30
+ }
31
+
32
+ function initVoices() {
33
+ if (!voicesCache) {
34
+ voicesCache = getWorker().then((worker) => {
35
+ const voices = worker.list_voices()
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ .map(({ name, identifier, languages }: any) => ({
38
+ name,
39
+ identifier,
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ languages: languages.filter((l: any) =>
42
+ SUPPORTED_LANGUAGES.includes(l.name.split("-")[0]),
43
+ ),
44
+ }))
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ .filter((v: any) => v.languages.length > 0);
47
+ const identifiers = new Set<string>();
48
+ for (const v of voices) {
49
+ identifiers.add(v.identifier);
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ for (const l of v.languages) identifiers.add(l.name);
52
+ }
53
+ return { identifiers };
54
+ });
55
+ }
56
+ return voicesCache;
57
+ }
58
+
59
+ // espeak-ng marks language-switched runs with `(xx)` tags — e.g. an English
60
+ // word inside a Marathi sentence comes out as `mˈi (en)ɡˈuːɡəl(mr) mˈədʰeː`.
61
+ // Tags reaching the tokenizer would corrupt it, but the language info itself
62
+ // is gold: it tells us per-phoneme which voice produced what. We use it to
63
+ // apply per-segment remap rules (English code-switches get Indic-English
64
+ // treatment; Marathi gets only espeak-mr bug fixes), then drop the tags.
65
+ const LANG_TAG_SPLIT = /\(([a-z]{2,3}(?:-[a-z0-9]+)?)\)/g;
66
+
67
+ function parseTaggedIpa(
68
+ raw: string,
69
+ defaultLang: string,
70
+ ): Array<{ lang: string; ipa: string }> {
71
+ // Split with a capture group: result alternates [text, lang, text, lang, ...].
72
+ const parts = raw.split(LANG_TAG_SPLIT);
73
+ const segments: Array<{ lang: string; ipa: string }> = [];
74
+ let lang = defaultLang;
75
+ for (let i = 0; i < parts.length; i++) {
76
+ if (i % 2 === 0) {
77
+ if (parts[i]) segments.push({ lang, ipa: parts[i] });
78
+ } else {
79
+ lang = parts[i];
80
+ }
81
+ }
82
+ return segments;
83
+ }
84
+
85
+ // espeak-ng emits some affricates as two-character digraphs (d + ʒ, t + ʃ, t + s)
86
+ // while Kokoro's vocab uses precomposed single characters (ʤ, ʧ, ʦ). Without
87
+ // normalization, the tokenizer drops the bare `ʒ`/`ʃ` and we lose the affricate
88
+ // ("technology" → "technolod(ʒ)i" → "technolodi"). Always applied.
89
+ const DIGRAPH_NORMALIZE: Array<[RegExp, string]> = [
90
+ [/dʒ/g, "ʤ"],
91
+ [/tʃ/g, "ʧ"],
92
+ [/ts/g, "ʦ"],
93
+ ];
94
+
95
+ // espeak-ng's Marathi voice has IPA bugs that aren't dialectal — they're flat
96
+ // wrong (no Marathi dialect uses these sounds). We patch them on `mr` segments:
97
+ // - `ɹ` (English alveolar approximant) — Marathi र is a tap /ɾ/, never /ɹ/.
98
+ // - `ɪ` (English lax i) — Marathi has only short /i/ and long /iː/.
99
+ // - Anusvara before a labial (ब, भ, प, फ, व): Marathi phonotactics says the
100
+ // anusvara assimilates to /m/ (the homorganic nasal). espeak sometimes
101
+ // collapses this to vowel-nasalization only (मुंबई → mũbiː instead of
102
+ // mumbiː) so Mumbai sounds like "mubi". Re-insert the missing /m/.
103
+ // NOTE: `z` is NOT in this list. Marathi ज has two valid pronunciations —
104
+ // /ʤ/ (जा) and /z/ (आज, झाडू, loanwords) — and espeak-mr correctly emits
105
+ // whichever fits. Remapping z→ʤ here would corrupt words like आज.
106
+ const MARATHI_FIXES: Array<[RegExp, string]> = [
107
+ [/ɹ/g, "ɾ"],
108
+ [/ɪ/g, "i"],
109
+ // Anusvara-to-labial: मुं → mum (not mũ).
110
+ [/ũ(?=[bpmfv])/g, "um"],
111
+ [/ã(?=[bpmfv])/g, "am"],
112
+ [/ĩ(?=[bpmfv])/g, "im"],
113
+ [/õ(?=[bpmfv])/g, "om"],
114
+ [/ẽ(?=[bpmfv])/g, "em"],
115
+ // Anusvara-to-velar: कटिं + ग → kəʈiŋ (not kəʈĩ). Same bug pattern in a
116
+ // different place of articulation — espeak collapses the ŋ into vowel
117
+ // nasalization when followed by क/ख/ग/घ (IPA: k, kʰ, ɡ, ɡʰ).
118
+ [/ũ(?=[kɡ])/g, "uŋ"],
119
+ [/ã(?=[kɡ])/g, "aŋ"],
120
+ [/ĩ(?=[kɡ])/g, "iŋ"],
121
+ [/õ(?=[kɡ])/g, "oŋ"],
122
+ [/ẽ(?=[kɡ])/g, "eŋ"],
123
+ // Stress placement: Marathi favors first-syllable or first-heavy-syllable
124
+ // stress, but espeak-mr often puts primary stress `ˈ` on later long vowels
125
+ // and demotes the first syllable to secondary `ˌ` (Mumbai → mˌumbˈiː should
126
+ // be mˈumbˌiː). Swap the first secondary-before-primary pair per word.
127
+ // NOTE: We *only* swap when both ˈ and ˌ exist — this is a high-confidence
128
+ // signal that espeak had the right stress position but chose the wrong
129
+ // level. A broader "move lone ˈ to start" rule was tried and regressed
130
+ // legitimately middle-stressed words (मित्रांना → audio dropout), so we
131
+ // trust espeak when it emits a bare primary stress.
132
+ [/ˌ([^ˈˌ]*)ˈ/, "ˈ$1ˌ"],
133
+ ];
134
+
135
+ // Additional remaps for English-only phonemes with no native Marathi slot.
136
+ // Applied to `en` segments (in addition to MARATHI_FIXES) so code-switch words
137
+ // route through densely-trained Indic slots and sound naturally Indic-accented
138
+ // — which matches how Marathi speakers actually pronounce these words. The
139
+ // alternative (raw English IPA) would hit undertrained slots in the fine-tune.
140
+ const ENGLISH_INDIC_EXTRAS: Array<[RegExp, string]> = [
141
+ [/z/g, "ʤ"], // "amazing" → "amaJing", "zoo" → "joo" (Indic-English only;
142
+ // Marathi keeps native /z/ in words like आज)
143
+ [/θ/g, "t"], // "thing" → "ting"
144
+ [/ð/g, "d"], // "this" → "dis"
145
+ [/ɛ/g, "e"], // "bet" → ए
146
+ [/ɒ/g, "ɔ"], // "cot" → ऑ
147
+ [/æ/g, "a"], // "cat" → अ
148
+ [/ɐ/g, "ə"], // schwa-ish
149
+ [/ʊ/g, "u"], // "put" → u
150
+ ];
151
+
152
+ // eSpeak's Marathi voice gets some proper nouns wrong only in their inflected
153
+ // forms. "मुंबई" alone sounds correct as `mˈumbˌi` in the current model, but
154
+ // "मुंबईचा" is emitted with a spurious long `i:` before the suffix, which
155
+ // makes the synthesis sound off. Keep these as narrow lexical overrides rather
156
+ // than a global phoneme rewrite so we don't perturb native Marathi `c` tokens
157
+ // elsewhere in the lexicon.
158
+ const MARATHI_WORD_OVERRIDES: Record<string, string> = {
159
+ "मुंबई": "mˈumbˌi",
160
+ "मुंबईचा": "mˈumbˌica:",
161
+ "मुंबईची": "mˈumbˌici",
162
+ "मुंबईचे": "mˈumbˌice",
163
+ };
164
+
165
+ function applyRules(ipa: string, rules: Array<[RegExp, string]>): string {
166
+ let out = ipa;
167
+ for (const [re, to] of rules) out = out.replace(re, to);
168
+ return out;
169
+ }
170
+
171
+ /**
172
+ * Phonemize a chunk of text and return raw IPA lines WITH `(xx)` language-switch
173
+ * tags preserved. Callers are expected to consume the tags via `parseTaggedIpa`
174
+ * before sending IPA downstream — the tokenizer doesn't understand parens.
175
+ */
176
+ export async function phonemize(text: string, language = "mr"): Promise<string[]> {
177
+ const worker = await getWorker();
178
+ const { identifiers } = await initVoices();
179
+ if (!identifiers.has(language)) {
180
+ throw new Error(
181
+ `Invalid language: "${language}". Supported: ${[...identifiers].sort().join(", ")}.`,
182
+ );
183
+ }
184
+ worker.set_voice(language);
185
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
186
+ const out = (worker as any).synthesize_ipa(text);
187
+ return (out.ipa ?? "")
188
+ .split("\n")
189
+ .map((line: string) => line.replace(/\s+/g, " ").trim())
190
+ .filter((x: string) => x.length > 0);
191
+ }
192
+
193
+ /**
194
+ * Phonemize word-by-word so we can align pred_dur → words for timestamps.
195
+ * Each word is a single phonemize() call. For mixed-script input, espeak
196
+ * inserts `(en)`/`(mr)` tags around language-switched runs; we use those to
197
+ * apply per-segment remaps:
198
+ * - `mr` segments get espeak-mr bug fixes only (z→ʤ, ɪ→i, ɹ→ɾ).
199
+ * - `en` segments get bug fixes PLUS Indic-English vowel/fricative remaps
200
+ * (θ→t, ð→d, ɛ→e, …) so code-switch words route through densely-trained
201
+ * Marathi slots and sound naturally Indic-accented.
202
+ */
203
+ export async function phonemizeByWord(
204
+ text: string,
205
+ language = "mr",
206
+ ): Promise<Array<{ word: string; ipa: string }>> {
207
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
208
+ const out: Array<{ word: string; ipa: string }> = [];
209
+ for (const w of words) {
210
+ const override = language === "mr" ? MARATHI_WORD_OVERRIDES[w] : undefined;
211
+ if (override) {
212
+ out.push({ word: w, ipa: override });
213
+ continue;
214
+ }
215
+ // NFC-normalize so `u + combining tilde` collapses to precomposed `ũ` —
216
+ // our MARATHI_FIXES regexes match the precomposed forms.
217
+ const raw = (await phonemize(w, language)).join(" ").trim().normalize("NFC");
218
+ let ipa = "";
219
+ for (const seg of parseTaggedIpa(raw, language)) {
220
+ let chunk = applyRules(seg.ipa, DIGRAPH_NORMALIZE);
221
+ chunk = applyRules(chunk, MARATHI_FIXES);
222
+ if (seg.lang.startsWith("en")) chunk = applyRules(chunk, ENGLISH_INDIC_EXTRAS);
223
+ ipa += chunk;
224
+ }
225
+ // Decompose to NFD for the tokenizer. Kokoro vocab stores the combining
226
+ // tilde (U+0303) as a standalone token at slot 17 — not the precomposed
227
+ // nasalized vowels (ã/ũ/ĩ/õ/ẽ are NOT in vocab). Any precomposed form
228
+ // surviving our MARATHI_FIXES (e.g., anusvara before a dental, where no
229
+ // rule fires) would be silently dropped by tokenize.ts and take nearby
230
+ // consonants' alignment down with it. NFD keeps the base vowel and lets
231
+ // the tilde ride as its own vocab slot.
232
+ out.push({
233
+ word: w,
234
+ ipa: ipa.replace(/\s+/g, " ").trim().normalize("NFD"),
235
+ });
236
+ }
237
+ return out;
238
+ }
src/main.ts ADDED
@@ -0,0 +1,789 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { KokoroSession, loadConfig, loadVoicepack, type KokoroConfig, type KokoroVocab } from "./model";
2
+ import { phonemizeByWord } from "./phonemize";
3
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
4
+ // @ts-ignore — sanscript ships JS with no types
5
+ import Sanscript from "@indic-transliteration/sanscript";
6
+ import { tokenizeByWord } from "./tokenize";
7
+ import { buildTimings, type WordTiming, type PhonemeTiming } from "./timestamps";
8
+ import type { PhonemizedWord } from "./phonemize";
9
+
10
+ // Voice list, per-voice default speeds, and gender-aware test phrases all
11
+ // come from public/voicepacks.json — single source of truth, edited there
12
+ // to add voices or update sample chips without touching code.
13
+ interface VoicepackEntry {
14
+ id: string;
15
+ name: string;
16
+ name_native: string;
17
+ gender: "male" | "female";
18
+ source: string;
19
+ default_speed: number;
20
+ ship_status: string;
21
+ }
22
+ interface ChipPhrase {
23
+ label: string;
24
+ text: string;
25
+ }
26
+ interface VoicepackManifest {
27
+ voices: VoicepackEntry[];
28
+ test_phrases: Record<string, ChipPhrase[]>;
29
+ }
30
+
31
+ // ── DOM refs ────────────────────────────────────────────────────────────────
32
+ const $ = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
33
+
34
+ const statusEl = $<HTMLElement>("status");
35
+ const textEl = $<HTMLTextAreaElement>("text");
36
+ const voiceEl = $<HTMLSelectElement>("voice");
37
+ const speedEl = $<HTMLInputElement>("speed");
38
+ const speedValEl = $<HTMLElement>("speed-val");
39
+ const synthBtn = $<HTMLButtonElement>("synth");
40
+ const transliterateBtn = $<HTMLButtonElement>("transliterate");
41
+ const outputSection = $<HTMLElement>("output-section");
42
+ const audioEl = $<HTMLAudioElement>("audio");
43
+ const transcriptEl = $<HTMLElement>("transcript");
44
+ const compactEl = $<HTMLElement>("transcript-compact");
45
+ const phonemesEl = $<HTMLElement>("phonemes");
46
+ const wordTimingsEl = $<HTMLElement>("word-timings");
47
+ const waveformCanvas = $<HTMLCanvasElement>("waveform");
48
+ const playPauseBtn = $<HTMLButtonElement>("playpause");
49
+ const timeCurEl = $<HTMLElement>("time-cur");
50
+ const timeTotalEl = $<HTMLElement>("time-total");
51
+ const loadModelBtn = $<HTMLButtonElement>("load-model");
52
+ const modelGateEl = $<HTMLElement>("model-gate");
53
+
54
+ // ── State ───────────────────────────────────────────────────────────────────
55
+ let session: KokoroSession | null = null;
56
+ let config: KokoroConfig | null = null;
57
+ let voicepackManifest: VoicepackManifest | null = null;
58
+ const voicepackCache: Record<string, Float32Array> = {};
59
+ let currentWords: WordTiming[] = [];
60
+ let currentAudio: Float32Array | null = null;
61
+ let currentAudioURL: string | null = null; // blob URL we must revoke before replacing
62
+ let highlightRaf: number | null = null;
63
+
64
+ async function loadVoicepackManifest(): Promise<VoicepackManifest> {
65
+ const res = await fetch("voicepacks.json");
66
+ if (!res.ok) throw new Error(`voicepacks.json: ${res.status}`);
67
+ return (await res.json()) as VoicepackManifest;
68
+ }
69
+
70
+ function populateVoiceSelector(manifest: VoicepackManifest): void {
71
+ voiceEl.innerHTML = "";
72
+ for (const v of manifest.voices) {
73
+ const opt = document.createElement("option");
74
+ opt.value = v.id;
75
+ const tag = v.ship_status === "stable" ? "" : ` [${v.ship_status.replace(/_/g, " ")}]`;
76
+ opt.textContent = `${v.name} (${v.name_native}) — ${v.gender}${tag}`;
77
+ voiceEl.appendChild(opt);
78
+ }
79
+ }
80
+
81
+ // Build chip <button>s under #chips-marathi and #chips-minglish, filtered by
82
+ // the active voice's gender. Called on initial load + every voice change.
83
+ function populateChips(manifest: VoicepackManifest, gender: "male" | "female"): void {
84
+ const marathiContainer = document.getElementById("chips-marathi") as HTMLDivElement | null;
85
+ const minglishContainer = document.getElementById("chips-minglish") as HTMLDivElement | null;
86
+ if (!marathiContainer || !minglishContainer) return;
87
+
88
+ // Strip out previously-injected chips (preserve the static label span).
89
+ marathiContainer.querySelectorAll(".chip").forEach((c) => c.remove());
90
+ minglishContainer.querySelectorAll(".chip").forEach((c) => c.remove());
91
+
92
+ const buildChip = (p: ChipPhrase): HTMLButtonElement => {
93
+ const b = document.createElement("button");
94
+ b.className = "chip";
95
+ b.type = "button";
96
+ b.setAttribute("data-text", p.text);
97
+ b.textContent = p.label;
98
+ b.addEventListener("click", () => {
99
+ textEl.value = p.text;
100
+ });
101
+ return b;
102
+ };
103
+
104
+ const marathi: ChipPhrase[] = [
105
+ ...(manifest.test_phrases.marathi_neutral ?? []),
106
+ ...(manifest.test_phrases[`marathi_${gender}`] ?? []),
107
+ ];
108
+ const minglish: ChipPhrase[] = [
109
+ ...(manifest.test_phrases.minglish_neutral ?? []),
110
+ ...(manifest.test_phrases[`minglish_${gender}`] ?? []),
111
+ ];
112
+
113
+ for (const p of marathi) marathiContainer.appendChild(buildChip(p));
114
+ for (const p of minglish) minglishContainer.appendChild(buildChip(p));
115
+ }
116
+
117
+ // Kokoro's PLBERT caps at 510 input tokens. Going over crashes with a shape
118
+ // assertion; we surface a clear user error before calling the model.
119
+ const PLBERT_MAX_TOKENS = 510;
120
+
121
+ // ── Boot ────────────────────────────────────────────────────────────────────
122
+ // Two-step boot: fetch small metadata on page load (config + one voicepack,
123
+ // together ~530 KB), then gate the big 325 MB model download behind a button
124
+ // so users don't accidentally consume data.
125
+ async function bootLight() {
126
+ setStatus("loading config…");
127
+ config = await loadConfig();
128
+ voicepackManifest = await loadVoicepackManifest();
129
+ populateVoiceSelector(voicepackManifest);
130
+ const initialVoice = voicepackManifest.voices[0];
131
+ voiceEl.value = initialVoice.id;
132
+ populateChips(voicepackManifest, initialVoice.gender);
133
+ speedEl.value = String(initialVoice.default_speed);
134
+ speedValEl.textContent = fmtSpeed(speedEl.value);
135
+ voicepackCache[initialVoice.id] = await loadVoicepack(initialVoice.id);
136
+ setStatus(
137
+ `ready to load model · vocab ${Object.keys(config.vocab).length} — tap "Load model" to proceed`,
138
+ );
139
+ modelGateEl.hidden = false;
140
+ loadModelBtn.disabled = false;
141
+ }
142
+
143
+ async function loadModel() {
144
+ if (!config) return;
145
+ loadModelBtn.disabled = true;
146
+ loadModelBtn.textContent = "Loading…";
147
+ try {
148
+ session = await KokoroSession.create((msg) => setStatus(msg));
149
+ // Belt-and-suspenders: explicitly tear down the gate card and re-enable
150
+ // synthesize. `.hidden` sometimes fights stylesheet rules in certain
151
+ // browsers; setting display:none as a backup guarantees it disappears.
152
+ modelGateEl.hidden = true;
153
+ modelGateEl.style.display = "none";
154
+ loadModelBtn.textContent = "Load model";
155
+ synthBtn.disabled = false;
156
+ setStatus(
157
+ `ready · vocab ${Object.keys(config.vocab).length} · ${session.backend}`,
158
+ "ready",
159
+ );
160
+ } catch (err) {
161
+ console.error("model load failed", err);
162
+ setStatus(`model load failed: ${(err as Error).message}`, "error");
163
+ loadModelBtn.disabled = false;
164
+ loadModelBtn.textContent = "Retry load";
165
+ }
166
+ }
167
+
168
+ loadModelBtn.addEventListener("click", loadModel);
169
+
170
+ function setStatus(msg: string, cls: "ready" | "error" | "" = "") {
171
+ statusEl.textContent = msg;
172
+ statusEl.className = "card status" + (cls ? ` ${cls}` : "");
173
+ }
174
+
175
+ // ── UI wiring ───────────────────────────────────────────────────────────────
176
+ const fmtSpeed = (v: string | number) => `${parseFloat(String(v)).toFixed(2)}×`;
177
+ voiceEl.addEventListener("change", () => {
178
+ if (!voicepackManifest) return;
179
+ const v = voicepackManifest.voices.find((x) => x.id === voiceEl.value);
180
+ if (!v) return;
181
+ speedEl.value = String(v.default_speed);
182
+ speedValEl.textContent = fmtSpeed(speedEl.value);
183
+ populateChips(voicepackManifest, v.gender);
184
+ });
185
+ speedEl.addEventListener("input", () => {
186
+ speedValEl.textContent = fmtSpeed(speedEl.value);
187
+ });
188
+
189
+ // Transliteration: Latin (ITRANS) → Devanagari, in-place. User can edit the
190
+ // result before synthesizing, so any quirks of the scheme (intuitive mapping
191
+ // for vowels, anusvara via "M", proper-noun handling) can be fixed manually
192
+ // rather than by us second-guessing them.
193
+ //
194
+ // `syncope: true` is sanscript's built-in Hindi/Marathi schwa-deletion mode —
195
+ // it drops the inherent /a/ and the corresponding `्` (halant) in positions
196
+ // where Indo-Aryan languages don't pronounce it (especially word-final
197
+ // consonants). Without it, ITRANS produces Sanskrit-style "नमस्कार्" instead
198
+ // of the form a Marathi reader expects, "नमस्कार".
199
+ function transliterateLatinToDevanagari(input: string): string {
200
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
201
+ return (Sanscript as any).t(input, "itrans", "devanagari", { syncope: true }) as string;
202
+ }
203
+ transliterateBtn.addEventListener("click", () => {
204
+ const before = textEl.value;
205
+ const after = transliterateLatinToDevanagari(before);
206
+ if (after !== before) {
207
+ textEl.value = after;
208
+ textEl.focus();
209
+ }
210
+ });
211
+
212
+ synthBtn.disabled = true;
213
+ synthBtn.addEventListener("click", onSynth);
214
+
215
+ // ── fused player wiring ──────────────────────────────────────────────────
216
+ playPauseBtn.addEventListener("click", () => {
217
+ if (audioEl.paused) audioEl.play().catch(() => {});
218
+ else audioEl.pause();
219
+ });
220
+ audioEl.addEventListener("play", () => { playPauseBtn.textContent = "⏸"; playPauseBtn.classList.add("playing"); });
221
+ audioEl.addEventListener("pause", () => { playPauseBtn.textContent = "▶"; playPauseBtn.classList.remove("playing"); });
222
+ audioEl.addEventListener("ended", () => { playPauseBtn.textContent = "▶"; playPauseBtn.classList.remove("playing"); });
223
+ // HTMLMediaElement.currentTime/duration are MEDIA-time seconds (independent of
224
+ // playbackRate) — same units as our pred_dur-derived word timings. Don't
225
+ // multiply by playbackRate; doing so made the clock drift proportional to the
226
+ // speed slider and broke seek/highlight alignment on non-1.0 speeds.
227
+ audioEl.addEventListener("loadedmetadata", () => {
228
+ timeTotalEl.textContent = fmtTime(audioEl.duration);
229
+ });
230
+ audioEl.addEventListener("timeupdate", () => {
231
+ timeCurEl.textContent = fmtTime(audioEl.currentTime);
232
+ });
233
+
234
+ // Click + drag (scrub) on the waveform. Pointer events cover mouse + touch.
235
+ let scrubbing = false;
236
+ let wasPlayingBeforeScrub = false;
237
+ function seekToPointer(e: PointerEvent | MouseEvent) {
238
+ if (!currentAudio) return;
239
+ const rect = waveformCanvas.getBoundingClientRect();
240
+ const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
241
+ const totalNat = currentAudio.length / 24000;
242
+ // audioEl.currentTime is media-time; our natural-second target matches directly.
243
+ audioEl.currentTime = pct * totalNat;
244
+ // Keep the waveform + timing rows visually in sync even while paused / dragging
245
+ tickHighlight();
246
+ }
247
+ waveformCanvas.addEventListener("pointerdown", (e) => {
248
+ if (!currentAudio) return;
249
+ scrubbing = true;
250
+ wasPlayingBeforeScrub = !audioEl.paused;
251
+ if (wasPlayingBeforeScrub) audioEl.pause();
252
+ waveformCanvas.setPointerCapture(e.pointerId);
253
+ seekToPointer(e);
254
+ });
255
+ waveformCanvas.addEventListener("pointermove", (e) => {
256
+ if (scrubbing) seekToPointer(e);
257
+ });
258
+ function endScrub(e: PointerEvent) {
259
+ if (!scrubbing) return;
260
+ scrubbing = false;
261
+ try { waveformCanvas.releasePointerCapture(e.pointerId); } catch {/* ignore */}
262
+ if (wasPlayingBeforeScrub) audioEl.play().catch(() => {});
263
+ }
264
+ waveformCanvas.addEventListener("pointerup", endScrub);
265
+ waveformCanvas.addEventListener("pointercancel", endScrub);
266
+
267
+ // Space bar toggles play/pause when focus isn't on text input
268
+ document.addEventListener("keydown", (e) => {
269
+ if (e.code !== "Space") return;
270
+ const t = e.target as HTMLElement;
271
+ if (t.tagName === "TEXTAREA" || t.tagName === "INPUT") return;
272
+ e.preventDefault();
273
+ playPauseBtn.click();
274
+ });
275
+
276
+ function fmtTime(sec: number): string {
277
+ if (!isFinite(sec) || sec < 0) sec = 0;
278
+ const m = Math.floor(sec / 60);
279
+ const s = Math.floor(sec % 60);
280
+ return `${m}:${s.toString().padStart(2, "0")}`;
281
+ }
282
+
283
+ // ── Chunk planning for long text ────────────────────────────────────────────
284
+ // Kokoro's PLBERT caps input at 510 phoneme tokens. For longer text we split
285
+ // on sentence boundaries (via Intl.Segmenter — locale-aware, understands
286
+ // Devanagari danda/double-danda as well as Latin .!?), phonemize each sentence,
287
+ // then greedy-pack sentences into chunks under the cap. If a single sentence
288
+ // still exceeds cap, we fall back to per-word greedy packing within that
289
+ // sentence — prosody at the sub-split boundary will be less clean but at
290
+ // least the synthesis runs.
291
+ async function planChunks(
292
+ text: string,
293
+ vocab: KokoroVocab,
294
+ cap: number,
295
+ ): Promise<PhonemizedWord[][]> {
296
+ const segmenter = new Intl.Segmenter("mr", { granularity: "sentence" });
297
+ const sentences = [...segmenter.segment(text)]
298
+ .map(s => s.segment.trim())
299
+ .filter(s => s.length > 0);
300
+
301
+ const tokCount = (ws: PhonemizedWord[]) => tokenizeByWord(ws, vocab).inputIds.length;
302
+
303
+ const chunks: PhonemizedWord[][] = [];
304
+ let current: PhonemizedWord[] = [];
305
+ let currentTokens = 0;
306
+
307
+ // tokenizeByWord inserts one boundary-space token BETWEEN each word (not
308
+ // before the first). So `tokCount(sA ++ sB) = tokCount(sA) + tokCount(sB) + 1`
309
+ // when we merge sentences A and B into the same chunk — that +1 is easy to
310
+ // miss and was previously causing near-cap inputs to overflow PLBERT. Account
311
+ // for it explicitly in the packing math.
312
+ const BOUNDARY_TOKEN_COST = 1;
313
+
314
+ for (const s of sentences) {
315
+ const sWords = await phonemizeByWord(s);
316
+ const sTokens = tokCount(sWords);
317
+
318
+ // Skip sentences that phonemize to nothing (whitespace, orphan punctuation,
319
+ // quote marks that espeak drops). Passing 0 tokens to PLBERT crashes at the
320
+ // Expand node with "invalid expand shape".
321
+ if (sTokens === 0) continue;
322
+
323
+ if (sTokens > cap) {
324
+ // Sentence alone exceeds cap — flush current, sub-split by greedy word packing.
325
+ if (current.length) { chunks.push(current); current = []; currentTokens = 0; }
326
+ let sub: PhonemizedWord[] = [];
327
+ for (const w of sWords) {
328
+ const probe = [...sub, w];
329
+ const probeTokens = tokCount(probe);
330
+ if (probeTokens > cap) {
331
+ if (sub.length > 0) {
332
+ chunks.push(sub);
333
+ sub = [w];
334
+ // If this single word is STILL > cap by itself, we can't split further
335
+ // without corrupting phoneme-to-grapheme alignment. Skip with a warning.
336
+ if (tokCount(sub) > cap) {
337
+ console.warn(`dropping oversized word (${tokCount(sub)} tokens > cap ${cap})`, w);
338
+ sub = [];
339
+ }
340
+ } else {
341
+ // sub was already empty; this lone word exceeds cap. Skip.
342
+ console.warn(`dropping oversized word (${probeTokens} tokens > cap ${cap})`, w);
343
+ }
344
+ } else {
345
+ sub = probe;
346
+ }
347
+ }
348
+ if (sub.length) chunks.push(sub);
349
+ continue;
350
+ }
351
+
352
+ const boundary = current.length > 0 ? BOUNDARY_TOKEN_COST : 0;
353
+ if (currentTokens + boundary + sTokens > cap) {
354
+ chunks.push(current);
355
+ current = [...sWords];
356
+ currentTokens = sTokens;
357
+ } else {
358
+ current.push(...sWords);
359
+ currentTokens += boundary + sTokens;
360
+ }
361
+ }
362
+ if (current.length) chunks.push(current);
363
+ // Final safety: drop any chunk that ended up with 0 tokens or, defensively,
364
+ // still over cap (belt-and-suspenders against any miscount above).
365
+ return chunks.filter(c => {
366
+ const t = tokCount(c);
367
+ if (t === 0) return false;
368
+ if (t > cap) {
369
+ console.warn(`dropping over-cap chunk (${t} tokens > ${cap}) after packing`, c);
370
+ return false;
371
+ }
372
+ return true;
373
+ });
374
+ }
375
+
376
+ // ── Main action ─────────────────────────────────────────────────────────────
377
+ async function onSynth() {
378
+ if (!session || !config) return;
379
+ const text = textEl.value.trim();
380
+ if (!text) return;
381
+
382
+ try {
383
+ synthBtn.disabled = true;
384
+ setStatus("phonemizing…");
385
+
386
+ const voiceId = voiceEl.value;
387
+ if (!voicepackCache[voiceId]) {
388
+ voicepackCache[voiceId] = await loadVoicepack(voiceId);
389
+ }
390
+ const voicepack = voicepackCache[voiceId];
391
+
392
+ const chunks = await planChunks(text, config.vocab, PLBERT_MAX_TOKENS);
393
+ if (chunks.length === 0) {
394
+ setStatus("no phonemes produced — check input text", "error");
395
+ return;
396
+ }
397
+
398
+ const speed = parseFloat(speedEl.value);
399
+ // 100 ms silence between chunks — keeps sentence boundaries audibly
400
+ // separated without introducing a long gap.
401
+ const INTER_CHUNK_PAD_SEC = 0.1;
402
+
403
+ const allAudio: Float32Array[] = [];
404
+ const allWords: WordTiming[] = [];
405
+ const allPhonemes: PhonemeTiming[] = [];
406
+ let totalTokens = 0;
407
+ let audioOffsetSamples = 0;
408
+ let sampleRate = 24000;
409
+
410
+ const t0 = performance.now();
411
+ for (let ci = 0; ci < chunks.length; ci++) {
412
+ const chunk = chunks[ci];
413
+ const tok = tokenizeByWord(chunk, config.vocab);
414
+ if (tok.inputIds.length === 0) {
415
+ console.warn(`skipping empty chunk ${ci + 1}/${chunks.length}`, chunk);
416
+ continue;
417
+ }
418
+ totalTokens += tok.inputIds.length;
419
+
420
+ if (chunks.length > 1) {
421
+ setStatus(`synthesizing chunk ${ci + 1}/${chunks.length} (${tok.inputIds.length} tokens)…`);
422
+ } else {
423
+ setStatus(`synthesizing (${tok.inputIds.length} tokens)…`);
424
+ }
425
+
426
+ // KPipeline convention: ref_s = voicepack[n_phonemes - 1] (voicepack is
427
+ // [510, 1, 256] flat). Per chunk, not per whole text — shorter chunks
428
+ // want the ref_s slice for their own length.
429
+ const idx = Math.min(tok.inputIds.length - 1, 509);
430
+ const refS = voicepack.slice(idx * 256, (idx + 1) * 256);
431
+
432
+ const result = await session.synthesize(tok.inputIds, refS);
433
+ sampleRate = result.sampleRate;
434
+ const PAD_SAMPLES = Math.floor(sampleRate * INTER_CHUNK_PAD_SEC);
435
+ const { phonemes, words } = buildTimings(tok, result.predDur, sampleRate);
436
+
437
+ // Tail-trim NON-final chunks only: ISTFTNet's residual frames would
438
+ // otherwise bleed into the next chunk's silence pad and onset, which
439
+ // sounds gross. The final chunk has nothing after it, so we keep its
440
+ // full decoder output — letting word-final semi-vowels (/j/ in सांगतोय,
441
+ // /w/ in हवा) and aspirated stops complete their natural release. Trim
442
+ // pad on internal chunks is 80 ms, which preserves codas while still
443
+ // dropping the worst of the ghost-vowel "uhh" tail.
444
+ let audio = result.audio;
445
+ const isFinalChunk = ci === chunks.length - 1;
446
+ if (!isFinalChunk && words.length > 0) {
447
+ const cutoff = Math.min(
448
+ audio.length,
449
+ Math.floor((words[words.length - 1].end + 0.08) * sampleRate),
450
+ );
451
+ if (cutoff < audio.length) audio = audio.slice(0, cutoff);
452
+ }
453
+
454
+ // Offset this chunk's timings by cumulative audio start.
455
+ const offsetSec = audioOffsetSamples / sampleRate;
456
+ for (const p of phonemes) { p.start += offsetSec; p.end += offsetSec; }
457
+ for (const w of words) {
458
+ w.start += offsetSec; w.end += offsetSec;
459
+ for (const pp of w.phonemes) { pp.start += offsetSec; pp.end += offsetSec; }
460
+ }
461
+
462
+ allAudio.push(audio);
463
+ audioOffsetSamples += audio.length;
464
+ allPhonemes.push(...phonemes);
465
+ allWords.push(...words);
466
+
467
+ // Silence pad between chunks (not after the last).
468
+ if (ci < chunks.length - 1) {
469
+ allAudio.push(new Float32Array(PAD_SAMPLES));
470
+ audioOffsetSamples += PAD_SAMPLES;
471
+ }
472
+ }
473
+ const t1 = performance.now();
474
+
475
+ // Concat audio into one Float32Array for the WAV encoder + waveform.
476
+ const audio = new Float32Array(audioOffsetSamples);
477
+ {
478
+ let off = 0;
479
+ for (const a of allAudio) { audio.set(a, off); off += a.length; }
480
+ }
481
+
482
+ const wavBlob = float32ToWav(audio, sampleRate);
483
+ // Revoke the previous blob URL before replacing — else each synth leaks
484
+ // ~N bytes of WAV data in the browser's URL table until the tab closes.
485
+ if (currentAudioURL) URL.revokeObjectURL(currentAudioURL);
486
+ currentAudioURL = URL.createObjectURL(wavBlob);
487
+ audioEl.src = currentAudioURL;
488
+ audioEl.preservesPitch = true;
489
+ audioEl.playbackRate = speed;
490
+
491
+ const phonemes = allPhonemes;
492
+ const words = allWords;
493
+ currentWords = words;
494
+
495
+ renderSideBySide(words);
496
+ currentAudio = audio;
497
+ drawWaveform(audio, words, sampleRate);
498
+
499
+ phonemesEl.textContent = phonemes
500
+ .map(p => `${p.phone} ${fmtSec(p.start)}–${fmtSec(p.end)} (${p.durFrames}f)`)
501
+ .join("\n");
502
+ wordTimingsEl.textContent = words
503
+ .map(w => `${w.word.padEnd(14)} ${fmtSec(w.start)}–${fmtSec(w.end)} [${w.ipa}]`)
504
+ .join("\n");
505
+
506
+ outputSection.hidden = false;
507
+ const chunkTag = chunks.length > 1 ? ` · ${chunks.length} chunks` : "";
508
+ setStatus(
509
+ `synth ${(t1 - t0).toFixed(0)}ms · ${(audio.length / sampleRate).toFixed(2)}s audio · ${totalTokens} tokens${chunkTag}`,
510
+ "ready",
511
+ );
512
+
513
+ audioEl.play().catch(() => {/* user may block autoplay; harmless */});
514
+
515
+ } catch (err) {
516
+ console.error(err);
517
+ setStatus(`error: ${(err as Error).message}`, "error");
518
+ } finally {
519
+ synthBtn.disabled = false;
520
+ }
521
+ }
522
+
523
+ // ── Transcript rendering ────────────────────────────────────────────────
524
+ // Two views:
525
+ // 1. Compact (default): one inline Devanagari sentence, per-word spans that
526
+ // highlight during playback. Click/Enter opens the details panel.
527
+ // 2. Details: 3-column Devanagari / IPA / timing table — one row per word.
528
+ // Both get updated live by tickHighlight().
529
+ function renderSideBySide(words: WordTiming[]) {
530
+ // Compact transcript — one span per word for live highlight
531
+ while (compactEl.firstChild) compactEl.removeChild(compactEl.firstChild);
532
+ words.forEach((w, i) => {
533
+ const span = document.createElement("span");
534
+ span.className = "word";
535
+ span.dataset.idx = String(i);
536
+ span.tabIndex = 0;
537
+ span.role = "button";
538
+ span.title = `Seek to ${w.start.toFixed(2)}s (${w.ipa})`;
539
+ span.textContent = w.word;
540
+ compactEl.appendChild(span);
541
+ if (i < words.length - 1) compactEl.appendChild(document.createTextNode(" "));
542
+ });
543
+
544
+ // Details table — clear old rows, keep the 3 col-heads, add one row per word
545
+ const heads = Array.from(transcriptEl.querySelectorAll(".col-head"));
546
+ while (transcriptEl.firstChild) transcriptEl.removeChild(transcriptEl.firstChild);
547
+ heads.forEach((h) => transcriptEl.appendChild(h));
548
+
549
+ words.forEach((w, i) => {
550
+ const row = document.createElement("div");
551
+ row.className = "row";
552
+ row.dataset.idx = String(i);
553
+
554
+ const devanagari = document.createElement("div");
555
+ devanagari.className = "cell devanagari";
556
+ devanagari.textContent = w.word;
557
+
558
+ const ipa = document.createElement("div");
559
+ ipa.className = "cell ipa";
560
+ ipa.textContent = w.ipa;
561
+
562
+ const timing = document.createElement("div");
563
+ timing.className = "cell timing";
564
+ const range = document.createElement("span");
565
+ range.className = "range";
566
+ range.textContent = `${w.start.toFixed(2)}–${w.end.toFixed(2)}s`;
567
+ const bar = document.createElement("div");
568
+ bar.className = "bar";
569
+ const barFill = document.createElement("div");
570
+ barFill.className = "bar-fill";
571
+ barFill.style.transform = "scaleX(0)";
572
+ bar.appendChild(barFill);
573
+ timing.appendChild(range);
574
+ timing.appendChild(bar);
575
+
576
+ row.appendChild(devanagari);
577
+ row.appendChild(ipa);
578
+ row.appendChild(timing);
579
+ transcriptEl.appendChild(row);
580
+ });
581
+ }
582
+
583
+ // ── Waveform drawing ───────────────────────────────────────────────────────
584
+ // Static silhouette of the audio colored by word segments. A playhead is
585
+ // drawn on top in requestAnimationFrame tick during playback.
586
+ function drawWaveform(audio: Float32Array, words: WordTiming[], sampleRate: number) {
587
+ const canvas = waveformCanvas;
588
+ const dpr = window.devicePixelRatio || 1;
589
+ const cssW = canvas.clientWidth || 600;
590
+ const cssH = canvas.clientHeight || 56;
591
+ canvas.width = cssW * dpr;
592
+ canvas.height = cssH * dpr;
593
+ const ctx = canvas.getContext("2d")!;
594
+ ctx.scale(dpr, dpr);
595
+ ctx.clearRect(0, 0, cssW, cssH);
596
+
597
+ const totalDur = audio.length / sampleRate;
598
+ if (totalDur <= 0) return;
599
+
600
+ // Downsample audio into one bin per pixel column
601
+ const cols = Math.floor(cssW);
602
+ const samplesPerCol = Math.max(1, Math.floor(audio.length / cols));
603
+
604
+ // Per-pixel colour based on which word's time range the column belongs to
605
+ const colorForT = (t: number): string => {
606
+ for (const w of words) {
607
+ if (t >= w.start && t <= w.end) {
608
+ // alternate two warm tones for adjacent words so boundaries are visible
609
+ const idx = words.indexOf(w);
610
+ return idx % 2 === 0 ? "#E07A1F" : "#B65F14";
611
+ }
612
+ }
613
+ return "#C4A97A";
614
+ };
615
+
616
+ const mid = cssH / 2;
617
+ ctx.lineWidth = 1;
618
+ for (let x = 0; x < cols; x++) {
619
+ const t = (x / cols) * totalDur;
620
+ let peak = 0;
621
+ const from = x * samplesPerCol;
622
+ const to = Math.min(audio.length, from + samplesPerCol);
623
+ for (let i = from; i < to; i++) {
624
+ const v = Math.abs(audio[i]);
625
+ if (v > peak) peak = v;
626
+ }
627
+ const h = Math.max(1, peak * (cssH * 0.9));
628
+ ctx.strokeStyle = colorForT(t);
629
+ ctx.beginPath();
630
+ ctx.moveTo(x + 0.5, mid - h / 2);
631
+ ctx.lineTo(x + 0.5, mid + h / 2);
632
+ ctx.stroke();
633
+ }
634
+ }
635
+
636
+ // Called each frame during playback to overlay a moving playhead + a dimmed
637
+ // "past" region covering the played portion of the waveform.
638
+ function drawPlayhead(currentSec: number, totalSec: number) {
639
+ if (!currentAudio) return;
640
+ const canvas = waveformCanvas;
641
+ const cssW = canvas.clientWidth || 600;
642
+ const cssH = canvas.clientHeight || 56;
643
+ const ctx = canvas.getContext("2d")!;
644
+ // redraw underlying (cheap — ~600 stroked lines is trivial)
645
+ drawWaveform(currentAudio, currentWords, 24000);
646
+ const x = (currentSec / totalSec) * cssW;
647
+ // Dimmed overlay on the played portion (shows progress visually)
648
+ ctx.save();
649
+ ctx.globalCompositeOperation = "multiply";
650
+ ctx.fillStyle = "#D9C9A7"; // muted tan dims the bright saffron
651
+ ctx.fillRect(0, 0, x, cssH);
652
+ ctx.restore();
653
+ // Thin playhead line at the current position
654
+ ctx.save();
655
+ ctx.strokeStyle = "#8E2E2E";
656
+ ctx.lineWidth = 1.5;
657
+ ctx.beginPath();
658
+ ctx.moveTo(x, 0);
659
+ ctx.lineTo(x, cssH);
660
+ ctx.stroke();
661
+ ctx.restore();
662
+ }
663
+
664
+ // ── Live word highlighting + timing bar fill + waveform playhead ──────────
665
+ // Listeners attached ONCE at module load. The tick function always reads
666
+ // current state from DOM + module state — no stale closure captures when
667
+ // the user re-synthesizes with a different voice.
668
+ function tickHighlight(): void {
669
+ // audioEl.currentTime is media-time (independent of playbackRate), which
670
+ // matches our pred_dur-derived word timings directly.
671
+ const natSec = audioEl.currentTime;
672
+ const totalNatSec = currentAudio ? currentAudio.length / 24000 : 1;
673
+
674
+ let activeIdx = -1;
675
+ for (let i = 0; i < currentWords.length; i++) {
676
+ if (natSec >= currentWords[i].start && natSec <= currentWords[i].end) {
677
+ activeIdx = i;
678
+ break;
679
+ }
680
+ if (natSec > currentWords[i].end) activeIdx = i;
681
+ }
682
+
683
+ // Fresh DOM read every tick — always targets the currently-rendered nodes.
684
+ // Update compact transcript spans (the default-visible highlight).
685
+ compactEl.querySelectorAll<HTMLElement>(".word").forEach((span, i) => {
686
+ span.classList.toggle("active", i === activeIdx);
687
+ span.classList.toggle("past", i < activeIdx);
688
+ });
689
+ // Update detail rows + their timing-bar fills (only visible when details expanded).
690
+ const rows = transcriptEl.querySelectorAll<HTMLElement>(".row");
691
+ rows.forEach((row, i) => {
692
+ row.classList.toggle("active", i === activeIdx);
693
+ row.classList.toggle("past", i < activeIdx);
694
+ const fill = row.querySelector<HTMLElement>(".bar-fill");
695
+ if (!fill) return;
696
+ if (i < activeIdx) {
697
+ fill.style.transform = "scaleX(1)";
698
+ } else if (i === activeIdx) {
699
+ const w = currentWords[i];
700
+ const dur = Math.max(0.001, w.end - w.start);
701
+ const pct = Math.min(1, Math.max(0, (natSec - w.start) / dur));
702
+ fill.style.transform = `scaleX(${pct.toFixed(3)})`;
703
+ } else {
704
+ fill.style.transform = "scaleX(0)";
705
+ }
706
+ });
707
+
708
+ drawPlayhead(natSec, totalNatSec);
709
+
710
+ if (!audioEl.paused && !audioEl.ended) {
711
+ highlightRaf = requestAnimationFrame(tickHighlight);
712
+ } else {
713
+ highlightRaf = null;
714
+ }
715
+ }
716
+
717
+ audioEl.addEventListener("play", () => {
718
+ if (highlightRaf === null) highlightRaf = requestAnimationFrame(tickHighlight);
719
+ });
720
+ audioEl.addEventListener("ended", () => {
721
+ transcriptEl.querySelectorAll<HTMLElement>(".row").forEach((r) => r.classList.remove("active"));
722
+ compactEl.querySelectorAll<HTMLElement>(".word").forEach((s) => s.classList.remove("active"));
723
+ });
724
+
725
+ // Click a word in the compact transcript → seek audio playback to that word's
726
+ // start time. Also clickable in the per-word detail table.
727
+ function seekToWord(idx: number) {
728
+ const w = currentWords[idx];
729
+ if (!w || !currentAudio) return;
730
+ const wasPlaying = !audioEl.paused;
731
+ // Word timings are media-time seconds — assign directly (see loadedmetadata
732
+ // handler comment re: why `/ playbackRate` was removed).
733
+ audioEl.currentTime = w.start;
734
+ tickHighlight(); // update highlight instantly even if paused
735
+ if (!wasPlaying) audioEl.play().catch(() => {});
736
+ }
737
+ compactEl.addEventListener("click", (e) => {
738
+ const t = e.target as HTMLElement;
739
+ if (!t.classList.contains("word")) return;
740
+ const idx = parseInt(t.dataset.idx ?? "-1", 10);
741
+ if (idx >= 0) seekToWord(idx);
742
+ });
743
+ compactEl.addEventListener("keydown", (e) => {
744
+ if (e.key !== "Enter" && e.key !== " ") return;
745
+ const t = e.target as HTMLElement;
746
+ if (!t.classList.contains("word")) return;
747
+ e.preventDefault();
748
+ const idx = parseInt(t.dataset.idx ?? "-1", 10);
749
+ if (idx >= 0) seekToWord(idx);
750
+ });
751
+ // Same seek-on-click for Devanagari cell in the detail table rows
752
+ transcriptEl.addEventListener("click", (e) => {
753
+ const row = (e.target as HTMLElement).closest<HTMLElement>(".row");
754
+ if (!row) return;
755
+ const idx = parseInt(row.dataset.idx ?? "-1", 10);
756
+ if (idx >= 0) seekToWord(idx);
757
+ });
758
+
759
+ // ── Helpers ────────────────────────────────────────────────────────────────
760
+ function fmtSec(s: number): string { return s.toFixed(2) + "s"; }
761
+
762
+ function float32ToWav(samples: Float32Array, sr: number): Blob {
763
+ const buf = new ArrayBuffer(44 + samples.length * 2);
764
+ const view = new DataView(buf);
765
+ writeStr(view, 0, "RIFF");
766
+ view.setUint32(4, 36 + samples.length * 2, true);
767
+ writeStr(view, 8, "WAVE");
768
+ writeStr(view, 12, "fmt ");
769
+ view.setUint32(16, 16, true);
770
+ view.setUint16(20, 1, true);
771
+ view.setUint16(22, 1, true);
772
+ view.setUint32(24, sr, true);
773
+ view.setUint32(28, sr * 2, true);
774
+ view.setUint16(32, 2, true);
775
+ view.setUint16(34, 16, true);
776
+ writeStr(view, 36, "data");
777
+ view.setUint32(40, samples.length * 2, true);
778
+ let off = 44;
779
+ for (let i = 0; i < samples.length; i++, off += 2) {
780
+ const v = Math.max(-1, Math.min(1, samples[i]));
781
+ view.setInt16(off, v < 0 ? v * 0x8000 : v * 0x7fff, true);
782
+ }
783
+ return new Blob([buf], { type: "audio/wav" });
784
+ }
785
+ function writeStr(view: DataView, off: number, s: string) {
786
+ for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
787
+ }
788
+
789
+ bootLight().catch(err => setStatus(`boot failed: ${err.message}`, "error"));
src/model.ts ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AutoModel, Tensor, env } from "@huggingface/transformers";
2
+
3
+ // Where the ONNX model lives. The AutoModel loader will fetch:
4
+ // onnx/model.onnx + config.json from this repo on HF Hub.
5
+ const MODEL_REPO = "shreyask/bol-tts-marathi-onnx";
6
+
7
+ export interface KokoroVocab { [phone: string]: number; }
8
+ export interface KokoroConfig { vocab: KokoroVocab; n_token: number; }
9
+
10
+ export async function loadConfig(): Promise<KokoroConfig> {
11
+ const res = await fetch("/config.json");
12
+ if (!res.ok) throw new Error(`config.json fetch failed: ${res.status}`);
13
+ return res.json();
14
+ }
15
+
16
+ export async function loadVoicepack(voiceId: string): Promise<Float32Array> {
17
+ const res = await fetch(`/voices/${voiceId}.bin`);
18
+ if (!res.ok) throw new Error(`${voiceId}.bin fetch failed: ${res.status}`);
19
+ const buf = await res.arrayBuffer();
20
+ const f32 = new Float32Array(buf);
21
+ if (f32.length !== 510 * 1 * 256) {
22
+ throw new Error(`voicepack ${voiceId}: expected ${510 * 1 * 256} floats, got ${f32.length}`);
23
+ }
24
+ return f32;
25
+ }
26
+
27
+ export interface SynthesizeResult {
28
+ audio: Float32Array;
29
+ predDur: Int32Array;
30
+ sampleRate: number;
31
+ }
32
+
33
+ export class KokoroSession {
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ private model: any;
36
+ private isWebGpu: boolean;
37
+ private constructor(model: any, isWebGpu: boolean) { this.model = model; this.isWebGpu = isWebGpu; }
38
+
39
+ get backend(): string { return this.isWebGpu ? "webgpu" : "wasm"; }
40
+
41
+ static async create(onProgress?: (msg: string) => void): Promise<KokoroSession> {
42
+ env.allowRemoteModels = true;
43
+ env.allowLocalModels = false;
44
+
45
+ const hasWebGpu = typeof (navigator as any).gpu !== "undefined";
46
+ const device = hasWebGpu ? "webgpu" : "wasm";
47
+
48
+ // fp32 only — Kokoro's ISTFTNet decoder is sensitive to int8/int4 weight
49
+ // quantization (produces NaN or clipped audio). kokoro.js and HeadTTS both
50
+ // default to fp32 on WebGPU for this reason. Size is the cost of quality.
51
+ onProgress?.(`loading model from HF (${device}, ~325 MB one-time)…`);
52
+ const model = await AutoModel.from_pretrained(MODEL_REPO, {
53
+ dtype: "fp32",
54
+ device,
55
+ progress_callback: (p: any) => {
56
+ if (p.status === "progress" && p.file?.endsWith(".onnx")) {
57
+ const mb = (p.loaded / 1e6).toFixed(1);
58
+ const pct = p.progress ? `${p.progress.toFixed(1)}%` : "";
59
+ onProgress?.(`downloading ${p.file}: ${mb} MB ${pct}`);
60
+ } else if (p.status === "done") {
61
+ onProgress?.(`loaded ${p.file}`);
62
+ }
63
+ },
64
+ } as any);
65
+ return new KokoroSession(model, hasWebGpu);
66
+ }
67
+
68
+ async synthesize(inputIds: number[], refS: Float32Array): Promise<SynthesizeResult> {
69
+ const ids = BigInt64Array.from(inputIds.map(v => BigInt(v)));
70
+ const idTensor = new Tensor("int64", ids, [1, ids.length]);
71
+ const refTensor = new Tensor("float32", refS, [1, 256]);
72
+
73
+ const out = await this.model({ input_ids: idTensor, ref_s: refTensor });
74
+
75
+ // transformers.js wraps ort outputs as Tensor objects; unwrap .data
76
+ const audio = out.audio.data as Float32Array;
77
+
78
+ // pred_dur comes back as BigInt64Array from int64 ONNX output — downcast
79
+ const raw = out.pred_dur.data as BigInt64Array;
80
+ const predDur = new Int32Array(raw.length);
81
+ for (let i = 0; i < raw.length; i++) predDur[i] = Number(raw[i]);
82
+
83
+ return { audio, predDur, sampleRate: 24000 };
84
+ }
85
+ }
src/phonemize.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Public phonemize API — delegates to the espeak-ng WASM worker.
3
+ */
4
+
5
+ import { phonemizeByWord as _phonemizeByWord } from "./espeakng/phonemizer";
6
+
7
+ export interface PhonemizedWord { word: string; ipa: string; }
8
+
9
+ export async function phonemizeByWord(text: string): Promise<PhonemizedWord[]> {
10
+ return _phonemizeByWord(text, "mr");
11
+ }
src/styles.css ADDED
@@ -0,0 +1,478 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ─── palette ─────────────────────────────────────────────────────────────
2
+ * Maharashtra Warli-art palette: gerua (geru / गेरू, ochre-red earth pigment
3
+ * traditional to Warli wall paintings) + warm cream + kumkum highlight.
4
+ * Aligned to the SVG header (`warli-strip.svg`) which uses #c34a19 → #9f320f
5
+ * gradient on #fff8ec for the dancers/sun/microphones.
6
+ *
7
+ * Cream paper #FAF3E7
8
+ * Ivory card #F2E8D3
9
+ * Gerua (Warli red-ochre) #C34A19 ← was #E07A1F (too saffron-yellow)
10
+ * Gerua dark #9F320F ← matches SVG gradient endpoint
11
+ * Kumkum (deep red) #8E2E2E
12
+ * Ink brown #2E1D10
13
+ * Muted cocoa #8C6E56
14
+ * Warli white #FFF8EC ← matches SVG #fff8ec
15
+ * Sage (success) #6B8B4E
16
+ */
17
+ :root {
18
+ --bg: #FAF3E7;
19
+ --card: #F2E8D3;
20
+ --card-border: #D9C9A7;
21
+ --accent: #C34A19; /* gerua — earth-pigment red, authentic Warli */
22
+ --accent-dark: #9F320F; /* deep gerua, matches SVG gradient bottom */
23
+ --accent-soft: #C34A1922;
24
+ --red: #8E2E2E;
25
+ --ink: #2E1D10;
26
+ --muted: #8C6E56;
27
+ --good: #6B8B4E;
28
+ --bad: #A63A3A;
29
+ --warli: #FFF8EC;
30
+ font-family: "Inter", -apple-system, "Segoe UI", sans-serif;
31
+ }
32
+
33
+ * { box-sizing: border-box; }
34
+
35
+ html, body {
36
+ margin: 0;
37
+ background: var(--bg);
38
+ color: var(--ink);
39
+ min-height: 100vh;
40
+ line-height: 1.5;
41
+ }
42
+
43
+ /* ─── typography ──────────────────────────────────────────────────────────
44
+ * Yatra One: festival/display feel for title + ornamentals
45
+ * Tiro Devanagari Marathi: elegant serif for Devanagari body text
46
+ */
47
+ .font-display { font-family: "Yatra One", "Tiro Devanagari Marathi", serif; }
48
+ .font-devanagari {
49
+ font-family: "Inter", "Tiro Devanagari Marathi", "Noto Sans Devanagari", serif;
50
+ font-weight: 400;
51
+ }
52
+
53
+ /* ─── warli border strip ─────────────────────────────────────────────────
54
+ * Tribal art band — dancers, hut, deer, microphones, sun motifs in white on
55
+ * saffron. Originals are 2172×154 PNG; we display at fixed pixel height and
56
+ * stretch horizontally to the viewport (object-fit: cover).
57
+ */
58
+ .warli-strip {
59
+ width: 100%;
60
+ margin-top: 24px;
61
+ display: flex;
62
+ justify-content: center;
63
+ overflow: hidden;
64
+ }
65
+ .warli-strip img {
66
+ /* Natural aspect (2172×154 ≈ 14:1). width:100% fills the viewport up to
67
+ * max-width; past that the strip caps and cream gutters appear on either
68
+ * side. No object-fit cropping, no distortion, triangle borders always
69
+ * visible. */
70
+ width: 100%;
71
+ height: auto;
72
+ max-width: 1600px;
73
+ display: block;
74
+ }
75
+ .warli-strip--bottom {
76
+ margin-top: 24px;
77
+ /* No rotation — the SVG already has triangle borders on BOTH the top and
78
+ * bottom edges of the strip, so the same image works for both header and
79
+ * footer. Rotating 180° would have flipped figures upside-down. */
80
+ }
81
+
82
+ /* ─── layout ──────────────────────────────────────────────────────────── */
83
+ main {
84
+ max-width: 860px;
85
+ margin: 0 auto;
86
+ padding: 40px 24px 80px;
87
+ }
88
+
89
+ header {
90
+ text-align: center;
91
+ margin-bottom: 32px;
92
+ }
93
+ header h1 {
94
+ font-family: "Yatra One", "Tiro Devanagari Marathi", serif;
95
+ font-size: 38px;
96
+ margin: 0 0 6px;
97
+ color: var(--red);
98
+ letter-spacing: 0;
99
+ }
100
+ header .sub {
101
+ margin: 0;
102
+ color: var(--muted);
103
+ font-size: 14px;
104
+ }
105
+ header .namaste {
106
+ font-family: "Tiro Devanagari Marathi", serif;
107
+ font-size: 18px;
108
+ color: var(--accent-dark);
109
+ margin-bottom: 8px;
110
+ display: block;
111
+ }
112
+
113
+ .card {
114
+ background: var(--card);
115
+ border: 1px solid var(--card-border);
116
+ border-radius: 14px;
117
+ padding: 18px 20px;
118
+ margin-bottom: 16px;
119
+ box-shadow: 0 1px 2px rgba(46, 29, 16, 0.04);
120
+ }
121
+
122
+ .status {
123
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
124
+ font-size: 13px;
125
+ color: var(--muted);
126
+ padding: 10px 14px;
127
+ }
128
+ .status.ready { color: var(--good); }
129
+ .status.error { color: var(--bad); }
130
+
131
+ .model-gate {
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: space-between;
135
+ gap: 16px;
136
+ border-left: 3px solid var(--accent);
137
+ }
138
+ .model-gate-text { font-size: 14px; color: var(--ink); line-height: 1.7; }
139
+ .model-gate-text strong { color: var(--red); }
140
+ .model-gate-sub { color: var(--muted); font-size: 12px; display: block; margin-top: 4px; }
141
+ .model-gate-variant {
142
+ display: block;
143
+ font-size: 13px;
144
+ cursor: pointer;
145
+ padding: 2px 0;
146
+ }
147
+ .model-gate-variant input[type=radio] { accent-color: var(--accent); margin-right: 6px; }
148
+ .model-gate button { flex-shrink: 0; }
149
+
150
+ label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 6px; }
151
+
152
+ /* Row that puts the textarea label and the Transliterate utility button on
153
+ the same line, label flush left and button flush right. */
154
+ .text-label-row {
155
+ display: flex;
156
+ align-items: baseline;
157
+ justify-content: space-between;
158
+ gap: 12px;
159
+ margin-bottom: 6px;
160
+ }
161
+ .text-label-row label { margin: 0; }
162
+ .text-tool {
163
+ background: transparent;
164
+ color: var(--accent-dark);
165
+ border: 1px solid var(--card-border);
166
+ border-radius: 999px;
167
+ padding: 3px 10px;
168
+ font-size: 12px;
169
+ font-family: "Inter", "Tiro Devanagari Marathi", sans-serif;
170
+ cursor: pointer;
171
+ transition: background 120ms, color 120ms, border-color 120ms;
172
+ }
173
+ .text-tool:hover {
174
+ background: var(--accent-soft);
175
+ border-color: var(--accent);
176
+ color: var(--accent-dark);
177
+ }
178
+
179
+ textarea {
180
+ width: 100%;
181
+ background: var(--warli);
182
+ color: var(--ink);
183
+ border: 1px solid var(--card-border);
184
+ border-radius: 10px;
185
+ padding: 12px 14px;
186
+ font-size: 19px;
187
+ font-family: "Inter", "Tiro Devanagari Marathi", "Noto Sans Devanagari", serif;
188
+ line-height: 1.55;
189
+ resize: vertical;
190
+ }
191
+ textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
192
+
193
+ .examples { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 6px; align-items: center; }
194
+ .examples-label { color: var(--muted); font-size: 12px; margin-right: 4px; }
195
+ .chip {
196
+ background: transparent;
197
+ color: var(--ink);
198
+ border: 1px solid var(--card-border);
199
+ border-radius: 999px;
200
+ padding: 4px 10px;
201
+ font-size: 13px;
202
+ font-family: "Inter", "Tiro Devanagari Marathi", "Noto Sans Devanagari", serif;
203
+ cursor: pointer;
204
+ transition: background 120ms, color 120ms, border-color 120ms;
205
+ }
206
+ .chip:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-dark); }
207
+
208
+ .controls {
209
+ display: grid;
210
+ grid-template-columns: 1fr 1fr auto;
211
+ gap: 14px;
212
+ align-items: end;
213
+ margin-top: 14px;
214
+ }
215
+ .controls select, .controls input[type=range] {
216
+ width: 100%;
217
+ background: var(--warli);
218
+ color: var(--ink);
219
+ border: 1px solid var(--card-border);
220
+ border-radius: 8px;
221
+ padding: 7px 10px;
222
+ }
223
+ .controls input[type=range] {
224
+ accent-color: var(--accent);
225
+ padding: 4px 0;
226
+ }
227
+ .speed-control { margin-bottom: 0; }
228
+ .speed-control-row {
229
+ display: flex;
230
+ justify-content: space-between;
231
+ align-items: baseline;
232
+ margin-bottom: 6px;
233
+ font-size: 13px;
234
+ color: var(--muted);
235
+ }
236
+ .speed-control-row #speed-val {
237
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
238
+ font-size: 12px;
239
+ color: var(--accent-dark);
240
+ font-variant-numeric: tabular-nums;
241
+ }
242
+
243
+ button.primary {
244
+ background: var(--accent);
245
+ color: #fff;
246
+ border: none;
247
+ border-radius: 8px;
248
+ padding: 9px 20px;
249
+ font-weight: 600;
250
+ cursor: pointer;
251
+ font-size: 14px;
252
+ font-family: "Inter", "Tiro Devanagari Marathi", sans-serif;
253
+ }
254
+ button.primary:hover { background: var(--accent-dark); }
255
+ button.primary:disabled { opacity: 0.45; cursor: default; }
256
+
257
+ .ab-test { margin-top: 10px; }
258
+
259
+ /* ─── audio output area ──────────────────────────────────────────────── */
260
+ /* ─── fused player: play/pause button + seekable waveform + time ─────── */
261
+ .player {
262
+ display: grid;
263
+ grid-template-columns: 48px 1fr;
264
+ gap: 12px;
265
+ align-items: center;
266
+ background: var(--warli);
267
+ border: 1px solid var(--card-border);
268
+ border-radius: 10px;
269
+ padding: 10px 12px;
270
+ margin-bottom: 14px;
271
+ }
272
+ .playpause {
273
+ width: 44px;
274
+ height: 44px;
275
+ border-radius: 50%;
276
+ background: var(--accent);
277
+ color: #fff;
278
+ border: none;
279
+ font-size: 18px;
280
+ cursor: pointer;
281
+ display: flex;
282
+ align-items: center;
283
+ justify-content: center;
284
+ line-height: 1;
285
+ padding: 0;
286
+ transition: background 120ms;
287
+ }
288
+ .playpause:hover { background: var(--accent-dark); }
289
+ .playpause.playing { font-size: 16px; }
290
+
291
+ .player-main {
292
+ position: relative;
293
+ display: flex;
294
+ flex-direction: column;
295
+ gap: 2px;
296
+ }
297
+ .waveform {
298
+ width: 100%;
299
+ height: 56px;
300
+ background: transparent;
301
+ cursor: pointer;
302
+ display: block;
303
+ }
304
+ .waveform:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-radius: 4px; }
305
+ .player-time {
306
+ display: flex;
307
+ justify-content: space-between;
308
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
309
+ font-size: 11px;
310
+ color: var(--muted);
311
+ font-feature-settings: "tnum";
312
+ padding: 0 2px;
313
+ }
314
+
315
+ /* ─── compact transcript (default view) — Marathi text with per-word hl ── */
316
+ .transcript-compact {
317
+ font-family: "Inter", "Tiro Devanagari Marathi", "Noto Sans Devanagari", serif;
318
+ font-size: 24px;
319
+ line-height: 1.7;
320
+ padding: 14px 16px;
321
+ border: 1px solid var(--card-border);
322
+ border-radius: 10px;
323
+ background: var(--warli);
324
+ color: var(--ink);
325
+ min-height: 60px;
326
+ }
327
+ .transcript-compact .word {
328
+ display: inline-block;
329
+ padding: 1px 4px;
330
+ border-radius: 4px;
331
+ cursor: pointer;
332
+ transition: background-color 150ms ease, color 150ms ease;
333
+ }
334
+ .transcript-compact .word:hover { background: var(--accent-soft); }
335
+ .transcript-compact .word:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
336
+ .transcript-compact .word.active {
337
+ background: var(--accent);
338
+ color: #fff;
339
+ }
340
+ .transcript-compact .word.past { color: var(--muted); }
341
+
342
+ /* ─── expandable per-word detail (IPA + timing) ───────────────────────── */
343
+ .transcript-details {
344
+ margin-top: 10px;
345
+ }
346
+ .transcript-details > summary {
347
+ cursor: pointer;
348
+ color: var(--muted);
349
+ font-size: 12px;
350
+ padding: 4px 6px;
351
+ user-select: none;
352
+ }
353
+ .transcript-details > summary:hover { color: var(--accent-dark); }
354
+ .transcript-details[open] > summary { margin-bottom: 8px; }
355
+
356
+ /* ─── side-by-side Devanagari / IPA / timing columns ─────────────────── */
357
+ .sidebyside {
358
+ display: grid;
359
+ grid-template-columns: minmax(140px, 1.2fr) minmax(160px, 1.8fr) 2fr;
360
+ gap: 0;
361
+ border: 1px solid var(--card-border);
362
+ border-radius: 10px;
363
+ overflow: hidden;
364
+ background: var(--warli);
365
+ }
366
+ .sidebyside .col-head {
367
+ font-family: "Yatra One", serif;
368
+ font-size: 13px;
369
+ color: var(--red);
370
+ padding: 8px 12px;
371
+ border-bottom: 1px solid var(--card-border);
372
+ background: #EFE2C5;
373
+ letter-spacing: 0.02em;
374
+ }
375
+ .sidebyside .row {
376
+ display: contents;
377
+ }
378
+ .sidebyside .cell {
379
+ padding: 10px 12px;
380
+ border-bottom: 1px solid #E8DBBF;
381
+ display: flex;
382
+ align-items: center;
383
+ transition: background-color 180ms ease;
384
+ }
385
+ .sidebyside .row.active .cell { background: var(--accent-soft); }
386
+ .sidebyside .row.past .cell { color: var(--muted); }
387
+ .sidebyside .row:last-child .cell { border-bottom: none; }
388
+
389
+ .cell.devanagari {
390
+ font-family: "Inter", "Tiro Devanagari Marathi", "Noto Sans Devanagari", serif;
391
+ font-size: 22px;
392
+ font-weight: 500;
393
+ color: var(--ink);
394
+ }
395
+ .sidebyside .row.active .cell.devanagari { color: var(--red); }
396
+
397
+ .cell.ipa {
398
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
399
+ font-size: 14px;
400
+ color: var(--accent-dark);
401
+ }
402
+
403
+ .cell.timing {
404
+ position: relative;
405
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
406
+ font-size: 12px;
407
+ color: var(--muted);
408
+ padding: 10px 12px;
409
+ }
410
+ .cell.timing .bar {
411
+ position: absolute;
412
+ left: 10px;
413
+ right: 10px;
414
+ bottom: 6px;
415
+ height: 4px;
416
+ background: #E8DBBF;
417
+ border-radius: 2px;
418
+ overflow: hidden;
419
+ }
420
+ .cell.timing .bar-fill {
421
+ height: 100%;
422
+ background: var(--accent);
423
+ border-radius: 2px;
424
+ transform-origin: left center;
425
+ transition: transform 80ms linear;
426
+ }
427
+ .sidebyside .row.past .cell.timing .bar-fill {
428
+ background: var(--muted);
429
+ transform: scaleX(1);
430
+ }
431
+ .cell.timing .range {
432
+ font-feature-settings: "tnum";
433
+ margin-right: 10px;
434
+ }
435
+
436
+ /* ─── Marathi facts sidebar ──────────────────────────────────────────── */
437
+ .facts {
438
+ background: #EFE2C5;
439
+ border-left: 3px solid var(--accent);
440
+ padding: 10px 14px;
441
+ border-radius: 6px;
442
+ font-size: 13px;
443
+ color: var(--ink);
444
+ margin-top: 14px;
445
+ line-height: 1.55;
446
+ }
447
+ .facts strong { color: var(--red); font-weight: 600; }
448
+ .facts .ipa-inline {
449
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
450
+ color: var(--accent-dark);
451
+ }
452
+
453
+ /* ─── misc ───────────────────────────────────────────────────────────── */
454
+ .details { margin-top: 12px; }
455
+ .details summary { cursor: pointer; color: var(--muted); font-size: 12px; }
456
+ .phonemes, .word-timings {
457
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
458
+ font-size: 12px;
459
+ color: var(--muted);
460
+ white-space: pre-wrap;
461
+ margin: 8px 0 0;
462
+ padding: 10px;
463
+ background: #EAE0C5;
464
+ border-radius: 6px;
465
+ max-height: 220px;
466
+ overflow: auto;
467
+ }
468
+
469
+ footer {
470
+ margin-top: 40px;
471
+ color: var(--muted);
472
+ font-size: 12px;
473
+ text-align: center;
474
+ }
475
+ footer a,
476
+ header p a { color: var(--accent-dark); text-decoration: underline; }
477
+ footer a:hover,
478
+ header p a:hover { color: var(--red); }
src/timestamps.ts ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { TokenizeResult } from "./tokenize";
2
+
3
+ // pred_dur is in PREDICTOR frames, not mel frames. Empirically:
4
+ // audio.length / pred_dur.sum === 600 (not 300 as you'd guess from mel hop).
5
+ // The prosody predictor runs at half the mel-frame rate; the decoder
6
+ // upsamples 2× internally before iSTFT. So frame-to-seconds uses hop=600.
7
+ const HOP = 600;
8
+ const DEFAULT_SR = 24000;
9
+
10
+ export interface PhonemeTiming {
11
+ phone: string;
12
+ start: number; // seconds
13
+ end: number;
14
+ durFrames: number;
15
+ }
16
+
17
+ export interface WordTiming {
18
+ word: string;
19
+ ipa: string;
20
+ start: number; // seconds
21
+ end: number;
22
+ phonemes: PhonemeTiming[];
23
+ }
24
+
25
+ /**
26
+ * Turn pred_dur (per-phoneme duration in frames) + the per-word index ranges
27
+ * from tokenize.ts into phoneme + word level timestamps in seconds.
28
+ *
29
+ * Kokoro's prosody predictor outputs durations in *predictor frames* where
30
+ * 1 frame = 600 audio samples at 24 kHz (25 ms/frame). We accumulate and
31
+ * attribute to the originating word via the tokenStart/tokenEnd ranges that
32
+ * tokenizeByWord wrote down.
33
+ */
34
+ export function buildTimings(
35
+ tokenizeResult: TokenizeResult,
36
+ predDur: Int32Array | number[],
37
+ sampleRate: number = DEFAULT_SR,
38
+ ): { phonemes: PhonemeTiming[]; words: WordTiming[] } {
39
+ const durArr: number[] = Array.from(predDur as Iterable<number>);
40
+
41
+ if (durArr.length !== tokenizeResult.inputIds.length) {
42
+ // Kokoro sometimes returns pred_dur aligned to internal sequence length
43
+ // (post-padding, post-space-injection). We expect them to match; if they
44
+ // don't, warn and continue with the shorter of the two so we don't crash.
45
+ console.warn(
46
+ `pred_dur length (${durArr.length}) != input_ids length (${tokenizeResult.inputIds.length}) — truncating`,
47
+ );
48
+ }
49
+ const n = Math.min(durArr.length, tokenizeResult.inputIds.length);
50
+
51
+ // Phoneme-level
52
+ const phonemes: PhonemeTiming[] = [];
53
+ let cumFrames = 0;
54
+ for (let i = 0; i < n; i++) {
55
+ const df = durArr[i];
56
+ const start = (cumFrames * HOP) / sampleRate;
57
+ cumFrames += df;
58
+ const end = (cumFrames * HOP) / sampleRate;
59
+ phonemes.push({
60
+ phone: tokenizeResult.phonemes[i] ?? "?",
61
+ start,
62
+ end,
63
+ durFrames: df,
64
+ });
65
+ }
66
+
67
+ // Word-level: for each word, take start of first in-range phoneme and end of last
68
+ const words: WordTiming[] = [];
69
+ for (const w of tokenizeResult.words) {
70
+ const slice = phonemes.slice(w.tokenStart, Math.min(w.tokenEnd, phonemes.length));
71
+ if (slice.length === 0) continue;
72
+ words.push({
73
+ word: w.word,
74
+ ipa: w.ipa,
75
+ start: slice[0].start,
76
+ end: slice[slice.length - 1].end,
77
+ phonemes: slice,
78
+ });
79
+ }
80
+
81
+ return { phonemes, words };
82
+ }
src/tokenize.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { KokoroVocab } from "./model";
2
+
3
+ /**
4
+ * Result of turning text → model input, with word-level bookkeeping so we can
5
+ * align per-phoneme `pred_dur` back to words for timestamps.
6
+ */
7
+ export interface TokenizeResult {
8
+ inputIds: number[]; // token IDs for KModel
9
+ phonemes: string[]; // flat phoneme sequence (each ID's char)
10
+ words: Array<{
11
+ word: string; // Devanagari source
12
+ ipa: string; // raw espeak-ng output for this word
13
+ tokenStart: number; // index into inputIds for first kept phoneme
14
+ tokenEnd: number; // exclusive
15
+ }>;
16
+ droppedChars: Record<string, number>; // chars not in vocab, counts for diagnostics
17
+ }
18
+
19
+ /**
20
+ * Convert per-word IPA (from phonemize.ts) into model-ready input_ids, keeping
21
+ * track of which input_ids came from which word for downstream timestamping.
22
+ *
23
+ * Characters not in `vocab` are silently dropped — this mirrors KModel.forward's
24
+ * own tokenization (`input_ids = filter(lambda i: i is not None, map(vocab.get, phonemes))`).
25
+ */
26
+ export function tokenizeByWord(
27
+ wordIpas: Array<{ word: string; ipa: string }>,
28
+ vocab: KokoroVocab,
29
+ ): TokenizeResult {
30
+ const inputIds: number[] = [];
31
+ const phonemes: string[] = [];
32
+ const words: TokenizeResult["words"] = [];
33
+ const droppedChars: Record<string, number> = {};
34
+
35
+ const spaceId = vocab[" "];
36
+ for (let wi = 0; wi < wordIpas.length; wi++) {
37
+ const { word, ipa } = wordIpas[wi];
38
+ // Insert word-boundary space BEFORE every word except the first.
39
+ // (Trailing-space-after-last-word tends to get stripped by the predictor,
40
+ // which throws off pred_dur ↔ input_ids alignment and drops the last
41
+ // word from our timestamps. Leading-space avoids the issue.)
42
+ if (wi > 0 && spaceId !== undefined) {
43
+ inputIds.push(spaceId);
44
+ phonemes.push(" ");
45
+ }
46
+ const tokenStart = inputIds.length;
47
+ for (const c of ipa) {
48
+ const id = vocab[c];
49
+ if (id === undefined) {
50
+ droppedChars[c] = (droppedChars[c] ?? 0) + 1;
51
+ } else {
52
+ inputIds.push(id);
53
+ phonemes.push(c);
54
+ }
55
+ }
56
+ const tokenEnd = inputIds.length;
57
+ words.push({ word, ipa, tokenStart, tokenEnd });
58
+ }
59
+
60
+ return { inputIds, phonemes, words, droppedChars };
61
+ }
tsconfig.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "noImplicitAny": true,
9
+ "noUnusedLocals": true,
10
+ "esModuleInterop": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "isolatedModules": true,
14
+ "skipLibCheck": true,
15
+ "noEmit": true,
16
+ "resolveJsonModule": true,
17
+ "types": ["vite/client"]
18
+ },
19
+ "include": ["src"]
20
+ }
vite.config.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+
3
+ export default defineConfig({
4
+ // onnxruntime-web ships .wasm files that must be served as static assets.
5
+ // Vite's default public/ dir handles them, but the package's internal wasm
6
+ // paths need to be resolved correctly when bundling.
7
+ optimizeDeps: {
8
+ exclude: ["onnxruntime-web"],
9
+ },
10
+ server: {
11
+ port: 5173,
12
+ // Required for SharedArrayBuffer (onnxruntime-web multithreading uses it)
13
+ headers: {
14
+ "Cross-Origin-Opener-Policy": "same-origin",
15
+ "Cross-Origin-Embedder-Policy": "require-corp",
16
+ },
17
+ },
18
+ preview: {
19
+ headers: {
20
+ "Cross-Origin-Opener-Policy": "same-origin",
21
+ "Cross-Origin-Embedder-Policy": "require-corp",
22
+ },
23
+ },
24
+ });
voicepacks.json ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema_version": 1,
3
+ "_comment": "Source of truth for the bol-tts-marathi WebGPU demo's voice list + per-voice metadata + test phrases. Edit this to add/remove voices or update sample chips — main.ts consumes it dynamically on page load.",
4
+ "voices": [
5
+ {
6
+ "id": "mf_asha",
7
+ "name": "Asha",
8
+ "name_native": "आशा",
9
+ "gender": "female",
10
+ "source": "rasa",
11
+ "default_speed": 1.0,
12
+ "ship_status": "stable"
13
+ },
14
+ {
15
+ "id": "mm_vivek",
16
+ "name": "Vivek",
17
+ "name_native": "विवेक",
18
+ "gender": "male",
19
+ "source": "rasa",
20
+ "default_speed": 1.0,
21
+ "ship_status": "stable"
22
+ },
23
+ {
24
+ "id": "mf_mukta",
25
+ "name": "Mukta",
26
+ "name_native": "मुक्ता",
27
+ "gender": "female",
28
+ "source": "indicvoices_r",
29
+ "default_speed": 1.0,
30
+ "ship_status": "stable"
31
+ },
32
+ {
33
+ "id": "mm_dnyanesh",
34
+ "name": "Dnyanesh",
35
+ "name_native": "ज्ञानेश",
36
+ "gender": "male",
37
+ "source": "indicvoices_r",
38
+ "default_speed": 1.0,
39
+ "ship_status": "stable"
40
+ },
41
+ {
42
+ "id": "mf_priya",
43
+ "name": "Priya",
44
+ "name_native": "प्रिया",
45
+ "gender": "female",
46
+ "source": "springlab",
47
+ "default_speed": 1.0,
48
+ "ship_status": "v0.2_preview"
49
+ },
50
+ {
51
+ "id": "mm_arjun",
52
+ "name": "Arjun",
53
+ "name_native": "अर्जुन",
54
+ "gender": "male",
55
+ "source": "springlab",
56
+ "default_speed": 1.0,
57
+ "ship_status": "v0.2_preview"
58
+ }
59
+ ],
60
+ "test_phrases": {
61
+ "marathi_neutral": [
62
+ { "label": "हवामान", "text": "आज हवामान खूप छान आहे, पाऊस पडतो आहे." },
63
+ { "label": "महाराष्ट्र", "text": "महाराष्ट्र हे भारतातील एक महत्त्वाचे राज्य आहे." },
64
+ { "label": "मुंबई", "text": "मुंबई ही महाराष्ट्राची राजधानी आणि भारताची आर्थिक राजधानी आहे." },
65
+ { "label": "पुणे", "text": "पुणे ही महाराष्ट्राची सांस्कृतिक राजधानी मानली जाते." },
66
+ { "label": "आकडे", "text": "एक दोन तीन चार पाच सहा सात आठ." },
67
+ { "label": "कथा", "text": "पूर्वी एका छोट्या गावात एक शहाणा शेतकरी राहत होता." },
68
+ { "label": "गुढी पाडवा", "text": "गुढी पाडवा हा मराठी नववर्षाचा सण आहे." },
69
+ { "label": "भजी", "text": "पावसाळी संध्याकाळी गरम भजी आणि कटिंग चहा म्हणजे स्वर्गच!" },
70
+ { "label": "वडापाव", "text": "मुंबईचा वडापाव आणि पुण्याचा चहा — आपली खरी ओळख." }
71
+ ],
72
+ "marathi_female": [
73
+ { "label": "थकले", "text": "आज खूप काम केले, मी थकले आहे." }
74
+ ],
75
+ "marathi_male": [
76
+ { "label": "थकलो", "text": "आज खूप काम केले, मी थकलो आहे." }
77
+ ],
78
+ "minglish_neutral": [
79
+ { "label": "coffee?", "text": "Coffee पिऊया का?" },
80
+ { "label": "AI tech", "text": "Artificial intelligence ही technology खूप amazing आहे." },
81
+ { "label": "weekend plan", "text": "Weekend ला movie बघायचा plan आहे का?" }
82
+ ],
83
+ "minglish_female": [
84
+ { "label": "Google job", "text": "मी Google मध्ये काम करते." },
85
+ { "label": "late meeting", "text": "आज meeting ला late झाले." }
86
+ ],
87
+ "minglish_male": [
88
+ { "label": "Google job", "text": "मी Google मध्ये काम करतो." },
89
+ { "label": "late meeting", "text": "आज meeting ला late झालो." }
90
+ ]
91
+ }
92
+ }
voices/mf_priya.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c09561d3fda1dfddb88c461a79ff5d518198035a8906feece9fe38d6973225a1
3
+ size 522240
voices/mm_arjun.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d2eb6136af364a684e0504a77817e54ce6a96771284dee629c0d938e76b334b0
3
+ size 522240