@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,506 @@
1
+ /**
2
+ * `transcribe` — the fourth inference type, next to `generate`, `stream` and
3
+ * `decide`. Turns audio into text through any registered STT handler, with the
4
+ * optional layers that make self-hosted engines usable in production:
5
+ *
6
+ * - a dictionary of names and jargon, sent to the engine as context and used
7
+ * to repair mis-hearings after the fact;
8
+ * - a decision-model guard (`tryDecide`) that asks, per dictionary candidate,
9
+ * whether the speaker meant the term or the literal word, so a name never
10
+ * rewrites the ordinary word it sounds like; fails open like every other
11
+ * `decide` consumer;
12
+ * - an LLM rewrite (`generate`/`stream`) that fixes spelling and punctuation
13
+ * and writes Indic-script words in Latin letters the way people type
14
+ * (Hinglish, Tanglish …) without ever translating them;
15
+ * - a second-opinion engine whose text the rewrite reconciles with the
16
+ * primary one, and a fallback engine for utterances the primary cannot read
17
+ * (an Indic model on English speech reports a low language score, or
18
+ * nothing at all);
19
+ * - client-side streaming for batch engines: a chunked adapter with an energy
20
+ * gate, rolling re-transcription and LocalAgreement commits.
21
+ *
22
+ * Nothing in these layers may lose words: a rewrite that drops more than
23
+ * `maxDropRatio` of them is discarded, a timed-out guard applies the dictionary
24
+ * as is, and a final pass that returns less than the live text keeps the live
25
+ * text.
26
+ *
27
+ * @module types/transcribe
28
+ */
29
+ import type { TTSAudioFormat } from "./tts.js";
30
+ import type { STTHandler, STTOptions, STTResult, TranscriptionSegment, WordTiming } from "./stt.js";
31
+ /**
32
+ * Audio for `transcribe()`: bytes, a path or `file://` / `http(s)://` URL, or
33
+ * an async stream of PCM16LE frames (see `STTStreamingOptions.sampleRate`).
34
+ */
35
+ export type TranscribeAudioInput = Buffer | ArrayBuffer | Uint8Array | string | AsyncIterable<Buffer | Uint8Array>;
36
+ /** One dictionary entry: the correct spelling, how engines mis-hear it, and what it means. */
37
+ export type STTDictionaryEntry = {
38
+ /** Correct spelling, written exactly like this in the output. */
39
+ term: string;
40
+ /** Mis-hearings, in any script ("jasper", "जसपे"). Each one found in a transcript becomes a candidate. */
41
+ heardAs?: string[];
42
+ /** Meaning or usage, given to the decision model and the rewrite model. */
43
+ meaning?: string;
44
+ };
45
+ /** The correction layer that runs on a finished transcript (or each final utterance when streaming). */
46
+ export type STTCorrectionOptions = {
47
+ /**
48
+ * Default: on when a dictionary is given, or when this object is given at
49
+ * all with `rewrite` not `false`; off otherwise.
50
+ */
51
+ enabled?: boolean;
52
+ /**
53
+ * Guard for dictionary substitutions. `"decide"` asks the decision provider
54
+ * per candidate (term or literal?) and applies the dictionary as is when no
55
+ * decision provider is configured or the call fails; `"none"` always applies it.
56
+ */
57
+ guard?: "decide" | "none";
58
+ /** Timeout for the guard call; on expiry the dictionary is applied as is. Default 4000. */
59
+ decideTimeoutMs?: number;
60
+ /**
61
+ * LLM rewrite: spelling, punctuation, dictionary terms, transliteration.
62
+ * `false` disables it (dictionary-only correction). `provider`/`model` name
63
+ * any text provider; the instance default is used when omitted.
64
+ */
65
+ rewrite?: false | {
66
+ provider?: string;
67
+ model?: string;
68
+ /** On expiry the un-rewritten text is kept. Default 12000. */
69
+ timeoutMs?: number;
70
+ };
71
+ /**
72
+ * How Indic-script text is written: `"latin"` the way people type on phones
73
+ * (Hinglish / Tanglish — the default), `"native"` keeps the script. Never a translation.
74
+ */
75
+ transliterate?: "latin" | "native";
76
+ /** Add punctuation and capitalization in the rewrite. Default true. */
77
+ punctuate?: boolean;
78
+ /**
79
+ * A second engine run on the same audio; the rewrite reconciles both
80
+ * transcripts (Indian-language words from an Indic engine, English words
81
+ * from an English-oriented one).
82
+ */
83
+ secondOpinion?: {
84
+ provider: string;
85
+ model?: string;
86
+ };
87
+ /** One sentence about who is talking and about what; sent to the engines and the guard. */
88
+ context?: string;
89
+ /** A rewrite that drops more than this share of the words is discarded. Default 0.25. */
90
+ maxDropRatio?: number;
91
+ };
92
+ /**
93
+ * Where an utterance goes when the primary engine cannot read it. `when`
94
+ * defaults to `["unsure", "empty"]`: a low language-detection score, or no
95
+ * text at all.
96
+ */
97
+ export type STTFallbackOptions = {
98
+ provider: string;
99
+ model?: string;
100
+ when?: Array<"unsure" | "empty" | "error">;
101
+ };
102
+ /** Client-side streaming for engines without a native stream (and tuning for the adapter). */
103
+ export type STTStreamingOptions = {
104
+ /** `"auto"` uses the handler's native stream when it has one, else the chunked adapter. */
105
+ mode?: "auto" | "native" | "chunked";
106
+ /** Sample rate of the incoming PCM16LE frames. Default 16000. */
107
+ sampleRate?: number;
108
+ /** Silence that ends an utterance. Default 750. */
109
+ endSilenceMs?: number;
110
+ /** Cadence of interim passes on the current utterance. Default 900. */
111
+ intervalMs?: number;
112
+ /** After this many seconds a turn ends at the next short pause. Default 16. */
113
+ softCutSeconds?: number;
114
+ /** Hard cut at the quietest recent block, remainder carried over. Default 28. */
115
+ maxUtteranceSeconds?: number;
116
+ /** Absolute RMS floor for speech onset. Default 0.006. */
117
+ onsetRms?: number;
118
+ /** Audio kept before the detected onset. Default 350. */
119
+ prerollMs?: number;
120
+ /** Shortest utterance worth a pass. Default 250. */
121
+ minSpeechMs?: number;
122
+ };
123
+ /**
124
+ * Options for `neurolink.transcribe()`. `provider` defaults to
125
+ * `NEUROLINK_STT_PROVIDER`, then the first configured STT provider in
126
+ * descriptor order, then the built-in local engine (Whistle), which needs no
127
+ * credentials. `language` omitted or `"auto"` lets the engine detect it.
128
+ */
129
+ export type TranscribeOptions = Omit<STTOptions, "enabled" | "provider" | "language" | "model" | "format"> & {
130
+ audio: TranscribeAudioInput;
131
+ provider?: string;
132
+ model?: string;
133
+ /** ISO / BCP-47 code, or `"auto"` (the default) to let the engine detect. */
134
+ language?: string;
135
+ /** Audio container of a buffer input; inferred from a path or URL. */
136
+ format?: TTSAudioFormat;
137
+ /** Context prompt for engines that bias on one (Whisper, Qwen3-ASR …). Dictionary terms are appended. */
138
+ prompt?: string;
139
+ dictionary?: STTDictionaryEntry[];
140
+ correction?: STTCorrectionOptions;
141
+ fallback?: STTFallbackOptions;
142
+ streaming?: STTStreamingOptions;
143
+ /** Whole-call timeout. */
144
+ timeoutMs?: number;
145
+ /** Per-call credentials; the `stt` slice reaches the handlers. */
146
+ credentials?: STTCredentialsCarrier;
147
+ };
148
+ /** `transcribeStream()` takes frames instead of a whole buffer. */
149
+ export type TranscribeStreamOptions = Omit<TranscribeOptions, "audio"> & {
150
+ audio: AsyncIterable<Buffer | Uint8Array>;
151
+ };
152
+ /**
153
+ * Only the `stt` slice is read here; it is typed loosely so this file does
154
+ * not have to import `NeurolinkCredentials` (which imports this file).
155
+ */
156
+ export type STTCredentialsCarrier = {
157
+ stt?: STTCredentials;
158
+ };
159
+ /** What the decision model answered for one dictionary candidate. */
160
+ export type STTDecisionRecord = {
161
+ /** The mis-hearing found in the transcript. */
162
+ heard: string;
163
+ /** The dictionary term it may stand for. */
164
+ term: string;
165
+ choice: "term" | "literal";
166
+ /** Probability of the chosen reading, when the guard ran. */
167
+ probability?: number;
168
+ confidence?: number;
169
+ /** Why no decision was made ("decision model unavailable", "guard off", …). */
170
+ note?: string;
171
+ };
172
+ /** One candidate language and its engine-specific score (higher is better; scale is the engine's). */
173
+ export type STTLanguageScore = {
174
+ language: string;
175
+ score: number;
176
+ };
177
+ /** Which engines produced the result. */
178
+ export type STTEngineInfo = {
179
+ provider: string;
180
+ model?: string;
181
+ /** The fallback engine took the whole request (or an utterance, when streaming). */
182
+ fallbackUsed?: boolean;
183
+ /** The second-opinion engine that was reconciled into the text. */
184
+ secondOpinion?: string;
185
+ };
186
+ export type TranscribeTimings = {
187
+ transcribeMs: number;
188
+ secondOpinionMs?: number;
189
+ decideMs?: number;
190
+ rewriteMs?: number;
191
+ correctionMs?: number;
192
+ totalMs: number;
193
+ };
194
+ /** The result of `transcribe()`: an `STTResult` plus what the layers did to it. */
195
+ export type TranscribeResult = STTResult & {
196
+ /** Engine text before any correction. */
197
+ raw: string;
198
+ /** Present when the correction changed the text; `text` then holds the corrected version. */
199
+ corrected?: string;
200
+ /** `false` when the engine reported a low language-detection score. */
201
+ languageDetected?: boolean;
202
+ languageScores?: STTLanguageScore[];
203
+ decisions?: STTDecisionRecord[];
204
+ engine: STTEngineInfo;
205
+ timings: TranscribeTimings;
206
+ /** Human-readable trail of what happened ("dictionary 2/2", "rewrite timed out · kept text"). */
207
+ steps: string[];
208
+ };
209
+ export type TranscribeStreamEvent = {
210
+ type: "interim";
211
+ utterance: number;
212
+ /** Full current text of the utterance. */
213
+ text: string;
214
+ /** Prefix two consecutive passes agreed on (LocalAgreement). */
215
+ committed: string;
216
+ /** The rest, still subject to change. */
217
+ tail: string;
218
+ seconds: number;
219
+ latencyMs: number;
220
+ engine: string;
221
+ } | {
222
+ type: "language";
223
+ utterance: number;
224
+ language: string;
225
+ detected: boolean;
226
+ scores?: STTLanguageScore[];
227
+ } | {
228
+ type: "final";
229
+ utterance: number;
230
+ text: string;
231
+ segment: TranscriptionSegment;
232
+ words?: WordTiming[];
233
+ language?: string;
234
+ languageDetected?: boolean;
235
+ engine: string;
236
+ fallbackUsed?: boolean;
237
+ seconds: number;
238
+ } | {
239
+ /** Partial text of the streamed rewrite, in order. */
240
+ type: "correcting";
241
+ utterance: number;
242
+ text: string;
243
+ } | {
244
+ type: "corrected";
245
+ utterance: number;
246
+ raw: string;
247
+ text: string;
248
+ decisions: STTDecisionRecord[];
249
+ steps: string[];
250
+ timings: TranscribeTimings;
251
+ } | {
252
+ /** An utterance no engine could read; its audio length is reported so nothing disappears silently. */
253
+ type: "silence";
254
+ utterance: number;
255
+ seconds: number;
256
+ } | {
257
+ type: "error";
258
+ utterance?: number;
259
+ message: string;
260
+ /** `true` when the stream continues with the next utterance. */
261
+ recoverable: boolean;
262
+ };
263
+ /**
264
+ * One OpenAI-compatible `/audio/transcriptions` endpoint registered under a
265
+ * name of its own, so several self-hosted engines can be told apart — and
266
+ * used as each other's `fallback` or `secondOpinion`. `model` is the default
267
+ * `model` field sent to it.
268
+ */
269
+ export type STTEndpointConfig = {
270
+ baseURL: string;
271
+ apiKey?: string;
272
+ model?: string;
273
+ timeoutMs?: number;
274
+ };
275
+ /** Credential slices for STT handlers; `credentials.stt.<key>` beats the env var. */
276
+ export type STTCredentials = {
277
+ /** Any OpenAI-compatible `/audio/transcriptions` endpoint (OpenAI, vLLM, LiteLLM, self-hosted wrappers). */
278
+ whisper?: {
279
+ apiKey?: string;
280
+ baseURL?: string;
281
+ timeoutMs?: number;
282
+ /** Default model name sent to the endpoint (`whisper-1` when omitted). */
283
+ model?: string;
284
+ };
285
+ /**
286
+ * Extra OpenAI-compatible endpoints, each registered as a provider under
287
+ * its key (`stt.endpoints.indic` → `provider: "indic"`). The env form is
288
+ * `NEUROLINK_STT_ENDPOINTS`, a JSON object of the same shape. A key that
289
+ * collides with a shipped provider name or alias is ignored with a warning.
290
+ */
291
+ endpoints?: Record<string, STTEndpointConfig>;
292
+ deepgram?: {
293
+ apiKey?: string;
294
+ baseURL?: string;
295
+ };
296
+ elevenlabs?: {
297
+ apiKey?: string;
298
+ baseURL?: string;
299
+ timeoutMs?: number;
300
+ };
301
+ google?: {
302
+ apiKey?: string;
303
+ credentialsPath?: string;
304
+ };
305
+ azure?: {
306
+ apiKey?: string;
307
+ region?: string;
308
+ };
309
+ /** The built-in local engine: where its files live and whether they may be fetched. */
310
+ whistle?: {
311
+ modelDir?: string;
312
+ autoDownload?: boolean;
313
+ };
314
+ };
315
+ export type STTProviderCapabilities = {
316
+ /** `"native"`: the handler streams itself; `"chunked"`: the adapter streams a batch handler. */
317
+ streaming: "native" | "chunked";
318
+ diarization: boolean;
319
+ /** The engine reports a detected language (and, where it can, a confidence). */
320
+ languageDetect: boolean;
321
+ /** Accepts a context prompt for biasing. */
322
+ prompt: boolean;
323
+ wordTimestamps: boolean;
324
+ /** Runs on this machine; needs no key. */
325
+ local: boolean;
326
+ /** Languages the engine supports when it is not open-ended. */
327
+ languages?: readonly string[];
328
+ };
329
+ /**
330
+ * Metadata for one STT provider — the single source of truth for its name,
331
+ * aliases, credentials, env vars and capabilities. Order in the list is
332
+ * precedence for the default provider, exactly as with decision providers.
333
+ */
334
+ export type STTProviderDescriptor = {
335
+ name: string;
336
+ aliases?: readonly string[];
337
+ label: string;
338
+ credentialsKey: keyof STTCredentials;
339
+ /** Env vars that configure it; the first present one makes it "configured". */
340
+ envVars: readonly string[];
341
+ baseUrlEnv?: string;
342
+ defaultModel?: string;
343
+ capabilities: STTProviderCapabilities;
344
+ };
345
+ /**
346
+ * What the correction layer needs from the SDK, injected by `NeuroLink` so the
347
+ * layer itself stays free of provider plumbing and can be exercised with
348
+ * stand-ins. `decide` is `tryDecide` (returns `null` on any failure);
349
+ * `rewrite` streams the text so far (cumulative, not deltas) through `onPartial`
350
+ * when the caller wants it.
351
+ */
352
+ export type STTCorrectionDeps = {
353
+ decide: (opts: {
354
+ state: Record<string, unknown>;
355
+ questions: Record<string, unknown>;
356
+ timeoutMs?: number;
357
+ }) => Promise<{
358
+ answers: Record<string, unknown>;
359
+ } | null>;
360
+ rewrite: (opts: {
361
+ system: string;
362
+ user: string;
363
+ provider?: string;
364
+ model?: string;
365
+ maxTokens: number;
366
+ timeoutMs: number;
367
+ onPartial?: (text: string) => void;
368
+ }) => Promise<string>;
369
+ };
370
+ /** Input to the correction layer, independent of where the text came from. */
371
+ export type STTCorrectionInput = {
372
+ text: string;
373
+ /** Second-opinion transcript of the same audio, when one exists. */
374
+ secondOpinion?: string;
375
+ dictionary?: STTDictionaryEntry[];
376
+ options?: STTCorrectionOptions;
377
+ /** `false` when the primary engine reported a low language score: the second opinion then leads. */
378
+ languageDetected?: boolean;
379
+ onPartial?: (text: string) => void;
380
+ };
381
+ /** A dictionary alias found in a transcript: a candidate substitution for the guard to judge. */
382
+ export type STTDictionaryCandidate = {
383
+ entry: STTDictionaryEntry;
384
+ /** The alias as written in the dictionary (its match in the text may differ in case). */
385
+ heard: string;
386
+ };
387
+ /** What one rewrite call did; `text` is always usable (the input when the rewrite was not kept). */
388
+ export type STTRewriteOutcome = {
389
+ text: string;
390
+ status: "ok" | "timeout" | "error" | "empty" | "dropped";
391
+ ms: number;
392
+ /** Share of words the rewrite dropped, when `status` is `"dropped"`. */
393
+ dropRatio?: number;
394
+ /** Message of the error, when `status` is `"error"`. */
395
+ error?: string;
396
+ };
397
+ /** What a batched line rewrite did; `lines` has exactly as many entries as the input. */
398
+ export type STTRewriteLinesOutcome = {
399
+ lines: string[];
400
+ /** Batches whose output was used, of all batches sent. */
401
+ batchesOk: number;
402
+ batches: number;
403
+ /** Lines whose rewrite was discarded for dropping words. */
404
+ linesKept: number;
405
+ ms: number;
406
+ };
407
+ export type STTCorrectionOutput = {
408
+ text: string;
409
+ decisions: STTDecisionRecord[];
410
+ steps: string[];
411
+ timings: Pick<TranscribeTimings, "decideMs" | "rewriteMs" | "correctionMs">;
412
+ };
413
+ /**
414
+ * Everything the transcribe orchestration (batch and stream) needs from the
415
+ * SDK, bound by `NeuroLink` so the orchestration modules stay testable.
416
+ */
417
+ export type TranscribeDeps = STTCorrectionDeps & {
418
+ /** `STTProcessor.transcribe` with the request's credentials already applied. */
419
+ transcribe: (audio: Buffer, provider: string, options: STTOptions) => Promise<STTResult>;
420
+ /** Resolves `undefined`, `""` or an alias to a registered provider name, or throws a typed error. */
421
+ resolveProvider: (name?: string) => string;
422
+ getHandler: (name: string) => STTHandler | undefined;
423
+ };
424
+ /** Resolved endpointer tuning: `STTStreamingOptions` with every default applied, plus the input rate. */
425
+ export type STTEndpointerConfig = {
426
+ /** Sample rate of the samples given to `push()`. */
427
+ inputRate: number;
428
+ endSilenceMs: number;
429
+ softCutSeconds: number;
430
+ maxUtteranceSeconds: number;
431
+ onsetRms: number;
432
+ prerollMs: number;
433
+ minSpeechMs: number;
434
+ };
435
+ /** One utterance as the energy gate builds it, at the input rate. */
436
+ export type STTEndpointerUtterance = {
437
+ /** 1-based, in order of onset. A dropped cough consumes an id, so ids can skip. */
438
+ id: number;
439
+ /** Offset of the first sample (pre-roll included) from the start of the stream. */
440
+ startSeconds: number;
441
+ /** 20 ms blocks, pre-roll included. */
442
+ blocks: Float32Array[];
443
+ /** RMS of each block, parallel to `blocks`. */
444
+ rms: number[];
445
+ /** Total samples in `blocks`. */
446
+ length: number;
447
+ speechMs: number;
448
+ silenceMs: number;
449
+ /** Set by the caller once a pass produced words: such an utterance is never dropped as a cough. */
450
+ heard: boolean;
451
+ };
452
+ export type STTEndpointerEvent = {
453
+ type: "start";
454
+ utterance: STTEndpointerUtterance;
455
+ } | {
456
+ type: "end";
457
+ utterance: STTEndpointerUtterance;
458
+ reason: "silence" | "softCut" | "hardCut" | "flush";
459
+ }
460
+ /** Too little speech and nothing heard: a cough or a click. */
461
+ | {
462
+ type: "drop";
463
+ utterance: STTEndpointerUtterance;
464
+ };
465
+ /** LocalAgreement bookkeeping for one utterance. */
466
+ export type STTLocalAgreementState = {
467
+ /** Words of the previous pass. */
468
+ prevWords: string[];
469
+ /** The committed words themselves; a later pass never rewrites them. */
470
+ committedWords: string[];
471
+ /** `committedWords.length`; never shrinks while the state lives. */
472
+ committedN: number;
473
+ };
474
+ export type STTLocalAgreementUpdate = {
475
+ words: string[];
476
+ committed: string;
477
+ tail: string;
478
+ };
479
+ /** Which engine of the adapter a pass runs on; the orchestration maps roles to providers. */
480
+ export type STTStreamEngineRole = "primary" | "fallback";
481
+ /** What the adapter tells `run` about a pass. */
482
+ export type STTChunkedPassOptions = {
483
+ utterance: number;
484
+ final: boolean;
485
+ engine: STTStreamEngineRole;
486
+ /** Audio length of the utterance so far. */
487
+ seconds: number;
488
+ };
489
+ /** One engine pass over a whole utterance, 16 kHz mono float samples in. */
490
+ export type STTChunkedRun = (pcm16k: Float32Array, options: STTChunkedPassOptions) => Promise<STTResult>;
491
+ /** Options of `chunkedTranscribeStream`: the streaming tuning plus engine labels and hooks. */
492
+ export type STTChunkedStreamOptions = STTStreamingOptions & {
493
+ /** Label of the primary engine in events. Default "primary". */
494
+ engineLabel?: string;
495
+ /** Language takeover: hand an utterance the primary cannot read to the `fallback` role. */
496
+ fallback?: {
497
+ label: string;
498
+ when?: STTFallbackOptions["when"];
499
+ };
500
+ /**
501
+ * Called once per utterance, synchronously, when it ends and before its
502
+ * final pass is queued, with the utterance audio at 16 kHz and the role
503
+ * that will run the final — so a second opinion can run in parallel.
504
+ */
505
+ onUtteranceEnd?: (pcm16k: Float32Array, utterance: number, engine: STTStreamEngineRole) => void;
506
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `transcribe` — the fourth inference type, next to `generate`, `stream` and
3
+ * `decide`. Turns audio into text through any registered STT handler, with the
4
+ * optional layers that make self-hosted engines usable in production:
5
+ *
6
+ * - a dictionary of names and jargon, sent to the engine as context and used
7
+ * to repair mis-hearings after the fact;
8
+ * - a decision-model guard (`tryDecide`) that asks, per dictionary candidate,
9
+ * whether the speaker meant the term or the literal word, so a name never
10
+ * rewrites the ordinary word it sounds like; fails open like every other
11
+ * `decide` consumer;
12
+ * - an LLM rewrite (`generate`/`stream`) that fixes spelling and punctuation
13
+ * and writes Indic-script words in Latin letters the way people type
14
+ * (Hinglish, Tanglish …) without ever translating them;
15
+ * - a second-opinion engine whose text the rewrite reconciles with the
16
+ * primary one, and a fallback engine for utterances the primary cannot read
17
+ * (an Indic model on English speech reports a low language score, or
18
+ * nothing at all);
19
+ * - client-side streaming for batch engines: a chunked adapter with an energy
20
+ * gate, rolling re-transcription and LocalAgreement commits.
21
+ *
22
+ * Nothing in these layers may lose words: a rewrite that drops more than
23
+ * `maxDropRatio` of them is discarded, a timed-out guard applies the dictionary
24
+ * as is, and a final pass that returns less than the live text keeps the live
25
+ * text.
26
+ *
27
+ * @module types/transcribe
28
+ */
29
+ export {};
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Provider-neutral speaker turns.
3
+ *
4
+ * Diarizing STT providers disagree about where the speaker label lives:
5
+ * Deepgram and Google put it on every word, ElevenLabs Scribe puts it on
6
+ * every token, the diarization sidecar puts it on each segment. Callers want
7
+ * one shape — a list of turns, each with a speaker, a time range and text —
8
+ * so `STTProcessor` derives it here whenever a provider returned speaker
9
+ * labels on words but no speaker-labelled segments of its own.
10
+ *
11
+ * @module utils/speakerTurns
12
+ */
13
+ import type { TranscriptionSegment, WordTiming } from "../types/index.js";
14
+ /**
15
+ * Merge consecutive same-speaker words into turns.
16
+ *
17
+ * A word without a speaker continues the current turn (providers emit
18
+ * unlabelled punctuation and spacing tokens); a word with a different
19
+ * speaker starts a new one. Words are joined with single spaces and the
20
+ * result trimmed, so text built from punctuated words reads naturally.
21
+ * Returns `[]` when no word carries a speaker, so callers can tell "no
22
+ * diarization" apart from "one speaker".
23
+ */
24
+ export declare function buildSpeakerTurns(words: WordTiming[]): TranscriptionSegment[];
25
+ /** Distinct speaker labels in order of first appearance. */
26
+ export declare function listSpeakers(items: ReadonlyArray<{
27
+ speaker?: string;
28
+ }>): string[];
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Provider-neutral speaker turns.
3
+ *
4
+ * Diarizing STT providers disagree about where the speaker label lives:
5
+ * Deepgram and Google put it on every word, ElevenLabs Scribe puts it on
6
+ * every token, the diarization sidecar puts it on each segment. Callers want
7
+ * one shape — a list of turns, each with a speaker, a time range and text —
8
+ * so `STTProcessor` derives it here whenever a provider returned speaker
9
+ * labels on words but no speaker-labelled segments of its own.
10
+ *
11
+ * @module utils/speakerTurns
12
+ */
13
+ /**
14
+ * Merge consecutive same-speaker words into turns.
15
+ *
16
+ * A word without a speaker continues the current turn (providers emit
17
+ * unlabelled punctuation and spacing tokens); a word with a different
18
+ * speaker starts a new one. Words are joined with single spaces and the
19
+ * result trimmed, so text built from punctuated words reads naturally.
20
+ * Returns `[]` when no word carries a speaker, so callers can tell "no
21
+ * diarization" apart from "one speaker".
22
+ */
23
+ export function buildSpeakerTurns(words) {
24
+ if (!words.some((w) => w.speaker !== undefined)) {
25
+ return [];
26
+ }
27
+ const turns = [];
28
+ let current;
29
+ for (const word of words) {
30
+ const speaker = word.speaker ?? current?.speaker;
31
+ if (speaker === undefined) {
32
+ continue; // leading unlabelled tokens before the first speaker
33
+ }
34
+ if (!current || current.speaker !== speaker) {
35
+ if (current) {
36
+ turns.push(toSegment(current.speaker, current.words, turns.length));
37
+ }
38
+ current = { speaker, words: [] };
39
+ }
40
+ current.words.push(word);
41
+ }
42
+ if (current && current.words.length > 0) {
43
+ turns.push(toSegment(current.speaker, current.words, turns.length));
44
+ }
45
+ return turns;
46
+ }
47
+ function toSegment(speaker, words, index) {
48
+ const starts = words
49
+ .map((w) => w.startTime ?? w.start)
50
+ .filter((t) => typeof t === "number");
51
+ const ends = words
52
+ .map((w) => w.endTime ?? w.end)
53
+ .filter((t) => typeof t === "number");
54
+ const confidences = words
55
+ .map((w) => w.confidence)
56
+ .filter((c) => typeof c === "number");
57
+ const segment = {
58
+ index,
59
+ // Collapse whitespace first, so the punctuation pass only ever sees one
60
+ // space; `\s+` before a character class is quadratic on a long run of
61
+ // whitespace with no punctuation after it (CodeQL js/polynomial-redos).
62
+ text: words
63
+ .map((w) => w.word)
64
+ .join(" ")
65
+ .replace(/\s+/g, " ")
66
+ .replace(/ ([,.;:!?])/g, "$1")
67
+ .trim(),
68
+ isFinal: true,
69
+ speaker,
70
+ words,
71
+ };
72
+ if (starts.length > 0) {
73
+ segment.startTime = Math.min(...starts);
74
+ }
75
+ if (ends.length > 0) {
76
+ segment.endTime = Math.max(...ends);
77
+ }
78
+ if (confidences.length > 0) {
79
+ segment.confidence =
80
+ confidences.reduce((sum, c) => sum + c, 0) / confidences.length;
81
+ }
82
+ return segment;
83
+ }
84
+ /** Distinct speaker labels in order of first appearance. */
85
+ export function listSpeakers(items) {
86
+ const seen = new Set();
87
+ for (const item of items) {
88
+ if (item.speaker !== undefined) {
89
+ seen.add(item.speaker);
90
+ }
91
+ }
92
+ return [...seen];
93
+ }