@juspay/neurolink 12.48.2 → 12.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/browser/neurolink.min.js +616 -595
  3. package/dist/cli/commands/serve.d.ts +0 -3
  4. package/dist/cli/commands/serve.js +85 -3
  5. package/dist/cli/commands/transcribe.d.ts +24 -0
  6. package/dist/cli/commands/transcribe.js +718 -0
  7. package/dist/cli/factories/commandFactory.d.ts +10 -0
  8. package/dist/cli/factories/commandFactory.js +13 -0
  9. package/dist/cli/parser.js +3 -0
  10. package/dist/factories/mediaHandlerCatalog.js +11 -9
  11. package/dist/factories/sttDescriptors.d.ts +63 -0
  12. package/dist/factories/sttDescriptors.js +286 -0
  13. package/dist/index.d.ts +4 -1
  14. package/dist/index.js +7 -1
  15. package/dist/neurolink.d.ts +68 -1
  16. package/dist/neurolink.js +202 -4
  17. package/dist/processors/media/AudioProcessor.d.ts +15 -56
  18. package/dist/processors/media/AudioProcessor.js +105 -276
  19. package/dist/processors/media/VideoProcessor.d.ts +3 -5
  20. package/dist/processors/media/VideoProcessor.js +52 -51
  21. package/dist/server/abstract/baseServerAdapter.d.ts +7 -0
  22. package/dist/server/abstract/baseServerAdapter.js +13 -1
  23. package/dist/server/adapters/expressAdapter.d.ts +1 -0
  24. package/dist/server/adapters/expressAdapter.js +48 -4
  25. package/dist/server/adapters/fastifyAdapter.d.ts +1 -0
  26. package/dist/server/adapters/fastifyAdapter.js +38 -1
  27. package/dist/server/adapters/honoAdapter.d.ts +1 -4
  28. package/dist/server/adapters/honoAdapter.js +83 -1
  29. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  30. package/dist/server/adapters/koaAdapter.js +43 -0
  31. package/dist/server/index.d.ts +3 -2
  32. package/dist/server/index.js +3 -2
  33. package/dist/server/routes/index.d.ts +1 -0
  34. package/dist/server/routes/index.js +3 -0
  35. package/dist/server/routes/transcribeRoutes.d.ts +23 -0
  36. package/dist/server/routes/transcribeRoutes.js +506 -0
  37. package/dist/server/utils/rawBody.d.ts +32 -0
  38. package/dist/server/utils/rawBody.js +76 -0
  39. package/dist/server/utils/validation.d.ts +192 -0
  40. package/dist/server/utils/validation.js +100 -0
  41. package/dist/server/websocket/transcribeWebSocket.d.ts +42 -0
  42. package/dist/server/websocket/transcribeWebSocket.js +264 -0
  43. package/dist/types/cli.d.ts +46 -0
  44. package/dist/types/index.d.ts +1 -0
  45. package/dist/types/index.js +1 -0
  46. package/dist/types/providers.d.ts +7 -1
  47. package/dist/types/server.d.ts +117 -1
  48. package/dist/types/stt.d.ts +160 -7
  49. package/dist/types/transcribe.d.ts +506 -0
  50. package/dist/types/transcribe.js +29 -0
  51. package/dist/utils/speakerTurns.d.ts +28 -0
  52. package/dist/utils/speakerTurns.js +93 -0
  53. package/dist/utils/sttProcessor.d.ts +20 -2
  54. package/dist/utils/sttProcessor.js +73 -5
  55. package/dist/voice/correction/dictionary.d.ts +49 -0
  56. package/dist/voice/correction/dictionary.js +134 -0
  57. package/dist/voice/correction/guard.d.ts +35 -0
  58. package/dist/voice/correction/guard.js +161 -0
  59. package/dist/voice/correction/index.d.ts +47 -0
  60. package/dist/voice/correction/index.js +211 -0
  61. package/dist/voice/correction/rewrite.d.ts +92 -0
  62. package/dist/voice/correction/rewrite.js +322 -0
  63. package/dist/voice/errors.d.ts +15 -0
  64. package/dist/voice/errors.js +45 -0
  65. package/dist/voice/index.d.ts +20 -2
  66. package/dist/voice/index.js +75 -9
  67. package/dist/voice/providers/AzureSTT.d.ts +10 -7
  68. package/dist/voice/providers/AzureSTT.js +23 -75
  69. package/dist/voice/providers/DeepgramSTT.d.ts +17 -2
  70. package/dist/voice/providers/DeepgramSTT.js +109 -13
  71. package/dist/voice/providers/ElevenLabsSTT.d.ts +12 -2
  72. package/dist/voice/providers/ElevenLabsSTT.js +34 -8
  73. package/dist/voice/providers/GoogleSTT.d.ts +10 -8
  74. package/dist/voice/providers/GoogleSTT.js +21 -75
  75. package/dist/voice/providers/OpenAISTT.d.ts +30 -9
  76. package/dist/voice/providers/OpenAISTT.js +196 -21
  77. package/dist/voice/providers/WhistleSTT.d.ts +87 -0
  78. package/dist/voice/providers/WhistleSTT.js +313 -0
  79. package/dist/voice/streaming/chunkedStreamAdapter.d.ts +19 -0
  80. package/dist/voice/streaming/chunkedStreamAdapter.js +519 -0
  81. package/dist/voice/streaming/endpointer.d.ts +81 -0
  82. package/dist/voice/streaming/endpointer.js +247 -0
  83. package/dist/voice/streaming/localAgreement.d.ts +30 -0
  84. package/dist/voice/streaming/localAgreement.js +44 -0
  85. package/dist/voice/sttEnvCredentials.d.ts +12 -0
  86. package/dist/voice/sttEnvCredentials.js +12 -0
  87. package/dist/voice/transcribe.d.ts +47 -0
  88. package/dist/voice/transcribe.js +519 -0
  89. package/dist/voice/transcribeStream.d.ts +4 -0
  90. package/dist/voice/transcribeStream.js +584 -0
  91. package/dist/voice/whistle/assets.d.ts +47 -0
  92. package/dist/voice/whistle/assets.js +270 -0
  93. package/dist/voice/whistle/audio.d.ts +32 -0
  94. package/dist/voice/whistle/audio.js +267 -0
  95. package/dist/voice/whistle/engine.d.ts +32 -0
  96. package/dist/voice/whistle/engine.js +193 -0
  97. package/dist/voice/whistle/worker.d.ts +18 -0
  98. package/dist/voice/whistle/worker.js +294 -0
  99. package/docs-site/static/search-index.json +66 -8
  100. package/models/whistle/README.md +18 -0
  101. package/models/whistle/needle.js +2 -0
  102. package/models/whistle/needle.wasm +0 -0
  103. package/models/whistle/whistle.cact +0 -0
  104. package/package.json +7 -3
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Main-thread side of the Whistle engine: one warm `worker_threads` Worker
3
+ * that holds the WASM engine and the model, requests answered in order, and a
4
+ * fresh worker after a crash.
5
+ *
6
+ * The worker is unreferenced while idle, so a warm engine never keeps the
7
+ * process alive on its own; it is referenced again while a request is open.
8
+ *
9
+ * @module voice/whistle/engine
10
+ */
11
+ import { existsSync } from "node:fs";
12
+ import { fileURLToPath } from "node:url";
13
+ import { Worker } from "node:worker_threads";
14
+ import { logger } from "../../utils/logger.js";
15
+ import { STTError } from "../errors.js";
16
+ /** Model load is ~0.1 s; a boot that takes this long is stuck. */
17
+ const BOOT_TIMEOUT_MS = 60_000;
18
+ /**
19
+ * The compiled entry is `worker.js` next to this file. Running from source
20
+ * (tsx), only `worker.ts` exists; the worker inherits the loader through
21
+ * `execArgv`, so it can run the TypeScript file directly.
22
+ */
23
+ function workerEntry() {
24
+ const compiled = new URL("./worker.js", import.meta.url);
25
+ if (existsSync(fileURLToPath(compiled))) {
26
+ return compiled;
27
+ }
28
+ const source = new URL("./worker.ts", import.meta.url);
29
+ return existsSync(fileURLToPath(source)) ? source : compiled;
30
+ }
31
+ export class WhistleEngine {
32
+ paths;
33
+ worker;
34
+ ready;
35
+ nextId = 1;
36
+ pending = new Map();
37
+ constructor(paths) {
38
+ this.paths = paths;
39
+ }
40
+ /** Batch transcription of 16 kHz mono samples; long audio is chunked in the worker. */
41
+ async transcribe(pcm, language, keywords, timestamps) {
42
+ const msg = await this.request({
43
+ op: "transcribe",
44
+ pcm,
45
+ language,
46
+ keywords,
47
+ timestamps,
48
+ });
49
+ if (msg.kind !== "transcribed") {
50
+ throw STTError.transcriptionFailed("unexpected worker reply", "whistle");
51
+ }
52
+ return msg.result;
53
+ }
54
+ /** Feed about a second of samples to the engine's native stream. */
55
+ async streamProcess(pcm, language, keywords) {
56
+ const msg = await this.request({
57
+ op: "streamProcess",
58
+ pcm,
59
+ language,
60
+ keywords,
61
+ });
62
+ if (msg.kind !== "streamStep") {
63
+ throw STTError.streamError("unexpected worker reply", "whistle");
64
+ }
65
+ return msg.result;
66
+ }
67
+ /** Flush the stream's tail and reset its state. */
68
+ async streamStop() {
69
+ const msg = await this.request({ op: "streamStop" });
70
+ if (msg.kind !== "streamStep") {
71
+ throw STTError.streamError("unexpected worker reply", "whistle");
72
+ }
73
+ return msg.result;
74
+ }
75
+ /** Terminate the worker; the next call starts a new one. */
76
+ async dispose() {
77
+ const worker = this.worker;
78
+ this.worker = undefined;
79
+ this.ready = undefined;
80
+ this.failAll(new Error("Whistle engine disposed"));
81
+ if (worker) {
82
+ await worker.terminate().catch(() => undefined);
83
+ }
84
+ }
85
+ async request(body) {
86
+ await this.start();
87
+ const worker = this.worker;
88
+ if (!worker) {
89
+ throw STTError.transcriptionFailed("Whistle worker is not running", "whistle");
90
+ }
91
+ const id = this.nextId++;
92
+ // Hand the worker a copy it owns: transferred rather than cloned, and the
93
+ // caller's array stays usable.
94
+ let req;
95
+ const transfer = [];
96
+ if ("pcm" in body) {
97
+ const pcm = new Float32Array(body.pcm);
98
+ transfer.push(pcm.buffer);
99
+ req = { ...body, id, pcm };
100
+ }
101
+ else {
102
+ req = { ...body, id };
103
+ }
104
+ const response = new Promise((resolve, reject) => {
105
+ this.pending.set(id, { resolve, reject });
106
+ });
107
+ worker.ref();
108
+ worker.postMessage(req, transfer);
109
+ try {
110
+ const msg = await response;
111
+ if (msg.kind === "failed") {
112
+ throw STTError.transcriptionFailed(`Whistle engine: ${msg.error}`, "whistle");
113
+ }
114
+ return msg;
115
+ }
116
+ finally {
117
+ if (this.pending.size === 0 && this.worker === worker) {
118
+ worker.unref();
119
+ }
120
+ }
121
+ }
122
+ start() {
123
+ if (this.ready) {
124
+ return this.ready;
125
+ }
126
+ const workerData = { paths: this.paths };
127
+ const worker = new Worker(workerEntry(), {
128
+ workerData,
129
+ execArgv: process.execArgv.filter((arg) => !arg.startsWith("--input-type")),
130
+ });
131
+ this.worker = worker;
132
+ const ready = new Promise((resolve, reject) => {
133
+ const timer = setTimeout(() => {
134
+ reject(STTError.transcriptionFailed("Whistle worker did not start in time", "whistle"));
135
+ void this.crash(worker, new Error("boot timeout"));
136
+ }, BOOT_TIMEOUT_MS);
137
+ worker.on("message", (msg) => {
138
+ if (msg.kind === "ready") {
139
+ clearTimeout(timer);
140
+ logger.debug(`[whistle] engine ready in ${msg.loadMs} ms`);
141
+ resolve();
142
+ return;
143
+ }
144
+ if (msg.kind === "bootError") {
145
+ clearTimeout(timer);
146
+ reject(STTError.transcriptionFailed(`Whistle engine failed to load: ${msg.error}`, "whistle"));
147
+ void this.crash(worker, new Error(msg.error));
148
+ return;
149
+ }
150
+ const entry = this.pending.get(msg.id);
151
+ if (entry) {
152
+ this.pending.delete(msg.id);
153
+ entry.resolve(msg);
154
+ }
155
+ });
156
+ worker.on("error", (error) => {
157
+ clearTimeout(timer);
158
+ reject(STTError.transcriptionFailed(`Whistle worker crashed: ${error.message}`, "whistle", error));
159
+ void this.crash(worker, error);
160
+ });
161
+ worker.on("exit", (code) => {
162
+ clearTimeout(timer);
163
+ reject(STTError.transcriptionFailed(`Whistle worker exited (code ${code})`, "whistle"));
164
+ void this.crash(worker, new Error(`worker exited with code ${code}`));
165
+ });
166
+ });
167
+ this.ready = ready;
168
+ // A failed boot must not stick: the next call tries again.
169
+ ready.catch(() => {
170
+ if (this.ready === ready) {
171
+ this.ready = undefined;
172
+ }
173
+ });
174
+ return ready;
175
+ }
176
+ /** Drop a dead worker: reject what it held, and let the next call start a new one. */
177
+ async crash(worker, error) {
178
+ if (this.worker !== worker) {
179
+ return;
180
+ }
181
+ logger.warn(`[whistle] worker stopped (${error.message}); it will be restarted on the next call`);
182
+ this.worker = undefined;
183
+ this.ready = undefined;
184
+ this.failAll(error);
185
+ await worker.terminate().catch(() => undefined);
186
+ }
187
+ failAll(error) {
188
+ for (const [id, entry] of this.pending) {
189
+ this.pending.delete(id);
190
+ entry.reject(STTError.transcriptionFailed(`Whistle worker stopped: ${error.message}`, "whistle", error));
191
+ }
192
+ }
193
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Whistle worker thread: owns one instance of the Cactus "needle" WASM engine
3
+ * with the Whistle model loaded, and runs its synchronous calls off the main
4
+ * event loop. Messages are handled one at a time, in arrival order.
5
+ *
6
+ * `needle.js` is an Emscripten CommonJS build. It is evaluated through an
7
+ * explicit CommonJS wrapper rather than `require()`d, because a model
8
+ * directory that sits under a `"type": "module"` package would make Node load
9
+ * it as ESM, where its `module.exports` assignment is lost.
10
+ *
11
+ * Self-contained — node builtins and type-only imports, nothing else — so the
12
+ * thread starts quickly, never initialises the SDK's logger or config, and
13
+ * also runs straight from the TypeScript source under Node's type stripping
14
+ * (which does not map a sibling `./x.js` import onto `./x.ts`).
15
+ *
16
+ * @module voice/whistle/worker
17
+ */
18
+ export {};
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Whistle worker thread: owns one instance of the Cactus "needle" WASM engine
3
+ * with the Whistle model loaded, and runs its synchronous calls off the main
4
+ * event loop. Messages are handled one at a time, in arrival order.
5
+ *
6
+ * `needle.js` is an Emscripten CommonJS build. It is evaluated through an
7
+ * explicit CommonJS wrapper rather than `require()`d, because a model
8
+ * directory that sits under a `"type": "module"` package would make Node load
9
+ * it as ESM, where its `module.exports` assignment is lost.
10
+ *
11
+ * Self-contained — node builtins and type-only imports, nothing else — so the
12
+ * thread starts quickly, never initialises the SDK's logger or config, and
13
+ * also runs straight from the TypeScript source under Node's type stripping
14
+ * (which does not map a sibling `./x.js` import onto `./x.ts`).
15
+ *
16
+ * @module voice/whistle/worker
17
+ */
18
+ import { readFileSync } from "node:fs";
19
+ import { createRequire } from "node:module";
20
+ import { dirname } from "node:path";
21
+ import { runInThisContext } from "node:vm";
22
+ import { parentPort, workerData } from "node:worker_threads";
23
+ /** The engine's fixed input rate. */
24
+ const WHISTLE_SAMPLE_RATE = 16_000;
25
+ /** The engine's per-call cap, in seconds. */
26
+ const WHISTLE_MAX_CHUNK_SECONDS = 30;
27
+ /**
28
+ * Cut long audio at the quietest 20 ms frame near each boundary. Every piece
29
+ * stays under the 30 s cap: targets are 24 s apart with a ±3 s search, and the
30
+ * tail is never longer than 30 s because cutting continues while more than
31
+ * 30 s remain. Returns `[start, end)` sample offsets.
32
+ */
33
+ function whistleChunkBounds(pcm) {
34
+ const sr = WHISTLE_SAMPLE_RATE;
35
+ const n = pcm.length;
36
+ const frame = sr / 50;
37
+ const target = 24 * sr;
38
+ const search = 3 * sr;
39
+ const cap = WHISTLE_MAX_CHUNK_SECONDS * sr;
40
+ if (n <= cap) {
41
+ return [[0, n]];
42
+ }
43
+ const frames = Math.floor(n / frame);
44
+ const energy = new Float32Array(frames);
45
+ for (let f = 0; f < frames; f++) {
46
+ let e = 0;
47
+ for (let i = f * frame; i < (f + 1) * frame; i++) {
48
+ e += pcm[i] * pcm[i];
49
+ }
50
+ energy[f] = e;
51
+ }
52
+ const cuts = [0];
53
+ while (n - cuts[cuts.length - 1] > cap) {
54
+ const t = cuts[cuts.length - 1] + target;
55
+ const lo = Math.floor((t - search) / frame);
56
+ const hi = Math.min(frames, Math.floor((t + search) / frame));
57
+ let best = lo;
58
+ for (let f = lo; f < hi; f++) {
59
+ if (energy[f] < energy[best]) {
60
+ best = f;
61
+ }
62
+ }
63
+ cuts.push(best * frame);
64
+ }
65
+ cuts.push(n);
66
+ const out = [];
67
+ for (let i = 0; i + 1 < cuts.length; i++) {
68
+ out.push([cuts[i], cuts[i + 1]]);
69
+ }
70
+ return out;
71
+ }
72
+ /** JSON output buffer: text plus word timestamps for one ≤30 s pass. */
73
+ const OUT_CAP = 1 << 20;
74
+ function post(message) {
75
+ parentPort?.postMessage(message);
76
+ }
77
+ function loadFactory(jsPath) {
78
+ const source = readFileSync(jsPath, "utf8");
79
+ const wrapper = runInThisContext(`(function (exports, require, module, __filename, __dirname) {${source}\n})`, { filename: jsPath });
80
+ if (typeof wrapper !== "function") {
81
+ throw new Error("needle.js did not evaluate to a module wrapper");
82
+ }
83
+ const mod = { exports: {} };
84
+ wrapper(mod.exports, createRequire(jsPath), mod, jsPath, dirname(jsPath));
85
+ if (typeof mod.exports !== "function") {
86
+ throw new Error("needle.js did not export createNeedle()");
87
+ }
88
+ return mod.exports;
89
+ }
90
+ let engine;
91
+ let outPtr = 0;
92
+ async function boot(data) {
93
+ const t0 = performance.now();
94
+ const createNeedle = loadFactory(data.paths["needle.js"]);
95
+ const M = await createNeedle({
96
+ wasmBinary: readFileSync(data.paths["needle.wasm"]),
97
+ print: () => undefined,
98
+ printErr: () => undefined,
99
+ });
100
+ const weights = readFileSync(data.paths["whistle.cact"]);
101
+ const ptr = M._malloc(weights.length);
102
+ M.HEAPU8.set(weights, ptr);
103
+ const rc = M._needle_load(ptr, BigInt(weights.length));
104
+ M._free(ptr);
105
+ if (rc < 0) {
106
+ throw new Error(`needle_load failed: ${M.UTF8ToString(M._needle_last_error())}`);
107
+ }
108
+ outPtr = M._malloc(OUT_CAP);
109
+ engine = M;
110
+ return Math.round(performance.now() - t0);
111
+ }
112
+ function cString(M, s) {
113
+ if (!s) {
114
+ return 0;
115
+ }
116
+ const bytes = Buffer.from(s, "utf8");
117
+ const p = M._malloc(bytes.length + 1);
118
+ M.HEAPU8.set(bytes, p);
119
+ M.HEAPU8[p + bytes.length] = 0;
120
+ return p;
121
+ }
122
+ function copyIn(M, pcm) {
123
+ const p = M._malloc(Math.max(1, pcm.length) * 4);
124
+ // Re-read HEAPU8 after malloc: memory growth replaces the buffer.
125
+ new Float32Array(M.HEAPU8.buffer, p, pcm.length).set(pcm);
126
+ return p;
127
+ }
128
+ function readJson(M, rc) {
129
+ if (rc < 0) {
130
+ throw new Error(M.UTF8ToString(M._needle_last_error()) || "needle call failed");
131
+ }
132
+ const parsed = JSON.parse(M.UTF8ToString(outPtr));
133
+ if (!parsed || typeof parsed !== "object") {
134
+ throw new Error("needle returned a non-object result");
135
+ }
136
+ return parsed;
137
+ }
138
+ function num(v) {
139
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
140
+ }
141
+ function words(v, offset) {
142
+ if (!Array.isArray(v)) {
143
+ return [];
144
+ }
145
+ const out = [];
146
+ for (const w of v) {
147
+ if (!w || typeof w !== "object") {
148
+ continue;
149
+ }
150
+ const r = w;
151
+ if (typeof r.word !== "string") {
152
+ continue;
153
+ }
154
+ out.push({
155
+ word: r.word,
156
+ start: round3((num(r.start) ?? 0) + offset),
157
+ end: round3((num(r.end) ?? 0) + offset),
158
+ probability: num(r.probability) ?? 0,
159
+ });
160
+ }
161
+ return out;
162
+ }
163
+ function round3(x) {
164
+ return Math.round(x * 1000) / 1000;
165
+ }
166
+ function str(v) {
167
+ return typeof v === "string" && v.length > 0 ? v : null;
168
+ }
169
+ function transcribe(M, pcm, language, keywords, timestamps) {
170
+ const bounds = whistleChunkBounds(pcm);
171
+ const texts = [];
172
+ const allWords = [];
173
+ const tps = [];
174
+ let ttft = null;
175
+ let detected = null;
176
+ const kwPtr = cString(M, keywords);
177
+ try {
178
+ for (const [a, b] of bounds) {
179
+ // Once a language is detected, hold it for the remaining chunks.
180
+ const langPtr = cString(M, language ?? detected);
181
+ const pcmPtr = copyIn(M, pcm.subarray(a, b));
182
+ try {
183
+ const r = readJson(M, M._needle_transcribe(pcmPtr, b - a, langPtr, kwPtr, timestamps ? 1 : 0, outPtr, OUT_CAP));
184
+ const text = typeof r.text === "string" ? r.text.trim() : "";
185
+ if (text) {
186
+ texts.push(text);
187
+ }
188
+ ttft ??= num(r.ttft_ms);
189
+ detected ??= str(r.language);
190
+ const speed = num(r.decode_tps);
191
+ if (speed !== null && speed > 0) {
192
+ tps.push(speed);
193
+ }
194
+ allWords.push(...words(r.words, a / WHISTLE_SAMPLE_RATE));
195
+ }
196
+ finally {
197
+ M._free(pcmPtr);
198
+ if (langPtr) {
199
+ M._free(langPtr);
200
+ }
201
+ }
202
+ }
203
+ }
204
+ finally {
205
+ if (kwPtr) {
206
+ M._free(kwPtr);
207
+ }
208
+ }
209
+ return {
210
+ text: texts.join(" "),
211
+ language: detected,
212
+ words: allWords,
213
+ ttftMs: ttft,
214
+ decodeTps: tps.length
215
+ ? Math.round(tps.reduce((x, y) => x + y, 0) / tps.length)
216
+ : null,
217
+ chunks: bounds.length,
218
+ };
219
+ }
220
+ function toStep(r) {
221
+ return {
222
+ text: typeof r.text === "string" ? r.text.trim() : "",
223
+ words: words(r.words, 0),
224
+ pending: typeof r.pending === "string" ? r.pending.trim() : "",
225
+ language: str(r.language),
226
+ received: num(r.received) ?? 0,
227
+ passMs: num(r.pass_ms) ?? 0,
228
+ };
229
+ }
230
+ function handle(M, req) {
231
+ try {
232
+ if (req.op === "transcribe") {
233
+ post({
234
+ kind: "transcribed",
235
+ id: req.id,
236
+ result: transcribe(M, req.pcm, req.language, req.keywords, req.timestamps),
237
+ });
238
+ return;
239
+ }
240
+ if (req.op === "streamProcess") {
241
+ const langPtr = cString(M, req.language);
242
+ const kwPtr = cString(M, req.keywords);
243
+ const pcmPtr = copyIn(M, req.pcm);
244
+ try {
245
+ const r = readJson(M, M._needle_stream_transcribe_process(pcmPtr, req.pcm.length, langPtr, kwPtr, outPtr, OUT_CAP));
246
+ post({ kind: "streamStep", id: req.id, result: toStep(r) });
247
+ }
248
+ finally {
249
+ M._free(pcmPtr);
250
+ if (langPtr) {
251
+ M._free(langPtr);
252
+ }
253
+ if (kwPtr) {
254
+ M._free(kwPtr);
255
+ }
256
+ }
257
+ return;
258
+ }
259
+ const r = readJson(M, M._needle_stream_transcribe_stop(outPtr, OUT_CAP));
260
+ post({ kind: "streamStep", id: req.id, result: toStep(r) });
261
+ }
262
+ catch (error) {
263
+ post({
264
+ kind: "failed",
265
+ id: req.id,
266
+ error: error instanceof Error ? error.message : String(error),
267
+ });
268
+ }
269
+ }
270
+ const queue = [];
271
+ parentPort?.on("message", (req) => {
272
+ if (engine) {
273
+ handle(engine, req);
274
+ }
275
+ else {
276
+ queue.push(req);
277
+ }
278
+ });
279
+ boot(workerData)
280
+ .then((loadMs) => {
281
+ post({ kind: "ready", loadMs });
282
+ const M = engine;
283
+ if (M) {
284
+ for (const req of queue.splice(0)) {
285
+ handle(M, req);
286
+ }
287
+ }
288
+ })
289
+ .catch((error) => {
290
+ post({
291
+ kind: "bootError",
292
+ error: error instanceof Error ? error.message : String(error),
293
+ });
294
+ });