@juspay/neurolink 13.7.0 → 13.8.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 (84) hide show
  1. package/CHANGELOG.md +3 -6
  2. package/dist/browser/neurolink.min.js +480 -460
  3. package/dist/cli/commands/serve.js +11 -1
  4. package/dist/cli/commands/transcribe.js +39 -3
  5. package/dist/constants/langfuse.d.ts +2 -0
  6. package/dist/constants/langfuse.js +2 -0
  7. package/dist/factories/sttDescriptors.js +7 -0
  8. package/dist/index.d.ts +3 -2
  9. package/dist/index.js +4 -2
  10. package/dist/mcp/externalServerManager.js +4 -1
  11. package/dist/mcp/mcpClientFactory.d.ts +6 -0
  12. package/dist/mcp/mcpClientFactory.js +44 -0
  13. package/dist/neurolink.js +42 -3
  14. package/dist/observability/exporters/langfuseExporter.d.ts +4 -0
  15. package/dist/observability/exporters/langfuseExporter.js +29 -8
  16. package/dist/observability/otlpEncoding.js +2 -2
  17. package/dist/server/abstract/baseServerAdapter.js +2 -0
  18. package/dist/server/adapters/expressAdapter.js +1 -1
  19. package/dist/server/adapters/fastifyAdapter.js +4 -1
  20. package/dist/server/adapters/honoAdapter.js +1 -1
  21. package/dist/server/adapters/koaAdapter.js +1 -1
  22. package/dist/server/routes/transcribeRoutes.d.ts +0 -8
  23. package/dist/server/routes/transcribeRoutes.js +130 -3
  24. package/dist/server/utils/validation.d.ts +409 -15
  25. package/dist/server/utils/validation.js +106 -4
  26. package/dist/server/websocket/frameQueue.d.ts +16 -0
  27. package/dist/server/websocket/frameQueue.js +55 -0
  28. package/dist/server/websocket/realtimeTranscription.d.ts +48 -0
  29. package/dist/server/websocket/realtimeTranscription.js +603 -0
  30. package/dist/server/websocket/transcribeWebSocket.d.ts +17 -1
  31. package/dist/server/websocket/transcribeWebSocket.js +263 -64
  32. package/dist/services/server/ai/observability/langfuseSetup.js +6 -1
  33. package/dist/types/cli.d.ts +6 -0
  34. package/dist/types/exporter.d.ts +3 -1
  35. package/dist/types/server.d.ts +304 -2
  36. package/dist/types/stt.d.ts +38 -3
  37. package/dist/types/transcribe.d.ts +401 -5
  38. package/dist/voice/audioLevel.d.ts +14 -0
  39. package/dist/voice/audioLevel.js +60 -0
  40. package/dist/voice/codeMix.d.ts +40 -0
  41. package/dist/voice/codeMix.js +282 -0
  42. package/dist/voice/correction/align.d.ts +12 -0
  43. package/dist/voice/correction/align.js +48 -0
  44. package/dist/voice/correction/guards.d.ts +39 -0
  45. package/dist/voice/correction/guards.js +122 -0
  46. package/dist/voice/correction/index.d.ts +25 -3
  47. package/dist/voice/correction/index.js +132 -4
  48. package/dist/voice/correction/rewrite.d.ts +64 -3
  49. package/dist/voice/correction/rewrite.js +295 -25
  50. package/dist/voice/eval/sttEvalLangfuse.d.ts +14 -0
  51. package/dist/voice/eval/sttEvalLangfuse.js +182 -0
  52. package/dist/voice/eval/sttEvalStore.d.ts +27 -0
  53. package/dist/voice/eval/sttEvalStore.js +397 -0
  54. package/dist/voice/index.js +3 -0
  55. package/dist/voice/languages.d.ts +34 -0
  56. package/dist/voice/languages.js +290 -0
  57. package/dist/voice/modelAssets.d.ts +28 -0
  58. package/dist/voice/modelAssets.js +113 -0
  59. package/dist/voice/output/render.d.ts +32 -0
  60. package/dist/voice/output/render.js +368 -0
  61. package/dist/voice/providers/OpenAISTT.d.ts +11 -1
  62. package/dist/voice/providers/OpenAISTT.js +57 -6
  63. package/dist/voice/session/transcriptionSession.d.ts +55 -0
  64. package/dist/voice/session/transcriptionSession.js +225 -0
  65. package/dist/voice/streaming/chunkedStreamAdapter.d.ts +7 -0
  66. package/dist/voice/streaming/chunkedStreamAdapter.js +711 -28
  67. package/dist/voice/streaming/endpointer.d.ts +50 -17
  68. package/dist/voice/streaming/endpointer.js +115 -19
  69. package/dist/voice/transcribe.d.ts +18 -1
  70. package/dist/voice/transcribe.js +459 -18
  71. package/dist/voice/transcribeStream.d.ts +18 -1
  72. package/dist/voice/transcribeStream.js +413 -16
  73. package/dist/voice/vad/assets.d.ts +36 -0
  74. package/dist/voice/vad/assets.js +142 -0
  75. package/dist/voice/vad/frameScorer.d.ts +28 -0
  76. package/dist/voice/vad/frameScorer.js +90 -0
  77. package/dist/voice/vad/sileroVad.d.ts +43 -0
  78. package/dist/voice/vad/sileroVad.js +178 -0
  79. package/dist/voice/whistle/assets.d.ts +3 -4
  80. package/dist/voice/whistle/assets.js +11 -95
  81. package/docs-site/static/search-index.json +21 -6
  82. package/models/silero-vad/README.md +19 -0
  83. package/models/silero-vad/silero_vad.onnx +0 -0
  84. package/package.json +3 -1
