@juspay/neurolink 12.6.0 → 12.7.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 -4
- package/dist/browser/neurolink.min.js +382 -382
- package/dist/cli/proxy-clients/gemini.js +55 -13
- package/dist/providers/sagemaker/client.js +30 -2
- package/dist/providers/sagemaker/language-model.d.ts +9 -0
- package/dist/providers/sagemaker/language-model.js +37 -0
- package/dist/proxy/clientAttribution.d.ts +40 -0
- package/dist/proxy/clientAttribution.js +28 -5
- package/dist/types/common.d.ts +87 -0
- package/dist/types/providers.d.ts +10 -0
- package/dist/types/voice.d.ts +13 -0
- package/dist/utils/ttsProcessor.d.ts +130 -5
- package/dist/utils/ttsProcessor.js +642 -97
- package/dist/utils/ttsStream.js +4 -0
- package/dist/voice/providers/OpenAITTS.d.ts +26 -8
- package/dist/voice/providers/OpenAITTS.js +327 -85
- package/package.json +1 -1
package/dist/utils/ttsStream.js
CHANGED
|
@@ -229,6 +229,10 @@ export async function* interleaveTTSStream(params) {
|
|
|
229
229
|
// `return()`, so this side channel is the only thing that actually
|
|
230
230
|
// reaches — and closes — the provider stream at the bottom.
|
|
231
231
|
cancelStream(stream);
|
|
232
|
+
// The audio side has its own wrapper chain. Native TTS can be parked in a
|
|
233
|
+
// response-body read even after text ingestion stops, so its cancel hook
|
|
234
|
+
// must be reached directly rather than waiting for queued `.return()`.
|
|
235
|
+
cancelStream(audioIterator);
|
|
232
236
|
}
|
|
233
237
|
textQueue.end();
|
|
234
238
|
const releases = [];
|
|
@@ -5,14 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module voice/providers/OpenAITTS
|
|
7
7
|
*/
|
|
8
|
-
import type { TTSHandler, TTSOptions, TTSResult, TTSVoice } from "../../types/index.js";
|
|
9
|
-
/**
|
|
10
|
-
* OpenAI Text-to-Speech Handler
|
|
11
|
-
*
|
|
12
|
-
* Supports high-quality neural TTS with multiple voices.
|
|
13
|
-
*
|
|
14
|
-
* @see https://platform.openai.com/docs/api-reference/audio/createSpeech
|
|
15
|
-
*/
|
|
8
|
+
import type { TTSChunk, TTSHandler, TTSOptions, TTSResult, TTSVoice } from "../../types/index.js";
|
|
16
9
|
export declare class OpenAITTS implements TTSHandler {
|
|
17
10
|
private readonly apiKey;
|
|
18
11
|
private readonly baseUrl;
|
|
@@ -27,7 +20,32 @@ export declare class OpenAITTS implements TTSHandler {
|
|
|
27
20
|
constructor(apiKey?: string);
|
|
28
21
|
isConfigured(): boolean;
|
|
29
22
|
getVoices(languageCode?: string): Promise<TTSVoice[]>;
|
|
23
|
+
private requireApiKey;
|
|
24
|
+
private prepareRequest;
|
|
25
|
+
/**
|
|
26
|
+
* Issue the speech request and reject non-2xx responses.
|
|
27
|
+
*
|
|
28
|
+
* `onHeaders` runs as soon as the request settles, before the response body
|
|
29
|
+
* is touched at all. The buffered path clears its request timeout there,
|
|
30
|
+
* which is where it has always been cleared: the 30-second bound covers
|
|
31
|
+
* getting a response, not downloading one. The native path passes no
|
|
32
|
+
* callback, keeping its own timeout armed across the body reads it owns.
|
|
33
|
+
*/
|
|
34
|
+
private createSpeechResponse;
|
|
35
|
+
private synthesisError;
|
|
30
36
|
synthesize(text: string, options?: TTSOptions): Promise<TTSResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Stream one segment's audio as the response body arrives.
|
|
39
|
+
*
|
|
40
|
+
* Returns `undefined` for any format without direct wire proof of
|
|
41
|
+
* incremental delivery, which selects the buffered `synthesize()` path.
|
|
42
|
+
*
|
|
43
|
+
* Every non-empty body read is yielded as soon as it is available and
|
|
44
|
+
* carries `isFinal: false`: assigning finality here would require a
|
|
45
|
+
* one-read lookahead, delaying every fragment by a full body read, and
|
|
46
|
+
* `TTSProcessor` recomputes finality globally anyway.
|
|
47
|
+
*/
|
|
48
|
+
synthesizeStream(text: string, options?: TTSOptions): AsyncIterable<TTSChunk> | undefined;
|
|
31
49
|
/**
|
|
32
50
|
* Map TTSAudioFormat to OpenAI response_format.
|
|
33
51
|
*
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js";
|
|
9
9
|
import { logger } from "../../utils/logger.js";
|
|
10
|
+
import { attachStreamCancel } from "../../utils/streamCancellation.js";
|
|
10
11
|
import { TTS_ERROR_CODES, TTSError } from "../../utils/ttsProcessor.js";
|
|
11
12
|
/**
|
|
12
13
|
* OpenAI Text-to-Speech Handler
|
|
@@ -15,6 +16,109 @@ import { TTS_ERROR_CODES, TTSError } from "../../utils/ttsProcessor.js";
|
|
|
15
16
|
*
|
|
16
17
|
* @see https://platform.openai.com/docs/api-reference/audio/createSpeech
|
|
17
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* Converts an arbitrary thrown value to a loggable string without running
|
|
21
|
+
* code the value controls unguarded — `.message` can be a throwing accessor
|
|
22
|
+
* and `String()` can hit a hostile `toString`/`Symbol.toPrimitive` or a
|
|
23
|
+
* null-prototype object. Mirrors the processor-side helper of the same name.
|
|
24
|
+
*/
|
|
25
|
+
function safeErrorMessage(error) {
|
|
26
|
+
try {
|
|
27
|
+
if (error instanceof Error) {
|
|
28
|
+
return String(error.message);
|
|
29
|
+
}
|
|
30
|
+
return String(error);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return "[unprintable error]";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `instanceof` performs a prototype lookup, which a `Proxy` with a throwing
|
|
38
|
+
* `getPrototypeOf` trap — or a revoked one — turns into a throw of its own.
|
|
39
|
+
* Classifying a transport failure must never become the failure. Mirrors the
|
|
40
|
+
* processor-side helper of the same name.
|
|
41
|
+
*/
|
|
42
|
+
function safeInstanceOf(value, ctor) {
|
|
43
|
+
try {
|
|
44
|
+
return value instanceof ctor;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Both the class test and the `.name` read can execute code the thrown value
|
|
52
|
+
* controls, and the abort branch needs the answer to both.
|
|
53
|
+
*/
|
|
54
|
+
function isAbortError(error) {
|
|
55
|
+
try {
|
|
56
|
+
return error instanceof Error && error.name === "AbortError";
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Reduce a caught value to an `Error` that is safe to attach as
|
|
64
|
+
* `originalError`. The structured-error constructor reads `.stack` and
|
|
65
|
+
* `.message` unguarded, so a throwing accessor there escapes the shaper
|
|
66
|
+
* itself. Forward the original only once those reads are proven to succeed,
|
|
67
|
+
* and otherwise carry the already-safe message on a plain `Error` with no
|
|
68
|
+
* accessors of its own. Mirrors the processor-side helper of the same name.
|
|
69
|
+
*/
|
|
70
|
+
function toSafeOriginalError(error, safeMessage) {
|
|
71
|
+
if (!safeInstanceOf(error, Error)) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
String(error.name);
|
|
76
|
+
String(error.message);
|
|
77
|
+
String(error.stack);
|
|
78
|
+
return error;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return new Error(safeMessage);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build the shaped error, retrying without the original if attaching it
|
|
86
|
+
* throws. A one-shot accessor can answer one way when it is checked and
|
|
87
|
+
* another when the structured-error constructor reads it; the retry drops the
|
|
88
|
+
* original rather than the shaped failure. Mirrors the processor-side helper
|
|
89
|
+
* of the same name.
|
|
90
|
+
*/
|
|
91
|
+
function buildTTSError(details, originalError) {
|
|
92
|
+
try {
|
|
93
|
+
return new TTSError({ ...details, originalError });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return new TTSError(details);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A value may pass `safeInstanceOf` by LYING — a `Proxy` whose
|
|
101
|
+
* `getPrototypeOf` answers `TTSError.prototype` while every `get` detonates.
|
|
102
|
+
* Passing one through as "already shaped" hands the hostile value to every
|
|
103
|
+
* downstream consumer (mirrors the processor-side helper). Trust the claim only when the reads those consumers
|
|
104
|
+
* perform succeed here, where a throw is contained.
|
|
105
|
+
*/
|
|
106
|
+
function isReadableTTSError(value) {
|
|
107
|
+
if (!safeInstanceOf(value, TTSError)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
String(value.name);
|
|
112
|
+
String(value.message);
|
|
113
|
+
String(value.code);
|
|
114
|
+
void value.retriable;
|
|
115
|
+
String(value.stack);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
18
122
|
export class OpenAITTS {
|
|
19
123
|
apiKey;
|
|
20
124
|
baseUrl = "https://api.openai.com/v1";
|
|
@@ -90,7 +194,7 @@ export class OpenAITTS {
|
|
|
90
194
|
}
|
|
91
195
|
return OpenAITTS.VOICES;
|
|
92
196
|
}
|
|
93
|
-
|
|
197
|
+
requireApiKey() {
|
|
94
198
|
if (!this.apiKey) {
|
|
95
199
|
throw new TTSError({
|
|
96
200
|
code: TTS_ERROR_CODES.PROVIDER_NOT_CONFIGURED,
|
|
@@ -100,115 +204,253 @@ export class OpenAITTS {
|
|
|
100
204
|
retriable: false,
|
|
101
205
|
});
|
|
102
206
|
}
|
|
103
|
-
|
|
207
|
+
return this.apiKey;
|
|
208
|
+
}
|
|
209
|
+
prepareRequest(text, options) {
|
|
104
210
|
const openaiOptions = options;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const requestBody = {
|
|
211
|
+
const model = openaiOptions.model ?? (options.quality === "hd" ? "tts-1-hd" : "tts-1");
|
|
212
|
+
const voice = options.voice ?? "alloy";
|
|
213
|
+
const responseFormat = this.mapFormat(options.format ?? "mp3");
|
|
214
|
+
return {
|
|
215
|
+
model,
|
|
216
|
+
voice,
|
|
217
|
+
responseFormat,
|
|
218
|
+
effectiveFormat: this.effectiveFormat(responseFormat),
|
|
219
|
+
body: {
|
|
115
220
|
model,
|
|
116
221
|
input: text,
|
|
117
222
|
voice,
|
|
118
223
|
response_format: responseFormat,
|
|
119
224
|
speed: options.speed ?? 1.0,
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Issue the speech request and reject non-2xx responses.
|
|
230
|
+
*
|
|
231
|
+
* `onHeaders` runs as soon as the request settles, before the response body
|
|
232
|
+
* is touched at all. The buffered path clears its request timeout there,
|
|
233
|
+
* which is where it has always been cleared: the 30-second bound covers
|
|
234
|
+
* getting a response, not downloading one. The native path passes no
|
|
235
|
+
* callback, keeping its own timeout armed across the body reads it owns.
|
|
236
|
+
*/
|
|
237
|
+
async createSpeechResponse(request, signal, onHeaders) {
|
|
238
|
+
let response;
|
|
239
|
+
try {
|
|
240
|
+
response = await fetch(`${this.baseUrl}/audio/speech`, {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers: {
|
|
243
|
+
Authorization: `Bearer ${this.requireApiKey()}`,
|
|
244
|
+
"Content-Type": "application/json",
|
|
245
|
+
},
|
|
246
|
+
body: JSON.stringify(request.body),
|
|
247
|
+
signal,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
onHeaders?.();
|
|
252
|
+
}
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
const errorData = await response
|
|
255
|
+
.json()
|
|
256
|
+
.catch(() => Object.create(null));
|
|
257
|
+
const errorMessage = errorData.error?.message ||
|
|
258
|
+
`HTTP ${response.status}`;
|
|
259
|
+
const retriable = response.status === 408 ||
|
|
260
|
+
response.status === 429 ||
|
|
261
|
+
response.status >= 500;
|
|
262
|
+
throw new TTSError({
|
|
263
|
+
code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
|
|
264
|
+
message: errorMessage,
|
|
265
|
+
category: retriable ? ErrorCategory.NETWORK : ErrorCategory.EXECUTION,
|
|
266
|
+
severity: ErrorSeverity.HIGH,
|
|
267
|
+
retriable,
|
|
268
|
+
context: {
|
|
269
|
+
status: response.status,
|
|
270
|
+
model: request.model,
|
|
271
|
+
responseFormat: request.responseFormat,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return response;
|
|
276
|
+
}
|
|
277
|
+
synthesisError(error, textLength, timedOut) {
|
|
278
|
+
if (isReadableTTSError(error)) {
|
|
279
|
+
return error;
|
|
280
|
+
}
|
|
281
|
+
if (isAbortError(error) && timedOut) {
|
|
282
|
+
return buildTTSError({
|
|
283
|
+
code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
|
|
284
|
+
message: "OpenAI TTS request timed out after 30 seconds",
|
|
285
|
+
category: ErrorCategory.NETWORK,
|
|
286
|
+
severity: ErrorSeverity.HIGH,
|
|
287
|
+
retriable: true,
|
|
288
|
+
}, toSafeOriginalError(error, safeErrorMessage(error)));
|
|
289
|
+
}
|
|
290
|
+
const errorMessage = error ? safeErrorMessage(error) : "Unknown error";
|
|
291
|
+
logger.error(`[OpenAITTSHandler] Synthesis failed: ${errorMessage}`);
|
|
292
|
+
return buildTTSError({
|
|
293
|
+
code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
|
|
294
|
+
message: `Synthesis failed: ${errorMessage}`,
|
|
295
|
+
category: ErrorCategory.EXECUTION,
|
|
296
|
+
severity: ErrorSeverity.HIGH,
|
|
297
|
+
retriable: true,
|
|
298
|
+
context: { textLength },
|
|
299
|
+
}, toSafeOriginalError(error, errorMessage));
|
|
300
|
+
}
|
|
301
|
+
async synthesize(text, options = {}) {
|
|
302
|
+
this.requireApiKey();
|
|
303
|
+
const startTime = Date.now();
|
|
304
|
+
const request = this.prepareRequest(text, options);
|
|
305
|
+
const controller = new AbortController();
|
|
306
|
+
let timedOut = false;
|
|
307
|
+
let timeoutId = setTimeout(() => {
|
|
308
|
+
timedOut = true;
|
|
309
|
+
controller.abort();
|
|
310
|
+
}, 30000);
|
|
311
|
+
// Disarmed the moment a response comes back, so the timeout bounds the
|
|
312
|
+
// request and not the audio download that follows it. Bounding the
|
|
313
|
+
// download too would fail a large but perfectly healthy synthesis that
|
|
314
|
+
// takes more than 30 seconds to transfer.
|
|
315
|
+
const clearRequestTimeout = () => {
|
|
316
|
+
if (timeoutId !== undefined) {
|
|
149
317
|
clearTimeout(timeoutId);
|
|
318
|
+
timeoutId = undefined;
|
|
150
319
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const retriable = response.status === 408 ||
|
|
160
|
-
response.status === 429 ||
|
|
161
|
-
response.status >= 500;
|
|
162
|
-
throw new TTSError({
|
|
163
|
-
code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
|
|
164
|
-
message: errorMessage,
|
|
165
|
-
category: retriable ? ErrorCategory.NETWORK : ErrorCategory.EXECUTION,
|
|
166
|
-
severity: ErrorSeverity.HIGH,
|
|
167
|
-
retriable,
|
|
168
|
-
context: { status: response.status, model, responseFormat },
|
|
169
|
-
});
|
|
170
|
-
}
|
|
320
|
+
};
|
|
321
|
+
try {
|
|
322
|
+
const response = await this.createSpeechResponse(request, controller.signal, clearRequestTimeout);
|
|
323
|
+
// Measured BEFORE the body download, exactly as it was before this
|
|
324
|
+
// method was refactored: `metadata.latency` reports time-to-response,
|
|
325
|
+
// not time-to-last-byte. Moving it past `arrayBuffer()` silently changed
|
|
326
|
+
// a public metric — `TTSResult.metadata` reaches callers through
|
|
327
|
+
// `TTSProcessor.synthesize()` and through `generate({ tts })`.
|
|
171
328
|
const latency = Date.now() - startTime;
|
|
172
|
-
// Get audio buffer
|
|
173
329
|
const arrayBuffer = await response.arrayBuffer();
|
|
174
330
|
const audioBuffer = Buffer.from(arrayBuffer);
|
|
175
|
-
// Use the *effective* output format (post-mapFormat fallback), not the
|
|
176
|
-
// requested format — otherwise mp3-coerced "m4a" requests would mislabel
|
|
177
|
-
// the buffer and break consumer file-extension routing.
|
|
178
|
-
const effectiveFormat = this.effectiveFormat(responseFormat);
|
|
179
331
|
const result = {
|
|
180
332
|
buffer: audioBuffer,
|
|
181
|
-
format: effectiveFormat,
|
|
333
|
+
format: request.effectiveFormat,
|
|
182
334
|
size: audioBuffer.length,
|
|
183
|
-
voice,
|
|
184
|
-
sampleRate: this.getSampleRate(effectiveFormat),
|
|
335
|
+
voice: request.voice,
|
|
336
|
+
sampleRate: this.getSampleRate(request.effectiveFormat),
|
|
185
337
|
metadata: {
|
|
186
338
|
latency,
|
|
187
339
|
provider: "openai-tts",
|
|
188
|
-
model,
|
|
340
|
+
model: request.model,
|
|
189
341
|
requestedFormat: options.format,
|
|
190
|
-
responseFormat,
|
|
342
|
+
responseFormat: request.responseFormat,
|
|
191
343
|
},
|
|
192
344
|
};
|
|
193
345
|
logger.info(`[OpenAITTSHandler] Synthesized ${audioBuffer.length} bytes in ${latency}ms`);
|
|
194
346
|
return result;
|
|
195
347
|
}
|
|
196
348
|
catch (err) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
349
|
+
throw this.synthesisError(err, text.length, timedOut);
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
clearRequestTimeout();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Stream one segment's audio as the response body arrives.
|
|
357
|
+
*
|
|
358
|
+
* Returns `undefined` for any format without direct wire proof of
|
|
359
|
+
* incremental delivery, which selects the buffered `synthesize()` path.
|
|
360
|
+
*
|
|
361
|
+
* Every non-empty body read is yielded as soon as it is available and
|
|
362
|
+
* carries `isFinal: false`: assigning finality here would require a
|
|
363
|
+
* one-read lookahead, delaying every fragment by a full body read, and
|
|
364
|
+
* `TTSProcessor` recomputes finality globally anyway.
|
|
365
|
+
*/
|
|
366
|
+
synthesizeStream(text, options = {}) {
|
|
367
|
+
const requestedFormat = options.format ?? "mp3";
|
|
368
|
+
if (requestedFormat !== "mp3" && requestedFormat !== "pcm16") {
|
|
369
|
+
return undefined;
|
|
211
370
|
}
|
|
371
|
+
const request = this.prepareRequest(text, options);
|
|
372
|
+
const handler = this;
|
|
373
|
+
let controller;
|
|
374
|
+
let reader;
|
|
375
|
+
let cancelled = false;
|
|
376
|
+
let timedOut = false;
|
|
377
|
+
let bodyComplete = false;
|
|
378
|
+
const stream = (async function* () {
|
|
379
|
+
handler.requireApiKey();
|
|
380
|
+
const startedAt = Date.now();
|
|
381
|
+
controller = new AbortController();
|
|
382
|
+
const timeoutId = setTimeout(() => {
|
|
383
|
+
timedOut = true;
|
|
384
|
+
controller?.abort();
|
|
385
|
+
}, 30000);
|
|
386
|
+
try {
|
|
387
|
+
const response = await handler.createSpeechResponse(request, controller.signal);
|
|
388
|
+
if (!response.body) {
|
|
389
|
+
throw new TTSError({
|
|
390
|
+
code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
|
|
391
|
+
message: "OpenAI TTS response did not include an audio body",
|
|
392
|
+
category: ErrorCategory.NETWORK,
|
|
393
|
+
severity: ErrorSeverity.HIGH,
|
|
394
|
+
retriable: true,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
reader = response.body.getReader();
|
|
398
|
+
let index = 0;
|
|
399
|
+
let cumulativeSize = 0;
|
|
400
|
+
while (true) {
|
|
401
|
+
const result = await reader.read();
|
|
402
|
+
if (result.done) {
|
|
403
|
+
bodyComplete = true;
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
const data = Buffer.from(result.value);
|
|
407
|
+
if (data.length === 0) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
cumulativeSize += data.length;
|
|
411
|
+
yield {
|
|
412
|
+
data,
|
|
413
|
+
format: request.effectiveFormat,
|
|
414
|
+
index: index++,
|
|
415
|
+
// Deliberately never `true`. Marking the last read would mean
|
|
416
|
+
// holding one read back until the next one arrives, which delays
|
|
417
|
+
// every fragment by a full body read; `TTSProcessor` already
|
|
418
|
+
// recomputes exactly one final chunk across all segments and
|
|
419
|
+
// discards whatever finality a handler reports. See the
|
|
420
|
+
// `synthesizeStream` contract in `TTSHandler`.
|
|
421
|
+
isFinal: false,
|
|
422
|
+
cumulativeSize,
|
|
423
|
+
voice: request.voice,
|
|
424
|
+
sampleRate: handler.getSampleRate(request.effectiveFormat),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
logger.info(`[OpenAITTSHandler] Streamed ${cumulativeSize} bytes in ${Date.now() - startedAt}ms`);
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
if (cancelled && isAbortError(err)) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
throw handler.synthesisError(err, text.length, timedOut);
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
clearTimeout(timeoutId);
|
|
437
|
+
if (!bodyComplete) {
|
|
438
|
+
controller?.abort();
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
reader?.releaseLock();
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// The reader may already be errored or released during cancellation.
|
|
445
|
+
}
|
|
446
|
+
reader = undefined;
|
|
447
|
+
controller = undefined;
|
|
448
|
+
}
|
|
449
|
+
})();
|
|
450
|
+
return attachStreamCancel(stream, () => {
|
|
451
|
+
cancelled = true;
|
|
452
|
+
controller?.abort();
|
|
453
|
+
});
|
|
212
454
|
}
|
|
213
455
|
/**
|
|
214
456
|
* Map TTSAudioFormat to OpenAI response_format.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.7.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": {
|