@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
@@ -12,8 +12,8 @@
12
12
  */
13
13
  import { applyDictionary, dictionaryBlock, dictionaryVocabulary, findDictionaryCandidates, } from "./dictionary.js";
14
14
  import { DEFAULT_DECIDE_TIMEOUT_MS, decideCandidates } from "./guard.js";
15
- import { buildRewriteSystemPrompt, buildRewriteUserPrompt, rewriteLines, rewriteTranscript, } from "./rewrite.js";
16
- export { applyDictionary, buildRewriteSystemPrompt, buildRewriteUserPrompt, decideCandidates, dictionaryBlock, dictionaryVocabulary, findDictionaryCandidates, rewriteLines, rewriteTranscript, };
15
+ import { buildRewriteSystemPrompt, buildRewriteUserPrompt, reconcilePairs, rewriteLines, rewriteTranscript, rewriteWanted, } from "./rewrite.js";
16
+ export { applyDictionary, buildRewriteSystemPrompt, buildRewriteUserPrompt, decideCandidates, dictionaryBlock, dictionaryVocabulary, findDictionaryCandidates, rewriteLines, rewriteTranscript, rewriteWanted, };
17
17
  /** Correction is on when asked for, or by default when a dictionary or a rewrite is configured. */
18
18
  export function isCorrectionEnabled(dictionary, options) {
19
19
  if (options?.enabled !== undefined) {
@@ -102,10 +102,25 @@ export async function correctTranscript(input, deps) {
102
102
  if (guarded.decideMs !== undefined) {
103
103
  timings.decideMs = guarded.decideMs;
104
104
  }
105
- if (options?.rewrite !== false) {
105
+ const want = rewriteWanted(text, options, {
106
+ hasSecondOpinion: !!secondary,
107
+ termsApplied: guarded.decisions.filter((d) => d.choice === "term").length,
108
+ language: input.language,
109
+ });
110
+ if (!want.wanted) {
111
+ if (options?.rewrite !== false) {
112
+ steps.push(`rewrite skipped · ${want.reason}`);
113
+ }
114
+ }
115
+ else {
116
+ const aligned = reconcilePairs(text, secondary);
117
+ if (aligned) {
118
+ steps.push(`aligned two readings · ${Math.round(aligned.englishShare * 100)}% of words heard alike`);
119
+ }
106
120
  const outcome = await rewriteTranscript({
107
121
  primary: text,
108
122
  secondOpinion: secondary,
123
+ ...(aligned ? { pairs: aligned.pairs } : {}),
109
124
  dictionary,
110
125
  options,
111
126
  labels: secondLed
@@ -117,6 +132,8 @@ export async function correctTranscript(input, deps) {
117
132
  literal: guarded.decisions
118
133
  .filter((d) => d.choice === "literal")
119
134
  .map((d) => d.heard),
135
+ language: input.language,
136
+ previous: input.previous,
120
137
  onPartial: input.onPartial,
121
138
  }, deps.rewrite);
122
139
  timings.rewriteMs = outcome.ms;
@@ -137,6 +154,9 @@ export async function correctTranscript(input, deps) {
137
154
  case "dropped":
138
155
  steps.push(`rewrite dropped ${Math.round((outcome.dropRatio ?? 0) * 100)}% of words · kept text`);
139
156
  break;
157
+ case "added":
158
+ steps.push("rewrite added words that were not said · kept text");
159
+ break;
140
160
  }
141
161
  }
142
162
  timings.correctionMs = Date.now() - started;
@@ -147,6 +167,113 @@ export async function correctTranscript(input, deps) {
147
167
  timings,
148
168
  };
149
169
  }
170
+ /**
171
+ * Split a transcript into sentences on terminal punctuation (., !, ?, the
172
+ * danda, an ellipsis), keeping the marks. Text without any mark is one
173
+ * sentence; a split that would leave a fragment under three words glues it
174
+ * to its neighbour so a stray "No." never travels alone.
175
+ */
176
+ export function splitSentences(text) {
177
+ const trimmed = text.trim();
178
+ if (!trimmed) {
179
+ return [];
180
+ }
181
+ const parts = trimmed
182
+ .split(/(?<=[.!?\u0964\u0965\u2026]["'\u201D\u2019)\]]*)\s+/u)
183
+ .map((p) => p.trim())
184
+ .filter(Boolean);
185
+ const out = [];
186
+ for (const part of parts) {
187
+ const words = part.split(/\s+/u).length;
188
+ if (out.length > 0 && words < 3) {
189
+ out[out.length - 1] = `${out[out.length - 1]} ${part}`;
190
+ }
191
+ else {
192
+ out.push(part);
193
+ }
194
+ }
195
+ return out;
196
+ }
197
+ /**
198
+ * Correct one final sentence by sentence: each sentence gets its own
199
+ * dictionary pass, its own "does this need the rewrite" decision (an
200
+ * English sentence that is already clean costs nothing; a Devanagari one
201
+ * is romanised) and sentences run three at a time. The joined text has the
202
+ * same sentences in the same order; a sentence whose correction fails keeps
203
+ * its words. `onPartial` receives the whole utterance with the sentences
204
+ * corrected so far, so a caption can update progressively.
205
+ *
206
+ * Falls back to a single whole-text pass when there is one sentence or a
207
+ * second opinion (whole-utterance by nature) to reconcile.
208
+ */
209
+ export async function correctTranscriptSentences(input, deps) {
210
+ const sentences = splitSentences(input.text ?? "");
211
+ if (sentences.length < 2 || input.secondOpinion?.trim()) {
212
+ const one = await correctTranscript(input, deps);
213
+ return { ...one, sentences: Math.max(1, sentences.length) };
214
+ }
215
+ const started = Date.now();
216
+ const out = [...sentences];
217
+ const results = sentences.map(() => undefined);
218
+ let next = 0;
219
+ const emit = () => {
220
+ input.onPartial?.(out.join(" "));
221
+ };
222
+ const worker = async () => {
223
+ while (next < sentences.length) {
224
+ const index = next++;
225
+ try {
226
+ const res = await correctTranscript({
227
+ ...input,
228
+ text: sentences[index],
229
+ secondOpinion: undefined,
230
+ onPartial: undefined,
231
+ }, deps);
232
+ results[index] = res;
233
+ out[index] = res.text.trim() ? res.text : sentences[index];
234
+ }
235
+ catch {
236
+ out[index] = sentences[index];
237
+ }
238
+ emit();
239
+ }
240
+ };
241
+ await Promise.all(Array.from({ length: Math.min(3, sentences.length) }, () => worker()));
242
+ const decisions = results.flatMap((r) => r?.decisions ?? []);
243
+ const rewrites = results.filter((r) => r?.steps.some((st) => st.startsWith("rewrite ") && st.endsWith(" ms"))).length;
244
+ const skipped = results.filter((r) => r?.steps.some((st) => st.startsWith("rewrite skipped"))).length;
245
+ const kept = results.filter((r) => r?.steps.some((st) => st.includes("kept text"))).length;
246
+ const steps = [
247
+ `per sentence (${sentences.length})`,
248
+ ...(decisions.length
249
+ ? [
250
+ `dictionary ${decisions.filter((d) => d.choice === "term").length}/${decisions.length}`,
251
+ ]
252
+ : []),
253
+ ...(rewrites ? [`rewrite ${rewrites} sentence(s)`] : []),
254
+ ...(skipped ? [`rewrite skipped ${skipped} sentence(s)`] : []),
255
+ ...(kept ? [`${kept} sentence(s) kept their text`] : []),
256
+ ];
257
+ const sum = (key) => {
258
+ const values = results
259
+ .map((r) => r?.timings[key])
260
+ .filter((v) => typeof v === "number");
261
+ return values.length ? values.reduce((a, b) => a + b, 0) : undefined;
262
+ };
263
+ const decideMs = sum("decideMs");
264
+ const rewriteMs = sum("rewriteMs");
265
+ return {
266
+ text: out.join(" "),
267
+ decisions,
268
+ steps,
269
+ timings: {
270
+ ...(decideMs !== undefined ? { decideMs } : {}),
271
+ ...(rewriteMs !== undefined ? { rewriteMs } : {}),
272
+ correctionMs: Date.now() - started,
273
+ },
274
+ sentences: sentences.length,
275
+ };
276
+ }
150
277
  /**
151
278
  * Correct a long transcript line by line (file transcription, diarized
152
279
  * turns). With the rewrite on, lines go to the model in numbered batches of
@@ -156,7 +283,7 @@ export async function correctTranscript(input, deps) {
156
283
  * four at a time) and the dictionary is applied per line. The result always
157
284
  * has exactly as many lines as the input.
158
285
  */
159
- export async function correctTranscriptLines(lines, dictionary, options, deps) {
286
+ export async function correctTranscriptLines(lines, dictionary, options, deps, language) {
160
287
  const started = Date.now();
161
288
  if (!isCorrectionEnabled(dictionary, options) || lines.length === 0) {
162
289
  return { lines: [...lines], decisions: [], steps: [], timings: {} };
@@ -165,6 +292,7 @@ export async function correctTranscriptLines(lines, dictionary, options, deps) {
165
292
  const outcome = await rewriteLines(lines, deps.rewrite, {
166
293
  dictionary,
167
294
  correction: options,
295
+ language,
168
296
  });
169
297
  const steps = [
170
298
  `rewrite ${outcome.batchesOk}/${outcome.batches} batches (${outcome.ms} ms)`,
@@ -17,9 +17,39 @@
17
17
  *
18
18
  * @module voice/correction/rewrite
19
19
  */
20
- import type { STTCorrectionDeps, STTCorrectionOptions, STTDictionaryEntry, STTRewriteLinesOutcome, STTRewriteOutcome } from "../../types/index.js";
21
- /** Default per-call rewrite timeout; on expiry the input text is kept. */
22
- export declare const DEFAULT_REWRITE_TIMEOUT_MS = 12000;
20
+ import type { STTCorrectionDeps, STTCorrectionOptions, STTDictionaryEntry, STTRewriteLinesOutcome, STTRewriteOutcome, STTWordPair } from "../../types/index.js";
21
+ /**
22
+ * The rewrite's provider/model/timeout settings, whatever shape `rewrite`
23
+ * took: `false` and `"auto"` carry none, an object carries its own.
24
+ */
25
+ export declare function rewriteSettings(rewrite: STTCorrectionOptions["rewrite"]): {
26
+ provider?: string;
27
+ model?: string;
28
+ timeoutMs?: number;
29
+ } | undefined;
30
+ /**
31
+ * Whether the rewrite is wanted for a text, under the "auto" rule: it runs
32
+ * when there is something for it to do and is skipped otherwise, which is
33
+ * what makes an already clean English sentence cost nothing. `"always"`
34
+ * (object form with `mode: "always"`) runs it every time; `false` never.
35
+ */
36
+ export declare function rewriteWanted(text: string, options: STTCorrectionOptions | undefined, signals?: {
37
+ /** The rewrite has a second transcript to reconcile. */
38
+ hasSecondOpinion?: boolean;
39
+ /** The dictionary changed something (a term was applied). */
40
+ termsApplied?: number;
41
+ /** Detected language of the speech. */
42
+ language?: string;
43
+ }): {
44
+ wanted: boolean;
45
+ reason: string;
46
+ };
47
+ /**
48
+ * The script setting in effect. Unset, Indian languages (and speech whose
49
+ * language is unknown, as before) are written in Latin letters; any other
50
+ * known language keeps its own script.
51
+ */
52
+ export declare function resolveTransliterate(options: Pick<STTCorrectionOptions, "transliterate"> | undefined, language: string | undefined): "latin" | "native";
23
53
  /** Default per-batch timeout for `rewriteLines`: a batch carries up to 25 lines. */
24
54
  export declare const DEFAULT_REWRITE_LINES_TIMEOUT_MS = 60000;
25
55
  /** Default share of words a rewrite may drop before it is discarded. */
@@ -44,6 +74,9 @@ export declare function buildRewriteSystemPrompt(options?: {
44
74
  punctuate?: boolean;
45
75
  dictionary?: readonly STTDictionaryEntry[];
46
76
  lines?: boolean;
77
+ cleanup?: "verbatim" | "clean";
78
+ /** Detected language of the speech; a known language outside India gets the general prompt. */
79
+ language?: string;
47
80
  }): string;
48
81
  /**
49
82
  * User message for the rewrite. With a second opinion the two transcripts are
@@ -56,7 +89,27 @@ export declare function buildRewriteUserPrompt(primary: string, secondOpinion?:
56
89
  secondary?: string;
57
90
  };
58
91
  literal?: readonly string[];
92
+ /** Earlier corrected utterances, oldest first: context the model must not echo. */
93
+ previous?: readonly string[];
94
+ /** The two transcripts aligned word by word (Indic reading against English reading). */
95
+ pairs?: readonly STTWordPair[];
59
96
  }): string;
97
+ /**
98
+ * The word alignment of two transcripts as a prompt block: same-sounding
99
+ * words side by side ("मीटिंग ≈ meeting"), and runs where only one engine
100
+ * heard something, so the rewrite decides word by word which reading holds
101
+ * the speaker's word in which language.
102
+ */
103
+ export declare function alignmentBlock(pairs: readonly STTWordPair[] | undefined): string | undefined;
104
+ /**
105
+ * Word pairs for a reconciliation: the Indic-script transcript aligned
106
+ * against the Latin-script one, whichever order they came in; `undefined`
107
+ * when both are in the same script (nothing to line up across scripts).
108
+ */
109
+ export declare function reconcilePairs(primary: string, secondOpinion: string | undefined): {
110
+ pairs: STTWordPair[];
111
+ englishShare: number;
112
+ } | undefined;
60
113
  /**
61
114
  * The model's answer: the last `lineCount` non-empty lines, since a preamble,
62
115
  * if any, comes first. An echoed "Transcript:" label is stripped.
@@ -77,6 +130,12 @@ export declare function rewriteTranscript(input: {
77
130
  secondary?: string;
78
131
  };
79
132
  literal?: readonly string[];
133
+ /** Detected language of the speech. */
134
+ language?: string;
135
+ /** Earlier corrected utterances, oldest first. */
136
+ previous?: readonly string[];
137
+ /** The two transcripts aligned word by word (see `reconcilePairs`). */
138
+ pairs?: readonly STTWordPair[];
80
139
  onPartial?: (text: string) => void;
81
140
  }, rewrite: STTCorrectionDeps["rewrite"]): Promise<STTRewriteOutcome>;
82
141
  /**
@@ -89,4 +148,6 @@ export declare function rewriteTranscript(input: {
89
148
  export declare function rewriteLines(lines: readonly string[], rewrite: STTCorrectionDeps["rewrite"], options?: {
90
149
  dictionary?: readonly STTDictionaryEntry[];
91
150
  correction?: STTCorrectionOptions;
151
+ /** Detected language of the recording. */
152
+ language?: string;
92
153
  }): Promise<STTRewriteLinesOutcome>;