@juspay/neurolink 13.6.5 → 13.8.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.
- package/CHANGELOG.md +6 -2
- package/dist/auth/sessionManager.js +8 -3
- package/dist/browser/neurolink.min.js +480 -460
- package/dist/cli/commands/serve.js +11 -1
- package/dist/cli/commands/transcribe.js +39 -3
- package/dist/constants/langfuse.d.ts +2 -0
- package/dist/constants/langfuse.js +2 -0
- package/dist/core/redisConversationMemoryManager.js +6 -16
- package/dist/factories/sttDescriptors.js +7 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -2
- package/dist/neurolink.js +33 -1
- package/dist/observability/exporters/langfuseExporter.d.ts +4 -0
- package/dist/observability/exporters/langfuseExporter.js +29 -8
- package/dist/observability/otlpEncoding.js +2 -2
- package/dist/server/abstract/baseServerAdapter.js +2 -0
- package/dist/server/adapters/expressAdapter.js +1 -1
- package/dist/server/adapters/fastifyAdapter.js +4 -1
- package/dist/server/adapters/honoAdapter.js +1 -1
- package/dist/server/adapters/koaAdapter.js +1 -1
- package/dist/server/routes/transcribeRoutes.d.ts +0 -8
- package/dist/server/routes/transcribeRoutes.js +130 -3
- package/dist/server/utils/validation.d.ts +409 -15
- package/dist/server/utils/validation.js +106 -4
- package/dist/server/websocket/frameQueue.d.ts +16 -0
- package/dist/server/websocket/frameQueue.js +55 -0
- package/dist/server/websocket/realtimeTranscription.d.ts +48 -0
- package/dist/server/websocket/realtimeTranscription.js +603 -0
- package/dist/server/websocket/transcribeWebSocket.d.ts +17 -1
- package/dist/server/websocket/transcribeWebSocket.js +263 -64
- package/dist/services/server/ai/observability/langfuseSetup.js +6 -1
- package/dist/telemetry/attributes.d.ts +1 -1
- package/dist/telemetry/attributes.js +84 -8
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/exporter.d.ts +3 -1
- package/dist/types/server.d.ts +304 -2
- package/dist/types/stt.d.ts +38 -3
- package/dist/types/transcribe.d.ts +401 -5
- package/dist/utils/redis.d.ts +17 -0
- package/dist/utils/redis.js +31 -0
- package/dist/voice/audioLevel.d.ts +14 -0
- package/dist/voice/audioLevel.js +60 -0
- package/dist/voice/codeMix.d.ts +40 -0
- package/dist/voice/codeMix.js +282 -0
- package/dist/voice/correction/align.d.ts +12 -0
- package/dist/voice/correction/align.js +48 -0
- package/dist/voice/correction/guards.d.ts +39 -0
- package/dist/voice/correction/guards.js +122 -0
- package/dist/voice/correction/index.d.ts +25 -3
- package/dist/voice/correction/index.js +132 -4
- package/dist/voice/correction/rewrite.d.ts +64 -3
- package/dist/voice/correction/rewrite.js +295 -25
- package/dist/voice/eval/sttEvalLangfuse.d.ts +14 -0
- package/dist/voice/eval/sttEvalLangfuse.js +182 -0
- package/dist/voice/eval/sttEvalStore.d.ts +27 -0
- package/dist/voice/eval/sttEvalStore.js +397 -0
- package/dist/voice/index.js +3 -0
- package/dist/voice/languages.d.ts +34 -0
- package/dist/voice/languages.js +290 -0
- package/dist/voice/modelAssets.d.ts +28 -0
- package/dist/voice/modelAssets.js +113 -0
- package/dist/voice/output/render.d.ts +32 -0
- package/dist/voice/output/render.js +368 -0
- package/dist/voice/providers/OpenAISTT.d.ts +11 -1
- package/dist/voice/providers/OpenAISTT.js +57 -6
- package/dist/voice/session/transcriptionSession.d.ts +55 -0
- package/dist/voice/session/transcriptionSession.js +225 -0
- package/dist/voice/streaming/chunkedStreamAdapter.d.ts +7 -0
- package/dist/voice/streaming/chunkedStreamAdapter.js +711 -28
- package/dist/voice/streaming/endpointer.d.ts +50 -17
- package/dist/voice/streaming/endpointer.js +115 -19
- package/dist/voice/transcribe.d.ts +18 -1
- package/dist/voice/transcribe.js +459 -18
- package/dist/voice/transcribeStream.d.ts +18 -1
- package/dist/voice/transcribeStream.js +413 -16
- package/dist/voice/vad/assets.d.ts +36 -0
- package/dist/voice/vad/assets.js +142 -0
- package/dist/voice/vad/frameScorer.d.ts +28 -0
- package/dist/voice/vad/frameScorer.js +90 -0
- package/dist/voice/vad/sileroVad.d.ts +43 -0
- package/dist/voice/vad/sileroVad.js +178 -0
- package/dist/voice/whistle/assets.d.ts +3 -4
- package/dist/voice/whistle/assets.js +11 -95
- package/docs-site/static/search-index.json +21 -6
- package/models/silero-vad/README.md +19 -0
- package/models/silero-vad/silero_vad.onnx +0 -0
- package/package.json +4 -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
|
-
|
|
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
|
-
/**
|
|
22
|
-
|
|
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>;
|