@livekit/agents 1.0.43 → 1.0.45
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/dist/inference/stt.cjs +19 -2
- package/dist/inference/stt.cjs.map +1 -1
- package/dist/inference/stt.d.cts +1 -1
- package/dist/inference/stt.d.ts +1 -1
- package/dist/inference/stt.d.ts.map +1 -1
- package/dist/inference/stt.js +19 -2
- package/dist/inference/stt.js.map +1 -1
- package/dist/ipc/job_proc_lazy_main.cjs +6 -0
- package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
- package/dist/ipc/job_proc_lazy_main.js +7 -1
- package/dist/ipc/job_proc_lazy_main.js.map +1 -1
- package/dist/ipc/supervised_proc.cjs +1 -1
- package/dist/ipc/supervised_proc.cjs.map +1 -1
- package/dist/ipc/supervised_proc.js +1 -1
- package/dist/ipc/supervised_proc.js.map +1 -1
- package/dist/llm/llm.cjs +1 -1
- package/dist/llm/llm.cjs.map +1 -1
- package/dist/llm/llm.js +1 -1
- package/dist/llm/llm.js.map +1 -1
- package/dist/log.cjs +13 -9
- package/dist/log.cjs.map +1 -1
- package/dist/log.d.cts +1 -1
- package/dist/log.d.ts +1 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +13 -9
- package/dist/log.js.map +1 -1
- package/dist/stt/stt.cjs +6 -2
- package/dist/stt/stt.cjs.map +1 -1
- package/dist/stt/stt.d.ts.map +1 -1
- package/dist/stt/stt.js +6 -2
- package/dist/stt/stt.js.map +1 -1
- package/dist/tts/fallback_adapter.cjs +466 -0
- package/dist/tts/fallback_adapter.cjs.map +1 -0
- package/dist/tts/fallback_adapter.d.cts +110 -0
- package/dist/tts/fallback_adapter.d.ts +110 -0
- package/dist/tts/fallback_adapter.d.ts.map +1 -0
- package/dist/tts/fallback_adapter.js +442 -0
- package/dist/tts/fallback_adapter.js.map +1 -0
- package/dist/tts/index.cjs +3 -0
- package/dist/tts/index.cjs.map +1 -1
- package/dist/tts/index.d.cts +1 -0
- package/dist/tts/index.d.ts +1 -0
- package/dist/tts/index.d.ts.map +1 -1
- package/dist/tts/index.js +2 -0
- package/dist/tts/index.js.map +1 -1
- package/dist/tts/tts.cjs +2 -2
- package/dist/tts/tts.cjs.map +1 -1
- package/dist/tts/tts.js +2 -2
- package/dist/tts/tts.js.map +1 -1
- package/dist/utils.cjs +10 -2
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +10 -2
- package/dist/utils.js.map +1 -1
- package/dist/vad.cjs +11 -10
- package/dist/vad.cjs.map +1 -1
- package/dist/vad.d.cts +5 -3
- package/dist/vad.d.ts +5 -3
- package/dist/vad.d.ts.map +1 -1
- package/dist/vad.js +11 -10
- package/dist/vad.js.map +1 -1
- package/dist/voice/room_io/_input.cjs +6 -3
- package/dist/voice/room_io/_input.cjs.map +1 -1
- package/dist/voice/room_io/_input.d.ts.map +1 -1
- package/dist/voice/room_io/_input.js +6 -3
- package/dist/voice/room_io/_input.js.map +1 -1
- package/package.json +1 -1
- package/src/inference/stt.ts +21 -3
- package/src/ipc/job_proc_lazy_main.ts +13 -1
- package/src/ipc/supervised_proc.ts +1 -1
- package/src/llm/llm.ts +1 -1
- package/src/log.ts +22 -11
- package/src/stt/stt.ts +7 -2
- package/src/tts/fallback_adapter.ts +579 -0
- package/src/tts/index.ts +1 -0
- package/src/tts/tts.ts +2 -2
- package/src/utils.ts +10 -2
- package/src/vad.ts +12 -11
- package/src/voice/room_io/_input.ts +5 -3
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { AudioResampler } from '@livekit/rtc-node';
|
|
2
|
+
import { type APIConnectOptions } from '../types.js';
|
|
3
|
+
import { Task } from '../utils.js';
|
|
4
|
+
import { ChunkedStream, SynthesizeStream, TTS } from './tts.js';
|
|
5
|
+
/**
|
|
6
|
+
* Internal status tracking for each TTS instance.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
interface TTSStatus {
|
|
10
|
+
available: boolean;
|
|
11
|
+
recoveringTask: Task<void> | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Options for creating a FallbackAdapter.
|
|
15
|
+
*/
|
|
16
|
+
export interface FallbackAdapterOptions {
|
|
17
|
+
/** List of TTS instances to use for fallback (in priority order). At least one is required. */
|
|
18
|
+
ttsInstances: TTS[];
|
|
19
|
+
/** Number of internal retries per TTS instance before moving to the next one. Defaults to 2. */
|
|
20
|
+
maxRetryPerTTS?: number;
|
|
21
|
+
/** Delay in milliseconds before attempting to recover a failed TTS instance. Defaults to 1000. */
|
|
22
|
+
recoveryDelayMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Event emitted when a TTS instance's availability changes.
|
|
26
|
+
*/
|
|
27
|
+
export interface AvailabilityChangedEvent {
|
|
28
|
+
/** The TTS instance whose availability changed. */
|
|
29
|
+
tts: TTS;
|
|
30
|
+
/** Whether the TTS instance is now available. */
|
|
31
|
+
available: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* FallbackAdapter is a TTS wrapper that provides automatic failover between multiple TTS providers.
|
|
35
|
+
*
|
|
36
|
+
* When the primary TTS fails, it automatically switches to the next available provider in the list.
|
|
37
|
+
* Failed providers are monitored in the background and restored when they recover.
|
|
38
|
+
*
|
|
39
|
+
* Features:
|
|
40
|
+
* - Automatic failover to backup TTS providers on failure
|
|
41
|
+
* - Background health checks to restore recovered providers
|
|
42
|
+
* - Automatic audio resampling when TTS providers have different sample rates
|
|
43
|
+
* - Support for both streaming and non-streaming TTS providers
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* import { FallbackAdapter } from '@livekit/agents';
|
|
48
|
+
* import { TTS as OpenAITTS } from '@livekit/agents-plugin-openai';
|
|
49
|
+
* import { TTS as ElevenLabsTTS } from '@livekit/agents-plugin-elevenlabs';
|
|
50
|
+
*
|
|
51
|
+
* const fallbackTTS = new FallbackAdapter({
|
|
52
|
+
* ttsInstances: [
|
|
53
|
+
* new OpenAITTS(), // Primary
|
|
54
|
+
* new ElevenLabsTTS(), // Fallback
|
|
55
|
+
* ],
|
|
56
|
+
* maxRetryPerTTS: 2, // Retry each TTS twice before moving to next
|
|
57
|
+
* recoveryDelayMs: 1000, // Check recovery every 1 second
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare class FallbackAdapter extends TTS {
|
|
63
|
+
/** The list of TTS instances used for fallback (in priority order). */
|
|
64
|
+
readonly ttsInstances: TTS[];
|
|
65
|
+
/** Number of retries per TTS instance before falling back to the next one. */
|
|
66
|
+
readonly maxRetryPerTTS: number;
|
|
67
|
+
/** Delay in milliseconds before attempting to recover a failed TTS instance. */
|
|
68
|
+
readonly recoveryDelayMs: number;
|
|
69
|
+
private _status;
|
|
70
|
+
private _logger;
|
|
71
|
+
private _recoveryTimeouts;
|
|
72
|
+
label: string;
|
|
73
|
+
constructor(opts: FallbackAdapterOptions);
|
|
74
|
+
private static aggregateCapabilities;
|
|
75
|
+
private setupEventForwarding;
|
|
76
|
+
/**
|
|
77
|
+
* Returns the current status of all TTS instances, including availability and recovery state.
|
|
78
|
+
*/
|
|
79
|
+
get status(): TTSStatus[];
|
|
80
|
+
getStreamingInstance(index: number): TTS;
|
|
81
|
+
/**
|
|
82
|
+
* Creates a new AudioResampler for the given TTS index if needed.
|
|
83
|
+
* Returns null if the TTS sample rate matches the adapter's output rate.
|
|
84
|
+
* Each stream should create its own resampler to avoid concurrency issues.
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
createResamplerForTTS(index: number): AudioResampler | null;
|
|
88
|
+
private emitAvailabilityChanged;
|
|
89
|
+
private tryRecovery;
|
|
90
|
+
markUnAvailable(index: number): void;
|
|
91
|
+
/**
|
|
92
|
+
* Receives text and returns synthesis in the form of a {@link ChunkedStream}
|
|
93
|
+
*/
|
|
94
|
+
synthesize(text: string, connOptions?: APIConnectOptions, abortSignal?: AbortSignal): ChunkedStream;
|
|
95
|
+
/**
|
|
96
|
+
* Returns a {@link SynthesizeStream} that can be used to push text and receive audio data
|
|
97
|
+
*
|
|
98
|
+
* @param options - Optional configuration including connection options
|
|
99
|
+
*/
|
|
100
|
+
stream(options?: {
|
|
101
|
+
connOptions?: APIConnectOptions;
|
|
102
|
+
}): SynthesizeStream;
|
|
103
|
+
/**
|
|
104
|
+
* Close the FallbackAdapter and all underlying TTS instances.
|
|
105
|
+
* This cancels any ongoing recovery tasks and cleans up resources.
|
|
106
|
+
*/
|
|
107
|
+
close(): Promise<void>;
|
|
108
|
+
}
|
|
109
|
+
export {};
|
|
110
|
+
//# sourceMappingURL=fallback_adapter.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { AudioResampler } from '@livekit/rtc-node';
|
|
2
|
+
import { type APIConnectOptions } from '../types.js';
|
|
3
|
+
import { Task } from '../utils.js';
|
|
4
|
+
import { ChunkedStream, SynthesizeStream, TTS } from './tts.js';
|
|
5
|
+
/**
|
|
6
|
+
* Internal status tracking for each TTS instance.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
interface TTSStatus {
|
|
10
|
+
available: boolean;
|
|
11
|
+
recoveringTask: Task<void> | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Options for creating a FallbackAdapter.
|
|
15
|
+
*/
|
|
16
|
+
export interface FallbackAdapterOptions {
|
|
17
|
+
/** List of TTS instances to use for fallback (in priority order). At least one is required. */
|
|
18
|
+
ttsInstances: TTS[];
|
|
19
|
+
/** Number of internal retries per TTS instance before moving to the next one. Defaults to 2. */
|
|
20
|
+
maxRetryPerTTS?: number;
|
|
21
|
+
/** Delay in milliseconds before attempting to recover a failed TTS instance. Defaults to 1000. */
|
|
22
|
+
recoveryDelayMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Event emitted when a TTS instance's availability changes.
|
|
26
|
+
*/
|
|
27
|
+
export interface AvailabilityChangedEvent {
|
|
28
|
+
/** The TTS instance whose availability changed. */
|
|
29
|
+
tts: TTS;
|
|
30
|
+
/** Whether the TTS instance is now available. */
|
|
31
|
+
available: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* FallbackAdapter is a TTS wrapper that provides automatic failover between multiple TTS providers.
|
|
35
|
+
*
|
|
36
|
+
* When the primary TTS fails, it automatically switches to the next available provider in the list.
|
|
37
|
+
* Failed providers are monitored in the background and restored when they recover.
|
|
38
|
+
*
|
|
39
|
+
* Features:
|
|
40
|
+
* - Automatic failover to backup TTS providers on failure
|
|
41
|
+
* - Background health checks to restore recovered providers
|
|
42
|
+
* - Automatic audio resampling when TTS providers have different sample rates
|
|
43
|
+
* - Support for both streaming and non-streaming TTS providers
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* import { FallbackAdapter } from '@livekit/agents';
|
|
48
|
+
* import { TTS as OpenAITTS } from '@livekit/agents-plugin-openai';
|
|
49
|
+
* import { TTS as ElevenLabsTTS } from '@livekit/agents-plugin-elevenlabs';
|
|
50
|
+
*
|
|
51
|
+
* const fallbackTTS = new FallbackAdapter({
|
|
52
|
+
* ttsInstances: [
|
|
53
|
+
* new OpenAITTS(), // Primary
|
|
54
|
+
* new ElevenLabsTTS(), // Fallback
|
|
55
|
+
* ],
|
|
56
|
+
* maxRetryPerTTS: 2, // Retry each TTS twice before moving to next
|
|
57
|
+
* recoveryDelayMs: 1000, // Check recovery every 1 second
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare class FallbackAdapter extends TTS {
|
|
63
|
+
/** The list of TTS instances used for fallback (in priority order). */
|
|
64
|
+
readonly ttsInstances: TTS[];
|
|
65
|
+
/** Number of retries per TTS instance before falling back to the next one. */
|
|
66
|
+
readonly maxRetryPerTTS: number;
|
|
67
|
+
/** Delay in milliseconds before attempting to recover a failed TTS instance. */
|
|
68
|
+
readonly recoveryDelayMs: number;
|
|
69
|
+
private _status;
|
|
70
|
+
private _logger;
|
|
71
|
+
private _recoveryTimeouts;
|
|
72
|
+
label: string;
|
|
73
|
+
constructor(opts: FallbackAdapterOptions);
|
|
74
|
+
private static aggregateCapabilities;
|
|
75
|
+
private setupEventForwarding;
|
|
76
|
+
/**
|
|
77
|
+
* Returns the current status of all TTS instances, including availability and recovery state.
|
|
78
|
+
*/
|
|
79
|
+
get status(): TTSStatus[];
|
|
80
|
+
getStreamingInstance(index: number): TTS;
|
|
81
|
+
/**
|
|
82
|
+
* Creates a new AudioResampler for the given TTS index if needed.
|
|
83
|
+
* Returns null if the TTS sample rate matches the adapter's output rate.
|
|
84
|
+
* Each stream should create its own resampler to avoid concurrency issues.
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
createResamplerForTTS(index: number): AudioResampler | null;
|
|
88
|
+
private emitAvailabilityChanged;
|
|
89
|
+
private tryRecovery;
|
|
90
|
+
markUnAvailable(index: number): void;
|
|
91
|
+
/**
|
|
92
|
+
* Receives text and returns synthesis in the form of a {@link ChunkedStream}
|
|
93
|
+
*/
|
|
94
|
+
synthesize(text: string, connOptions?: APIConnectOptions, abortSignal?: AbortSignal): ChunkedStream;
|
|
95
|
+
/**
|
|
96
|
+
* Returns a {@link SynthesizeStream} that can be used to push text and receive audio data
|
|
97
|
+
*
|
|
98
|
+
* @param options - Optional configuration including connection options
|
|
99
|
+
*/
|
|
100
|
+
stream(options?: {
|
|
101
|
+
connOptions?: APIConnectOptions;
|
|
102
|
+
}): SynthesizeStream;
|
|
103
|
+
/**
|
|
104
|
+
* Close the FallbackAdapter and all underlying TTS instances.
|
|
105
|
+
* This cancels any ongoing recovery tasks and cleans up resources.
|
|
106
|
+
*/
|
|
107
|
+
close(): Promise<void>;
|
|
108
|
+
}
|
|
109
|
+
export {};
|
|
110
|
+
//# sourceMappingURL=fallback_adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fallback_adapter.d.ts","sourceRoot":"","sources":["../../src/tts/fallback_adapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAInD,OAAO,EAAE,KAAK,iBAAiB,EAA+B,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,IAAI,EAAiB,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,GAAG,EAAwB,MAAM,UAAU,CAAC;AAEtF;;;GAGG;AACH,UAAU,SAAS;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,+FAA+F;IAC/F,YAAY,EAAE,GAAG,EAAE,CAAC;IACpB,gGAAgG;IAChG,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kGAAkG;IAClG,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,mDAAmD;IACnD,GAAG,EAAE,GAAG,CAAC;IACT,iDAAiD;IACjD,SAAS,EAAE,OAAO,CAAC;CACpB;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,eAAgB,SAAQ,GAAG;IACtC,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAEjC,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,iBAAiB,CAA0C;IAEnE,KAAK,EAAE,MAAM,CAAyB;gBAE1B,IAAI,EAAE,sBAAsB;IAqBxC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAMpC,OAAO,CAAC,oBAAoB;IAW5B;;OAEG;IACH,IAAI,MAAM,IAAI,SAAS,EAAE,CAExB;IAED,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG;IASxC;;;;;OAKG;IACH,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAW3D,OAAO,CAAC,uBAAuB;IAQ/B,OAAO,CAAC,WAAW;IA8CnB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAYpC;;OAEG;IACH,UAAU,CACR,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,iBAAiB,EAC/B,WAAW,CAAC,EAAE,WAAW,GACxB,aAAa;IAShB;;;;OAIG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,iBAAiB,CAAA;KAAE,GAAG,gBAAgB;IAOvE;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAyB7B"}
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { AudioResampler } from "@livekit/rtc-node";
|
|
2
|
+
import { APIConnectionError, APIError } from "../_exceptions.js";
|
|
3
|
+
import { log } from "../log.js";
|
|
4
|
+
import { basic } from "../tokenize/index.js";
|
|
5
|
+
import { DEFAULT_API_CONNECT_OPTIONS } from "../types.js";
|
|
6
|
+
import { Task, cancelAndWait } from "../utils.js";
|
|
7
|
+
import { StreamAdapter } from "./stream_adapter.js";
|
|
8
|
+
import { ChunkedStream, SynthesizeStream, TTS } from "./tts.js";
|
|
9
|
+
const DEFAULT_FALLBACK_API_CONNECT_OPTIONS = {
|
|
10
|
+
maxRetry: 0,
|
|
11
|
+
timeoutMs: DEFAULT_API_CONNECT_OPTIONS.timeoutMs,
|
|
12
|
+
retryIntervalMs: DEFAULT_API_CONNECT_OPTIONS.retryIntervalMs
|
|
13
|
+
};
|
|
14
|
+
const FORWARD_POLL_MS = 10;
|
|
15
|
+
class FallbackAdapter extends TTS {
|
|
16
|
+
/** The list of TTS instances used for fallback (in priority order). */
|
|
17
|
+
ttsInstances;
|
|
18
|
+
/** Number of retries per TTS instance before falling back to the next one. */
|
|
19
|
+
maxRetryPerTTS;
|
|
20
|
+
/** Delay in milliseconds before attempting to recover a failed TTS instance. */
|
|
21
|
+
recoveryDelayMs;
|
|
22
|
+
_status = [];
|
|
23
|
+
_logger = log();
|
|
24
|
+
_recoveryTimeouts = /* @__PURE__ */ new Map();
|
|
25
|
+
label = `tts.FallbackAdapter`;
|
|
26
|
+
constructor(opts) {
|
|
27
|
+
if (!opts.ttsInstances || opts.ttsInstances.length < 1) {
|
|
28
|
+
throw new Error("at least one TTS instance must be provided.");
|
|
29
|
+
}
|
|
30
|
+
const numChannels = opts.ttsInstances[0].numChannels;
|
|
31
|
+
const allNumChannelsMatch = opts.ttsInstances.every((tts) => tts.numChannels === numChannels);
|
|
32
|
+
if (!allNumChannelsMatch) {
|
|
33
|
+
throw new Error("All TTS instances should have the same number of channels");
|
|
34
|
+
}
|
|
35
|
+
const sampleRate = Math.max(...opts.ttsInstances.map((t) => t.sampleRate));
|
|
36
|
+
const capabilities = FallbackAdapter.aggregateCapabilities(opts.ttsInstances);
|
|
37
|
+
super(sampleRate, numChannels, capabilities);
|
|
38
|
+
this.ttsInstances = opts.ttsInstances;
|
|
39
|
+
this.maxRetryPerTTS = opts.maxRetryPerTTS ?? 2;
|
|
40
|
+
this.recoveryDelayMs = opts.recoveryDelayMs ?? 1e3;
|
|
41
|
+
this._status = opts.ttsInstances.map(() => ({
|
|
42
|
+
available: true,
|
|
43
|
+
recoveringTask: null
|
|
44
|
+
}));
|
|
45
|
+
this.setupEventForwarding();
|
|
46
|
+
}
|
|
47
|
+
static aggregateCapabilities(instances) {
|
|
48
|
+
const streaming = instances.some((tts) => tts.capabilities.streaming);
|
|
49
|
+
const alignedTranscript = instances.every((tts) => tts.capabilities.alignedTranscript === true);
|
|
50
|
+
return { streaming, alignedTranscript };
|
|
51
|
+
}
|
|
52
|
+
setupEventForwarding() {
|
|
53
|
+
this.ttsInstances.forEach((tts) => {
|
|
54
|
+
tts.on("metrics_collected", (metrics) => {
|
|
55
|
+
this.emit("metrics_collected", metrics);
|
|
56
|
+
});
|
|
57
|
+
tts.on("error", (error) => {
|
|
58
|
+
this.emit("error", error);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Returns the current status of all TTS instances, including availability and recovery state.
|
|
64
|
+
*/
|
|
65
|
+
get status() {
|
|
66
|
+
return this._status;
|
|
67
|
+
}
|
|
68
|
+
getStreamingInstance(index) {
|
|
69
|
+
const tts = this.ttsInstances[index];
|
|
70
|
+
if (tts.capabilities.streaming) {
|
|
71
|
+
return tts;
|
|
72
|
+
}
|
|
73
|
+
return new StreamAdapter(tts, new basic.SentenceTokenizer());
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Creates a new AudioResampler for the given TTS index if needed.
|
|
77
|
+
* Returns null if the TTS sample rate matches the adapter's output rate.
|
|
78
|
+
* Each stream should create its own resampler to avoid concurrency issues.
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
createResamplerForTTS(index) {
|
|
82
|
+
const tts = this.ttsInstances[index];
|
|
83
|
+
if (this.sampleRate !== tts.sampleRate) {
|
|
84
|
+
this._logger.debug(
|
|
85
|
+
`resampling ${tts.label} from ${tts.sampleRate}Hz to ${this.sampleRate}Hz`
|
|
86
|
+
);
|
|
87
|
+
return new AudioResampler(tts.sampleRate, this.sampleRate, tts.numChannels);
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
emitAvailabilityChanged(tts, available) {
|
|
92
|
+
const event = { tts, available };
|
|
93
|
+
this.emit(
|
|
94
|
+
"tts_availability_changed",
|
|
95
|
+
event
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
tryRecovery(index) {
|
|
99
|
+
const status = this._status[index];
|
|
100
|
+
const tts = this.ttsInstances[index];
|
|
101
|
+
if (status.recoveringTask && !status.recoveringTask.done) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
status.recoveringTask = Task.from(async (controller) => {
|
|
105
|
+
try {
|
|
106
|
+
const testStream = tts.synthesize(
|
|
107
|
+
"Hello world, this is a recovery test.",
|
|
108
|
+
{
|
|
109
|
+
maxRetry: 0,
|
|
110
|
+
timeoutMs: 1e4,
|
|
111
|
+
retryIntervalMs: 1e3
|
|
112
|
+
},
|
|
113
|
+
controller.signal
|
|
114
|
+
);
|
|
115
|
+
let audioReceived = false;
|
|
116
|
+
for await (const _ of testStream) {
|
|
117
|
+
audioReceived = true;
|
|
118
|
+
}
|
|
119
|
+
if (!audioReceived) {
|
|
120
|
+
throw new Error("Recovery test completed but no audio was received");
|
|
121
|
+
}
|
|
122
|
+
status.available = true;
|
|
123
|
+
status.recoveringTask = null;
|
|
124
|
+
this._logger.info({ tts: tts.label }, "TTS recovered");
|
|
125
|
+
this.emitAvailabilityChanged(tts, true);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
status.recoveringTask = null;
|
|
128
|
+
if (controller.signal.aborted) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this._logger.debug({ tts: tts.label, error }, "TTS recovery failed, will retry");
|
|
132
|
+
const timeoutId = setTimeout(() => {
|
|
133
|
+
this._recoveryTimeouts.delete(index);
|
|
134
|
+
this.tryRecovery(index);
|
|
135
|
+
}, this.recoveryDelayMs);
|
|
136
|
+
this._recoveryTimeouts.set(index, timeoutId);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
markUnAvailable(index) {
|
|
141
|
+
const status = this._status[index];
|
|
142
|
+
if (status.recoveringTask && !status.recoveringTask.done) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (status.available) {
|
|
146
|
+
status.available = false;
|
|
147
|
+
this.emitAvailabilityChanged(this.ttsInstances[index], false);
|
|
148
|
+
}
|
|
149
|
+
this.tryRecovery(index);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Receives text and returns synthesis in the form of a {@link ChunkedStream}
|
|
153
|
+
*/
|
|
154
|
+
synthesize(text, connOptions, abortSignal) {
|
|
155
|
+
return new FallbackChunkedStream(
|
|
156
|
+
this,
|
|
157
|
+
text,
|
|
158
|
+
connOptions ?? DEFAULT_FALLBACK_API_CONNECT_OPTIONS,
|
|
159
|
+
abortSignal
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Returns a {@link SynthesizeStream} that can be used to push text and receive audio data
|
|
164
|
+
*
|
|
165
|
+
* @param options - Optional configuration including connection options
|
|
166
|
+
*/
|
|
167
|
+
stream(options) {
|
|
168
|
+
return new FallbackSynthesizeStream(
|
|
169
|
+
this,
|
|
170
|
+
(options == null ? void 0 : options.connOptions) ?? DEFAULT_FALLBACK_API_CONNECT_OPTIONS
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Close the FallbackAdapter and all underlying TTS instances.
|
|
175
|
+
* This cancels any ongoing recovery tasks and cleans up resources.
|
|
176
|
+
*/
|
|
177
|
+
async close() {
|
|
178
|
+
this._recoveryTimeouts.forEach((timeoutId) => {
|
|
179
|
+
clearTimeout(timeoutId);
|
|
180
|
+
});
|
|
181
|
+
this._recoveryTimeouts.clear();
|
|
182
|
+
const recoveryTasks = this._status.map((s) => s.recoveringTask).filter((t) => t !== null);
|
|
183
|
+
if (recoveryTasks.length > 0) {
|
|
184
|
+
await cancelAndWait(recoveryTasks, 1e3);
|
|
185
|
+
}
|
|
186
|
+
for (const tts of this.ttsInstances) {
|
|
187
|
+
tts.removeAllListeners("metrics_collected");
|
|
188
|
+
tts.removeAllListeners("error");
|
|
189
|
+
}
|
|
190
|
+
await Promise.all(this.ttsInstances.map((tts) => tts.close()));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
class FallbackChunkedStream extends ChunkedStream {
|
|
194
|
+
adapter;
|
|
195
|
+
connOptions;
|
|
196
|
+
_logger = log();
|
|
197
|
+
label = "tts.FallbackChunkedStream";
|
|
198
|
+
constructor(adapter, text, connOptions, abortSignal) {
|
|
199
|
+
super(text, adapter, connOptions, abortSignal);
|
|
200
|
+
this.adapter = adapter;
|
|
201
|
+
this.connOptions = connOptions;
|
|
202
|
+
}
|
|
203
|
+
async run() {
|
|
204
|
+
const allTTSFailed = this.adapter.status.every((s) => !s.available);
|
|
205
|
+
let lastRequestId = "";
|
|
206
|
+
let lastSegmentId = "";
|
|
207
|
+
if (allTTSFailed) {
|
|
208
|
+
this._logger.warn("All fallback TTS instances failed, retrying from first...");
|
|
209
|
+
}
|
|
210
|
+
for (let i = 0; i < this.adapter.ttsInstances.length; i++) {
|
|
211
|
+
const tts = this.adapter.ttsInstances[i];
|
|
212
|
+
const status = this.adapter.status[i];
|
|
213
|
+
if (!status.available && !allTTSFailed) {
|
|
214
|
+
this.adapter.markUnAvailable(i);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
this._logger.debug({ tts: tts.label }, "attempting TTS synthesis");
|
|
219
|
+
const connOptions = {
|
|
220
|
+
...this.connOptions,
|
|
221
|
+
maxRetry: this.adapter.maxRetryPerTTS
|
|
222
|
+
};
|
|
223
|
+
const stream = tts.synthesize(this.inputText, connOptions, this.abortSignal);
|
|
224
|
+
let audioReceived = false;
|
|
225
|
+
const resampler = this.adapter.createResamplerForTTS(i);
|
|
226
|
+
for await (const audio of stream) {
|
|
227
|
+
if (this.abortController.signal.aborted) {
|
|
228
|
+
stream.close();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (resampler) {
|
|
232
|
+
for (const frame of resampler.push(audio.frame)) {
|
|
233
|
+
this.queue.put({
|
|
234
|
+
...audio,
|
|
235
|
+
frame
|
|
236
|
+
});
|
|
237
|
+
audioReceived = true;
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
this.queue.put(audio);
|
|
241
|
+
audioReceived = true;
|
|
242
|
+
}
|
|
243
|
+
lastRequestId = audio.requestId;
|
|
244
|
+
lastSegmentId = audio.segmentId;
|
|
245
|
+
}
|
|
246
|
+
if (resampler) {
|
|
247
|
+
for (const frame of resampler.flush()) {
|
|
248
|
+
this.queue.put({
|
|
249
|
+
requestId: lastRequestId || "",
|
|
250
|
+
segmentId: lastSegmentId || "",
|
|
251
|
+
frame,
|
|
252
|
+
final: true
|
|
253
|
+
});
|
|
254
|
+
audioReceived = true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!audioReceived) {
|
|
258
|
+
throw new APIConnectionError({
|
|
259
|
+
message: "TTS synthesis completed but no audio was received"
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
this._logger.debug({ tts: tts.label }, "TTS synthesis succeeded");
|
|
263
|
+
return;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error instanceof APIError || error instanceof APIConnectionError) {
|
|
266
|
+
this._logger.warn({ tts: tts.label, error }, "TTS failed, switching to next instance");
|
|
267
|
+
this.adapter.markUnAvailable(i);
|
|
268
|
+
} else {
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const labels = this.adapter.ttsInstances.map((t) => t.label).join(", ");
|
|
274
|
+
throw new APIConnectionError({
|
|
275
|
+
message: `all TTS instances failed (${labels})`
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
class FallbackSynthesizeStream extends SynthesizeStream {
|
|
280
|
+
adapter;
|
|
281
|
+
tokenBuffer = [];
|
|
282
|
+
audioPushed = false;
|
|
283
|
+
_logger = log();
|
|
284
|
+
label = "tts.FallbackSynthesizeStream";
|
|
285
|
+
constructor(adapter, connOptions) {
|
|
286
|
+
super(adapter, connOptions);
|
|
287
|
+
this.adapter = adapter;
|
|
288
|
+
}
|
|
289
|
+
async run() {
|
|
290
|
+
const allTTSFailed = this.adapter.status.every((s) => !s.available);
|
|
291
|
+
if (allTTSFailed) {
|
|
292
|
+
this._logger.warn("All fallback TTS instances failed, retrying from first...");
|
|
293
|
+
}
|
|
294
|
+
const readInputLLMStream = (async () => {
|
|
295
|
+
try {
|
|
296
|
+
for await (const input of this.input) {
|
|
297
|
+
if (this.abortController.signal.aborted) break;
|
|
298
|
+
this.tokenBuffer.push(input);
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
this._logger.debug({ error }, "Error reading input LLM stream");
|
|
302
|
+
throw error;
|
|
303
|
+
} finally {
|
|
304
|
+
this.tokenBuffer.push(SynthesizeStream.END_OF_STREAM);
|
|
305
|
+
}
|
|
306
|
+
})();
|
|
307
|
+
for (let i = 0; i < this.adapter.ttsInstances.length; i++) {
|
|
308
|
+
const tts = this.adapter.getStreamingInstance(i);
|
|
309
|
+
const originalTts = this.adapter.ttsInstances[i];
|
|
310
|
+
const status = this.adapter.status[i];
|
|
311
|
+
let lastRequestId = "";
|
|
312
|
+
let lastSegmentId = "";
|
|
313
|
+
if (!status.available && !allTTSFailed) {
|
|
314
|
+
this.adapter.markUnAvailable(i);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
this._logger.debug({ tts: originalTts.label }, "attempting TTS stream");
|
|
319
|
+
const connOptions = {
|
|
320
|
+
...this.connOptions,
|
|
321
|
+
maxRetry: this.adapter.maxRetryPerTTS
|
|
322
|
+
};
|
|
323
|
+
const stream = tts.stream({ connOptions });
|
|
324
|
+
const resampler = this.adapter.createResamplerForTTS(i);
|
|
325
|
+
let bufferIndex = 0;
|
|
326
|
+
let streamOutputCompleted = false;
|
|
327
|
+
const forwardBufferToTTS = async () => {
|
|
328
|
+
while (true) {
|
|
329
|
+
while (bufferIndex < this.tokenBuffer.length) {
|
|
330
|
+
const token = this.tokenBuffer[bufferIndex++];
|
|
331
|
+
if (token === SynthesizeStream.FLUSH_SENTINEL) {
|
|
332
|
+
stream.flush();
|
|
333
|
+
} else if (token === SynthesizeStream.END_OF_STREAM) {
|
|
334
|
+
stream.endInput();
|
|
335
|
+
return;
|
|
336
|
+
} else {
|
|
337
|
+
stream.pushText(token);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
await new Promise((resolve) => setTimeout(resolve, FORWARD_POLL_MS));
|
|
341
|
+
if (this.abortController.signal.aborted || streamOutputCompleted) {
|
|
342
|
+
stream.endInput();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const processOutput = async () => {
|
|
348
|
+
try {
|
|
349
|
+
for await (const audio of stream) {
|
|
350
|
+
if (this.abortController.signal.aborted) {
|
|
351
|
+
stream.close();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (audio === SynthesizeStream.END_OF_STREAM) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (resampler) {
|
|
358
|
+
for (const frame of resampler.push(audio.frame)) {
|
|
359
|
+
this.queue.put({
|
|
360
|
+
...audio,
|
|
361
|
+
frame
|
|
362
|
+
});
|
|
363
|
+
this.audioPushed = true;
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
this.queue.put(audio);
|
|
367
|
+
this.audioPushed = true;
|
|
368
|
+
}
|
|
369
|
+
lastRequestId = audio.requestId;
|
|
370
|
+
lastSegmentId = audio.segmentId;
|
|
371
|
+
}
|
|
372
|
+
if (resampler) {
|
|
373
|
+
for (const frame of resampler.flush()) {
|
|
374
|
+
this.queue.put({
|
|
375
|
+
requestId: lastRequestId || "",
|
|
376
|
+
segmentId: lastSegmentId || "",
|
|
377
|
+
frame,
|
|
378
|
+
final: true
|
|
379
|
+
});
|
|
380
|
+
this.audioPushed = true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} finally {
|
|
384
|
+
streamOutputCompleted = true;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
const [outputResult, forwardBufferResult] = await Promise.allSettled([
|
|
388
|
+
processOutput(),
|
|
389
|
+
forwardBufferToTTS().catch((err) => {
|
|
390
|
+
stream.close();
|
|
391
|
+
throw err;
|
|
392
|
+
})
|
|
393
|
+
]);
|
|
394
|
+
if (outputResult.status === "rejected") {
|
|
395
|
+
stream.close();
|
|
396
|
+
throw outputResult.reason;
|
|
397
|
+
}
|
|
398
|
+
if (forwardBufferResult.status === "rejected") {
|
|
399
|
+
stream.close();
|
|
400
|
+
throw forwardBufferResult.reason;
|
|
401
|
+
}
|
|
402
|
+
if (!this.audioPushed) {
|
|
403
|
+
throw new APIConnectionError({
|
|
404
|
+
message: "TTS stream completed but no audio was received"
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
this.queue.put(SynthesizeStream.END_OF_STREAM);
|
|
408
|
+
this._logger.debug({ tts: originalTts.label }, "TTS stream succeeded");
|
|
409
|
+
await readInputLLMStream.catch(() => {
|
|
410
|
+
});
|
|
411
|
+
return;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (this.audioPushed) {
|
|
414
|
+
this._logger.error(
|
|
415
|
+
{ tts: originalTts.label },
|
|
416
|
+
"TTS failed after audio pushed, cannot fallback mid-utterance"
|
|
417
|
+
);
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
if (error instanceof APIError || error instanceof APIConnectionError) {
|
|
421
|
+
this._logger.warn(
|
|
422
|
+
{ tts: originalTts.label, error },
|
|
423
|
+
"TTS failed, switching to next instance"
|
|
424
|
+
);
|
|
425
|
+
this.adapter.markUnAvailable(i);
|
|
426
|
+
} else {
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
await readInputLLMStream.catch(() => {
|
|
432
|
+
});
|
|
433
|
+
const labels = this.adapter.ttsInstances.map((t) => t.label).join(", ");
|
|
434
|
+
throw new APIConnectionError({
|
|
435
|
+
message: `all TTS instances failed (${labels})`
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
export {
|
|
440
|
+
FallbackAdapter
|
|
441
|
+
};
|
|
442
|
+
//# sourceMappingURL=fallback_adapter.js.map
|