@juspay/neurolink 9.61.2 → 9.62.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 -0
- package/README.md +23 -17
- package/dist/adapters/tts/googleTTSHandler.js +1 -1
- package/dist/browser/neurolink.min.js +373 -355
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/voiceServer.d.ts +7 -0
- package/dist/cli/commands/voiceServer.js +9 -1
- package/dist/cli/factories/commandFactory.js +136 -11
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/utils/audioFileUtils.d.ts +3 -3
- package/dist/cli/utils/audioFileUtils.js +5 -1
- package/dist/core/baseProvider.js +29 -6
- package/dist/factories/providerRegistry.d.ts +14 -0
- package/dist/factories/providerRegistry.js +141 -2
- package/dist/lib/adapters/tts/googleTTSHandler.js +1 -1
- package/dist/lib/core/baseProvider.js +29 -6
- package/dist/lib/factories/providerRegistry.d.ts +14 -0
- package/dist/lib/factories/providerRegistry.js +141 -2
- package/dist/lib/neurolink.d.ts +19 -0
- package/dist/lib/neurolink.js +248 -12
- package/dist/lib/observability/exporters/laminarExporter.js +1 -0
- package/dist/lib/observability/exporters/posthogExporter.js +1 -0
- package/dist/lib/observability/utils/spanSerializer.js +1 -0
- package/dist/lib/server/voice/tokenCompare.d.ts +14 -0
- package/dist/lib/server/voice/tokenCompare.js +23 -0
- package/dist/lib/server/voice/voiceServerApp.js +62 -3
- package/dist/lib/server/voice/voiceWebSocketHandler.d.ts +20 -3
- package/dist/lib/server/voice/voiceWebSocketHandler.js +555 -435
- package/dist/lib/types/generate.d.ts +47 -0
- package/dist/lib/types/index.d.ts +1 -1
- package/dist/lib/types/index.js +1 -1
- package/dist/lib/types/realtime.d.ts +243 -0
- package/dist/lib/types/realtime.js +70 -0
- package/dist/lib/types/server.d.ts +68 -0
- package/dist/lib/types/span.d.ts +2 -0
- package/dist/lib/types/span.js +2 -0
- package/dist/lib/types/stream.d.ts +36 -14
- package/dist/lib/types/stt.d.ts +585 -0
- package/dist/lib/types/stt.js +90 -0
- package/dist/lib/types/tts.d.ts +23 -11
- package/dist/lib/types/tts.js +7 -0
- package/dist/lib/types/voice.d.ts +272 -0
- package/dist/lib/types/voice.js +137 -0
- package/dist/lib/utils/audioFormatDetector.d.ts +15 -0
- package/dist/lib/utils/audioFormatDetector.js +34 -0
- package/dist/lib/utils/sttProcessor.d.ts +115 -0
- package/dist/lib/utils/sttProcessor.js +295 -0
- package/dist/lib/voice/RealtimeVoiceAPI.d.ts +183 -0
- package/dist/lib/voice/RealtimeVoiceAPI.js +439 -0
- package/dist/lib/voice/audio-utils.d.ts +135 -0
- package/dist/lib/voice/audio-utils.js +435 -0
- package/dist/lib/voice/errors.d.ts +123 -0
- package/dist/lib/voice/errors.js +386 -0
- package/dist/lib/voice/index.d.ts +26 -0
- package/dist/lib/voice/index.js +55 -0
- package/dist/lib/voice/providers/AzureSTT.d.ts +47 -0
- package/dist/lib/voice/providers/AzureSTT.js +345 -0
- package/dist/lib/voice/providers/AzureTTS.d.ts +59 -0
- package/dist/lib/voice/providers/AzureTTS.js +349 -0
- package/dist/lib/voice/providers/DeepgramSTT.d.ts +40 -0
- package/dist/lib/voice/providers/DeepgramSTT.js +550 -0
- package/dist/lib/voice/providers/ElevenLabsTTS.d.ts +53 -0
- package/dist/lib/voice/providers/ElevenLabsTTS.js +311 -0
- package/dist/lib/voice/providers/GeminiLive.d.ts +52 -0
- package/dist/lib/voice/providers/GeminiLive.js +372 -0
- package/dist/lib/voice/providers/GoogleSTT.d.ts +60 -0
- package/dist/lib/voice/providers/GoogleSTT.js +454 -0
- package/dist/lib/voice/providers/OpenAIRealtime.d.ts +47 -0
- package/dist/lib/voice/providers/OpenAIRealtime.js +412 -0
- package/dist/lib/voice/providers/OpenAISTT.d.ts +41 -0
- package/dist/lib/voice/providers/OpenAISTT.js +286 -0
- package/dist/lib/voice/providers/OpenAITTS.d.ts +49 -0
- package/dist/lib/voice/providers/OpenAITTS.js +271 -0
- package/dist/lib/voice/stream-handler.d.ts +166 -0
- package/dist/lib/voice/stream-handler.js +514 -0
- package/dist/neurolink.d.ts +19 -0
- package/dist/neurolink.js +248 -12
- package/dist/observability/exporters/laminarExporter.js +1 -0
- package/dist/observability/exporters/posthogExporter.js +1 -0
- package/dist/observability/utils/spanSerializer.js +1 -0
- package/dist/server/voice/tokenCompare.d.ts +14 -0
- package/dist/server/voice/tokenCompare.js +22 -0
- package/dist/server/voice/voiceServerApp.js +62 -3
- package/dist/server/voice/voiceWebSocketHandler.d.ts +20 -3
- package/dist/server/voice/voiceWebSocketHandler.js +555 -435
- package/dist/types/generate.d.ts +47 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/realtime.d.ts +243 -0
- package/dist/types/realtime.js +69 -0
- package/dist/types/server.d.ts +68 -0
- package/dist/types/span.d.ts +2 -0
- package/dist/types/span.js +2 -0
- package/dist/types/stream.d.ts +36 -14
- package/dist/types/stt.d.ts +585 -0
- package/dist/types/stt.js +89 -0
- package/dist/types/tts.d.ts +23 -11
- package/dist/types/tts.js +7 -0
- package/dist/types/voice.d.ts +272 -0
- package/dist/types/voice.js +136 -0
- package/dist/utils/audioFormatDetector.d.ts +15 -0
- package/dist/utils/audioFormatDetector.js +33 -0
- package/dist/utils/sttProcessor.d.ts +115 -0
- package/dist/utils/sttProcessor.js +294 -0
- package/dist/voice/RealtimeVoiceAPI.d.ts +183 -0
- package/dist/voice/RealtimeVoiceAPI.js +438 -0
- package/dist/voice/audio-utils.d.ts +135 -0
- package/dist/voice/audio-utils.js +434 -0
- package/dist/voice/errors.d.ts +123 -0
- package/dist/voice/errors.js +385 -0
- package/dist/voice/index.d.ts +26 -0
- package/dist/voice/index.js +54 -0
- package/dist/voice/providers/AzureSTT.d.ts +47 -0
- package/dist/voice/providers/AzureSTT.js +344 -0
- package/dist/voice/providers/AzureTTS.d.ts +59 -0
- package/dist/voice/providers/AzureTTS.js +348 -0
- package/dist/voice/providers/DeepgramSTT.d.ts +40 -0
- package/dist/voice/providers/DeepgramSTT.js +549 -0
- package/dist/voice/providers/ElevenLabsTTS.d.ts +53 -0
- package/dist/voice/providers/ElevenLabsTTS.js +310 -0
- package/dist/voice/providers/GeminiLive.d.ts +52 -0
- package/dist/voice/providers/GeminiLive.js +371 -0
- package/dist/voice/providers/GoogleSTT.d.ts +60 -0
- package/dist/voice/providers/GoogleSTT.js +453 -0
- package/dist/voice/providers/OpenAIRealtime.d.ts +47 -0
- package/dist/voice/providers/OpenAIRealtime.js +411 -0
- package/dist/voice/providers/OpenAISTT.d.ts +41 -0
- package/dist/voice/providers/OpenAISTT.js +285 -0
- package/dist/voice/providers/OpenAITTS.d.ts +49 -0
- package/dist/voice/providers/OpenAITTS.js +270 -0
- package/dist/voice/stream-handler.d.ts +166 -0
- package/dist/voice/stream-handler.js +513 -0
- package/package.json +3 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream Handler for Voice Module
|
|
3
|
+
*
|
|
4
|
+
* Provides audio stream chunking, backpressure handling, and stream coordination.
|
|
5
|
+
*
|
|
6
|
+
* @module voice/stream-handler
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from "events";
|
|
9
|
+
import type { AudioStreamChunk, StreamHandlerConfig } from "../types/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* Chunked Audio Stream Handler
|
|
12
|
+
*
|
|
13
|
+
* Handles audio stream chunking with backpressure management.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const handler = new ChunkedAudioStream({
|
|
18
|
+
* chunkDurationMs: 100,
|
|
19
|
+
* sampleRate: 16000,
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* handler.on('chunk', (chunk) => {
|
|
23
|
+
* // Process audio chunk
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* handler.write(audioData);
|
|
27
|
+
* handler.end();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare class ChunkedAudioStream extends EventEmitter {
|
|
31
|
+
private readonly config;
|
|
32
|
+
private readonly chunkSize;
|
|
33
|
+
private buffer;
|
|
34
|
+
private chunkIndex;
|
|
35
|
+
private timestampMs;
|
|
36
|
+
private isPaused;
|
|
37
|
+
private isEnded;
|
|
38
|
+
private pendingData;
|
|
39
|
+
private bufferTimeout;
|
|
40
|
+
constructor(config?: StreamHandlerConfig);
|
|
41
|
+
/**
|
|
42
|
+
* Write audio data to the stream
|
|
43
|
+
*
|
|
44
|
+
* @param data - Audio data buffer
|
|
45
|
+
* @returns True if more data can be written, false if backpressure
|
|
46
|
+
*/
|
|
47
|
+
write(data: Buffer): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Process incoming data
|
|
50
|
+
*/
|
|
51
|
+
private processData;
|
|
52
|
+
/**
|
|
53
|
+
* End the stream
|
|
54
|
+
*/
|
|
55
|
+
end(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Reset buffer timeout
|
|
58
|
+
*/
|
|
59
|
+
private resetBufferTimeout;
|
|
60
|
+
/**
|
|
61
|
+
* Clear buffer timeout
|
|
62
|
+
*/
|
|
63
|
+
private clearBufferTimeout;
|
|
64
|
+
/**
|
|
65
|
+
* Cleanup resources
|
|
66
|
+
*/
|
|
67
|
+
private cleanup;
|
|
68
|
+
/**
|
|
69
|
+
* Get stream statistics
|
|
70
|
+
*/
|
|
71
|
+
getStats(): {
|
|
72
|
+
chunksEmitted: number;
|
|
73
|
+
bufferedBytes: number;
|
|
74
|
+
pendingChunks: number;
|
|
75
|
+
totalDurationMs: number;
|
|
76
|
+
isPaused: boolean;
|
|
77
|
+
isEnded: boolean;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Stream merger for combining multiple audio streams
|
|
82
|
+
*/
|
|
83
|
+
export declare class StreamMerger extends EventEmitter {
|
|
84
|
+
private readonly streams;
|
|
85
|
+
private readonly config;
|
|
86
|
+
constructor(config?: StreamHandlerConfig);
|
|
87
|
+
/**
|
|
88
|
+
* Add a stream to merge
|
|
89
|
+
*
|
|
90
|
+
* @param id - Stream identifier
|
|
91
|
+
* @returns The created stream
|
|
92
|
+
*/
|
|
93
|
+
addStream(id: string): ChunkedAudioStream;
|
|
94
|
+
/**
|
|
95
|
+
* Remove a stream
|
|
96
|
+
*
|
|
97
|
+
* @param id - Stream identifier
|
|
98
|
+
*/
|
|
99
|
+
removeStream(id: string): void;
|
|
100
|
+
/**
|
|
101
|
+
* Write to a specific stream
|
|
102
|
+
*
|
|
103
|
+
* @param id - Stream identifier
|
|
104
|
+
* @param data - Audio data
|
|
105
|
+
*/
|
|
106
|
+
write(id: string, data: Buffer): boolean;
|
|
107
|
+
/**
|
|
108
|
+
* End all streams
|
|
109
|
+
*/
|
|
110
|
+
endAll(): void;
|
|
111
|
+
/**
|
|
112
|
+
* Get number of active streams
|
|
113
|
+
*/
|
|
114
|
+
get activeStreams(): number;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Stream splitter for distributing audio to multiple consumers
|
|
118
|
+
*/
|
|
119
|
+
export declare class StreamSplitter extends EventEmitter {
|
|
120
|
+
private readonly consumers;
|
|
121
|
+
private readonly input;
|
|
122
|
+
constructor(config?: StreamHandlerConfig);
|
|
123
|
+
/**
|
|
124
|
+
* Write audio data
|
|
125
|
+
*
|
|
126
|
+
* @param data - Audio data buffer
|
|
127
|
+
*/
|
|
128
|
+
write(data: Buffer): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* End the stream
|
|
131
|
+
*/
|
|
132
|
+
end(): void;
|
|
133
|
+
/**
|
|
134
|
+
* Add a consumer
|
|
135
|
+
*
|
|
136
|
+
* @param id - Consumer identifier
|
|
137
|
+
* @param handler - Chunk handler function
|
|
138
|
+
*/
|
|
139
|
+
addConsumer(id: string, handler: (chunk: AudioStreamChunk) => void): void;
|
|
140
|
+
/**
|
|
141
|
+
* Remove a consumer
|
|
142
|
+
*
|
|
143
|
+
* @param id - Consumer identifier
|
|
144
|
+
*/
|
|
145
|
+
removeConsumer(id: string): void;
|
|
146
|
+
/**
|
|
147
|
+
* Get number of consumers
|
|
148
|
+
*/
|
|
149
|
+
get consumerCount(): number;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create an async iterable from a chunked audio stream
|
|
153
|
+
*
|
|
154
|
+
* @param stream - Chunked audio stream
|
|
155
|
+
* @returns Async iterable of audio chunks
|
|
156
|
+
*/
|
|
157
|
+
export declare function streamToAsyncIterable(stream: ChunkedAudioStream): AsyncIterable<AudioStreamChunk>;
|
|
158
|
+
/**
|
|
159
|
+
* Create a chunked audio stream from an async iterable
|
|
160
|
+
*
|
|
161
|
+
* @param iterable - Async iterable of audio buffers
|
|
162
|
+
* @param config - Stream configuration
|
|
163
|
+
* @returns Chunked audio stream
|
|
164
|
+
*/
|
|
165
|
+
export declare function asyncIterableToStream(iterable: AsyncIterable<Buffer>, config?: StreamHandlerConfig): Promise<ChunkedAudioStream>;
|
|
166
|
+
export { ChunkedAudioStream as StreamHandler };
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream Handler for Voice Module
|
|
3
|
+
*
|
|
4
|
+
* Provides audio stream chunking, backpressure handling, and stream coordination.
|
|
5
|
+
*
|
|
6
|
+
* @module voice/stream-handler
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from "events";
|
|
9
|
+
import { logger } from "../utils/logger.js";
|
|
10
|
+
/**
|
|
11
|
+
* Default configuration
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_CONFIG = {
|
|
14
|
+
chunkDurationMs: 100, // 100ms chunks
|
|
15
|
+
sampleRate: 16000,
|
|
16
|
+
bytesPerSample: 2, // 16-bit mono
|
|
17
|
+
format: "wav",
|
|
18
|
+
highWaterMark: 64 * 1024, // 64KB
|
|
19
|
+
bufferTimeoutMs: 5000, // 5 seconds
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Chunked Audio Stream Handler
|
|
23
|
+
*
|
|
24
|
+
* Handles audio stream chunking with backpressure management.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const handler = new ChunkedAudioStream({
|
|
29
|
+
* chunkDurationMs: 100,
|
|
30
|
+
* sampleRate: 16000,
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* handler.on('chunk', (chunk) => {
|
|
34
|
+
* // Process audio chunk
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* handler.write(audioData);
|
|
38
|
+
* handler.end();
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export class ChunkedAudioStream extends EventEmitter {
|
|
42
|
+
config;
|
|
43
|
+
chunkSize;
|
|
44
|
+
buffer;
|
|
45
|
+
chunkIndex;
|
|
46
|
+
timestampMs;
|
|
47
|
+
isPaused;
|
|
48
|
+
isEnded;
|
|
49
|
+
pendingData;
|
|
50
|
+
bufferTimeout;
|
|
51
|
+
constructor(config = {}) {
|
|
52
|
+
super();
|
|
53
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
54
|
+
if (this.config.sampleRate <= 0) {
|
|
55
|
+
throw new Error("Invalid stream configuration: sampleRate must be positive");
|
|
56
|
+
}
|
|
57
|
+
if (this.config.bytesPerSample <= 0) {
|
|
58
|
+
throw new Error("Invalid stream configuration: bytesPerSample must be positive");
|
|
59
|
+
}
|
|
60
|
+
// Calculate chunk size based on duration
|
|
61
|
+
const bytesPerMs = (this.config.sampleRate * this.config.bytesPerSample) / 1000;
|
|
62
|
+
this.chunkSize = Math.round(this.config.chunkDurationMs * bytesPerMs);
|
|
63
|
+
if (this.chunkSize <= 0) {
|
|
64
|
+
throw new Error("Invalid stream configuration: chunkSize must be positive (check chunkDurationMs, sampleRate, bytesPerSample)");
|
|
65
|
+
}
|
|
66
|
+
this.buffer = Buffer.alloc(0);
|
|
67
|
+
this.chunkIndex = 0;
|
|
68
|
+
this.timestampMs = 0;
|
|
69
|
+
this.isPaused = false;
|
|
70
|
+
this.isEnded = false;
|
|
71
|
+
this.pendingData = [];
|
|
72
|
+
this.bufferTimeout = null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Write audio data to the stream
|
|
76
|
+
*
|
|
77
|
+
* @param data - Audio data buffer
|
|
78
|
+
* @returns True if more data can be written, false if backpressure
|
|
79
|
+
*/
|
|
80
|
+
write(data) {
|
|
81
|
+
if (this.isEnded) {
|
|
82
|
+
throw new Error("Cannot write to ended stream");
|
|
83
|
+
}
|
|
84
|
+
// Decide backpressure state BEFORE processing so the producer can rely on
|
|
85
|
+
// the returned flag without depending on a follow-up `write()` to flip it.
|
|
86
|
+
// Also queue the chunk into pendingData when paused — `processData()` is
|
|
87
|
+
// what eventually triggers `drain` via the `resume`/`drain` path, so it
|
|
88
|
+
// must run after the resume condition is met (CodeRabbit review:
|
|
89
|
+
// previously, processing first could emit `drain` synchronously inside
|
|
90
|
+
// `processData()` if buffer drained below high-water mid-call, and a
|
|
91
|
+
// producer that waited for `drain` after seeing `false` could deadlock).
|
|
92
|
+
const willOverflow = this.buffer.length + data.length > this.config.highWaterMark;
|
|
93
|
+
if (willOverflow) {
|
|
94
|
+
if (!this.isPaused) {
|
|
95
|
+
this.isPaused = true;
|
|
96
|
+
this.emit("pause");
|
|
97
|
+
}
|
|
98
|
+
this.pendingData.push(data);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
this.processData(data);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Process incoming data
|
|
106
|
+
*/
|
|
107
|
+
processData(data) {
|
|
108
|
+
// Append to buffer
|
|
109
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
110
|
+
// Reset buffer timeout
|
|
111
|
+
this.resetBufferTimeout();
|
|
112
|
+
// Emit chunks while we have enough data
|
|
113
|
+
while (this.buffer.length >= this.chunkSize) {
|
|
114
|
+
const chunkData = this.buffer.subarray(0, this.chunkSize);
|
|
115
|
+
this.buffer = this.buffer.subarray(this.chunkSize);
|
|
116
|
+
const chunk = {
|
|
117
|
+
data: chunkData,
|
|
118
|
+
index: this.chunkIndex++,
|
|
119
|
+
isFinal: false,
|
|
120
|
+
format: this.config.format,
|
|
121
|
+
sampleRate: this.config.sampleRate,
|
|
122
|
+
timestampMs: this.timestampMs,
|
|
123
|
+
durationMs: this.config.chunkDurationMs,
|
|
124
|
+
};
|
|
125
|
+
this.timestampMs += this.config.chunkDurationMs;
|
|
126
|
+
this.emit("chunk", chunk);
|
|
127
|
+
}
|
|
128
|
+
// Process pending data if backpressure released
|
|
129
|
+
if (this.isPaused && this.buffer.length < this.config.highWaterMark / 2) {
|
|
130
|
+
this.isPaused = false;
|
|
131
|
+
this.emit("resume");
|
|
132
|
+
this.emit("drain");
|
|
133
|
+
// Process pending data. `shift()` returns `undefined` only when the
|
|
134
|
+
// array is empty, which the loop guard already rules out — but check
|
|
135
|
+
// explicitly so we don't carry a non-null assertion (Issue 9).
|
|
136
|
+
while (this.pendingData.length > 0 && !this.isPaused) {
|
|
137
|
+
const pending = this.pendingData.shift();
|
|
138
|
+
if (pending === undefined) {
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
if (!this.write(pending)) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* End the stream
|
|
149
|
+
*/
|
|
150
|
+
end() {
|
|
151
|
+
if (this.isEnded) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.isEnded = true;
|
|
155
|
+
this.clearBufferTimeout();
|
|
156
|
+
// Drain any pending data that was buffered during backpressure
|
|
157
|
+
for (const pending of this.pendingData) {
|
|
158
|
+
this.buffer = Buffer.concat([this.buffer, pending]);
|
|
159
|
+
}
|
|
160
|
+
this.pendingData = [];
|
|
161
|
+
// Emit final chunk with remaining data
|
|
162
|
+
if (this.buffer.length > 0) {
|
|
163
|
+
const durationMs = (this.buffer.length /
|
|
164
|
+
this.config.bytesPerSample /
|
|
165
|
+
this.config.sampleRate) *
|
|
166
|
+
1000;
|
|
167
|
+
const chunk = {
|
|
168
|
+
data: this.buffer,
|
|
169
|
+
index: this.chunkIndex++,
|
|
170
|
+
isFinal: true,
|
|
171
|
+
format: this.config.format,
|
|
172
|
+
sampleRate: this.config.sampleRate,
|
|
173
|
+
timestampMs: this.timestampMs,
|
|
174
|
+
durationMs,
|
|
175
|
+
};
|
|
176
|
+
this.emit("chunk", chunk);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// Emit empty final chunk to signal end
|
|
180
|
+
const chunk = {
|
|
181
|
+
data: Buffer.alloc(0),
|
|
182
|
+
index: this.chunkIndex,
|
|
183
|
+
isFinal: true,
|
|
184
|
+
format: this.config.format,
|
|
185
|
+
sampleRate: this.config.sampleRate,
|
|
186
|
+
timestampMs: this.timestampMs,
|
|
187
|
+
durationMs: 0,
|
|
188
|
+
};
|
|
189
|
+
this.emit("chunk", chunk);
|
|
190
|
+
}
|
|
191
|
+
this.emit("end");
|
|
192
|
+
this.cleanup();
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Reset buffer timeout
|
|
196
|
+
*/
|
|
197
|
+
resetBufferTimeout() {
|
|
198
|
+
this.clearBufferTimeout();
|
|
199
|
+
this.bufferTimeout = setTimeout(() => {
|
|
200
|
+
if (this.buffer.length > 0 && !this.isEnded) {
|
|
201
|
+
logger.warn(`[ChunkedAudioStream] Buffer timeout, forcing flush of ${this.buffer.length} bytes`);
|
|
202
|
+
this.end();
|
|
203
|
+
}
|
|
204
|
+
}, this.config.bufferTimeoutMs);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Clear buffer timeout
|
|
208
|
+
*/
|
|
209
|
+
clearBufferTimeout() {
|
|
210
|
+
if (this.bufferTimeout) {
|
|
211
|
+
clearTimeout(this.bufferTimeout);
|
|
212
|
+
this.bufferTimeout = null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Cleanup resources
|
|
217
|
+
*/
|
|
218
|
+
cleanup() {
|
|
219
|
+
this.clearBufferTimeout();
|
|
220
|
+
this.buffer = Buffer.alloc(0);
|
|
221
|
+
this.pendingData = [];
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Get stream statistics
|
|
225
|
+
*/
|
|
226
|
+
getStats() {
|
|
227
|
+
return {
|
|
228
|
+
chunksEmitted: this.chunkIndex,
|
|
229
|
+
bufferedBytes: this.buffer.length,
|
|
230
|
+
pendingChunks: this.pendingData.length,
|
|
231
|
+
totalDurationMs: this.timestampMs,
|
|
232
|
+
isPaused: this.isPaused,
|
|
233
|
+
isEnded: this.isEnded,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Stream merger for combining multiple audio streams
|
|
239
|
+
*/
|
|
240
|
+
export class StreamMerger extends EventEmitter {
|
|
241
|
+
streams;
|
|
242
|
+
config;
|
|
243
|
+
constructor(config = {}) {
|
|
244
|
+
super();
|
|
245
|
+
this.streams = new Map();
|
|
246
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Add a stream to merge
|
|
250
|
+
*
|
|
251
|
+
* @param id - Stream identifier
|
|
252
|
+
* @returns The created stream
|
|
253
|
+
*/
|
|
254
|
+
addStream(id) {
|
|
255
|
+
if (this.streams.has(id)) {
|
|
256
|
+
throw new Error(`Stream ${id} already exists`);
|
|
257
|
+
}
|
|
258
|
+
const stream = new ChunkedAudioStream(this.config);
|
|
259
|
+
stream.on("chunk", (chunk) => {
|
|
260
|
+
this.emit("chunk", { id, chunk });
|
|
261
|
+
});
|
|
262
|
+
stream.on("end", () => {
|
|
263
|
+
this.emit("streamEnd", id);
|
|
264
|
+
this.streams.delete(id);
|
|
265
|
+
if (this.streams.size === 0) {
|
|
266
|
+
this.emit("end");
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
stream.on("error", (error) => {
|
|
270
|
+
this.emit("error", { id, error });
|
|
271
|
+
});
|
|
272
|
+
this.streams.set(id, stream);
|
|
273
|
+
return stream;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Remove a stream
|
|
277
|
+
*
|
|
278
|
+
* @param id - Stream identifier
|
|
279
|
+
*/
|
|
280
|
+
removeStream(id) {
|
|
281
|
+
const stream = this.streams.get(id);
|
|
282
|
+
if (stream) {
|
|
283
|
+
stream.end();
|
|
284
|
+
this.streams.delete(id);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Write to a specific stream
|
|
289
|
+
*
|
|
290
|
+
* @param id - Stream identifier
|
|
291
|
+
* @param data - Audio data
|
|
292
|
+
*/
|
|
293
|
+
write(id, data) {
|
|
294
|
+
const stream = this.streams.get(id);
|
|
295
|
+
if (!stream) {
|
|
296
|
+
throw new Error(`Stream ${id} not found`);
|
|
297
|
+
}
|
|
298
|
+
return stream.write(data);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* End all streams
|
|
302
|
+
*/
|
|
303
|
+
endAll() {
|
|
304
|
+
for (const stream of this.streams.values()) {
|
|
305
|
+
stream.end();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Get number of active streams
|
|
310
|
+
*/
|
|
311
|
+
get activeStreams() {
|
|
312
|
+
return this.streams.size;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Stream splitter for distributing audio to multiple consumers
|
|
317
|
+
*/
|
|
318
|
+
export class StreamSplitter extends EventEmitter {
|
|
319
|
+
consumers;
|
|
320
|
+
input;
|
|
321
|
+
constructor(config = {}) {
|
|
322
|
+
super();
|
|
323
|
+
this.consumers = new Map();
|
|
324
|
+
this.input = new ChunkedAudioStream(config);
|
|
325
|
+
this.input.on("chunk", (chunk) => {
|
|
326
|
+
for (const [id, consumer] of this.consumers) {
|
|
327
|
+
try {
|
|
328
|
+
consumer(chunk);
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
this.emit("error", {
|
|
332
|
+
consumerId: id,
|
|
333
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
this.input.on("end", () => {
|
|
339
|
+
this.emit("end");
|
|
340
|
+
});
|
|
341
|
+
this.input.on("error", (error) => {
|
|
342
|
+
this.emit("error", { error });
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Write audio data
|
|
347
|
+
*
|
|
348
|
+
* @param data - Audio data buffer
|
|
349
|
+
*/
|
|
350
|
+
write(data) {
|
|
351
|
+
return this.input.write(data);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* End the stream
|
|
355
|
+
*/
|
|
356
|
+
end() {
|
|
357
|
+
this.input.end();
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Add a consumer
|
|
361
|
+
*
|
|
362
|
+
* @param id - Consumer identifier
|
|
363
|
+
* @param handler - Chunk handler function
|
|
364
|
+
*/
|
|
365
|
+
addConsumer(id, handler) {
|
|
366
|
+
if (this.consumers.has(id)) {
|
|
367
|
+
throw new Error(`Consumer ${id} already exists`);
|
|
368
|
+
}
|
|
369
|
+
this.consumers.set(id, handler);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Remove a consumer
|
|
373
|
+
*
|
|
374
|
+
* @param id - Consumer identifier
|
|
375
|
+
*/
|
|
376
|
+
removeConsumer(id) {
|
|
377
|
+
this.consumers.delete(id);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Get number of consumers
|
|
381
|
+
*/
|
|
382
|
+
get consumerCount() {
|
|
383
|
+
return this.consumers.size;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Create an async iterable from a chunked audio stream
|
|
388
|
+
*
|
|
389
|
+
* @param stream - Chunked audio stream
|
|
390
|
+
* @returns Async iterable of audio chunks
|
|
391
|
+
*/
|
|
392
|
+
export function streamToAsyncIterable(stream) {
|
|
393
|
+
return {
|
|
394
|
+
[Symbol.asyncIterator]() {
|
|
395
|
+
const queue = [];
|
|
396
|
+
let resolveNext = null;
|
|
397
|
+
let rejectNext = null;
|
|
398
|
+
let done = false;
|
|
399
|
+
let error = null;
|
|
400
|
+
const onChunk = (chunk) => {
|
|
401
|
+
if (resolveNext) {
|
|
402
|
+
resolveNext({ value: chunk, done: false });
|
|
403
|
+
resolveNext = null;
|
|
404
|
+
rejectNext = null;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
queue.push(chunk);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const onEnd = () => {
|
|
411
|
+
done = true;
|
|
412
|
+
if (resolveNext) {
|
|
413
|
+
resolveNext({
|
|
414
|
+
value: undefined,
|
|
415
|
+
done: true,
|
|
416
|
+
});
|
|
417
|
+
resolveNext = null;
|
|
418
|
+
rejectNext = null;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
const onError = (err) => {
|
|
422
|
+
// NEW5: surface stream errors to the consumer instead of resolving
|
|
423
|
+
// with `done: true` (which silently terminated the for-await loop
|
|
424
|
+
// and lost the error entirely). When a `next()` is pending, reject
|
|
425
|
+
// it. When no `next()` is pending, store the error so the next call
|
|
426
|
+
// throws it.
|
|
427
|
+
error = err;
|
|
428
|
+
if (rejectNext) {
|
|
429
|
+
rejectNext(err);
|
|
430
|
+
resolveNext = null;
|
|
431
|
+
rejectNext = null;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
stream.on("chunk", onChunk);
|
|
435
|
+
stream.on("end", onEnd);
|
|
436
|
+
stream.on("error", onError);
|
|
437
|
+
// M8: track listener removal so the iterator's `return()` can detach
|
|
438
|
+
// them when the consumer breaks early; otherwise the three listeners
|
|
439
|
+
// hang on the EventEmitter for the stream's lifetime, leaking closures.
|
|
440
|
+
const cleanup = () => {
|
|
441
|
+
stream.off("chunk", onChunk);
|
|
442
|
+
stream.off("end", onEnd);
|
|
443
|
+
stream.off("error", onError);
|
|
444
|
+
};
|
|
445
|
+
return {
|
|
446
|
+
async next() {
|
|
447
|
+
if (error) {
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
if (queue.length > 0) {
|
|
451
|
+
// The length-guard above proves `shift()` returns a value, but
|
|
452
|
+
// narrow explicitly to avoid the non-null assertion (Issue 9).
|
|
453
|
+
const next = queue.shift();
|
|
454
|
+
if (next !== undefined) {
|
|
455
|
+
return { value: next, done: false };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (done) {
|
|
459
|
+
return {
|
|
460
|
+
value: undefined,
|
|
461
|
+
done: true,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return new Promise((resolve, reject) => {
|
|
465
|
+
resolveNext = resolve;
|
|
466
|
+
rejectNext = reject;
|
|
467
|
+
});
|
|
468
|
+
},
|
|
469
|
+
async return() {
|
|
470
|
+
// M8: called when the consumer breaks out of `for await` early —
|
|
471
|
+
// detach listeners to prevent leak.
|
|
472
|
+
cleanup();
|
|
473
|
+
done = true;
|
|
474
|
+
return {
|
|
475
|
+
value: undefined,
|
|
476
|
+
done: true,
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Create a chunked audio stream from an async iterable
|
|
485
|
+
*
|
|
486
|
+
* @param iterable - Async iterable of audio buffers
|
|
487
|
+
* @param config - Stream configuration
|
|
488
|
+
* @returns Chunked audio stream
|
|
489
|
+
*/
|
|
490
|
+
export async function asyncIterableToStream(iterable, config = {}) {
|
|
491
|
+
const stream = new ChunkedAudioStream(config);
|
|
492
|
+
// Process iterable in background.
|
|
493
|
+
// Honour backpressure — `write()` returns false on overflow and pushes the
|
|
494
|
+
// chunk into pendingData. Without awaiting 'drain' here, a slow consumer
|
|
495
|
+
// would let pendingData grow unbounded for a long/live iterable.
|
|
496
|
+
(async () => {
|
|
497
|
+
try {
|
|
498
|
+
for await (const data of iterable) {
|
|
499
|
+
const ok = stream.write(data);
|
|
500
|
+
if (!ok) {
|
|
501
|
+
await new Promise((resolve) => stream.once("drain", resolve));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
stream.end();
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
stream.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
508
|
+
}
|
|
509
|
+
})();
|
|
510
|
+
return stream;
|
|
511
|
+
}
|
|
512
|
+
// Export main class with alias
|
|
513
|
+
export { ChunkedAudioStream as StreamHandler };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.62.0",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
|
|
6
6
|
"author": {
|
|
@@ -84,6 +84,8 @@
|
|
|
84
84
|
"test:tool-reliability": "npx tsx test/continuous-test-suite-tool-reliability.ts",
|
|
85
85
|
"test:tracing": "npx tsx test/continuous-test-suite-tracing.ts",
|
|
86
86
|
"test:tts": "npx tsx test/continuous-test-suite-tts.ts",
|
|
87
|
+
"test:voice": "npx tsx test/continuous-test-suite-voice.ts",
|
|
88
|
+
"test:voice-server": "npx tsx test/continuous-test-suite-voice-server.ts",
|
|
87
89
|
"test:credentials": "tsx test/continuous-test-suite-credentials.ts",
|
|
88
90
|
"test:dynamic": "npx tsx test/continuous-test-suite-dynamic.ts",
|
|
89
91
|
"test:proxy": "npx tsx test/continuous-test-suite-proxy.ts",
|