@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.
- package/CHANGELOG.md +3 -5
- package/dist/browser/neurolink.min.js +388 -388
- package/dist/core/baseProvider.d.ts +5 -0
- package/dist/core/baseProvider.js +99 -54
- package/dist/neurolink.d.ts +7 -20
- package/dist/neurolink.js +99 -85
- package/dist/types/stream.d.ts +44 -6
- package/dist/types/tts.d.ts +9 -0
- package/dist/types/tts.js +6 -0
- package/dist/utils/ttsProcessor.d.ts +20 -1
- package/dist/utils/ttsProcessor.js +167 -0
- package/dist/utils/ttsStream.d.ts +15 -0
- package/dist/utils/ttsStream.js +225 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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": {
|