@juspay/neurolink 12.48.2 → 12.49.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +2 -2
  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/providerDescriptors.js +5 -4
  12. package/dist/factories/sttDescriptors.d.ts +63 -0
  13. package/dist/factories/sttDescriptors.js +286 -0
  14. package/dist/index.d.ts +4 -1
  15. package/dist/index.js +7 -1
  16. package/dist/neurolink.d.ts +68 -1
  17. package/dist/neurolink.js +202 -4
  18. package/dist/processors/media/AudioProcessor.d.ts +15 -56
  19. package/dist/processors/media/AudioProcessor.js +105 -276
  20. package/dist/processors/media/VideoProcessor.d.ts +3 -5
  21. package/dist/processors/media/VideoProcessor.js +52 -51
  22. package/dist/providers/cloudflareClef.js +9 -1
  23. package/dist/server/abstract/baseServerAdapter.d.ts +7 -0
  24. package/dist/server/abstract/baseServerAdapter.js +13 -1
  25. package/dist/server/adapters/expressAdapter.d.ts +1 -0
  26. package/dist/server/adapters/expressAdapter.js +48 -4
  27. package/dist/server/adapters/fastifyAdapter.d.ts +1 -0
  28. package/dist/server/adapters/fastifyAdapter.js +38 -1
  29. package/dist/server/adapters/honoAdapter.d.ts +1 -4
  30. package/dist/server/adapters/honoAdapter.js +83 -1
  31. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  32. package/dist/server/adapters/koaAdapter.js +43 -0
  33. package/dist/server/index.d.ts +3 -2
  34. package/dist/server/index.js +3 -2
  35. package/dist/server/routes/index.d.ts +1 -0
  36. package/dist/server/routes/index.js +3 -0
  37. package/dist/server/routes/transcribeRoutes.d.ts +23 -0
  38. package/dist/server/routes/transcribeRoutes.js +506 -0
  39. package/dist/server/utils/rawBody.d.ts +32 -0
  40. package/dist/server/utils/rawBody.js +76 -0
  41. package/dist/server/utils/validation.d.ts +192 -0
  42. package/dist/server/utils/validation.js +100 -0
  43. package/dist/server/websocket/transcribeWebSocket.d.ts +42 -0
  44. package/dist/server/websocket/transcribeWebSocket.js +264 -0
  45. package/dist/types/cli.d.ts +46 -0
  46. package/dist/types/index.d.ts +1 -0
  47. package/dist/types/index.js +1 -0
  48. package/dist/types/providers.d.ts +7 -1
  49. package/dist/types/server.d.ts +117 -1
  50. package/dist/types/stt.d.ts +160 -7
  51. package/dist/types/transcribe.d.ts +506 -0
  52. package/dist/types/transcribe.js +29 -0
  53. package/dist/utils/speakerTurns.d.ts +28 -0
  54. package/dist/utils/speakerTurns.js +93 -0
  55. package/dist/utils/sttProcessor.d.ts +20 -2
  56. package/dist/utils/sttProcessor.js +73 -5
  57. package/dist/voice/correction/dictionary.d.ts +49 -0
  58. package/dist/voice/correction/dictionary.js +134 -0
  59. package/dist/voice/correction/guard.d.ts +35 -0
  60. package/dist/voice/correction/guard.js +161 -0
  61. package/dist/voice/correction/index.d.ts +47 -0
  62. package/dist/voice/correction/index.js +211 -0
  63. package/dist/voice/correction/rewrite.d.ts +92 -0
  64. package/dist/voice/correction/rewrite.js +322 -0
  65. package/dist/voice/errors.d.ts +15 -0
  66. package/dist/voice/errors.js +45 -0
  67. package/dist/voice/index.d.ts +20 -2
  68. package/dist/voice/index.js +75 -9
  69. package/dist/voice/providers/AzureSTT.d.ts +10 -7
  70. package/dist/voice/providers/AzureSTT.js +23 -75
  71. package/dist/voice/providers/DeepgramSTT.d.ts +17 -2
  72. package/dist/voice/providers/DeepgramSTT.js +109 -13
  73. package/dist/voice/providers/ElevenLabsSTT.d.ts +12 -2
  74. package/dist/voice/providers/ElevenLabsSTT.js +34 -8
  75. package/dist/voice/providers/GoogleSTT.d.ts +10 -8
  76. package/dist/voice/providers/GoogleSTT.js +21 -75
  77. package/dist/voice/providers/OpenAISTT.d.ts +30 -9
  78. package/dist/voice/providers/OpenAISTT.js +196 -21
  79. package/dist/voice/providers/WhistleSTT.d.ts +87 -0
  80. package/dist/voice/providers/WhistleSTT.js +313 -0
  81. package/dist/voice/streaming/chunkedStreamAdapter.d.ts +19 -0
  82. package/dist/voice/streaming/chunkedStreamAdapter.js +519 -0
  83. package/dist/voice/streaming/endpointer.d.ts +81 -0
  84. package/dist/voice/streaming/endpointer.js +247 -0
  85. package/dist/voice/streaming/localAgreement.d.ts +30 -0
  86. package/dist/voice/streaming/localAgreement.js +44 -0
  87. package/dist/voice/sttEnvCredentials.d.ts +12 -0
  88. package/dist/voice/sttEnvCredentials.js +12 -0
  89. package/dist/voice/transcribe.d.ts +47 -0
  90. package/dist/voice/transcribe.js +519 -0
  91. package/dist/voice/transcribeStream.d.ts +4 -0
  92. package/dist/voice/transcribeStream.js +584 -0
  93. package/dist/voice/whistle/assets.d.ts +47 -0
  94. package/dist/voice/whistle/assets.js +270 -0
  95. package/dist/voice/whistle/audio.d.ts +32 -0
  96. package/dist/voice/whistle/audio.js +267 -0
  97. package/dist/voice/whistle/engine.d.ts +32 -0
  98. package/dist/voice/whistle/engine.js +193 -0
  99. package/dist/voice/whistle/worker.d.ts +18 -0
  100. package/dist/voice/whistle/worker.js +294 -0
  101. package/docs-site/static/search-index.json +71 -12
  102. package/models/whistle/README.md +18 -0
  103. package/models/whistle/needle.js +2 -0
  104. package/models/whistle/needle.wasm +0 -0
  105. package/models/whistle/whistle.cact +0 -0
  106. package/package.json +7 -3
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Whistle — the built-in, zero-config, on-device speech-to-text engine.
3
+ *
4
+ * Runs Cactus's Whistle model (17 MB) on the Cactus "needle" engine, an
5
+ * Emscripten WebAssembly build, inside a `worker_threads` Worker. No API key,
6
+ * no network after the first use, no native addon: it is the STT provider
7
+ * NeuroLink falls back to when no other one is configured.
8
+ *
9
+ * **Files.** `needle.js`, `needle.wasm` and `whistle.cact` (~17.9 MB in all)
10
+ * are downloaded from pinned Hugging Face revisions on first use, checked
11
+ * against SHA-256 and kept in the model directory:
12
+ * `credentials.stt.whistle.modelDir` → `NEUROLINK_WHISTLE_DIR` →
13
+ * `$NEUROLINK_MODEL_DIR/whistle` → `~/.neurolink/models/whistle`. Set
14
+ * `NEUROLINK_WHISTLE_AUTO_DOWNLOAD=0` (or `autoDownload: false`) to forbid the
15
+ * download; a missing file then fails with an error naming each file, where it
16
+ * goes and where to get it.
17
+ *
18
+ * **Languages.** English, German, French, Spanish, Italian, Dutch and Polish
19
+ * (`en de fr es it nl pl`). `options.language` accepts a code or a locale
20
+ * (`"de-DE"`); `"auto"`, omitted or any other language lets the engine detect
21
+ * one of the seven.
22
+ *
23
+ * **Limits.** The engine reads 30 s per call; longer audio is cut at the
24
+ * quietest 20 ms frame near every 24 s mark and the word times are offset
25
+ * back onto the whole recording. One hour per request. Roughly 0.5 s of CPU
26
+ * per 10 s of audio on a laptop core; one warm worker per handler instance,
27
+ * requests served in order.
28
+ *
29
+ * **Keyword biasing.** `options.vocabulary` is passed to the engine as
30
+ * keywords only when `options.model` is `"whistle-keywords"` or
31
+ * `NEUROLINK_WHISTLE_KEYWORDS=1` — the engine can loop on long keyword lists,
32
+ * so it is opt-in and capped at {@link MAX_KEYWORDS} entries.
33
+ *
34
+ * **Formats.** WAV (any PCM/float layout, rate and channel count) and raw
35
+ * `pcm16` are decoded in-process; mp3, m4a, ogg/opus, webm, flac and mp4 need
36
+ * ffmpeg (`ffmpeg-static`, `FFMPEG_PATH` or `ffmpeg` on PATH).
37
+ *
38
+ * **Streaming.** Native: `transcribeStream()` feeds the engine's own streaming
39
+ * decoder about a second at a time, on a dedicated worker per stream. Each
40
+ * step yields a final segment for the words two passes agreed on, and an
41
+ * interim segment for the unconfirmed tail. Input frames are PCM16LE mono at
42
+ * `options.sampleRate` (default 16 kHz).
43
+ *
44
+ * **Accuracy.** A 17 MB model: good on clear speech in its seven languages,
45
+ * noticeably behind cloud engines and large Whisper-class models on accents,
46
+ * noise, crosstalk, names and jargon. Use a dictionary/correction pass, or a
47
+ * cloud provider, where that matters.
48
+ *
49
+ * @module voice/providers/WhistleSTT
50
+ */
51
+ import type { STTCredentials, STTHandler, STTLanguage, STTOptions, STTResult, TranscriptionSegment, TTSAudioFormat } from "../../types/index.js";
52
+ export declare class WhistleSTT implements STTHandler {
53
+ /** One hour per request (16 kHz float samples: ~230 MB in memory). */
54
+ readonly maxAudioDuration: number;
55
+ /** The engine has its own streaming decoder. */
56
+ readonly supportsStreaming = true;
57
+ private readonly modelDir;
58
+ private readonly autoDownload;
59
+ private engine;
60
+ constructor(slice?: STTCredentials["whistle"]);
61
+ /**
62
+ * True when the engine can run: its files are already in the model dir, or
63
+ * it is allowed to fetch them. With `NEUROLINK_WHISTLE_AUTO_DOWNLOAD=0` (or
64
+ * `autoDownload: false`) and no files, it reports unconfigured, so a caller
65
+ * asserting "no STT backend" is not answered by a 17 MB download.
66
+ */
67
+ isConfigured(): boolean;
68
+ describeConfiguration(): string;
69
+ /** Directory the engine's files live in. */
70
+ getModelDir(): string;
71
+ getSupportedFormats(): TTSAudioFormat[];
72
+ getSupportedLanguages(): Promise<STTLanguage[]>;
73
+ /** Download/verify the files and start the worker now rather than on the first request. */
74
+ warmUp(): Promise<void>;
75
+ /** Stop the warm worker. The handler stays usable; the next call starts a new one. */
76
+ dispose(): Promise<void>;
77
+ transcribe(audio: Buffer | ArrayBuffer, options?: STTOptions): Promise<STTResult>;
78
+ /**
79
+ * Native streaming. `audioStream` yields PCM16LE mono frames at
80
+ * `options.sampleRate` (default 16 kHz), any frame size. Yields a final
81
+ * segment for each run of words the engine commits (times from the start of
82
+ * the stream; join the finals with a space for the transcript) and an
83
+ * interim segment carrying the unconfirmed tail whenever it changes.
84
+ */
85
+ transcribeStream(audioStream: AsyncIterable<Buffer>, options?: STTOptions): AsyncIterable<TranscriptionSegment>;
86
+ private getEngine;
87
+ }
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Whistle — the built-in, zero-config, on-device speech-to-text engine.
3
+ *
4
+ * Runs Cactus's Whistle model (17 MB) on the Cactus "needle" engine, an
5
+ * Emscripten WebAssembly build, inside a `worker_threads` Worker. No API key,
6
+ * no network after the first use, no native addon: it is the STT provider
7
+ * NeuroLink falls back to when no other one is configured.
8
+ *
9
+ * **Files.** `needle.js`, `needle.wasm` and `whistle.cact` (~17.9 MB in all)
10
+ * are downloaded from pinned Hugging Face revisions on first use, checked
11
+ * against SHA-256 and kept in the model directory:
12
+ * `credentials.stt.whistle.modelDir` → `NEUROLINK_WHISTLE_DIR` →
13
+ * `$NEUROLINK_MODEL_DIR/whistle` → `~/.neurolink/models/whistle`. Set
14
+ * `NEUROLINK_WHISTLE_AUTO_DOWNLOAD=0` (or `autoDownload: false`) to forbid the
15
+ * download; a missing file then fails with an error naming each file, where it
16
+ * goes and where to get it.
17
+ *
18
+ * **Languages.** English, German, French, Spanish, Italian, Dutch and Polish
19
+ * (`en de fr es it nl pl`). `options.language` accepts a code or a locale
20
+ * (`"de-DE"`); `"auto"`, omitted or any other language lets the engine detect
21
+ * one of the seven.
22
+ *
23
+ * **Limits.** The engine reads 30 s per call; longer audio is cut at the
24
+ * quietest 20 ms frame near every 24 s mark and the word times are offset
25
+ * back onto the whole recording. One hour per request. Roughly 0.5 s of CPU
26
+ * per 10 s of audio on a laptop core; one warm worker per handler instance,
27
+ * requests served in order.
28
+ *
29
+ * **Keyword biasing.** `options.vocabulary` is passed to the engine as
30
+ * keywords only when `options.model` is `"whistle-keywords"` or
31
+ * `NEUROLINK_WHISTLE_KEYWORDS=1` — the engine can loop on long keyword lists,
32
+ * so it is opt-in and capped at {@link MAX_KEYWORDS} entries.
33
+ *
34
+ * **Formats.** WAV (any PCM/float layout, rate and channel count) and raw
35
+ * `pcm16` are decoded in-process; mp3, m4a, ogg/opus, webm, flac and mp4 need
36
+ * ffmpeg (`ffmpeg-static`, `FFMPEG_PATH` or `ffmpeg` on PATH).
37
+ *
38
+ * **Streaming.** Native: `transcribeStream()` feeds the engine's own streaming
39
+ * decoder about a second at a time, on a dedicated worker per stream. Each
40
+ * step yields a final segment for the words two passes agreed on, and an
41
+ * interim segment for the unconfirmed tail. Input frames are PCM16LE mono at
42
+ * `options.sampleRate` (default 16 kHz).
43
+ *
44
+ * **Accuracy.** A 17 MB model: good on clear speech in its seven languages,
45
+ * noticeably behind cloud engines and large Whisper-class models on accents,
46
+ * noise, crosstalk, names and jargon. Use a dictionary/correction pass, or a
47
+ * cloud provider, where that matters.
48
+ *
49
+ * @module voice/providers/WhistleSTT
50
+ */
51
+ import { logger } from "../../utils/logger.js";
52
+ import { STTError } from "../errors.js";
53
+ import { describeWhistleAssets, ensureWhistleAssets, resolveWhistleDir, whistleAssetsPresent, whistleAutoDownloadEnabled, } from "../whistle/assets.js";
54
+ import { WHISTLE_FFMPEG_FORMATS, WHISTLE_NATIVE_FORMATS, WHISTLE_SAMPLE_RATE, decodePcm16, decodeToWhistlePcm, } from "../whistle/audio.js";
55
+ import { WhistleEngine } from "../whistle/engine.js";
56
+ const PROVIDER = "whistle";
57
+ const MODEL = "whistle";
58
+ /** `options.model` value that turns keyword biasing on. */
59
+ const KEYWORDS_MODEL = "whistle-keywords";
60
+ /** Longer keyword lists make the decoder loop. */
61
+ const MAX_KEYWORDS = 32;
62
+ /** Confidence reported for non-empty text when the engine gave no word scores. */
63
+ const DEFAULT_CONFIDENCE = 0.9;
64
+ const LANGUAGES = [
65
+ { code: "en", name: "English" },
66
+ { code: "de", name: "German" },
67
+ { code: "fr", name: "French" },
68
+ { code: "es", name: "Spanish" },
69
+ { code: "it", name: "Italian" },
70
+ { code: "nl", name: "Dutch" },
71
+ { code: "pl", name: "Polish" },
72
+ ];
73
+ const LANGUAGE_CODES = new Set(LANGUAGES.map((l) => l.code));
74
+ export class WhistleSTT {
75
+ /** One hour per request (16 kHz float samples: ~230 MB in memory). */
76
+ maxAudioDuration = 60 * 60;
77
+ /** The engine has its own streaming decoder. */
78
+ supportsStreaming = true;
79
+ modelDir;
80
+ autoDownload;
81
+ engine;
82
+ constructor(slice) {
83
+ this.modelDir = resolveWhistleDir(slice);
84
+ this.autoDownload = whistleAutoDownloadEnabled(slice);
85
+ }
86
+ /**
87
+ * True when the engine can run: its files are already in the model dir, or
88
+ * it is allowed to fetch them. With `NEUROLINK_WHISTLE_AUTO_DOWNLOAD=0` (or
89
+ * `autoDownload: false`) and no files, it reports unconfigured, so a caller
90
+ * asserting "no STT backend" is not answered by a 17 MB download.
91
+ */
92
+ isConfigured() {
93
+ return whistleAssetsPresent(this.modelDir) || this.autoDownload;
94
+ }
95
+ describeConfiguration() {
96
+ return describeWhistleAssets(this.modelDir, "automatic download is off (NEUROLINK_WHISTLE_AUTO_DOWNLOAD=0 or autoDownload: false)");
97
+ }
98
+ /** Directory the engine's files live in. */
99
+ getModelDir() {
100
+ return this.modelDir;
101
+ }
102
+ getSupportedFormats() {
103
+ return [...WHISTLE_NATIVE_FORMATS, ...WHISTLE_FFMPEG_FORMATS];
104
+ }
105
+ async getSupportedLanguages() {
106
+ return LANGUAGES.map((l) => ({
107
+ code: l.code,
108
+ name: l.name,
109
+ supportsDiarization: false,
110
+ supportsPunctuation: true,
111
+ }));
112
+ }
113
+ /** Download/verify the files and start the worker now rather than on the first request. */
114
+ async warmUp() {
115
+ await this.getEngine();
116
+ }
117
+ /** Stop the warm worker. The handler stays usable; the next call starts a new one. */
118
+ async dispose() {
119
+ const engine = this.engine;
120
+ this.engine = undefined;
121
+ if (engine) {
122
+ await (await engine.catch(() => undefined))?.dispose();
123
+ }
124
+ }
125
+ async transcribe(audio, options = {}) {
126
+ const started = Date.now();
127
+ const buffer = Buffer.isBuffer(audio) ? audio : Buffer.from(audio);
128
+ if (buffer.length === 0) {
129
+ throw STTError.audioEmpty(PROVIDER);
130
+ }
131
+ const pcm = await decodeToWhistlePcm(buffer, options.format, options.sampleRate);
132
+ if (pcm.length === 0) {
133
+ throw STTError.audioEmpty(PROVIDER);
134
+ }
135
+ const duration = pcm.length / WHISTLE_SAMPLE_RATE;
136
+ if (duration > this.maxAudioDuration) {
137
+ throw STTError.audioTooLong(Math.round(duration), this.maxAudioDuration, PROVIDER);
138
+ }
139
+ const language = normalizeLanguage(options.language);
140
+ const keywords = keywordsFor(options);
141
+ const engine = await this.getEngine();
142
+ // Timestamps are always requested: the per-word probabilities are the only confidence signal.
143
+ // Engine failures already arrive as STTErrors.
144
+ const out = await engine.transcribe(pcm, language, keywords, true);
145
+ const text = out.text.trim();
146
+ const confidence = confidenceOf(text, out.words);
147
+ const result = {
148
+ text,
149
+ confidence,
150
+ language: out.language ?? language ?? undefined,
151
+ duration: Math.round(duration * 1000) / 1000,
152
+ metadata: {
153
+ latency: Date.now() - started,
154
+ provider: PROVIDER,
155
+ model: MODEL,
156
+ confidenceSource: !text
157
+ ? "none"
158
+ : out.words.length
159
+ ? "word_logprobs"
160
+ : undefined,
161
+ ttftMs: out.ttftMs,
162
+ decodeTps: out.decodeTps,
163
+ chunks: out.chunks,
164
+ keywords: keywords !== null,
165
+ },
166
+ };
167
+ if (options.wordTimestamps) {
168
+ result.words = out.words.map(toWordTiming);
169
+ }
170
+ return result;
171
+ }
172
+ /**
173
+ * Native streaming. `audioStream` yields PCM16LE mono frames at
174
+ * `options.sampleRate` (default 16 kHz), any frame size. Yields a final
175
+ * segment for each run of words the engine commits (times from the start of
176
+ * the stream; join the finals with a space for the transcript) and an
177
+ * interim segment carrying the unconfirmed tail whenever it changes.
178
+ */
179
+ async *transcribeStream(audioStream, options = {}) {
180
+ const rate = options.sampleRate ?? WHISTLE_SAMPLE_RATE;
181
+ const language = normalizeLanguage(options.language);
182
+ const keywords = keywordsFor(options);
183
+ const paths = await ensureWhistleAssets(this.modelDir, this.autoDownload);
184
+ // The engine's stream state is global to its instance: a stream gets a worker of its own,
185
+ // so batch calls on this handler are never interleaved with it.
186
+ const engine = new WhistleEngine(paths);
187
+ const blockBytes = rate * 2; // about a second per engine pass
188
+ let pending = Buffer.alloc(0);
189
+ let index = 0;
190
+ let lastInterim = "";
191
+ let finished = false;
192
+ const segmentsOf = function* (step) {
193
+ if (step.text) {
194
+ const words = step.words.map(toWordTiming);
195
+ yield {
196
+ index: index++,
197
+ text: step.text,
198
+ isFinal: true,
199
+ confidence: confidenceOf(step.text, step.words),
200
+ startTime: step.words[0]?.start,
201
+ endTime: step.words[step.words.length - 1]?.end,
202
+ words,
203
+ language: step.language ?? language ?? undefined,
204
+ };
205
+ }
206
+ if (step.pending !== lastInterim) {
207
+ lastInterim = step.pending;
208
+ yield {
209
+ index,
210
+ text: step.pending,
211
+ isFinal: false,
212
+ language: step.language ?? language ?? undefined,
213
+ };
214
+ }
215
+ };
216
+ try {
217
+ for await (const frame of audioStream) {
218
+ pending = pending.length
219
+ ? Buffer.concat([pending, frame])
220
+ : Buffer.from(frame);
221
+ while (pending.length >= blockBytes) {
222
+ const block = pending.subarray(0, blockBytes);
223
+ pending = pending.subarray(blockBytes);
224
+ const step = await engine.streamProcess(decodePcm16(block, rate), language, keywords);
225
+ yield* segmentsOf(step);
226
+ }
227
+ }
228
+ const tail = pending.subarray(0, pending.length - (pending.length & 1));
229
+ if (tail.length > 0) {
230
+ const step = await engine.streamProcess(decodePcm16(tail, rate), language, keywords);
231
+ yield* segmentsOf(step);
232
+ }
233
+ const last = await engine.streamStop();
234
+ yield* segmentsOf(last);
235
+ finished = true;
236
+ }
237
+ catch (error) {
238
+ if (error instanceof STTError) {
239
+ throw error;
240
+ }
241
+ throw STTError.streamError(error instanceof Error ? error.message : String(error), PROVIDER);
242
+ }
243
+ finally {
244
+ if (!finished) {
245
+ logger.debug("[whistle] stream ended early; discarding its worker");
246
+ }
247
+ await engine.dispose();
248
+ }
249
+ }
250
+ getEngine() {
251
+ if (!this.engine) {
252
+ const job = ensureWhistleAssets(this.modelDir, this.autoDownload).then((paths) => new WhistleEngine(paths));
253
+ this.engine = job;
254
+ // A failed preparation (offline, download off) must not stick.
255
+ job.catch(() => {
256
+ if (this.engine === job) {
257
+ this.engine = undefined;
258
+ }
259
+ });
260
+ }
261
+ return this.engine;
262
+ }
263
+ }
264
+ /** `"de-DE"` → `"de"`; `"auto"`, omitted, or a language Whistle does not know → `null` (detect). */
265
+ function normalizeLanguage(language) {
266
+ if (!language) {
267
+ return null;
268
+ }
269
+ const base = language.trim().toLowerCase().split(/[-_]/)[0];
270
+ if (!base || base === "auto") {
271
+ return null;
272
+ }
273
+ if (!LANGUAGE_CODES.has(base)) {
274
+ logger.debug(`[whistle] language "${language}" is not one of ${[...LANGUAGE_CODES].join(", ")}; detecting instead`);
275
+ return null;
276
+ }
277
+ return base;
278
+ }
279
+ /** Newline-separated keywords, only when biasing is switched on. */
280
+ function keywordsFor(options) {
281
+ const enabled = options.model === KEYWORDS_MODEL ||
282
+ ["1", "true", "on", "yes"].includes((process.env.NEUROLINK_WHISTLE_KEYWORDS ?? "").trim().toLowerCase());
283
+ if (!enabled || !options.vocabulary?.length) {
284
+ return null;
285
+ }
286
+ const terms = [
287
+ ...new Set(options.vocabulary.map((t) => t.replace(/\s+/g, " ").trim())),
288
+ ].filter(Boolean);
289
+ if (terms.length > MAX_KEYWORDS) {
290
+ logger.debug(`[whistle] keyword list cut from ${terms.length} to ${MAX_KEYWORDS}`);
291
+ }
292
+ return terms.length ? terms.slice(0, MAX_KEYWORDS).join("\n") : null;
293
+ }
294
+ function confidenceOf(text, words) {
295
+ if (!text) {
296
+ return 0;
297
+ }
298
+ if (!words.length) {
299
+ return DEFAULT_CONFIDENCE;
300
+ }
301
+ const mean = words.reduce((sum, w) => sum + w.probability, 0) / words.length;
302
+ return Math.min(1, Math.max(0, Math.round(mean * 1000) / 1000));
303
+ }
304
+ function toWordTiming(w) {
305
+ return {
306
+ word: w.word,
307
+ start: w.start,
308
+ end: w.end,
309
+ startTime: w.start,
310
+ endTime: w.end,
311
+ confidence: w.probability,
312
+ };
313
+ }
@@ -0,0 +1,19 @@
1
+ import type { STTChunkedRun, STTChunkedStreamOptions, TranscribeStreamEvent } from "../../types/index.js";
2
+ /** Minimal unbounded async queue: the producer never waits for the consumer. */
3
+ export declare class StreamEventQueue<T> {
4
+ private readonly items;
5
+ private waiter;
6
+ private closed;
7
+ push(item: T): void;
8
+ close(): void;
9
+ drain(): AsyncGenerator<T>;
10
+ private wake;
11
+ }
12
+ /**
13
+ * Streams a batch STT engine: `frames` are PCM16LE mono at
14
+ * `options.sampleRate` (default 16000); `run` transcribes one utterance at
15
+ * 16 kHz. Yields `interim`, `language`, `final`, `silence` and `error`
16
+ * events; utterance numbers are in order but may skip (a dropped cough
17
+ * consumes one). Ends after the source ends and every final is emitted.
18
+ */
19
+ export declare function chunkedTranscribeStream(frames: AsyncIterable<Buffer | Uint8Array>, run: STTChunkedRun, options?: STTChunkedStreamOptions): AsyncIterable<TranscribeStreamEvent>;