@@ -26,6 +26,7 @@
26
26
  *
27
27
  * @module types/transcribe
28
28
  */
29
+ import type { ZodUnknownSchema } from "./aliases.js";
29
30
  import type { TTSAudioFormat } from "./tts.js";
30
31
  import type { STTHandler, STTOptions, STTResult, TranscriptionSegment, WordTiming } from "./stt.js";
31
32
  /**
@@ -62,32 +63,61 @@ export type STTCorrectionOptions = {
62
63
  * `false` disables it (dictionary-only correction). `provider`/`model` name
63
64
  * any text provider; the instance default is used when omitted.
64
65
  */
65
- rewrite?: false | {
66
+ rewrite?: false | "auto" | {
66
67
  provider?: string;
67
68
  model?: string;
68
69
  /** On expiry the un-rewritten text is kept. Default 12000. */
69
70
  timeoutMs?: number;
71
+ /** `"auto"` (the default) runs the rewrite only when there is something for it to do; `"always"` runs it on every text. */
72
+ mode?: "auto" | "always";
70
73
  };
74
+ /**
75
+ * Correct a final utterance sentence by sentence (split on terminal
76
+ * punctuation and the danda): each sentence decides its own script and
77
+ * whether it needs the rewrite, and sentences are corrected in parallel.
78
+ * Default true. A second opinion is whole-utterance and keeps one pass.
79
+ */
80
+ perSentence?: boolean;
71
81
  /**
72
82
  * 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.
83
+ * (Hinglish / Tanglish), `"native"` keeps the script. Never a translation.
84
+ * Unset, Indian languages default to `"latin"` and every other language
85
+ * keeps its own script (Arabic, CJK, Cyrillic … are never romanized unless
86
+ * asked for). For any other rendering of the text, see `output.languages`.
74
87
  */
75
88
  transliterate?: "latin" | "native";
76
89
  /** Add punctuation and capitalization in the rewrite. Default true. */
77
90
  punctuate?: boolean;
91
+ /**
92
+ * `"verbatim"` (the default) keeps every word, fillers and stutters
93
+ * included. `"clean"` lets the rewrite remove fillers ("um", "like" as a
94
+ * filler) and stutters ("they they they"), never content, and allows the
95
+ * rewrite to drop up to half the words to do so.
96
+ */
97
+ cleanup?: "verbatim" | "clean";
78
98
  /**
79
99
  * A second engine run on the same audio; the rewrite reconciles both
80
100
  * transcripts (Indian-language words from an Indic engine, English words
81
- * from an English-oriented one).
101
+ * from an English-oriented one). `"auto"` uses the `fallback` engine as the
102
+ * second opinion on every utterance the primary leads, so code-mixed speech
103
+ * gets both readings without naming a provider twice.
82
104
  */
83
105
  secondOpinion?: {
84
106
  provider: string;
85
107
  model?: string;
86
- };
108
+ } | "auto";
87
109
  /** One sentence about who is talking and about what; sent to the engines and the guard. */
88
110
  context?: string;
89
111
  /** A rewrite that drops more than this share of the words is discarded. Default 0.25. */
90
112
  maxDropRatio?: number;
113
+ /**
114
+ * Corrected utterances before this one that the rewrite sees as read-only
115
+ * context, so names, spellings and script stay consistent across a
116
+ * conversation. Streaming and `chunking: "utterances"` only. Default 0
117
+ * (off): a rewrite may copy context into its answer, which the added-words
118
+ * guard rejects, so it is opt-in until measured.
119
+ */
120
+ contextUtterances?: number;
91
121
  };
92
122
  /**
93
123
  * Where an utterance goes when the primary engine cannot read it. `when`
@@ -99,6 +129,34 @@ export type STTFallbackOptions = {
99
129
  model?: string;
100
130
  when?: Array<"unsure" | "empty" | "error">;
101
131
  };
132
+ /**
133
+ * How the engines share an utterance. With a `fallback` set, the default is
134
+ * to probe: the first pass of every utterance runs the primary and the
135
+ * fallback in parallel, and whichever reads the language leads from then on,
136
+ * so a sentence in the "wrong" language never waits for the slow takeover.
137
+ */
138
+ export type STTRoutingOptions = {
139
+ /**
140
+ * Streaming: run the fallback engine beside the primary on the first pass
141
+ * of each utterance and lead with the one that reads the language.
142
+ * Batch: ask the primary for a language verdict first (its
143
+ * `languageProbeModel`, when it has one) and transcribe with the engine
144
+ * that fits. Default: on whenever a fallback is set.
145
+ */
146
+ probe?: boolean;
147
+ /** Streaming: passes both engines keep running before the utterance's engine locks. Default 2. */
148
+ lockAfterPasses?: number;
149
+ /** Carry the last confidently detected language into the next utterance's detection (`"carry"`, default) or start each utterance cold (`"none"`). */
150
+ prior?: "carry" | "none";
151
+ /**
152
+ * Which engine transcribes which language: `{ hi: "indic", ta: "indic",
153
+ * en: "spark", "*": "spark" }`. A final whose detected language belongs to
154
+ * another engine than the one that produced it is transcribed again by
155
+ * that engine, and its text is used when it reads the language. `"*"` is
156
+ * the engine for every language not listed. Values are provider names.
157
+ */
158
+ engines?: Record<string, string>;
159
+ };
102
160
  /** Client-side streaming for engines without a native stream (and tuning for the adapter). */
