@juspay/neurolink 11.25.4 → 11.26.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.
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module utils/ttsProcessor
8
8
  */
9
- import type { TTSOptions, TTSResult, TTSHandler } from "../types/index.js";
9
+ import type { TTSChunk, TTSOptions, TTSResult, TTSHandler } from "../types/index.js";
10
10
  import { ErrorCategory, ErrorSeverity } from "../constants/enums.js";
11
11
  import { NeuroLinkError } from "./errorHandling.js";
12
12
  /**
@@ -20,6 +20,12 @@ export declare const TTS_ERROR_CODES: {
20
20
  readonly SYNTHESIS_FAILED: "TTS_SYNTHESIS_FAILED";
21
21
  readonly INVALID_INPUT: "TTS_INVALID_INPUT";
22
22
  };
23
+ /** Internal signal raised after all buffered segments have been attempted. */
24
+ export declare class IncrementalTTSSynthesisError extends Error {
25
+ readonly firstError: unknown;
26
+ readonly failedSegments: readonly number[];
27
+ constructor(firstError: unknown, failedSegments: number[]);
28
+ }
23
29
  /**
24
30
  * TTS Error class for text-to-speech specific errors
25
31
  */
@@ -156,4 +162,17 @@ export declare class TTSProcessor {
156
162
  * ```
157
163
  */
158
164
  static synthesize(text: string, provider: string, options: TTSOptions): Promise<TTSResult>;
165
+ /**
166
+ * Incrementally synthesize sentence-buffered text chunks.
167
+ *
168
+ * Text is flushed at a sentence boundary after `streamingBufferSize`
169
+ * characters, or hard-split before the provider's maximum text length.
170
+ * Each segment goes through `synthesize()`, preserving the existing handler
171
+ * registry, validation, error normalization, and telemetry seam.
172
+ *
173
+ * The most recent successful audio chunk is held until another succeeds or
174
+ * the input ends, so exactly one real audio chunk carries `isFinal: true`
175
+ * without emitting a separate empty terminator chunk.
176
+ */
177
+ static synthesizeStream(textChunks: AsyncIterable<string>, provider: string, options: TTSOptions, shouldStop?: () => boolean): AsyncGenerator<TTSChunk>;
159
178
  }
@@ -22,6 +22,75 @@ export const TTS_ERROR_CODES = {
22
22
  SYNTHESIS_FAILED: "TTS_SYNTHESIS_FAILED",
23
23
  INVALID_INPUT: "TTS_INVALID_INPUT",
24
24
  };
25
+ const DEFAULT_STREAMING_BUFFER_SIZE = 120;
26
+ const SENTENCE_BOUNDARY = /[.!?]+(?:["')\]]+)?(?=\s|$)/g;
27
+ /** Internal signal raised after all buffered segments have been attempted. */
28
+ export class IncrementalTTSSynthesisError extends Error {
29
+ firstError;
30
+ failedSegments;
31
+ constructor(firstError, failedSegments) {
32
+ super(`Incremental TTS failed for ${failedSegments.length} segment${failedSegments.length === 1 ? "" : "s"}`);
33
+ this.name = "IncrementalTTSSynthesisError";
34
+ this.firstError = firstError;
35
+ this.failedSegments = [...failedSegments];
36
+ }
37
+ }
38
+ function findSentenceEnds(text) {
39
+ const ends = [];
40
+ for (const match of text.matchAll(SENTENCE_BOUNDARY)) {
41
+ ends.push((match.index ?? 0) + match[0].length);
42
+ }
43
+ return ends;
44
+ }
45
+ const HIGH_SURROGATE_START = 0xd800;
46
+ const HIGH_SURROGATE_END = 0xdbff;
47
+ const LOW_SURROGATE_START = 0xdc00;
48
+ const LOW_SURROGATE_END = 0xdfff;
49
+ /**
50
+ * Move a split index off the middle of a surrogate pair.
51
+ *
52
+ * The cap is measured in UTF-16 code units, so a hard split can land between
53
+ * the two halves of an astral character (emoji, rarer CJK). That would end one
54
+ * segment with a lone high surrogate and start the next with its low half, and
55
+ * providers receive U+FFFD instead of the character. Backing the split off by
56
+ * one code unit keeps the pair whole in the next segment.
57
+ *
58
+ * A split at index 1 is left alone: backing off would yield an empty segment
59
+ * with an unchanged remainder, and a cap that small cannot hold the pair anyway.
60
+ */
61
+ function avoidSurrogateSplit(text, splitAt) {
62
+ if (splitAt <= 1 || splitAt >= text.length) {
63
+ return splitAt;
64
+ }
65
+ const high = text.charCodeAt(splitAt - 1);
66
+ const low = text.charCodeAt(splitAt);
67
+ const splitsPair = high >= HIGH_SURROGATE_START &&
68
+ high <= HIGH_SURROGATE_END &&
69
+ low >= LOW_SURROGATE_START &&
70
+ low <= LOW_SURROGATE_END;
71
+ return splitsPair ? splitAt - 1 : splitAt;
72
+ }
73
+ function takeBufferedSegment(buffer, flushBoundary, maxTextLength, inputComplete) {
74
+ const cappedText = buffer.slice(0, maxTextLength);
75
+ const sentenceEnds = findSentenceEnds(cappedText);
76
+ let splitAt = buffer.length >= flushBoundary ? sentenceEnds.at(-1) : undefined;
77
+ if (splitAt === undefined && buffer.length >= maxTextLength) {
78
+ splitAt = sentenceEnds.at(-1) ?? maxTextLength;
79
+ }
80
+ if (splitAt === undefined && inputComplete && buffer.trim()) {
81
+ splitAt = Math.min(buffer.length, maxTextLength);
82
+ }
83
+ if (splitAt === undefined) {
84
+ return undefined;
85
+ }
86
+ splitAt = avoidSurrogateSplit(buffer, splitAt);
87
+ const segment = buffer.slice(0, splitAt).trim();
88
+ const remainder = buffer.slice(splitAt).trimStart();
89
+ if (!segment) {
90
+ return { segment: "", remainder };
91
+ }
92
+ return { segment, remainder };
93
+ }
25
94
  /**
26
95
  * TTS Error class for text-to-speech specific errors
27
96
  */
@@ -289,4 +358,102 @@ export class TTSProcessor {
289
358
  });
290
359
  }
291
360
  }
361
+ /**
362
+ * Incrementally synthesize sentence-buffered text chunks.
363
+ *
364
+ * Text is flushed at a sentence boundary after `streamingBufferSize`
365
+ * characters, or hard-split before the provider's maximum text length.
366
+ * Each segment goes through `synthesize()`, preserving the existing handler
367
+ * registry, validation, error normalization, and telemetry seam.
368
+ *
369
+ * The most recent successful audio chunk is held until another succeeds or
370
+ * the input ends, so exactly one real audio chunk carries `isFinal: true`
371
+ * without emitting a separate empty terminator chunk.
372
+ */
373
+ static async *synthesizeStream(textChunks, provider, options, shouldStop) {
374
+ const handler = this.getHandler(provider);
375
+ const maxTextLength = Math.max(1, handler?.maxTextLength ?? this.DEFAULT_MAX_TEXT_LENGTH);
376
+ const requestedBoundary = options.streamingBufferSize ?? DEFAULT_STREAMING_BUFFER_SIZE;
377
+ const flushBoundary = Math.min(Math.max(1, Math.trunc(requestedBoundary)), maxTextLength);
378
+ let buffer = "";
379
+ let chunkIndex = 0;
380
+ let cumulativeSize = 0;
381
+ let cumulativeDuration = 0;
382
+ let pendingChunk;
383
+ let segmentNumber = 0;
384
+ let firstFailure;
385
+ const failedSegments = [];
386
+ const synthesizeSegment = async (segment) => {
387
+ const currentSegment = ++segmentNumber;
388
+ try {
389
+ const result = await this.synthesize(segment, provider, options);
390
+ cumulativeSize += result.size;
391
+ cumulativeDuration += result.duration ?? 0;
392
+ return {
393
+ data: result.buffer,
394
+ format: result.format,
395
+ index: chunkIndex++,
396
+ isFinal: false,
397
+ cumulativeSize,
398
+ estimatedDuration: cumulativeDuration || undefined,
399
+ voice: result.voice,
400
+ sampleRate: result.sampleRate,
401
+ };
402
+ }
403
+ catch (error) {
404
+ if (failedSegments.length === 0) {
405
+ firstFailure = error;
406
+ }
407
+ failedSegments.push(currentSegment);
408
+ logger.warn(`[TTSProcessor] Incremental synthesis skipped a buffered segment: ${error instanceof Error ? error.message : String(error)}`);
409
+ return undefined;
410
+ }
411
+ };
412
+ for await (const textChunk of textChunks) {
413
+ if (shouldStop?.()) {
414
+ break;
415
+ }
416
+ buffer += textChunk;
417
+ while (!shouldStop?.()) {
418
+ const buffered = takeBufferedSegment(buffer, flushBoundary, maxTextLength, false);
419
+ if (!buffered) {
420
+ break;
421
+ }
422
+ buffer = buffered.remainder;
423
+ if (!buffered.segment) {
424
+ continue;
425
+ }
426
+ const chunk = await synthesizeSegment(buffered.segment);
427
+ if (chunk) {
428
+ if (pendingChunk) {
429
+ yield pendingChunk;
430
+ }
431
+ pendingChunk = chunk;
432
+ }
433
+ }
434
+ }
435
+ while (!shouldStop?.()) {
436
+ const buffered = takeBufferedSegment(buffer, flushBoundary, maxTextLength, true);
437
+ if (!buffered) {
438
+ break;
439
+ }
440
+ buffer = buffered.remainder;
441
+ if (!buffered.segment) {
442
+ continue;
443
+ }
444
+ const chunk = await synthesizeSegment(buffered.segment);
445
+ if (chunk) {
446
+ if (pendingChunk) {
447
+ yield pendingChunk;
448
+ }
449
+ pendingChunk = chunk;
450
+ }
451
+ }
452
+ if (pendingChunk) {
453
+ yield { ...pendingChunk, isFinal: true };
454
+ }
455
+ if (failedSegments.length > 0) {
456
+ throw new IncrementalTTSSynthesisError(firstFailure, failedSegments);
457
+ }
458
+ }
292
459
  }
@@ -0,0 +1,15 @@
1
+ import type { TTSChunk, TTSMetadata, TTSOptions, TTSResult } from "../types/index.js";
2
+ /**
3
+ * Preserve source-stream backpressure while interleaving incremental TTS audio.
4
+ * Source chunks are yielded before their derived audio, and TTS failures degrade
5
+ * to the unchanged source stream.
6
+ */
7
+ export declare function interleaveTTSStream<T>(params: {
8
+ stream: AsyncIterable<T>;
9
+ provider: string;
10
+ options: TTSOptions;
11
+ onComplete?: (result: TTSResult | undefined, error?: NonNullable<TTSMetadata["error"]>) => void;
12
+ }): AsyncGenerator<T | {
13
+ type: "tts_audio";
14
+ audio: TTSChunk;
15
+ }>;
@@ -0,0 +1,225 @@
1
+ import { NeuroLinkError } from "./errorHandling.js";
2
+ import { sanitizeErrorCause } from "./logSanitize.js";
3
+ import { logger } from "./logger.js";
4
+ import { IncrementalTTSSynthesisError, TTS_ERROR_CODES, TTSProcessor, } from "./ttsProcessor.js";
5
+ import { TimeoutError as AsyncTimeoutError } from "./async/withTimeout.js";
6
+ function getStreamingTTSErrorDetails(error) {
7
+ const incrementalFailure = error instanceof IncrementalTTSSynthesisError ? error : undefined;
8
+ const cause = incrementalFailure?.firstError ?? error;
9
+ const safeMessage = sanitizeErrorCause(cause).message;
10
+ let detail;
11
+ if (cause instanceof AsyncTimeoutError) {
12
+ detail = {
13
+ code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
14
+ message: safeMessage,
15
+ retriable: true,
16
+ };
17
+ }
18
+ else if (cause instanceof NeuroLinkError) {
19
+ detail = {
20
+ code: cause.code,
21
+ message: safeMessage,
22
+ retriable: cause.retriable,
23
+ };
24
+ }
25
+ else {
26
+ detail = {
27
+ code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
28
+ message: safeMessage,
29
+ };
30
+ }
31
+ const failedSegments = incrementalFailure?.failedSegments;
32
+ const count = failedSegments?.length ?? 1;
33
+ const positions = failedSegments?.join(", ") ?? "unknown";
34
+ return {
35
+ ...detail,
36
+ message: `Incremental TTS failed for ${count} segment${count === 1 ? "" : "s"} (segment${count === 1 ? "" : "s"} ${positions}): ${detail.message}`,
37
+ };
38
+ }
39
+ class AsyncTextQueue {
40
+ values = [];
41
+ waiters = [];
42
+ ended = false;
43
+ failure;
44
+ [Symbol.asyncIterator]() {
45
+ return this;
46
+ }
47
+ next() {
48
+ if (this.values.length > 0) {
49
+ const value = this.values.shift();
50
+ if (value !== undefined) {
51
+ return Promise.resolve({ value, done: false });
52
+ }
53
+ }
54
+ if (this.failure !== undefined) {
55
+ return Promise.reject(this.failure);
56
+ }
57
+ if (this.ended) {
58
+ return Promise.resolve({ value: undefined, done: true });
59
+ }
60
+ return new Promise((resolve, reject) => {
61
+ this.waiters.push({ resolve, reject });
62
+ });
63
+ }
64
+ push(value) {
65
+ if (this.ended || this.failure !== undefined) {
66
+ return;
67
+ }
68
+ const waiter = this.waiters.shift();
69
+ if (waiter) {
70
+ waiter.resolve({ value, done: false });
71
+ return;
72
+ }
73
+ this.values.push(value);
74
+ }
75
+ end() {
76
+ if (this.ended || this.failure !== undefined) {
77
+ return;
78
+ }
79
+ this.ended = true;
80
+ for (const waiter of this.waiters.splice(0)) {
81
+ waiter.resolve({ value: undefined, done: true });
82
+ }
83
+ }
84
+ fail(error) {
85
+ if (this.ended || this.failure !== undefined) {
86
+ return;
87
+ }
88
+ this.failure = error;
89
+ for (const waiter of this.waiters.splice(0)) {
90
+ waiter.reject(error);
91
+ }
92
+ }
93
+ }
94
+ function sourceEvent(iterator) {
95
+ return iterator.next().then((result) => ({ kind: "source", result }), (error) => ({ kind: "source-error", error }));
96
+ }
97
+ function audioEvent(iterator) {
98
+ return iterator.next().then((result) => ({ kind: "audio", result }), (error) => ({ kind: "audio-error", error }));
99
+ }
100
+ function textFromChunk(chunk) {
101
+ if (chunk &&
102
+ typeof chunk === "object" &&
103
+ "content" in chunk &&
104
+ typeof chunk.content === "string" &&
105
+ chunk.content.length > 0) {
106
+ return chunk.content;
107
+ }
108
+ return undefined;
109
+ }
110
+ function aggregateTTSChunks(chunks) {
111
+ const last = chunks.at(-1);
112
+ if (!last) {
113
+ return undefined;
114
+ }
115
+ const buffer = Buffer.concat(chunks.map((chunk) => chunk.data));
116
+ return {
117
+ buffer,
118
+ format: last.format,
119
+ size: buffer.length,
120
+ duration: last.estimatedDuration,
121
+ voice: last.voice,
122
+ sampleRate: last.sampleRate,
123
+ };
124
+ }
125
+ /**
126
+ * Preserve source-stream backpressure while interleaving incremental TTS audio.
127
+ * Source chunks are yielded before their derived audio, and TTS failures degrade
128
+ * to the unchanged source stream.
129
+ */
130
+ export async function* interleaveTTSStream(params) {
131
+ const { stream, provider, options, onComplete } = params;
132
+ if (!TTSProcessor.supports(provider)) {
133
+ try {
134
+ for await (const chunk of stream) {
135
+ yield chunk;
136
+ }
137
+ }
138
+ finally {
139
+ onComplete?.(undefined);
140
+ }
141
+ return;
142
+ }
143
+ const textQueue = new AsyncTextQueue();
144
+ const sourceIterator = stream[Symbol.asyncIterator]();
145
+ let cancelled = false;
146
+ const audioIterator = TTSProcessor.synthesizeStream(textQueue, provider, options, () => cancelled)[Symbol.asyncIterator]();
147
+ const audioChunks = [];
148
+ let nextSource = sourceEvent(sourceIterator);
149
+ let nextAudio = audioEvent(audioIterator);
150
+ let completed = false;
151
+ let preferAudio = false;
152
+ try {
153
+ while (nextSource || nextAudio) {
154
+ const event = nextSource && nextAudio
155
+ ? await Promise.race(preferAudio ? [nextAudio, nextSource] : [nextSource, nextAudio])
156
+ : nextSource
157
+ ? await nextSource
158
+ : nextAudio
159
+ ? await nextAudio
160
+ : undefined;
161
+ if (!event) {
162
+ break;
163
+ }
164
+ if (event.kind === "source-error") {
165
+ textQueue.fail(event.error);
166
+ throw event.error;
167
+ }
168
+ if (event.kind === "audio-error") {
169
+ textQueue.end();
170
+ const error = getStreamingTTSErrorDetails(event.error);
171
+ logger.warn(`[TTSProcessor] Incremental stream disabled after an audio error: ${event.error instanceof Error
172
+ ? event.error.message
173
+ : String(event.error)}`);
174
+ nextAudio = undefined;
175
+ preferAudio = false;
176
+ completed = true;
177
+ onComplete?.(aggregateTTSChunks(audioChunks), error);
178
+ continue;
179
+ }
180
+ if (event.kind === "source") {
181
+ if (event.result.done) {
182
+ nextSource = undefined;
183
+ textQueue.end();
184
+ preferAudio = true;
185
+ continue;
186
+ }
187
+ const text = textFromChunk(event.result.value);
188
+ if (text) {
189
+ textQueue.push(text);
190
+ }
191
+ nextSource = sourceEvent(sourceIterator);
192
+ preferAudio = true;
193
+ yield event.result.value;
194
+ continue;
195
+ }
196
+ if (event.result.done) {
197
+ nextAudio = undefined;
198
+ completed = true;
199
+ onComplete?.(aggregateTTSChunks(audioChunks));
200
+ continue;
201
+ }
202
+ audioChunks.push(event.result.value);
203
+ nextAudio = audioEvent(audioIterator);
204
+ preferAudio = false;
205
+ yield { type: "tts_audio", audio: event.result.value };
206
+ }
207
+ }
208
+ finally {
209
+ if (!completed) {
210
+ cancelled = true;
211
+ }
212
+ textQueue.end();
213
+ const releases = [];
214
+ if (nextSource && sourceIterator.return) {
215
+ releases.push(Promise.resolve().then(() => sourceIterator.return?.(undefined)));
216
+ }
217
+ if (nextAudio && audioIterator.return) {
218
+ releases.push(Promise.resolve().then(() => audioIterator.return?.(undefined)));
219
+ }
220
+ await Promise.allSettled(releases);
221
+ if (!completed) {
222
+ onComplete?.(undefined);
223
+ }
224
+ }
225
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.25.4",
3
+ "version": "11.26.0",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {