@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
|
@@ -72,6 +72,11 @@ export declare abstract class BaseProvider implements AIProvider {
|
|
|
72
72
|
* that supplied `onError`).
|
|
73
73
|
*/
|
|
74
74
|
private fireLifecycleErrorCallback;
|
|
75
|
+
/**
|
|
76
|
+
* Build the fake-stream output and apply the same incremental TTS wrapper
|
|
77
|
+
* used by the standard NeuroLink stream path.
|
|
78
|
+
*/
|
|
79
|
+
private createFakeStreamingOutput;
|
|
75
80
|
/**
|
|
76
81
|
* Execute fake streaming - extracted method for reusability
|
|
77
82
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { context, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
2
2
|
import { directAgentTools } from "../agent/directTools.js";
|
|
3
|
-
import { isImageGenerationModel } from "
|
|
3
|
+
import { isImageGenerationModel } from "./constants.js";
|
|
4
4
|
import { MiddlewareFactory } from "../middleware/factory.js";
|
|
5
5
|
import { modelSupports } from "../models/modelRegistry.js";
|
|
6
6
|
import { ATTR, tracers } from "../telemetry/index.js";
|
|
@@ -13,6 +13,7 @@ import { duckTypedStatusCode, extractRetryAfterMsFromError, } from "../utils/pro
|
|
|
13
13
|
import { hasLifecycleErrorFired, markLifecycleErrorFired, } from "../utils/lifecycleCallbacks.js";
|
|
14
14
|
import { resolveLifecycleTimeoutMs } from "../utils/lifecycleTimeout.js";
|
|
15
15
|
import { logger } from "../utils/logger.js";
|
|
16
|
+
import { interleaveTTSStream } from "../utils/ttsStream.js";
|
|
16
17
|
import { TimeoutError as AsyncTimeoutError, withTimeoutFn, } from "../utils/async/withTimeout.js";
|
|
17
18
|
import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
|
|
18
19
|
import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
|
|
@@ -486,6 +487,61 @@ export class BaseProvider {
|
|
|
486
487
|
logger.warn("[lifecycle] onError callback error:", e);
|
|
487
488
|
}
|
|
488
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* Build the fake-stream output and apply the same incremental TTS wrapper
|
|
492
|
+
* used by the standard NeuroLink stream path.
|
|
493
|
+
*/
|
|
494
|
+
createFakeStreamingOutput(result, options, onTTSComplete) {
|
|
495
|
+
const incrementalTTS = options.tts?.enabled === true;
|
|
496
|
+
const source = (async function* () {
|
|
497
|
+
if (result?.content) {
|
|
498
|
+
const words = result.content.split(/(\s+)/);
|
|
499
|
+
let buffer = "";
|
|
500
|
+
for (let i = 0; i < words.length; i++) {
|
|
501
|
+
buffer += words[i];
|
|
502
|
+
const shouldYield = i === words.length - 1 ||
|
|
503
|
+
buffer.length > 50 ||
|
|
504
|
+
/[.!?;,]\s*$/.test(buffer);
|
|
505
|
+
if (shouldYield && buffer.trim()) {
|
|
506
|
+
yield { content: buffer };
|
|
507
|
+
buffer = "";
|
|
508
|
+
await new Promise((resolve) => {
|
|
509
|
+
setTimeout(resolve, Math.random() * 9 + 1);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (buffer.trim()) {
|
|
514
|
+
yield { content: buffer };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (result?.imageOutput) {
|
|
518
|
+
yield { type: "image", imageOutput: result.imageOutput };
|
|
519
|
+
}
|
|
520
|
+
if (result?.audio && !incrementalTTS) {
|
|
521
|
+
yield {
|
|
522
|
+
type: "tts_audio",
|
|
523
|
+
audio: {
|
|
524
|
+
data: result.audio.buffer,
|
|
525
|
+
format: result.audio.format,
|
|
526
|
+
index: 0,
|
|
527
|
+
isFinal: true,
|
|
528
|
+
cumulativeSize: result.audio.size,
|
|
529
|
+
voice: result.audio.voice,
|
|
530
|
+
sampleRate: result.audio.sampleRate,
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
})();
|
|
535
|
+
if (!incrementalTTS || !options.tts) {
|
|
536
|
+
return source;
|
|
537
|
+
}
|
|
538
|
+
return interleaveTTSStream({
|
|
539
|
+
stream: source,
|
|
540
|
+
provider: options.tts.provider ?? options.provider ?? this.providerName,
|
|
541
|
+
options: options.tts,
|
|
542
|
+
onComplete: onTTSComplete,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
489
545
|
/**
|
|
490
546
|
* Execute fake streaming - extracted method for reusability
|
|
491
547
|
*/
|
|
@@ -525,10 +581,10 @@ export class BaseProvider {
|
|
|
525
581
|
skipToolPromptInjection: options.skipToolPromptInjection,
|
|
526
582
|
timeout: options.timeout,
|
|
527
583
|
stt: options.stt,
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
tts: options.tts,
|
|
584
|
+
// Streaming TTS is synthesized incrementally by
|
|
585
|
+
// createFakeStreamingOutput; do not let generate() perform a duplicate
|
|
586
|
+
// input- or whole-response synthesis first.
|
|
587
|
+
tts: options.tts?.enabled ? undefined : options.tts,
|
|
532
588
|
};
|
|
533
589
|
logger.debug(`Calling generate for fake streaming`, {
|
|
534
590
|
provider: this.providerName,
|
|
@@ -545,58 +601,45 @@ export class BaseProvider {
|
|
|
545
601
|
hasImageOutput: !!result?.imageOutput,
|
|
546
602
|
timestamp: Date.now(),
|
|
547
603
|
});
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
604
|
+
const incrementalTTS = options.tts?.enabled === true;
|
|
605
|
+
const ttsProvider = options.tts?.provider ?? options.provider ?? this.providerName;
|
|
606
|
+
const ttsStartedAt = Date.now();
|
|
607
|
+
let resolveAudio;
|
|
608
|
+
let audioSettled = false;
|
|
609
|
+
const audio = incrementalTTS
|
|
610
|
+
? new Promise((resolve) => {
|
|
611
|
+
resolveAudio = resolve;
|
|
612
|
+
}).catch(() => undefined)
|
|
613
|
+
: undefined;
|
|
614
|
+
const ttsMetadata = incrementalTTS
|
|
615
|
+
? {
|
|
616
|
+
attempted: TTSProcessor.supports(ttsProvider),
|
|
617
|
+
success: false,
|
|
618
|
+
}
|
|
619
|
+
: result?.ttsMetadata;
|
|
620
|
+
const onTTSComplete = incrementalTTS
|
|
621
|
+
? (ttsResult, error) => {
|
|
622
|
+
if (audioSettled) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
audioSettled = true;
|
|
626
|
+
if (ttsMetadata) {
|
|
627
|
+
ttsMetadata.success =
|
|
628
|
+
error === undefined && ttsResult !== undefined;
|
|
629
|
+
if (error) {
|
|
630
|
+
ttsMetadata.error = error;
|
|
569
631
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
yield { content: buffer };
|
|
632
|
+
else {
|
|
633
|
+
delete ttsMetadata.error;
|
|
573
634
|
}
|
|
635
|
+
ttsMetadata.latency = Date.now() - ttsStartedAt;
|
|
574
636
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
582
|
-
// Yield synthesized audio so callers using stream() with tts.enabled
|
|
583
|
-
// still receive a tts_audio chunk on the fake-streaming fallback
|
|
584
|
-
// path (matches the discriminator used by the real streaming path).
|
|
585
|
-
if (result?.audio) {
|
|
586
|
-
yield {
|
|
587
|
-
type: "tts_audio",
|
|
588
|
-
audio: {
|
|
589
|
-
data: result.audio.buffer,
|
|
590
|
-
format: result.audio.format,
|
|
591
|
-
index: 0,
|
|
592
|
-
isFinal: true,
|
|
593
|
-
cumulativeSize: result.audio.size,
|
|
594
|
-
voice: result.audio.voice,
|
|
595
|
-
sampleRate: result.audio.sampleRate,
|
|
596
|
-
},
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
})(),
|
|
637
|
+
resolveAudio?.(ttsResult);
|
|
638
|
+
}
|
|
639
|
+
: undefined;
|
|
640
|
+
// Create a synthetic stream from the generate result that simulates progressive delivery
|
|
641
|
+
return {
|
|
642
|
+
stream: this.createFakeStreamingOutput(result, options, onTTSComplete),
|
|
600
643
|
usage: result?.usage,
|
|
601
644
|
provider: result?.provider,
|
|
602
645
|
model: result?.model,
|
|
@@ -618,6 +661,8 @@ export class BaseProvider {
|
|
|
618
661
|
// 🔧 FIX: Include analytics and evaluation from generate result
|
|
619
662
|
analytics: result?.analytics,
|
|
620
663
|
evaluation: result?.evaluation,
|
|
664
|
+
audio,
|
|
665
|
+
ttsMetadata,
|
|
621
666
|
};
|
|
622
667
|
}
|
|
623
668
|
catch (error) {
|
package/dist/neurolink.d.ts
CHANGED
|
@@ -945,7 +945,9 @@ export declare class NeuroLink {
|
|
|
945
945
|
*
|
|
946
946
|
* // Consume the stream
|
|
947
947
|
* for await (const chunk of result.stream) {
|
|
948
|
-
*
|
|
948
|
+
* if ("content" in chunk) {
|
|
949
|
+
* process.stdout.write(chunk.content);
|
|
950
|
+
* }
|
|
949
951
|
* }
|
|
950
952
|
*
|
|
951
953
|
* // Advanced streaming with options
|
|
@@ -1031,25 +1033,10 @@ export declare class NeuroLink {
|
|
|
1031
1033
|
private validateStreamRequestOptions;
|
|
1032
1034
|
private maybeHandleWorkflowStreamRequest;
|
|
1033
1035
|
private runStandardStreamRequest;
|
|
1034
|
-
/**
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
* stays under the max-lines-per-function lint budget. Behaviour preserved
|
|
1039
|
-
* exactly:
|
|
1040
|
-
* - When Mode 2 is enabled (`tts.enabled && tts.useAiResponse`) AND the
|
|
1041
|
-
* model produced non-empty content: synthesises one final audio buffer
|
|
1042
|
-
* and returns it as an `audioChunk` for the caller to `yield`. Resolves
|
|
1043
|
-
* `ttsResolver` with the `TTSResult`.
|
|
1044
|
-
* - When Mode 2 is enabled but synthesis fails: logs a warning and resolves
|
|
1045
|
-
* `ttsResolver` with `undefined`.
|
|
1046
|
-
* - When Mode 2 is requested but skipped (empty content / wrong mode):
|
|
1047
|
-
* resolves `ttsResolver` with `undefined` early so callers awaiting
|
|
1048
|
-
* `result.audio` unblock before the surrounding `finally` cleanup
|
|
1049
|
-
* completes (Issue 7 latency micro-opt — the finally block also resolves
|
|
1050
|
-
* defensively, so this is a redundant early signal, not a coverage fix).
|
|
1051
|
-
*/
|
|
1052
|
-
private synthesizeStreamModeTwo;
|
|
1036
|
+
/** Wrap one provider stream with incremental TTS synthesis. */
|
|
1037
|
+
private createIncrementalTTSStream;
|
|
1038
|
+
/** Prevent provider fallback streams from duplicating outer streaming TTS. */
|
|
1039
|
+
private deferProviderStreamTTS;
|
|
1053
1040
|
/**
|
|
1054
1041
|
* Prepare stream options: initialize memory, MCP, retrieval, orchestration,
|
|
1055
1042
|
* Ollama tool auto-disable, factory processing, and tool detection.
|
package/dist/neurolink.js
CHANGED
|
@@ -73,6 +73,7 @@ import { getConversationMessages, storeConversationTurn, } from "./utils/convers
|
|
|
73
73
|
// Enhanced error handling imports
|
|
74
74
|
import { CircuitBreaker, ERROR_CODES, ErrorFactory, isAbortError, isRetriableError, logStructuredError, NeuroLinkError, withRetry, withTimeout, } from "./utils/errorHandling.js";
|
|
75
75
|
import { hasLifecycleErrorFired, markLifecycleErrorFired, } from "./utils/lifecycleCallbacks.js";
|
|
76
|
+
import { interleaveTTSStream } from "./utils/ttsStream.js";
|
|
76
77
|
import { resolveLifecycleTimeoutMs } from "./utils/lifecycleTimeout.js";
|
|
77
78
|
import { cloneOptionsForCallIsolation } from "./utils/cloneOptions.js";
|
|
78
79
|
import { coerceJsonToSchema, recoverScalarRoot, schemaAccepts, } from "./utils/json/coerce.js";
|
|
@@ -6376,7 +6377,9 @@ Current user's request: ${currentInput}`;
|
|
|
6376
6377
|
*
|
|
6377
6378
|
* // Consume the stream
|
|
6378
6379
|
* for await (const chunk of result.stream) {
|
|
6379
|
-
*
|
|
6380
|
+
* if ("content" in chunk) {
|
|
6381
|
+
* process.stdout.write(chunk.content);
|
|
6382
|
+
* }
|
|
6380
6383
|
* }
|
|
6381
6384
|
*
|
|
6382
6385
|
* // Advanced streaming with options
|
|
@@ -6684,15 +6687,17 @@ Current user's request: ${currentInput}`;
|
|
|
6684
6687
|
(!!(options.tools && Object.keys(options.tools).length > 0) ||
|
|
6685
6688
|
this.getCustomTools().size > 0), !!(options.input?.images && options.input.images.length > 0), options
|
|
6686
6689
|
.thinking?.thinkingLevel);
|
|
6687
|
-
// TTS
|
|
6688
|
-
//
|
|
6689
|
-
//
|
|
6690
|
+
// Streaming TTS deferred: stream() always synthesizes the streamed AI
|
|
6691
|
+
// response when TTS is enabled, regardless of generate()'s
|
|
6692
|
+
// input-vs-response `useAiResponse` switch. It resolves
|
|
6693
|
+
// `streamResult.audio` with the aggregate TTSResult. The resolver is
|
|
6690
6694
|
// plumbed explicitly through the params bag (M11: previously a
|
|
6691
6695
|
// `_streamTtsResolve` cast on the caller's options object — fragile if
|
|
6692
6696
|
// the same options object was reused across concurrent stream() calls).
|
|
6693
6697
|
const ttsOptions = options.tts;
|
|
6694
|
-
const wantsStreamTtsMode2 =
|
|
6698
|
+
const wantsStreamTtsMode2 = ttsOptions?.enabled === true;
|
|
6695
6699
|
let resolveStreamTtsAudio;
|
|
6700
|
+
let streamTtsMetadata;
|
|
6696
6701
|
const streamTtsAudioPromise = wantsStreamTtsMode2
|
|
6697
6702
|
? new Promise((resolve) => {
|
|
6698
6703
|
resolveStreamTtsAudio = resolve;
|
|
@@ -6707,12 +6712,16 @@ Current user's request: ${currentInput}`;
|
|
|
6707
6712
|
streamId,
|
|
6708
6713
|
originalPrompt,
|
|
6709
6714
|
ttsResolver: resolveStreamTtsAudio,
|
|
6715
|
+
ttsMetadataSink: (metadata) => {
|
|
6716
|
+
streamTtsMetadata = metadata;
|
|
6717
|
+
},
|
|
6710
6718
|
})));
|
|
6711
6719
|
if (streamSttTranscription) {
|
|
6712
6720
|
streamResult.transcription = streamSttTranscription;
|
|
6713
6721
|
}
|
|
6714
6722
|
if (streamTtsAudioPromise) {
|
|
6715
6723
|
streamResult.audio = streamTtsAudioPromise;
|
|
6724
|
+
streamResult.ttsMetadata = streamTtsMetadata;
|
|
6716
6725
|
}
|
|
6717
6726
|
return streamResult;
|
|
6718
6727
|
}
|
|
@@ -7243,7 +7252,7 @@ Current user's request: ${currentInput}`;
|
|
|
7243
7252
|
return result;
|
|
7244
7253
|
}
|
|
7245
7254
|
async runStandardStreamRequest(params) {
|
|
7246
|
-
const { options, streamSpan, spanStartTime, startTime, hrTimeStart, streamId, originalPrompt, ttsResolver, } = params;
|
|
7255
|
+
const { options, streamSpan, spanStartTime, startTime, hrTimeStart, streamId, originalPrompt, ttsResolver, ttsMetadataSink, } = params;
|
|
7247
7256
|
logger.debug("[NeuroLink] Running standard stream request", {
|
|
7248
7257
|
streamId,
|
|
7249
7258
|
provider: options.provider,
|
|
@@ -7265,6 +7274,17 @@ Current user's request: ${currentInput}`;
|
|
|
7265
7274
|
sessionId: enhancedOptions.context?.sessionId,
|
|
7266
7275
|
});
|
|
7267
7276
|
const { stream: mcpStream, provider: providerName, usage: streamUsage, model: streamModel, finishReason: streamFinishReason, toolCalls: streamToolCalls, toolResults: streamToolResults, analytics: streamAnalytics, metadata: providerStreamMetadata, } = await this.createMCPStream(enhancedOptions);
|
|
7277
|
+
let streamedTTSResult;
|
|
7278
|
+
const { stream: incrementalStream, ttsMetadata: streamTtsMetadata } = await this.createIncrementalTTSStream({
|
|
7279
|
+
stream: mcpStream,
|
|
7280
|
+
ttsOptions: enhancedOptions.tts,
|
|
7281
|
+
providerName,
|
|
7282
|
+
fallbackProvider: enhancedOptions.provider,
|
|
7283
|
+
onComplete: (result) => {
|
|
7284
|
+
streamedTTSResult = result;
|
|
7285
|
+
},
|
|
7286
|
+
});
|
|
7287
|
+
ttsMetadataSink?.(streamTtsMetadata);
|
|
7268
7288
|
const streamState = {
|
|
7269
7289
|
finishReason: streamFinishReason ?? "stop",
|
|
7270
7290
|
toolCalls: streamToolCalls,
|
|
@@ -7313,7 +7333,7 @@ Current user's request: ${currentInput}`;
|
|
|
7313
7333
|
// NoOutputGeneratedError, and we want fallback to fire there.
|
|
7314
7334
|
let realOutputChunks = 0;
|
|
7315
7335
|
try {
|
|
7316
|
-
for await (const chunk of
|
|
7336
|
+
for await (const chunk of incrementalStream) {
|
|
7317
7337
|
chunkCount++;
|
|
7318
7338
|
const isNoOutputSentinel = chunk !== null &&
|
|
7319
7339
|
typeof chunk === "object" &&
|
|
@@ -7369,26 +7389,22 @@ Current user's request: ${currentInput}`;
|
|
|
7369
7389
|
!enhancedOptions.disableInternalFallback &&
|
|
7370
7390
|
streamState.toolCalls.length === 0 &&
|
|
7371
7391
|
streamState.toolResults.length === 0) {
|
|
7372
|
-
|
|
7392
|
+
const fallbackStream = self.handleStreamFallback(metadata, streamState, originalPrompt, enhancedOptions, providerName, (content) => {
|
|
7373
7393
|
accumulatedContent += content;
|
|
7374
7394
|
});
|
|
7395
|
+
const { stream: incrementalFallback } = await self.createIncrementalTTSStream({
|
|
7396
|
+
stream: fallbackStream,
|
|
7397
|
+
ttsOptions: enhancedOptions.tts,
|
|
7398
|
+
providerName,
|
|
7399
|
+
fallbackProvider: enhancedOptions.provider,
|
|
7400
|
+
ttsMetadata: streamTtsMetadata,
|
|
7401
|
+
onComplete: (result) => {
|
|
7402
|
+
streamedTTSResult = result;
|
|
7403
|
+
},
|
|
7404
|
+
});
|
|
7405
|
+
yield* incrementalFallback;
|
|
7375
7406
|
}
|
|
7376
|
-
|
|
7377
|
-
// and yield ONE final audio chunk so callers iterating the stream
|
|
7378
|
-
// get the audio inline; also resolve `streamResult.audio` so the
|
|
7379
|
-
// ergonomic `await result.audio` pattern works post-iteration.
|
|
7380
|
-
// m5: synthesis logic lives in a dedicated helper to keep this
|
|
7381
|
-
// generator under the max-lines-per-function lint budget.
|
|
7382
|
-
const ttsModeResult = await self.synthesizeStreamModeTwo({
|
|
7383
|
-
ttsOptions: enhancedOptions.tts,
|
|
7384
|
-
providerName,
|
|
7385
|
-
fallbackProvider: enhancedOptions.provider,
|
|
7386
|
-
accumulatedContent,
|
|
7387
|
-
ttsResolver,
|
|
7388
|
-
});
|
|
7389
|
-
if (ttsModeResult.audioChunk) {
|
|
7390
|
-
yield ttsModeResult.audioChunk;
|
|
7391
|
-
}
|
|
7407
|
+
ttsResolver?.(streamedTTSResult);
|
|
7392
7408
|
resolvedUsage = streamUsage;
|
|
7393
7409
|
if (!resolvedUsage && streamAnalytics) {
|
|
7394
7410
|
try {
|
|
@@ -7625,72 +7641,70 @@ Current user's request: ${currentInput}`;
|
|
|
7625
7641
|
});
|
|
7626
7642
|
}
|
|
7627
7643
|
catch (error) {
|
|
7644
|
+
ttsResolver?.(undefined);
|
|
7628
7645
|
if (options.disableInternalFallback) {
|
|
7629
7646
|
throw error;
|
|
7630
7647
|
}
|
|
7631
7648
|
return this.handleStreamError(error, options, startTime, streamId, undefined, undefined);
|
|
7632
7649
|
}
|
|
7633
7650
|
}
|
|
7634
|
-
/**
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
const
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
}
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
return {
|
|
7675
|
-
audioChunk: {
|
|
7676
|
-
type: "tts_audio",
|
|
7677
|
-
audio: {
|
|
7678
|
-
data: ttsResult.buffer,
|
|
7679
|
-
format: ttsResult.format,
|
|
7680
|
-
index: 0,
|
|
7681
|
-
isFinal: true,
|
|
7682
|
-
cumulativeSize: ttsResult.size,
|
|
7683
|
-
voice: ttsResult.voice,
|
|
7684
|
-
sampleRate: ttsResult.sampleRate,
|
|
7685
|
-
},
|
|
7686
|
-
},
|
|
7687
|
-
};
|
|
7651
|
+
/** Wrap one provider stream with incremental TTS synthesis. */
|
|
7652
|
+
async createIncrementalTTSStream(params) {
|
|
7653
|
+
const { stream, ttsOptions, providerName, fallbackProvider, onComplete } = params;
|
|
7654
|
+
if (!ttsOptions?.enabled) {
|
|
7655
|
+
onComplete(undefined);
|
|
7656
|
+
return { stream };
|
|
7657
|
+
}
|
|
7658
|
+
const { TTSProcessor } = await import("./utils/ttsProcessor.js");
|
|
7659
|
+
const concreteFallback = fallbackProvider === "auto" ? undefined : fallbackProvider;
|
|
7660
|
+
const candidate = ttsOptions.provider ?? concreteFallback ?? providerName;
|
|
7661
|
+
const ttsProvider = candidate && TTSProcessor.supports(candidate) ? candidate : undefined;
|
|
7662
|
+
const ttsMetadata = params.ttsMetadata ?? {
|
|
7663
|
+
attempted: ttsProvider !== undefined,
|
|
7664
|
+
success: false,
|
|
7665
|
+
};
|
|
7666
|
+
ttsMetadata.attempted = ttsProvider !== undefined;
|
|
7667
|
+
ttsMetadata.success = false;
|
|
7668
|
+
delete ttsMetadata.error;
|
|
7669
|
+
delete ttsMetadata.latency;
|
|
7670
|
+
const ttsStartedAt = Date.now();
|
|
7671
|
+
let completionRecorded = false;
|
|
7672
|
+
const recordCompletion = (result, error) => {
|
|
7673
|
+
if (completionRecorded) {
|
|
7674
|
+
return;
|
|
7675
|
+
}
|
|
7676
|
+
completionRecorded = true;
|
|
7677
|
+
ttsMetadata.success = error === undefined && result !== undefined;
|
|
7678
|
+
if (error) {
|
|
7679
|
+
ttsMetadata.error = error;
|
|
7680
|
+
}
|
|
7681
|
+
else {
|
|
7682
|
+
delete ttsMetadata.error;
|
|
7683
|
+
}
|
|
7684
|
+
ttsMetadata.latency = Date.now() - ttsStartedAt;
|
|
7685
|
+
onComplete(result);
|
|
7686
|
+
};
|
|
7687
|
+
if (!ttsProvider) {
|
|
7688
|
+
logger.warn(`[NeuroLink.stream] No TTS provider resolved for incremental streaming (set tts.provider explicitly — chat provider "${candidate ?? "<unset>"}" is not a registered TTS handler)`);
|
|
7689
|
+
recordCompletion(undefined);
|
|
7690
|
+
return { stream, ttsMetadata };
|
|
7688
7691
|
}
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7692
|
+
return {
|
|
7693
|
+
stream: interleaveTTSStream({
|
|
7694
|
+
stream,
|
|
7695
|
+
provider: ttsProvider,
|
|
7696
|
+
options: ttsOptions,
|
|
7697
|
+
onComplete: recordCompletion,
|
|
7698
|
+
}),
|
|
7699
|
+
ttsMetadata,
|
|
7700
|
+
};
|
|
7701
|
+
}
|
|
7702
|
+
/** Prevent provider fallback streams from duplicating outer streaming TTS. */
|
|
7703
|
+
deferProviderStreamTTS(options) {
|
|
7704
|
+
if (options.tts?.enabled) {
|
|
7705
|
+
return { ...options, tts: undefined };
|
|
7693
7706
|
}
|
|
7707
|
+
return options;
|
|
7694
7708
|
}
|
|
7695
7709
|
/**
|
|
7696
7710
|
* Prepare stream options: initialize memory, MCP, retrieval, orchestration,
|
|
@@ -7966,7 +7980,7 @@ Current user's request: ${currentInput}`;
|
|
|
7966
7980
|
context: enhancedOptions.context,
|
|
7967
7981
|
});
|
|
7968
7982
|
const fallbackResult = await fallbackProvider.stream({
|
|
7969
|
-
...enhancedOptions,
|
|
7983
|
+
...this.deferProviderStreamTTS(enhancedOptions),
|
|
7970
7984
|
model: fallbackRoute.model,
|
|
7971
7985
|
conversationMessages,
|
|
7972
7986
|
});
|
|
@@ -8395,7 +8409,7 @@ Current user's request: ${currentInput}`;
|
|
|
8395
8409
|
}),
|
|
8396
8410
|
}, "NeuroLink.createMCPStream");
|
|
8397
8411
|
const poolStreamResult = await poolStreamProvider.stream({
|
|
8398
|
-
...options,
|
|
8412
|
+
...this.deferProviderStreamTTS(options),
|
|
8399
8413
|
provider: poolStreamProviderName,
|
|
8400
8414
|
model: poolStreamModel,
|
|
8401
8415
|
region: poolStreamRegion,
|
|
@@ -8467,7 +8481,7 @@ Current user's request: ${currentInput}`;
|
|
|
8467
8481
|
// 🔧 FIX: Pass enhanced system prompt to real streaming
|
|
8468
8482
|
// Tools will be accessed through the streamText call in executeStream
|
|
8469
8483
|
const streamResult = await provider.stream({
|
|
8470
|
-
...options,
|
|
8484
|
+
...this.deferProviderStreamTTS(options),
|
|
8471
8485
|
systemPrompt: enhancedSystemPrompt, // Use enhanced prompt with tool descriptions
|
|
8472
8486
|
conversationMessages,
|
|
8473
8487
|
});
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { JsonValue, UnknownRecord } from "./common.js";
|
|
|
10
10
|
import type { Content, ImageWithAltText } from "./content.js";
|
|
11
11
|
import type { ChatMessage } from "./conversation.js";
|
|
12
12
|
import type { StreamNoOutputSentinel } from "./noOutputSentinel.js";
|
|
13
|
-
import type { AdditionalMemoryUser, GenerateStopReason, ToolExecutionCaptureOptions } from "./generate.js";
|
|
13
|
+
import type { AdditionalMemoryUser, GenerateStopReason, TTSMetadata, ToolExecutionCaptureOptions } from "./generate.js";
|
|
14
14
|
import type { AIModelProviderConfig, NeurolinkCredentials } from "./providers.js";
|
|
15
15
|
import type { TTSChunk, TTSOptions, TTSResult } from "./tts.js";
|
|
16
16
|
import type { STTOptions, STTResult } from "./stt.js";
|
|
@@ -169,6 +169,21 @@ export type StreamChunk = {
|
|
|
169
169
|
/** TTS audio chunk data */
|
|
170
170
|
audio: TTSChunk;
|
|
171
171
|
};
|
|
172
|
+
/** Provider-level chunks accepted by NeuroLink's core streaming pipeline. */
|
|
173
|
+
export type ProviderStreamChunk = {
|
|
174
|
+
content: string;
|
|
175
|
+
} | {
|
|
176
|
+
type: "audio";
|
|
177
|
+
audio: AudioChunk;
|
|
178
|
+
} | {
|
|
179
|
+
type: "tts_audio";
|
|
180
|
+
audio: TTSChunk;
|
|
181
|
+
} | {
|
|
182
|
+
type: "image";
|
|
183
|
+
imageOutput: {
|
|
184
|
+
base64: string;
|
|
185
|
+
};
|
|
186
|
+
};
|
|
172
187
|
export type StreamOptions = {
|
|
173
188
|
/**
|
|
174
189
|
* Opt this stream call into the knowledge grounding configured on the
|
|
@@ -699,13 +714,36 @@ export type StreamResult = {
|
|
|
699
714
|
/** STT transcription result (when stt option is used) */
|
|
700
715
|
transcription?: STTResult;
|
|
701
716
|
/**
|
|
702
|
-
* TTS
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
717
|
+
* Streaming TTS result (when `tts.enabled`). `stream()` synthesizes the AI
|
|
718
|
+
* response incrementally; `useAiResponse` continues to select input vs
|
|
719
|
+
* response synthesis for non-streaming generation.
|
|
720
|
+
* Resolves with the synthesized audio after the caller drains `stream` to
|
|
721
|
+
* completion; like other stream-final fields, it remains pending while the
|
|
722
|
+
* lazy stream is unconsumed. It resolves with the aggregate of whatever
|
|
723
|
+
* segments were synthesized: a synthesis failure part-way through still
|
|
724
|
+
* resolves with the earlier segments rather than discarding them. It resolves
|
|
725
|
+
* to undefined only when no segment was produced — TTS was not enabled, no
|
|
726
|
+
* handler resolved for the requested provider, the model stream errored, every
|
|
727
|
+
* synthesis failed, or the caller stopped draining `stream` before it ended
|
|
728
|
+
* (an abandoned stream settles undefined rather than a partial aggregate).
|
|
729
|
+
* Audio is also yielded incrementally as ordered
|
|
730
|
+
* `tts_audio` chunks. Each chunk, including the final one, contains only its
|
|
731
|
+
* own buffered segment. The aggregate is a byte concatenation of those
|
|
732
|
+
* independently synthesized segments, so what it is depends on the format's
|
|
733
|
+
* framing: for frame- or sample-stream formats (`mp3`, `mpeg`, `mpga`,
|
|
734
|
+
* `pcm16`) it is one playable stream; for header-bearing container formats
|
|
735
|
+
* (`wav`, `flac`, `m4a`, `mp4`, `webm`) it is not a valid file, because each
|
|
736
|
+
* segment carries its own header; for `ogg`/`opus` it is a chained stream that
|
|
737
|
+
* some decoders read only through its first segment. Use the individual chunk
|
|
738
|
+
* buffers when each segment must be a valid container file.
|
|
707
739
|
*/
|
|
708
740
|
audio?: Promise<TTSResult | undefined>;
|
|
741
|
+
/**
|
|
742
|
+
* Outcome metadata for streaming TTS synthesis. This is a mutable reference
|
|
743
|
+
* whose success and latency fields are finalized asynchronously; read it
|
|
744
|
+
* after draining `stream`.
|
|
745
|
+
*/
|
|
746
|
+
ttsMetadata?: TTSMetadata;
|
|
709
747
|
};
|
|
710
748
|
/**
|
|
711
749
|
* Enhanced provider type with stream method
|
package/dist/types/tts.d.ts
CHANGED
|
@@ -34,6 +34,9 @@ export type TTSOptions = {
|
|
|
34
34
|
/**
|
|
35
35
|
* Use the AI-generated response for TTS instead of the input text
|
|
36
36
|
*
|
|
37
|
+
* This switch applies to non-streaming generation. `stream()` always
|
|
38
|
+
* synthesizes the streamed AI response incrementally when TTS is enabled.
|
|
39
|
+
*
|
|
37
40
|
* When false or undefined (default): TTS will synthesize the input text/prompt directly without calling AI generation
|
|
38
41
|
* When true: TTS will synthesize the AI-generated response after generation completes
|
|
39
42
|
*
|
|
@@ -78,6 +81,12 @@ export type TTSOptions = {
|
|
|
78
81
|
play?: boolean;
|
|
79
82
|
/** Override TTS provider (e.g., "elevenlabs", "openai-tts", "azure-tts") */
|
|
80
83
|
provider?: TTSProviderName;
|
|
84
|
+
/**
|
|
85
|
+
* Minimum buffered text length before incremental stream synthesis flushes
|
|
86
|
+
* at a sentence boundary. The provider's maximum text length remains a hard
|
|
87
|
+
* upper bound. Defaults to 120 characters.
|
|
88
|
+
*/
|
|
89
|
+
streamingBufferSize?: number;
|
|
81
90
|
};
|
|
82
91
|
/**
|
|
83
92
|
* TTS audio result returned from generation
|
package/dist/types/tts.js
CHANGED
|
@@ -60,5 +60,11 @@ export function isValidTTSOptions(options) {
|
|
|
60
60
|
return false;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
if (opts.streamingBufferSize !== undefined) {
|
|
64
|
+
if (!Number.isInteger(opts.streamingBufferSize) ||
|
|
65
|
+
opts.streamingBufferSize < 1) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
63
69
|
return true;
|
|
64
70
|
}
|