103
161
  export type STTStreamingOptions = {
104
162
  /** `"auto"` uses the handler's native stream when it has one, else the chunked adapter. */
@@ -119,6 +177,84 @@ export type STTStreamingOptions = {
119
177
  prerollMs?: number;
120
178
  /** Shortest utterance worth a pass. Default 250. */
121
179
  minSpeechMs?: number;
180
+ /**
181
+ * Engine passes allowed in flight at once. Live streaming keeps 1 (the
182
+ * default), so a caption never shows a stale pass; the file path
183
+ * (`chunking: "utterances"`) raises it so utterances are transcribed and
184
+ * corrected in parallel.
185
+ */
186
+ concurrency?: number;
187
+ /**
188
+ * Times a failed final pass is tried again before the utterance is
189
+ * reported as silence. Live streaming keeps 0 (a stale retry would delay
190
+ * the next utterance); the file path uses 1.
191
+ */
192
+ finalRetries?: number;
193
+ /** Which speech detector finds utterances, and its thresholds. Default: Silero when it loads, else the energy gate. */
194
+ vad?: STTVadOptions;
195
+ /**
196
+ * Raise quiet utterances to a speech level before an engine hears them
197
+ * (gain only goes up, capped at 8×, peaks limited). Far-field and
198
+ * low-gain microphones are the usual case. Default true.
199
+ */
200
+ normalize?: boolean;
201
+ };
202
+ /**
203
+ * The detector that decides where utterances start and end in a chunked
204
+ * stream. `"silero"` is the Silero VAD model (a 2.3 MB ONNX file shipped
205
+ * with the package, run through the optional `onnxruntime-node`); `"energy"`
206
+ * is the RMS gate with a noise-floor estimate; `"auto"` (the default) is
207
+ * Silero when its model and runtime load and the energy gate otherwise.
208
+ * `NEUROLINK_STT_VAD` sets the default.
209
+ */
210
+ export type STTVadEngine = "auto" | "silero" | "energy";
211
+ /** Speech-detector choice and tuning (`streaming.vad`). */
212
+ export type STTVadOptions = {
213
+ engine?: STTVadEngine;
214
+ /** Speech probability at or above which an utterance may open. Default 0.6. */
215
+ onset?: number;
216
+ /** Probability at or above which an open utterance counts a block as speech. Default 0.35. */
217
+ sustain?: number;
218
+ /**
219
+ * Consecutive milliseconds at or above `onset` before an utterance opens,
220
+ * so a click or a cough does not. Default 120; the pre-roll keeps the
221
+ * confirmation audio, so nothing is clipped.
222
+ */
223
+ startMs?: number;
224
+ /** Directory holding `silero_vad.onnx`. Default: `NEUROLINK_VAD_DIR`, `NEUROLINK_MODEL_DIR/silero-vad`, the package's own copy, then `~/.neurolink/models/silero-vad`. */
225
+ modelDir?: string;
226
+ /** Whether a missing model file may be downloaded (`NEUROLINK_VAD_AUTO_DOWNLOAD`). Default true. */
227
+ autoDownload?: boolean;
228
+ };
229
+ /**
230
+ * A frame-level speech detector: `frameSamples` of 16 kHz mono float audio
231
+ * in, the probability that the frame is speech out. It keeps state across
232
+ * calls (`reset()` starts a new stream), and one instance serves one stream.
233
+ */
234
+ export type STTSpeechDetector = {
235
+ /** Name reported on events (`vad`): `"silero"`, or whatever a caller's own detector calls itself. */
236
+ readonly name: string;
237
+ /** Samples per `score()` call at 16 kHz (Silero: 512, 32 ms). */
238
+ readonly frameSamples: number;
239
+ score(frame16k: Float32Array): Promise<number> | number;
240
+ reset(): void;
241
+ };
242
+ /**
243
+ * The slice of onnxruntime-node's API the Silero detector uses, typed here
244
+ * so a typecheck does not need the optional dependency installed.
245
+ */
246
+ export type STTOnnxTensor = {
247
+ readonly data: Float32Array | BigInt64Array;
248
+ readonly dims: readonly number[];
249
+ };
250
+ export type STTOnnxSession = {
251
+ run(feeds: Record<string, STTOnnxTensor>): Promise<Record<string, STTOnnxTensor>>;
252
+ };
253
+ export type STTOnnxRuntime = {
254
+ InferenceSession: {
255
+ create(path: string, options?: Record<string, unknown>): Promise<STTOnnxSession>;
256
+ };
257
+ Tensor: new (type: "float32" | "int64", data: Float32Array | BigInt64Array, dims: readonly number[]) => STTOnnxTensor;
122
258
  };
123
259
  /**
124
260
  * Options for `neurolink.transcribe()`. `provider` defaults to
@@ -139,11 +275,153 @@ export type TranscribeOptions = Omit<STTOptions, "enabled" | "provider" | "langu
139
275
  dictionary?: STTDictionaryEntry[];
140
276
  correction?: STTCorrectionOptions;
141
277
  fallback?: STTFallbackOptions;
278
+ routing?: STTRoutingOptions;
142
279
  streaming?: STTStreamingOptions;
280
+ /**
281
+ * How a recording is cut. `"engine"` (the default) hands the whole file to
282
+ * the engine in one request. `"utterances"` runs the recording through the
283
+ * same pipeline as live captions: the endpointer cuts it into utterances,
284
+ * each utterance gets the language probe, its own engine choice, the
285
+ * dictionary and the per-sentence correction, several utterances at a
286
+ * time, and the result carries one segment per utterance with its
287
+ * language and engine. Needs WAV, or ffmpeg for other containers.
288
+ */
289
+ chunking?: "engine" | "utterances";
143
290
  /** Whole-call timeout. */
144
291
  timeoutMs?: number;
145
292
  /** Per-call credentials; the `stt` slice reaches the handlers. */
146
293
  credentials?: STTCredentialsCarrier;
294
+ /** Renderings of the transcript in other languages or scripts (see `STTOutputOptions`). */
295
+ output?: STTOutputOptions;
296
+ };
297
+ /** A live list of output languages, read once per utterance (a session's subscribers, a socket's control frames). */
298
+ export type STTOutputLanguageSource = {
299
+ current(): readonly string[];
300
+ };
301
+ /**
302
+ * Renderings of every utterance in other languages and scripts. Each entry
303
+ * of `languages` is a BCP-47 tag: `"es"`, `"fr-CA"`, `"ta"` translate;
304
+ * `"hi-Latn"` is Hindi written in Latin letters (romanized when Hindi was
305
+ * spoken, translated into Hinglish when something else was); `"source"` is
306
+ * the transcript itself. One model call renders all of them, from the
307
+ * native-script transcript and its corrected form, with earlier utterances
308
+ * as context. A rendering never touches `text`: a failed one is reported with
309
+ * its status and the transcript is unchanged.
310
+ */
311
+ export type STTOutputOptions = {
312
+ languages: readonly string[] | STTOutputLanguageSource;
313
+ /** Text provider for the rendering; default `NEUROLINK_STT_OUTPUT_PROVIDER`, then the rewrite's. */
314
+ provider?: string;
315
+ /** Default `NEUROLINK_STT_OUTPUT_MODEL`, then the rewrite's. */
316
+ model?: string;
317
+ /** Per call; on expiry the rendering is reported as `"timeout"`. Default 15000. */
318
+ timeoutMs?: number;
319
+ /** Earlier utterances (and their renderings) given as context. Default 3; 0 turns it off. */
320
+ contextUtterances?: number;
321
+ /** Keep dictionary terms exactly as written in every language. Default true. */
322
+ glossary?: boolean;
323
+ };
324
+ export type STTOutputStatus =
325
+ /** Rendered by the model and accepted by the guards. */
326
+ "ok"
327
+ /** The target is the transcript's own language and script: the corrected text, no model call. */
328
+ | "same-language" | "timeout" | "error"
329
+ /** The model answered, and the guards refused it (empty, wrong script, untranslated). */
330
+ | "rejected"
331
+ /** A long transcript where some batches rendered and others did not. */
332
+ | "partial";
333
+ /** The transcript in one output language. `text` is empty unless the status is `ok`, `same-language` or `partial`. */
334
+ export type STTOutputRendering = {
335
+ language: string;
336
+ text: string;
337
+ status: STTOutputStatus;
338
+ ms: number;
339
+ /** Per segment (or utterance), in order, when the transcript has more than one. */
340
+ segments?: Array<{
341
+ index: number;
342
+ text: string;
343
+ }>;
344
+ /** Why a rendering was refused or failed. */
345
+ reason?: string;
346
+ };
347
+ /** One rendering request: lines of one transcript and the targets they go to. */
348
+ export type STTOutputRenderInput = {
349
+ /** The text as the engine wrote it (native script, dictionary applied), one entry per line. */
350
+ source: readonly string[];
351
+ /** The corrected text of the same lines, when correction changed it. */
352
+ corrected?: readonly string[];
353
+ /** Detected language of the speech, when known. */
354
+ language?: string;
355
+ languages: readonly string[];
356
+ /** Earlier lines, oldest first, with their renderings per target. */
357
+ previous?: ReadonlyArray<{
358
+ text: string;
359
+ outputs: Record<string, string>;
360
+ }>;
361
+ dictionary?: readonly STTDictionaryEntry[];
362
+ options?: Omit<STTOutputOptions, "languages">;
363
+ };
364
+ /** What one rendering request produced for one target. */
365
+ export type STTOutputBatchOutcome = {
366
+ lines?: string[];
367
+ status: STTOutputStatus;
368
+ reason?: string;
369
+ };
370
+ /** One aligned position of two readings: a word from either side, or both when they line up. */
371
+ export type STTWordPair = {
372
+ /** The Indic engine's word (native script). */
373
+ indic?: string;
374
+ /** The English-oriented engine's word. */
375
+ latin?: string;
376
+ /** Both present and phonetically alike: the speaker said the same thing in both readings. */
377
+ alike: boolean;
378
+ };
379
+ /** How two readings of one utterance relate (see `analyzeCodeMix`). */
380
+ export type STTCodeMixAnalysis = {
381
+ /** Share of the Indic reading's words with a phonetic partner in the English reading (0–1). */
382
+ englishShare: number;
383
+ /** Words in the Indic reading. */
384
+ words: number;
385
+ pairs: STTWordPair[];
386
+ };
387
+ /** ISO 15924 scripts the output guards recognise. */
388
+ export type ScriptCode = "Latn" | "Deva" | "Beng" | "Guru" | "Gujr" | "Orya" | "Taml" | "Telu" | "Knda" | "Mlym" | "Sinh" | "Arab" | "Cyrl" | "Grek" | "Hebr" | "Thai" | "Hang" | "Jpan" | "Hani" | "Geor" | "Armn" | "Ethi";
389
+ /** One output target, parsed: `"hi-Latn"` → `{ language: "hi", script: "Latn" }`. */
390
+ export type ParsedOutputTag = {
391
+ tag: string;
392
+ language: string;
393
+ /** The script the text must be written in: the subtag, else the language's own. */
394
+ script: ScriptCode;
395
+ /** True when the tag names a script (`hi-Latn`), not just a language. */
396
+ explicitScript: boolean;
397
+ };
398
+ export type TranscriptionSessionOptions = {
399
+ id?: string;
400
+ /** Utterances kept for listeners who join late. Default 50. */
401
+ backlog?: number;
402
+ /** Distinct output languages a session renders at once. Default 8. */
403
+ maxLanguages?: number;
404
+ /** Rendering settings shared by every speaker; the languages come from the listeners. */
405
+ output?: Omit<STTOutputOptions, "languages">;
406
+ };
407
+ /** A stream event from one speaker of a session. */
408
+ export type TranscriptionSessionEvent = TranscribeStreamEvent & {
409
+ speaker: string;
410
+ };
411
+ export type TranscriptionSubscriberOptions = {
412
+ /** Output languages this listener reads. Empty: the transcript only. */
413
+ languages?: readonly string[];
414
+ /** Speakers to follow; omitted, every speaker. */
415
+ speakers?: readonly string[];
416
+ /** Replay the backlog on subscribe. Default true. */
417
+ backlog?: boolean;
418
+ /** Also deliver `interim` captions. Default true. */
419
+ interim?: boolean;
420
+ };
421
+ export type TranscriptionSubscription = {
422
+ readonly languages: readonly string[];
423
+ update(options: Pick<TranscriptionSubscriberOptions, "languages" | "speakers">): void;
424
+ close(): void;
147
425
  };
