@juspay/neurolink 12.28.4 → 12.29.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 +9 -2
- package/dist/adapters/tts/googleTTSHandler.d.ts +71 -1
- package/dist/adapters/tts/googleTTSHandler.js +254 -0
- package/dist/browser/neurolink.min.js +399 -399
- package/dist/cli/commands/voices.d.ts +23 -0
- package/dist/cli/commands/voices.js +102 -0
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/parser.js +3 -0
- package/dist/constants/enums.d.ts +13 -0
- package/dist/constants/enums.js +15 -0
- package/dist/core/baseProvider.d.ts +4 -0
- package/dist/core/baseProvider.js +245 -148
- package/dist/core/modules/structuredOutputPolicy.d.ts +27 -0
- package/dist/core/modules/structuredOutputPolicy.js +36 -0
- package/dist/core/nativeGenerateLoop.js +7 -1
- package/dist/neurolink.d.ts +42 -0
- package/dist/neurolink.js +102 -2
- package/dist/providers/catalog/index.generated.d.ts +1 -1
- package/dist/providers/catalog/index.generated.js +6 -0
- package/dist/providers/catalog/morph.json +97 -0
- package/dist/providers/catalog/novita.json +114 -0
- package/dist/providers/openaiChatCompletionsBase.d.ts +22 -0
- package/dist/providers/openaiChatCompletionsBase.js +291 -8
- package/dist/server/utils/validation.d.ts +34 -0
- package/dist/server/utils/validation.js +27 -0
- package/dist/server/websocket/WebSocketHandler.d.ts +4 -6
- package/dist/server/websocket/WebSocketHandler.js +185 -18
- package/dist/types/cli.d.ts +11 -0
- package/dist/types/generate.d.ts +15 -0
- package/dist/types/isolatedAgent.d.ts +16 -7
- package/dist/types/openaiCompatible.d.ts +6 -0
- package/dist/types/providerCatalog.generated.d.ts +2 -2
- package/dist/types/providers.d.ts +50 -3
- package/dist/types/server.d.ts +8 -0
- package/dist/types/stream.d.ts +30 -0
- package/dist/types/tts.d.ts +10 -0
- package/dist/types/utilities.d.ts +16 -3
- package/dist/utils/logger.d.ts +58 -13
- package/dist/utils/logger.js +156 -11
- package/dist/utils/providerHealth.d.ts +65 -1
- package/dist/utils/providerHealth.js +216 -54
- package/dist/utils/ttsProcessor.d.ts +30 -1
- package/dist/utils/ttsProcessor.js +76 -0
- package/docs-site/static/search-index.json +4 -4
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
## [12.
|
|
1
|
+
## [12.29.0](https://github.com/juspay/neurolink/compare/v12.28.4...v12.29.0) (2026-09-26)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- **(providers):** onboard Novita and Morph as catalog providers ([d8a566d](https://github.com/juspay/neurolink/commit/d8a566d2dd9b8f1d7cb73ea5d69d7a42913f9085))
|
|
6
|
+
- **(tts):** stream Google audio natively and add a voices command ([ce538d0](https://github.com/juspay/neurolink/commit/ce538d022a7249ee24fd4965f0d8627d3a667918)), closes [#528](https://github.com/juspay/neurolink/issues/528)
|
|
2
7
|
|
|
3
8
|
### Bug Fixes
|
|
4
9
|
|
|
5
|
-
- **(
|
|
10
|
+
- **(health):** stop silently excluding most providers from the health sweep ([eda0239](https://github.com/juspay/neurolink/commit/eda0239ffb8e89d73fa33353044c4f0202701ad1)), closes [#1744](https://github.com/juspay/neurolink/issues/1744)
|
|
11
|
+
- **(logger):** route log events per NeuroLink instance so worker bridges attribute truthfully ([163a0fd](https://github.com/juspay/neurolink/commit/163a0fdc719077f801de21fb0b533d5f28fa5015))
|
|
12
|
+
- **(sdk):** stop four public surfaces accepting input and dropping it ([be2ee44](https://github.com/juspay/neurolink/commit/be2ee44e6cdd1061a681bd113a0861374b562282)), closes [#1576](https://github.com/juspay/neurolink/issues/1576) [#1576](https://github.com/juspay/neurolink/issues/1576) [#1550](https://github.com/juspay/neurolink/issues/1550)
|
|
6
13
|
|
|
7
14
|
## [11.2.3](https://github.com/juspay/neurolink/compare/v11.2.2...v11.2.3) (2026-08-19)
|
|
8
15
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TTSOptions, TTSResult, TTSVoice, TTSHandler } from "../../types/index.js";
|
|
1
|
+
import type { TTSChunk, TTSOptions, TTSResult, TTSVoice, TTSHandler } from "../../types/index.js";
|
|
2
2
|
export declare class GoogleTTSHandler implements TTSHandler {
|
|
3
3
|
private client;
|
|
4
4
|
private voicesCache;
|
|
@@ -59,6 +59,76 @@ export declare class GoogleTTSHandler implements TTSHandler {
|
|
|
59
59
|
* @returns Audio buffer with metadata
|
|
60
60
|
*/
|
|
61
61
|
synthesize(text: string, options: TTSOptions): Promise<TTSResult>;
|
|
62
|
+
/**
|
|
63
|
+
* Voice names Google's streaming endpoint will synthesize.
|
|
64
|
+
*
|
|
65
|
+
* Streaming is not a property of the API, it is a property of the voice:
|
|
66
|
+
* every other family is rejected outright with `INVALID_ARGUMENT:
|
|
67
|
+
* Currently, only Chirp 3: HD voices are supported for streaming
|
|
68
|
+
* synthesis`. Measured against the live API — `Chirp3-HD` (en-US, en-GB and
|
|
69
|
+
* de-DE), `Chirp-HD` and `Journey` all stream; `Neural2` and `Studio` are
|
|
70
|
+
* refused. The locale prefix is required so a malformed name cannot reach
|
|
71
|
+
* the wire and fail a segment that the buffered path would have served.
|
|
72
|
+
*/
|
|
73
|
+
private static readonly STREAMING_VOICE_PATTERN;
|
|
74
|
+
/**
|
|
75
|
+
* Sample rate requested for streamed audio, in Hz.
|
|
76
|
+
*
|
|
77
|
+
* Reported on every chunk so a consumer can interpret headerless `PCM`
|
|
78
|
+
* bytes. Google honours the request — 16000 was measured returning
|
|
79
|
+
* proportionally fewer bytes for the same text — and 24000 matches what
|
|
80
|
+
* `getSampleRate()` reports for the buffered path.
|
|
81
|
+
*/
|
|
82
|
+
private static readonly STREAMING_SAMPLE_RATE_HZ;
|
|
83
|
+
/**
|
|
84
|
+
* How long a stream may go without delivering a response before it is
|
|
85
|
+
* treated as stalled.
|
|
86
|
+
*
|
|
87
|
+
* Deliberately an *idle* bound rather than a bound on the whole call, which
|
|
88
|
+
* is what `DEFAULT_API_TIMEOUT_MS` gives `synthesize()`. Streaming produces
|
|
89
|
+
* audio at roughly playback speed, so a total bound would fail a long but
|
|
90
|
+
* perfectly healthy segment purely for being long.
|
|
91
|
+
*/
|
|
92
|
+
private static readonly STREAMING_IDLE_TIMEOUT_MS;
|
|
93
|
+
/**
|
|
94
|
+
* Map a requested audio format to a streaming audio encoding, or
|
|
95
|
+
* `undefined` when streaming cannot produce it.
|
|
96
|
+
*
|
|
97
|
+
* Only formats with direct wire proof of incremental delivery appear here.
|
|
98
|
+
* `PCM` delivered 41 reads with the first at 697ms and the body complete at
|
|
99
|
+
* 6131ms; `OGG_OPUS` delivered 11 reads with the first at 443ms. `MP3` and
|
|
100
|
+
* `LINEAR16` — both valid for `synthesizeSpeech` — are rejected by the
|
|
101
|
+
* streaming endpoint as unsupported encodings, so `mp3` and `wav` take the
|
|
102
|
+
* buffered path. `mp3` being the default format is why a caller has to opt
|
|
103
|
+
* in to streaming by asking for one of these two.
|
|
104
|
+
*/
|
|
105
|
+
private static mapStreamingFormat;
|
|
106
|
+
/**
|
|
107
|
+
* Coerce one streaming response into its audio payload, or `undefined` when
|
|
108
|
+
* it carries none.
|
|
109
|
+
*
|
|
110
|
+
* The generated response type says `audioContent` is `Uint8Array | string |
|
|
111
|
+
* null`, and which one arrives depends on how the transport was configured
|
|
112
|
+
* — so both are handled rather than trusted.
|
|
113
|
+
*/
|
|
114
|
+
private static streamedAudio;
|
|
115
|
+
/**
|
|
116
|
+
* Stream one pre-validated segment's audio as Google produces it.
|
|
117
|
+
*
|
|
118
|
+
* Returns `undefined` — the contract's "not incrementally deliverable"
|
|
119
|
+
* signal — unless the voice and the format are both ones the streaming
|
|
120
|
+
* endpoint was measured to accept, and the text is not SSML.
|
|
121
|
+
* `StreamingSynthesisInput` has no `ssml` field at all, so markup that
|
|
122
|
+
* `synthesize()` would honour has to stay on the buffered path rather than
|
|
123
|
+
* be sent as literal text.
|
|
124
|
+
*
|
|
125
|
+
* Every non-empty response is yielded as it arrives and carries `isFinal:
|
|
126
|
+
* false`. `TTSProcessor` recomputes indexes, cumulative sizes and finality
|
|
127
|
+
* globally across segments and discards whatever a handler reports, so
|
|
128
|
+
* labelling the last response here would buy nothing and would cost a
|
|
129
|
+
* one-response lookahead.
|
|
130
|
+
*/
|
|
131
|
+
synthesizeStream(text: string, options?: TTSOptions): AsyncIterable<TTSChunk> | undefined;
|
|
62
132
|
/**
|
|
63
133
|
* Extract language code from a Google Cloud voice name
|
|
64
134
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TTSError, TTS_ERROR_CODES } from "../../utils/ttsProcessor.js";
|
|
2
2
|
import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js";
|
|
3
3
|
import { logger } from "../../utils/logger.js";
|
|
4
|
+
import { attachStreamCancel } from "../../utils/streamCancellation.js";
|
|
4
5
|
import { SpanSerializer, SpanType, SpanStatus, getMetricsAggregator, } from "../../observability/index.js";
|
|
5
6
|
export class GoogleTTSHandler {
|
|
6
7
|
client = null;
|
|
@@ -265,6 +266,238 @@ export class GoogleTTSHandler {
|
|
|
265
266
|
});
|
|
266
267
|
}
|
|
267
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Voice names Google's streaming endpoint will synthesize.
|
|
271
|
+
*
|
|
272
|
+
* Streaming is not a property of the API, it is a property of the voice:
|
|
273
|
+
* every other family is rejected outright with `INVALID_ARGUMENT:
|
|
274
|
+
* Currently, only Chirp 3: HD voices are supported for streaming
|
|
275
|
+
* synthesis`. Measured against the live API — `Chirp3-HD` (en-US, en-GB and
|
|
276
|
+
* de-DE), `Chirp-HD` and `Journey` all stream; `Neural2` and `Studio` are
|
|
277
|
+
* refused. The locale prefix is required so a malformed name cannot reach
|
|
278
|
+
* the wire and fail a segment that the buffered path would have served.
|
|
279
|
+
*/
|
|
280
|
+
static STREAMING_VOICE_PATTERN = /^[a-z]{2,3}-[A-Z]{2}-(?:Chirp3-HD|Chirp-HD|Journey)-/;
|
|
281
|
+
/**
|
|
282
|
+
* Sample rate requested for streamed audio, in Hz.
|
|
283
|
+
*
|
|
284
|
+
* Reported on every chunk so a consumer can interpret headerless `PCM`
|
|
285
|
+
* bytes. Google honours the request — 16000 was measured returning
|
|
286
|
+
* proportionally fewer bytes for the same text — and 24000 matches what
|
|
287
|
+
* `getSampleRate()` reports for the buffered path.
|
|
288
|
+
*/
|
|
289
|
+
static STREAMING_SAMPLE_RATE_HZ = 24000;
|
|
290
|
+
/**
|
|
291
|
+
* How long a stream may go without delivering a response before it is
|
|
292
|
+
* treated as stalled.
|
|
293
|
+
*
|
|
294
|
+
* Deliberately an *idle* bound rather than a bound on the whole call, which
|
|
295
|
+
* is what `DEFAULT_API_TIMEOUT_MS` gives `synthesize()`. Streaming produces
|
|
296
|
+
* audio at roughly playback speed, so a total bound would fail a long but
|
|
297
|
+
* perfectly healthy segment purely for being long.
|
|
298
|
+
*/
|
|
299
|
+
static STREAMING_IDLE_TIMEOUT_MS = 30 * 1000;
|
|
300
|
+
/**
|
|
301
|
+
* Map a requested audio format to a streaming audio encoding, or
|
|
302
|
+
* `undefined` when streaming cannot produce it.
|
|
303
|
+
*
|
|
304
|
+
* Only formats with direct wire proof of incremental delivery appear here.
|
|
305
|
+
* `PCM` delivered 41 reads with the first at 697ms and the body complete at
|
|
306
|
+
* 6131ms; `OGG_OPUS` delivered 11 reads with the first at 443ms. `MP3` and
|
|
307
|
+
* `LINEAR16` — both valid for `synthesizeSpeech` — are rejected by the
|
|
308
|
+
* streaming endpoint as unsupported encodings, so `mp3` and `wav` take the
|
|
309
|
+
* buffered path. `mp3` being the default format is why a caller has to opt
|
|
310
|
+
* in to streaming by asking for one of these two.
|
|
311
|
+
*/
|
|
312
|
+
static mapStreamingFormat(format) {
|
|
313
|
+
switch (format) {
|
|
314
|
+
case "pcm16":
|
|
315
|
+
return "PCM";
|
|
316
|
+
case "ogg":
|
|
317
|
+
case "opus":
|
|
318
|
+
return "OGG_OPUS";
|
|
319
|
+
default:
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Coerce one streaming response into its audio payload, or `undefined` when
|
|
325
|
+
* it carries none.
|
|
326
|
+
*
|
|
327
|
+
* The generated response type says `audioContent` is `Uint8Array | string |
|
|
328
|
+
* null`, and which one arrives depends on how the transport was configured
|
|
329
|
+
* — so both are handled rather than trusted.
|
|
330
|
+
*/
|
|
331
|
+
static streamedAudio(response) {
|
|
332
|
+
if (response === null || typeof response !== "object") {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
const audioContent = response.audioContent;
|
|
336
|
+
if (audioContent instanceof Uint8Array) {
|
|
337
|
+
return audioContent.byteLength > 0
|
|
338
|
+
? Buffer.from(audioContent.buffer, audioContent.byteOffset, audioContent.byteLength)
|
|
339
|
+
: undefined;
|
|
340
|
+
}
|
|
341
|
+
if (typeof audioContent === "string" && audioContent.length > 0) {
|
|
342
|
+
const decoded = Buffer.from(audioContent, "base64");
|
|
343
|
+
return decoded.length > 0 ? decoded : undefined;
|
|
344
|
+
}
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Stream one pre-validated segment's audio as Google produces it.
|
|
349
|
+
*
|
|
350
|
+
* Returns `undefined` — the contract's "not incrementally deliverable"
|
|
351
|
+
* signal — unless the voice and the format are both ones the streaming
|
|
352
|
+
* endpoint was measured to accept, and the text is not SSML.
|
|
353
|
+
* `StreamingSynthesisInput` has no `ssml` field at all, so markup that
|
|
354
|
+
* `synthesize()` would honour has to stay on the buffered path rather than
|
|
355
|
+
* be sent as literal text.
|
|
356
|
+
*
|
|
357
|
+
* Every non-empty response is yielded as it arrives and carries `isFinal:
|
|
358
|
+
* false`. `TTSProcessor` recomputes indexes, cumulative sizes and finality
|
|
359
|
+
* globally across segments and discards whatever a handler reports, so
|
|
360
|
+
* labelling the last response here would buy nothing and would cost a
|
|
361
|
+
* one-response lookahead.
|
|
362
|
+
*/
|
|
363
|
+
synthesizeStream(text, options = {}) {
|
|
364
|
+
const voiceId = options.voice;
|
|
365
|
+
if (voiceId === undefined ||
|
|
366
|
+
!GoogleTTSHandler.STREAMING_VOICE_PATTERN.test(voiceId)) {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
const format = options.format ?? "mp3";
|
|
370
|
+
const audioEncoding = GoogleTTSHandler.mapStreamingFormat(format);
|
|
371
|
+
if (audioEncoding === undefined) {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
if (text.trimStart().startsWith("<speak")) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
const languageCode = this.extractLanguageCode(voiceId);
|
|
378
|
+
const sampleRateHertz = GoogleTTSHandler.STREAMING_SAMPLE_RATE_HZ;
|
|
379
|
+
const handler = this;
|
|
380
|
+
// Typed off the client rather than named: Critical Rule 2 puts every type
|
|
381
|
+
// ALIAS in src/lib/types/, and a gax duplex handle that exists only for
|
|
382
|
+
// the cancel hook below does not belong in the public type surface.
|
|
383
|
+
let active;
|
|
384
|
+
let cancelled = false;
|
|
385
|
+
let idleTimedOut = false;
|
|
386
|
+
const stream = (async function* () {
|
|
387
|
+
const client = await handler.getClient();
|
|
388
|
+
// `getClient()` can span a dynamic import on a process's first call, a
|
|
389
|
+
// real gap the `attachStreamCancel` hook below can fire inside. Without
|
|
390
|
+
// this check, a cancellation that lands before `active` even exists
|
|
391
|
+
// (it is assigned two lines below) sets `cancelled` and calls
|
|
392
|
+
// `active?.cancel()` as a no-op, and the generator would carry on and
|
|
393
|
+
// open a duplex the caller already abandoned.
|
|
394
|
+
if (cancelled) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const startedAt = Date.now();
|
|
398
|
+
const duplex = client.streamingSynthesize();
|
|
399
|
+
active = duplex;
|
|
400
|
+
let idleTimer;
|
|
401
|
+
const armIdleTimer = () => {
|
|
402
|
+
clearTimeout(idleTimer);
|
|
403
|
+
idleTimer = setTimeout(() => {
|
|
404
|
+
idleTimedOut = true;
|
|
405
|
+
// `.cancel()`, not `.destroy()`: this object is a
|
|
406
|
+
// `gax.CancellableStream` wrapping a real gRPC duplex, and only
|
|
407
|
+
// `.cancel()` reaches `ClientDuplexStreamImpl.cancel()` ->
|
|
408
|
+
// `call.cancelWithStatus(CANCELLED, ...)`, which actually tells
|
|
409
|
+
// Google's server to stop. `.destroy()` only tears down the local
|
|
410
|
+
// Node stream — the server-side synthesis (and its billing) keeps
|
|
411
|
+
// running until the RPC's own ~5-minute deadline elapses on its
|
|
412
|
+
// own.
|
|
413
|
+
duplex.cancel();
|
|
414
|
+
}, GoogleTTSHandler.STREAMING_IDLE_TIMEOUT_MS);
|
|
415
|
+
};
|
|
416
|
+
let index = 0;
|
|
417
|
+
let cumulativeSize = 0;
|
|
418
|
+
try {
|
|
419
|
+
duplex.write({
|
|
420
|
+
streamingConfig: {
|
|
421
|
+
voice: { name: voiceId, languageCode },
|
|
422
|
+
streamingAudioConfig: {
|
|
423
|
+
audioEncoding,
|
|
424
|
+
sampleRateHertz,
|
|
425
|
+
speakingRate: options.speed ?? 1.0,
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
duplex.write({ input: { text } });
|
|
430
|
+
duplex.end();
|
|
431
|
+
armIdleTimer();
|
|
432
|
+
for await (const response of duplex) {
|
|
433
|
+
// Clear rather than re-arm here: the bound is on time spent
|
|
434
|
+
// waiting for the SERVER, not on time spent suspended at the
|
|
435
|
+
// `yield` below waiting for THIS generator's own consumer. Arming
|
|
436
|
+
// unconditionally on every response (the prior behaviour) started
|
|
437
|
+
// the clock running before that suspension, so a consumer slower
|
|
438
|
+
// than STREAMING_IDLE_TIMEOUT_MS to pull the next chunk destroyed
|
|
439
|
+
// a healthy duplex and reported its own backpressure as a network
|
|
440
|
+
// stall. The timer is re-armed only once real waiting resumes:
|
|
441
|
+
// immediately, for an empty read that loops back to the next
|
|
442
|
+
// `duplex` response, or after the consumer has resumed us.
|
|
443
|
+
clearTimeout(idleTimer);
|
|
444
|
+
const data = GoogleTTSHandler.streamedAudio(response);
|
|
445
|
+
if (data === undefined) {
|
|
446
|
+
armIdleTimer();
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
cumulativeSize += data.length;
|
|
450
|
+
yield {
|
|
451
|
+
data,
|
|
452
|
+
format,
|
|
453
|
+
index: index++,
|
|
454
|
+
isFinal: false,
|
|
455
|
+
cumulativeSize,
|
|
456
|
+
voice: voiceId,
|
|
457
|
+
sampleRate: sampleRateHertz,
|
|
458
|
+
};
|
|
459
|
+
armIdleTimer();
|
|
460
|
+
}
|
|
461
|
+
logger.debug(`[GoogleTTSHandler] Streamed ${cumulativeSize} bytes in ${Date.now() - startedAt}ms`);
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
if (cancelled) {
|
|
465
|
+
// The consumer stopped and the cancel hook destroyed the stream
|
|
466
|
+
// underneath an in-flight read. That is not a synthesis failure.
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (err instanceof TTSError) {
|
|
470
|
+
throw err;
|
|
471
|
+
}
|
|
472
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
473
|
+
throw new TTSError({
|
|
474
|
+
code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
|
|
475
|
+
message: idleTimedOut
|
|
476
|
+
? `Google TTS streaming synthesis stalled for ${GoogleTTSHandler.STREAMING_IDLE_TIMEOUT_MS}ms`
|
|
477
|
+
: `Google TTS streaming synthesis failed: ${message}`,
|
|
478
|
+
category: idleTimedOut
|
|
479
|
+
? ErrorCategory.NETWORK
|
|
480
|
+
: ErrorCategory.EXECUTION,
|
|
481
|
+
severity: ErrorSeverity.HIGH,
|
|
482
|
+
retriable: true,
|
|
483
|
+
context: { voice: voiceId, audioEncoding },
|
|
484
|
+
originalError: err instanceof Error ? err : undefined,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
clearTimeout(idleTimer);
|
|
489
|
+
active = undefined;
|
|
490
|
+
// Same reasoning as the idle timer above: `.cancel()` signals the
|
|
491
|
+
// server this call is done (a no-op on one that already finished
|
|
492
|
+
// normally), where `.destroy()` only ever tore down the local end.
|
|
493
|
+
duplex.cancel();
|
|
494
|
+
}
|
|
495
|
+
})();
|
|
496
|
+
return attachStreamCancel(stream, () => {
|
|
497
|
+
cancelled = true;
|
|
498
|
+
active?.cancel();
|
|
499
|
+
});
|
|
500
|
+
}
|
|
268
501
|
/**
|
|
269
502
|
* Extract language code from a Google Cloud voice name
|
|
270
503
|
*
|
|
@@ -306,6 +539,27 @@ export class GoogleTTSHandler {
|
|
|
306
539
|
case "ogg":
|
|
307
540
|
case "opus":
|
|
308
541
|
return "OGG_OPUS";
|
|
542
|
+
case "pcm16":
|
|
543
|
+
// pcm16 is a real, supported format — just not on THIS (buffered)
|
|
544
|
+
// endpoint. Reaching this branch means `synthesizeStream()` already
|
|
545
|
+
// declined the request (wrong voice family, or SSML text, both of
|
|
546
|
+
// which return `undefined` silently), so a caller who picked a
|
|
547
|
+
// perfectly valid streaming voice/format pair but tripped the SSML
|
|
548
|
+
// gate would otherwise see the same generic "unsupported format" as
|
|
549
|
+
// someone who picked a fundamentally wrong combination. Naming the
|
|
550
|
+
// actual requirement here, once, is cheaper than threading the
|
|
551
|
+
// disqualifying reason back from `synthesizeStream()`.
|
|
552
|
+
throw new TTSError({
|
|
553
|
+
code: TTS_ERROR_CODES.INVALID_INPUT,
|
|
554
|
+
message: "Google Cloud TTS's buffered synthesis does not support pcm16 output. " +
|
|
555
|
+
"pcm16 is only delivered via native streaming, which requires a " +
|
|
556
|
+
"Chirp3-HD, Chirp-HD, or Journey voice and plain (non-SSML) text — " +
|
|
557
|
+
"check that both conditions hold, or request a different format.",
|
|
558
|
+
category: ErrorCategory.VALIDATION,
|
|
559
|
+
severity: ErrorSeverity.MEDIUM,
|
|
560
|
+
retriable: false,
|
|
561
|
+
context: { format },
|
|
562
|
+
});
|
|
309
563
|
default:
|
|
310
564
|
throw new TTSError({
|
|
311
565
|
code: TTS_ERROR_CODES.INVALID_INPUT,
|