148
426
  /** `transcribeStream()` takes frames instead of a whole buffer. */
149
427
  export type TranscribeStreamOptions = Omit<TranscribeOptions, "audio"> & {
@@ -189,6 +467,8 @@ export type TranscribeTimings = {
189
467
  decideMs?: number;
190
468
  rewriteMs?: number;
191
469
  correctionMs?: number;
470
+ /** Rendering into the output languages. */
471
+ outputMs?: number;
192
472
  totalMs: number;
193
473
  };
194
474
  /** The result of `transcribe()`: an `STTResult` plus what the layers did to it. */
@@ -205,6 +485,8 @@ export type TranscribeResult = STTResult & {
205
485
  timings: TranscribeTimings;
206
486
  /** Human-readable trail of what happened ("dictionary 2/2", "rewrite timed out · kept text"). */
207
487
  steps: string[];
488
+ /** One rendering per `output.languages` entry, keyed by the tag as given. */
489
+ outputs?: Record<string, STTOutputRendering>;
208
490
  };
209
491
  export type TranscribeStreamEvent = {
210
492
  type: "interim";
@@ -235,6 +517,19 @@ export type TranscribeStreamEvent = {
235
517
  engine: string;
236
518
  fallbackUsed?: boolean;
237
519
  seconds: number;
520
+ /** The detector that found this utterance: `"silero"`, `"energy"`, or a caller's own. */
521
+ vad?: string;
522
+ /** Seconds the detector counted as speech inside the utterance. */
523
+ speechSeconds?: number;
524
+ /**
525
+ * The other engine's reading of the same utterance, when the two
526
+ * disagreed on its language and the final ran both: code-mixed speech
527
+ * is reconciled from both readings (`secondOpinion: "auto"`).
528
+ */
529
+ alternative?: {
530
+ engine: string;
531
+ text: string;
532
+ };
238
533
  } | {
239
534
  /** Partial text of the streamed rewrite, in order. */
240
535
  type: "correcting";
@@ -248,11 +543,26 @@ export type TranscribeStreamEvent = {
248
543
  decisions: STTDecisionRecord[];
249
544
  steps: string[];
250
545
  timings: TranscribeTimings;
546
+ } | {
547
+ /**
548
+ * The utterance in one output language. Follows `corrected` when
549
+ * correction runs (`basedOn: "corrected"`), else the `final`.
550
+ */
551
+ type: "output";
552
+ utterance: number;
553
+ language: string;
554
+ text: string;
555
+ status: STTOutputStatus;
556
+ basedOn: "final" | "corrected";
557
+ ms: number;
558
+ reason?: string;
251
559
  } | {
252
560
  /** An utterance no engine could read; its audio length is reported so nothing disappears silently. */
253
561
  type: "silence";
254
562
  utterance: number;
255
563
  seconds: number;
564
+ vad?: string;
565
+ speechSeconds?: number;
256
566
  } | {
257
567
  type: "error";
258
568
  utterance?: number;
@@ -260,6 +570,18 @@ export type TranscribeStreamEvent = {
260
570
  /** `true` when the stream continues with the next utterance. */
261
571
  recoverable: boolean;
262
572
  };
573
+ /** The `final` event of a stream, as a named type for code that collects them. */
574
+ export type TranscribeStreamFinalEvent = Extract<TranscribeStreamEvent, {
575
+ type: "final";
576
+ }>;
577
+ /** The `corrected` event of a stream. */
578
+ export type TranscribeStreamCorrectedEvent = Extract<TranscribeStreamEvent, {
579
+ type: "corrected";
580
+ }>;
581
+ /** The `output` event of a stream. */
582
+ export type TranscribeStreamOutputEvent = Extract<TranscribeStreamEvent, {
583
+ type: "output";
584
+ }>;
263
585
  /**
264
586
  * One OpenAI-compatible `/audio/transcriptions` endpoint registered under a
265
587
  * name of its own, so several self-hosted engines can be told apart — and
@@ -271,6 +593,22 @@ export type STTEndpointConfig = {
271
593
  apiKey?: string;
272
594
  model?: string;
273
595
  timeoutMs?: number;
596
+ /**
597
+ * How this server takes a language prior in its `language` field, as a
598
+ * template with `{lang}` (the IndicConformer wrapper: `"auto-{lang}"`).
599
+ * Unset, no prior is ever sent: a server that does not understand one
600
+ * would refuse the request.
601
+ */
602
+ priorLanguageForm?: string;
603
+ /** A model name this server runs in language-identification-only mode (see `STTHandler.languageProbeModel`). */
604
+ lidModel?: string;
605
+ /**
606
+ * Whether the dictionary terms travel to this server as a "Names and
607
+ * terms used: …" prompt. Whisper biases on it; Qwen3-ASR and most others
608
+ * ignore it and some echo it back on a pause. Default `true`; set `false`
609
+ * for a server that echoes it.
610
+ */
611
+ vocabularyPrompt?: boolean;
274
612
  };
275
613
  /** Credential slices for STT handlers; `credentials.stt.<key>` beats the env var. */
276
614
  export type STTCredentials = {
@@ -281,6 +619,12 @@ export type STTCredentials = {
281
619
  timeoutMs?: number;
282
620
  /** Default model name sent to the endpoint (`whisper-1` when omitted). */
283
621
  model?: string;
622
+ /** See `STTEndpointConfig.priorLanguageForm`. Env: `OPENAI_STT_PRIOR_LANGUAGE_FORM`. */
623
+ priorLanguageForm?: string;
624
+ /** See `STTEndpointConfig.lidModel`. Env: `OPENAI_STT_LID_MODEL`. */
625
+ lidModel?: string;
626
+ /** See `STTEndpointConfig.vocabularyPrompt`. Env: `OPENAI_STT_VOCABULARY_PROMPT`. */
627
+ vocabularyPrompt?: boolean;
284
628
  };
285
629
  /**
286
630
  * Extra OpenAI-compatible endpoints, each registered as a provider under
@@ -366,6 +710,19 @@ export type STTCorrectionDeps = {
366
710
  timeoutMs: number;
367
711
  onPartial?: (text: string) => void;
368
712
  }) => Promise<string>;
713
+ /**
714
+ * A structured text call for output rendering: the parsed object the
715
+ * schema describes. Absent, renderings report `"error"`.
716
+ */
717
+ render?: (opts: {
718
+ system: string;
719
+ user: string;
720
+ schema: ZodUnknownSchema;
721
+ provider?: string;
722
+ model?: string;
723
+ maxTokens: number;
724
+ timeoutMs: number;
725
+ }) => Promise<unknown>;
369
726
  };
370
727
  /** Input to the correction layer, independent of where the text came from. */
371
728
  export type STTCorrectionInput = {
@@ -376,6 +733,10 @@ export type STTCorrectionInput = {
376
733
  options?: STTCorrectionOptions;
377
734
  /** `false` when the primary engine reported a low language score: the second opinion then leads. */
378
735
  languageDetected?: boolean;
736
+ /** Detected language of the speech; picks the rewrite's rules and script default. */
737
+ language?: string;
738
+ /** Corrected utterances before this one, oldest first: read-only context for the rewrite. */
739
+ previous?: readonly string[];
379
740
  onPartial?: (text: string) => void;
380
741
  };
381
742
  /** A dictionary alias found in a transcript: a candidate substitution for the guard to judge. */
@@ -387,7 +748,7 @@ export type STTDictionaryCandidate = {
387
748
  /** What one rewrite call did; `text` is always usable (the input when the rewrite was not kept). */
388
749
  export type STTRewriteOutcome = {
389
750
  text: string;
390
- status: "ok" | "timeout" | "error" | "empty" | "dropped";
751
+ status: "ok" | "timeout" | "error" | "empty" | "dropped" | "added";
391
752
  ms: number;
392
753
  /** Share of words the rewrite dropped, when `status` is `"dropped"`. */
393
754
  dropRatio?: number;
@@ -432,6 +793,24 @@ export type STTEndpointerConfig = {
432
793
  prerollMs: number;
433
794
  minSpeechMs: number;
434
795
  };
796
+ /**
797
+ * A speech detector's verdicts as the endpointer reads them: the thresholds
798
+ * and a lookup of the probability at a point on the stream clock. Attached,
799
+ * it replaces the RMS rules; the energy gate stays as the fallback.
800
+ */
801
+ export type STTEndpointerSpeechConfig = {
802
+ onset: number;
803
+ sustain: number;
804
+ startMs: number;
805
+ /** Speech probability of the frame covering `seconds`; `undefined` before the first scored frame. */
806
+ probabilityAt(seconds: number): number | undefined;
807
+ };
808
+ /** One block waiting in the endpointer's pre-roll: its samples, RMS and speech probability. */
809
+ export type STTEndpointerPreBlock = {
810
+ block: Float32Array;
811
+ rms: number;
812
+ speech: number;
813
+ };
435
814
  /** One utterance as the energy gate builds it, at the input rate. */
436
815
  export type STTEndpointerUtterance = {
437
816
  /** 1-based, in order of onset. A dropped cough consumes an id, so ids can skip. */
@@ -442,6 +821,8 @@ export type STTEndpointerUtterance = {
442
821
  blocks: Float32Array[];
443
822
  /** RMS of each block, parallel to `blocks`. */
444
823
  rms: number[];
824
+ /** Speech probability of each block, parallel to `blocks`, when a detector is attached. */
825
+ speech?: number[];
445
826
  /** Total samples in `blocks`. */
446
827
  length: number;
447
828
  speechMs: number;
@@ -485,6 +866,14 @@ export type STTChunkedPassOptions = {
485
866
  engine: STTStreamEngineRole;
486
867
  /** Audio length of the utterance so far. */
487
868
  seconds: number;
869
+ /** The last confidently detected language of this stream, for engines that take a prior. */
870
+ languagePrior?: string;
871
+ /**
872
+ * Transcribe in this language rather than detecting one: set on the
873
+ * fallback role once the utterance (or the stream) is settled as that
874
+ * language, so a short pass cannot drift into another script.
875
+ */
876
+ languageHint?: string;
488
877
  };
489
878
  /** One engine pass over a whole utterance, 16 kHz mono float samples in. */
490
879
  export type STTChunkedRun = (pcm16k: Float32Array, options: STTChunkedPassOptions) => Promise<STTResult>;
@@ -497,6 +886,13 @@ export type STTChunkedStreamOptions = STTStreamingOptions & {
497
886
  label: string;
498
887
  when?: STTFallbackOptions["when"];
499
888
  };
889
+ /** Probe, lock and prior behaviour (see `STTRoutingOptions`); the defaults apply when omitted. */
890
+ routing?: STTRoutingOptions;
891
+ /**
892
+ * A speech detector of the caller's own, used instead of what `vad`
893
+ * would load; `null` keeps the energy gate. Omitted, `vad` decides.
894
+ */
895
+ speechDetector?: STTSpeechDetector | null;
500
896
  /**
501
897
  * Called once per utterance, synchronously, when it ends and before its
502
898
  * final pass is queued, with the utterance audio at 16 kHz and the role
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Speech level for the engines: a quiet utterance (a far-field speaker, a
3
+ * low-gain microphone) is raised to a normal speech level before an engine
4
+ * hears it. Recognisers are trained on speech near -20 dBFS and lose words,
5
+ * and language-ID confidence, well below it.
6
+ *
7
+ * Gain only ever goes up, is capped, and is limited by the peak so nothing
8
+ * clips; digital silence stays silence (zero times any gain is zero), so the
9
+ * silence pad an engine relies on is untouched.
10
+ *
11
+ * @module voice/audioLevel
12
+ */
13
+ /** `pcm` with its speech raised to the target level, or `pcm` itself when it is loud enough already. */
14
+ export declare function normalizeSpeechLevel(pcm: Float32Array): Float32Array;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Speech level for the engines: a quiet utterance (a far-field speaker, a
3
+ * low-gain microphone) is raised to a normal speech level before an engine
4
+ * hears it. Recognisers are trained on speech near -20 dBFS and lose words,
5
+ * and language-ID confidence, well below it.
6
+ *
7
+ * Gain only ever goes up, is capped, and is limited by the peak so nothing
8
+ * clips; digital silence stays silence (zero times any gain is zero), so the
9
+ * silence pad an engine relies on is untouched.
10
+ *
11
+ * @module voice/audioLevel
12
+ */
13
+ /** RMS the speech is raised to: about -22 dBFS. */
14
+ const TARGET_SPEECH_RMS = 0.08;
15
+ /** Most gain applied to any utterance. */
16
+ const MAX_GAIN = 8;
17
+ /** Peak after gain; leaves headroom for the engine's own resampling. */
18
+ const PEAK_LIMIT = 0.95;
19
+ /** Blocks quieter than this do not count as speech when measuring the level. */
20
+ const SPEECH_FLOOR_RMS = 0.002;
21
+ /** 20 ms at 16 kHz. */
22
+ const BLOCK = 320;
23
+ /** Gain below this is not worth a copy of the audio. */
24
+ const MIN_USEFUL_GAIN = 1.1;
25
+ /** `pcm` with its speech raised to the target level, or `pcm` itself when it is loud enough already. */
26
+ export function normalizeSpeechLevel(pcm) {
27
+ let sum = 0;
28
+ let count = 0;
29
+ let peak = 0;
30
+ for (let at = 0; at < pcm.length; at += BLOCK) {
31
+ const end = Math.min(pcm.length, at + BLOCK);
32
+ let block = 0;
33
+ for (let i = at; i < end; i++) {
34
+ const v = pcm[i];
35
+ block += v * v;
36
+ const a = Math.abs(v);
37
+ if (a > peak) {
38
+ peak = a;
39
+ }
40
+ }
41
+ const rms = Math.sqrt(block / Math.max(1, end - at));
42
+ if (rms >= SPEECH_FLOOR_RMS) {
43
+ sum += block;
44
+ count += end - at;
45
+ }
46
+ }
47
+ if (count === 0 || peak === 0) {
48
+ return pcm;
49
+ }
50
+ const speechRms = Math.sqrt(sum / count);
51
+ const gain = Math.min(MAX_GAIN, TARGET_SPEECH_RMS / speechRms, PEAK_LIMIT / peak);
52
+ if (!(gain >= MIN_USEFUL_GAIN)) {
53
+ return pcm;
54
+ }
55
+ const out = new Float32Array(pcm.length);
56
+ for (let i = 0; i < pcm.length; i++) {
57
+ out[i] = pcm[i] * gain;
58
+ }
59
+ return out;
60
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Code-mix analysis between two engines' readings of the same speech.
3
+ *
4
+ * An Indic engine writes English words phonetically in its own script
5
+ * ("मीटिंग" for "meeting"); an English-oriented engine writes Indian-language
6
+ * words as English-sounding guesses ("page dinner" for "भेज देना"). Brought
7
+ * to one rough phonetic form — Indic text romanized, both reduced to a
8
+ * consonant skeleton with voicing and aspiration folded — the two sequences
9
+ * line up word for word wherever the speaker spoke English, and stop lining
10
+ * up where they spoke an Indian language. The share of the Indic engine's
11
+ * words that find a phonetic partner is therefore an estimate of how much of
12
+ * the utterance was English, independent of either engine's own (accent-
13
+ * sensitive) language score; the alignment itself is what a word-level
14
+ * combination of the two readings is built on.
15
+ *
16
+ * @module voice/codeMix
17
+ */
18
+ import type { STTCodeMixAnalysis, STTWordPair } from "../types/index.js";
19
+ /** Indic-script text in rough Latin letters; anything else passes through. */
20
+ export declare function romanizeIndic(text: string): string;
21
+ /**
22
+ * A word's consonant skeleton: digraphs folded, voicing pairs merged
23
+ * (t/d, k/g, p/b are where an accent and a script disagree most), vowels and
24
+ * h dropped, repeats collapsed. "meeting" and "मीटिंग" both give "mtnk".
25
+ */
26
+ export declare function phoneticKey(word: string): string;
27
+ /** Whether two skeletons sound alike: equal, or one edit apart on three or more consonants (two on six). */
28
+ export declare function soundsAlike(a: string, b: string): boolean;
29
+ /**
30
+ * Align two readings word by word (Needleman–Wunsch on skeletons): a pair
31
+ * for every word of either side, matched where the words sound alike.
32
+ */
33
+ export declare function alignReadings(indic: readonly string[], latin: readonly string[]): STTWordPair[];
34
+ /**
35
+ * How much of an utterance two readings agree is English: the share of the
36
+ * Indic engine's words with a phonetic partner in the English-oriented
37
+ * engine's text. Near 1 for English an Indic engine wrote phonetically; well
38
+ * under it for code-mixed speech, where the Indian-language words find none.
39
+ */
40
+ export declare function analyzeCodeMix(indicText: string, latinText: string): STTCodeMixAnalysis;