@elizaos/plugin-streaming 2.0.0-beta.1
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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/api/stream-persistence.d.ts +68 -0
- package/dist/api/stream-persistence.js +181 -0
- package/dist/api/stream-persistence.js.map +1 -0
- package/dist/api/stream-route-state.d.ts +59 -0
- package/dist/api/stream-route-state.js +1 -0
- package/dist/api/stream-route-state.js.map +1 -0
- package/dist/api/stream-routes.d.ts +43 -0
- package/dist/api/stream-routes.js +609 -0
- package/dist/api/stream-routes.js.map +1 -0
- package/dist/api/streaming-text.d.ts +10 -0
- package/dist/api/streaming-text.js +88 -0
- package/dist/api/streaming-text.js.map +1 -0
- package/dist/api/streaming-types.d.ts +2 -0
- package/dist/api/streaming-types.js +1 -0
- package/dist/api/streaming-types.js.map +1 -0
- package/dist/api/tts-routes.d.ts +25 -0
- package/dist/api/tts-routes.js +158 -0
- package/dist/api/tts-routes.js.map +1 -0
- package/dist/core.d.ts +164 -0
- package/dist/core.js +495 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/dist/services/stream-manager.d.ts +124 -0
- package/dist/services/stream-manager.js +531 -0
- package/dist/services/stream-manager.js.map +1 -0
- package/dist/services/tts-stream-bridge.d.ts +103 -0
- package/dist/services/tts-stream-bridge.js +327 -0
- package/dist/services/tts-stream-bridge.js.map +1 -0
- package/package.json +106 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TTS Stream Bridge — generates TTS audio server-side and pipes PCM data
|
|
5
|
+
* into FFmpeg's audio track for RTMP streaming.
|
|
6
|
+
*
|
|
7
|
+
* Uses pipe:3 (a 4th stdio fd) as the audio input to FFmpeg. Writes PCM
|
|
8
|
+
* silence when idle and decoded TTS audio when speaking. This avoids FIFO
|
|
9
|
+
* complexity and works cross-platform.
|
|
10
|
+
*
|
|
11
|
+
* @module services/tts-stream-bridge
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type TtsProvider = "elevenlabs" | "openai" | "edge" | (string & {});
|
|
15
|
+
type TtsConfig = {
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
provider?: TtsProvider;
|
|
18
|
+
elevenlabs?: {
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
voiceId?: string;
|
|
21
|
+
modelId?: string;
|
|
22
|
+
voiceSettings?: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
openai?: {
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
voice?: string;
|
|
28
|
+
};
|
|
29
|
+
edge?: {
|
|
30
|
+
voice?: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/** Resolved TTS configuration for a speak() call. */
|
|
34
|
+
interface ResolvedTtsConfig {
|
|
35
|
+
provider: TtsProvider;
|
|
36
|
+
elevenlabs?: {
|
|
37
|
+
apiKey: string;
|
|
38
|
+
voiceId: string;
|
|
39
|
+
modelId: string;
|
|
40
|
+
voiceSettings?: Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
openai?: {
|
|
43
|
+
apiKey: string;
|
|
44
|
+
model: string;
|
|
45
|
+
voice: string;
|
|
46
|
+
};
|
|
47
|
+
edge?: {
|
|
48
|
+
voice: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Public interface for the TTS stream bridge. */
|
|
52
|
+
interface ITtsStreamBridge {
|
|
53
|
+
attach(stream: Writable): void;
|
|
54
|
+
detach(): void;
|
|
55
|
+
isAttached(): boolean;
|
|
56
|
+
isSpeaking(): boolean;
|
|
57
|
+
speak(text: string, config: ResolvedTtsConfig): Promise<boolean>;
|
|
58
|
+
}
|
|
59
|
+
declare class TtsStreamBridge implements ITtsStreamBridge {
|
|
60
|
+
private writeStream;
|
|
61
|
+
private tickTimer;
|
|
62
|
+
private pcmQueue;
|
|
63
|
+
private _speaking;
|
|
64
|
+
private silenceChunk;
|
|
65
|
+
constructor();
|
|
66
|
+
/** Attach the bridge to an FFmpeg stdio pipe (pipe:3). */
|
|
67
|
+
attach(stream: Writable): void;
|
|
68
|
+
/** Detach from FFmpeg — stops tick loop and clears queue. */
|
|
69
|
+
detach(): void;
|
|
70
|
+
/** Whether the bridge is currently attached to an FFmpeg process. */
|
|
71
|
+
isAttached(): boolean;
|
|
72
|
+
/** Whether TTS audio is currently being played. */
|
|
73
|
+
isSpeaking(): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Generate TTS for the given text and queue PCM audio for playback.
|
|
76
|
+
* Non-blocking — queues audio and returns immediately after generation.
|
|
77
|
+
*/
|
|
78
|
+
speak(text: string, config: ResolvedTtsConfig): Promise<boolean>;
|
|
79
|
+
/** Called every CHUNK_MS — writes next PCM chunk (TTS or silence) to FFmpeg. */
|
|
80
|
+
private tick;
|
|
81
|
+
private generateTts;
|
|
82
|
+
private generateElevenlabs;
|
|
83
|
+
private generateOpenai;
|
|
84
|
+
private generateEdge;
|
|
85
|
+
/** Decode MP3 audio to raw s16le PCM using an FFmpeg subprocess. */
|
|
86
|
+
private decodeMp3ToPcm;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve TTS configuration from eliza config, finding the best available
|
|
90
|
+
* provider with valid API keys.
|
|
91
|
+
*/
|
|
92
|
+
declare function resolveTtsConfig(ttsConfig: TtsConfig | undefined): ResolvedTtsConfig | null;
|
|
93
|
+
/**
|
|
94
|
+
* Get a summary of available TTS providers and their status.
|
|
95
|
+
*/
|
|
96
|
+
declare function getTtsProviderStatus(ttsConfig: TtsConfig | undefined): {
|
|
97
|
+
configuredProvider: string | null;
|
|
98
|
+
hasApiKey: boolean;
|
|
99
|
+
resolvedProvider: string | null;
|
|
100
|
+
};
|
|
101
|
+
declare const ttsStreamBridge: TtsStreamBridge;
|
|
102
|
+
|
|
103
|
+
export { type ITtsStreamBridge, type ResolvedTtsConfig, type TtsConfig, type TtsProvider, getTtsProviderStatus, resolveTtsConfig, ttsStreamBridge };
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { logger, sanitizeSpeechText } from "@elizaos/core";
|
|
3
|
+
const TAG = "[TtsStreamBridge]";
|
|
4
|
+
const SAMPLE_RATE = 24e3;
|
|
5
|
+
const CHANNELS = 1;
|
|
6
|
+
const BYTES_PER_SAMPLE = 2;
|
|
7
|
+
const CHUNK_MS = 50;
|
|
8
|
+
const CHUNK_BYTES = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_MS / 1e3;
|
|
9
|
+
const ELEVENLABS_TIMEOUT_MS = 2e4;
|
|
10
|
+
const OPENAI_TIMEOUT_MS = 2e4;
|
|
11
|
+
class TtsStreamBridge {
|
|
12
|
+
writeStream = null;
|
|
13
|
+
tickTimer = null;
|
|
14
|
+
pcmQueue = [];
|
|
15
|
+
_speaking = false;
|
|
16
|
+
silenceChunk;
|
|
17
|
+
constructor() {
|
|
18
|
+
this.silenceChunk = Buffer.alloc(CHUNK_BYTES, 0);
|
|
19
|
+
}
|
|
20
|
+
/** Attach the bridge to an FFmpeg stdio pipe (pipe:3). */
|
|
21
|
+
attach(stream) {
|
|
22
|
+
this.detach();
|
|
23
|
+
this.writeStream = stream;
|
|
24
|
+
stream.on("error", (err) => {
|
|
25
|
+
logger.warn(`${TAG} Write stream error: ${err.message}`);
|
|
26
|
+
});
|
|
27
|
+
this.tickTimer = setInterval(() => this.tick(), CHUNK_MS);
|
|
28
|
+
logger.info(`${TAG} Attached to FFmpeg audio pipe`);
|
|
29
|
+
}
|
|
30
|
+
/** Detach from FFmpeg — stops tick loop and clears queue. */
|
|
31
|
+
detach() {
|
|
32
|
+
if (this.tickTimer) {
|
|
33
|
+
clearInterval(this.tickTimer);
|
|
34
|
+
this.tickTimer = null;
|
|
35
|
+
}
|
|
36
|
+
this.writeStream = null;
|
|
37
|
+
this.pcmQueue = [];
|
|
38
|
+
this._speaking = false;
|
|
39
|
+
}
|
|
40
|
+
/** Whether the bridge is currently attached to an FFmpeg process. */
|
|
41
|
+
isAttached() {
|
|
42
|
+
return this.writeStream !== null;
|
|
43
|
+
}
|
|
44
|
+
/** Whether TTS audio is currently being played. */
|
|
45
|
+
isSpeaking() {
|
|
46
|
+
return this._speaking;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Generate TTS for the given text and queue PCM audio for playback.
|
|
50
|
+
* Non-blocking — queues audio and returns immediately after generation.
|
|
51
|
+
*/
|
|
52
|
+
async speak(text, config) {
|
|
53
|
+
if (!this.writeStream) {
|
|
54
|
+
logger.warn(`${TAG} Cannot speak \u2014 not attached to FFmpeg`);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const speakableText = sanitizeSpeechText(text);
|
|
58
|
+
if (!speakableText) return false;
|
|
59
|
+
try {
|
|
60
|
+
logger.info(
|
|
61
|
+
`${TAG} Generating TTS (${config.provider}, ${speakableText.length} chars)`
|
|
62
|
+
);
|
|
63
|
+
const mp3 = await this.generateTts(speakableText, config);
|
|
64
|
+
if (!mp3 || mp3.length === 0) {
|
|
65
|
+
logger.warn(`${TAG} TTS returned empty audio`);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const pcm = await this.decodeMp3ToPcm(mp3);
|
|
69
|
+
if (!pcm || pcm.length === 0) {
|
|
70
|
+
logger.warn(`${TAG} PCM decode returned empty buffer`);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const chunks = [];
|
|
74
|
+
for (let i = 0; i < pcm.length; i += CHUNK_BYTES) {
|
|
75
|
+
const end = Math.min(i + CHUNK_BYTES, pcm.length);
|
|
76
|
+
const chunk = Buffer.alloc(CHUNK_BYTES, 0);
|
|
77
|
+
pcm.copy(chunk, 0, i, end);
|
|
78
|
+
chunks.push(chunk);
|
|
79
|
+
}
|
|
80
|
+
this.pcmQueue.push(...chunks);
|
|
81
|
+
this._speaking = true;
|
|
82
|
+
logger.info(
|
|
83
|
+
`${TAG} Queued ${chunks.length} PCM chunks (${(pcm.length / SAMPLE_RATE / BYTES_PER_SAMPLE).toFixed(1)}s)`
|
|
84
|
+
);
|
|
85
|
+
return true;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
logger.error(`${TAG} TTS generation failed: ${String(err)}`);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Called every CHUNK_MS — writes next PCM chunk (TTS or silence) to FFmpeg. */
|
|
92
|
+
tick() {
|
|
93
|
+
if (!this.writeStream) return;
|
|
94
|
+
let chunk;
|
|
95
|
+
if (this.pcmQueue.length > 0) {
|
|
96
|
+
chunk = this.pcmQueue.shift();
|
|
97
|
+
if (this.pcmQueue.length === 0) {
|
|
98
|
+
this._speaking = false;
|
|
99
|
+
logger.info(`${TAG} Finished speaking`);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
chunk = this.silenceChunk;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
this.writeStream.write(chunk);
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// ── TTS generation ─────────────────────────────────────────────────────
|
|
110
|
+
async generateTts(text, config) {
|
|
111
|
+
switch (config.provider) {
|
|
112
|
+
case "elevenlabs":
|
|
113
|
+
return this.generateElevenlabs(text, config);
|
|
114
|
+
case "openai":
|
|
115
|
+
return this.generateOpenai(text, config);
|
|
116
|
+
case "edge":
|
|
117
|
+
return this.generateEdge(text, config);
|
|
118
|
+
default:
|
|
119
|
+
throw new Error(`Unknown TTS provider: ${config.provider}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async generateElevenlabs(text, config) {
|
|
123
|
+
const el = config.elevenlabs;
|
|
124
|
+
if (!el?.apiKey) throw new Error("ElevenLabs API key not available");
|
|
125
|
+
const voiceId = el.voiceId || "EXAVITQu4vr4xnSDxMaL";
|
|
126
|
+
const modelId = el.modelId || "eleven_flash_v2_5";
|
|
127
|
+
const url = `https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voiceId)}/stream?output_format=mp3_22050_32`;
|
|
128
|
+
const payload = {
|
|
129
|
+
text,
|
|
130
|
+
model_id: modelId
|
|
131
|
+
};
|
|
132
|
+
if (el.voiceSettings && Object.keys(el.voiceSettings).length > 0) {
|
|
133
|
+
payload.voice_settings = el.voiceSettings;
|
|
134
|
+
}
|
|
135
|
+
const resp = await fetchWithTimeout(
|
|
136
|
+
url,
|
|
137
|
+
{
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
"xi-api-key": el.apiKey,
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
Accept: "audio/mpeg"
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify(payload)
|
|
145
|
+
},
|
|
146
|
+
ELEVENLABS_TIMEOUT_MS
|
|
147
|
+
);
|
|
148
|
+
if (!resp.ok) {
|
|
149
|
+
const body = await resp.text().catch(() => "");
|
|
150
|
+
throw new Error(`ElevenLabs ${resp.status}: ${body.slice(0, 200)}`);
|
|
151
|
+
}
|
|
152
|
+
return Buffer.from(await resp.arrayBuffer());
|
|
153
|
+
}
|
|
154
|
+
async generateOpenai(text, config) {
|
|
155
|
+
const oai = config.openai;
|
|
156
|
+
if (!oai?.apiKey) throw new Error("OpenAI API key not available");
|
|
157
|
+
const model = oai.model || "tts-1";
|
|
158
|
+
const voice = oai.voice || "alloy";
|
|
159
|
+
const resp = await fetchWithTimeout(
|
|
160
|
+
"https://api.openai.com/v1/audio/speech",
|
|
161
|
+
{
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
Authorization: `Bearer ${oai.apiKey}`,
|
|
165
|
+
"Content-Type": "application/json"
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
model,
|
|
169
|
+
input: text,
|
|
170
|
+
voice,
|
|
171
|
+
response_format: "mp3"
|
|
172
|
+
})
|
|
173
|
+
},
|
|
174
|
+
OPENAI_TIMEOUT_MS
|
|
175
|
+
);
|
|
176
|
+
if (!resp.ok) {
|
|
177
|
+
const body = await resp.text().catch(() => "");
|
|
178
|
+
throw new Error(`OpenAI TTS ${resp.status}: ${body.slice(0, 200)}`);
|
|
179
|
+
}
|
|
180
|
+
return Buffer.from(await resp.arrayBuffer());
|
|
181
|
+
}
|
|
182
|
+
async generateEdge(text, config) {
|
|
183
|
+
try {
|
|
184
|
+
const edgeTtsModule = "node-edge-tts";
|
|
185
|
+
const { MsEdgeTTS } = await import(edgeTtsModule);
|
|
186
|
+
const tts = new MsEdgeTTS();
|
|
187
|
+
const voice = config.edge?.voice || "en-US-AriaNeural";
|
|
188
|
+
await tts.setMetadata(voice, "audio-24khz-48kbitrate-mono-mp3");
|
|
189
|
+
const readable = tts.toStream(text);
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
const chunks = [];
|
|
192
|
+
readable.on("data", (chunk) => chunks.push(chunk));
|
|
193
|
+
readable.on("end", () => resolve(Buffer.concat(chunks)));
|
|
194
|
+
readable.on("error", reject);
|
|
195
|
+
});
|
|
196
|
+
} catch (err) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Edge TTS failed (node-edge-tts may not be installed): ${String(err)}`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// ── MP3 → PCM decode ──────────────────────────────────────────────────
|
|
203
|
+
/** Decode MP3 audio to raw s16le PCM using an FFmpeg subprocess. */
|
|
204
|
+
decodeMp3ToPcm(mp3) {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
const proc = spawn(
|
|
207
|
+
"ffmpeg",
|
|
208
|
+
[
|
|
209
|
+
"-i",
|
|
210
|
+
"pipe:0",
|
|
211
|
+
"-f",
|
|
212
|
+
"s16le",
|
|
213
|
+
"-ar",
|
|
214
|
+
String(SAMPLE_RATE),
|
|
215
|
+
"-ac",
|
|
216
|
+
String(CHANNELS),
|
|
217
|
+
"pipe:1"
|
|
218
|
+
],
|
|
219
|
+
{ stdio: ["pipe", "pipe", "ignore"] }
|
|
220
|
+
);
|
|
221
|
+
const chunks = [];
|
|
222
|
+
proc.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
223
|
+
proc.on("close", (code) => {
|
|
224
|
+
if (code === 0) {
|
|
225
|
+
resolve(Buffer.concat(chunks));
|
|
226
|
+
} else {
|
|
227
|
+
reject(new Error(`FFmpeg decode exited with code ${code}`));
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
proc.on("error", reject);
|
|
231
|
+
proc.stdin?.write(mp3);
|
|
232
|
+
proc.stdin?.end();
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function fetchWithTimeout(url, init, timeoutMs) {
|
|
237
|
+
const controller = new AbortController();
|
|
238
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
239
|
+
return fetch(url, { ...init, signal: controller.signal }).finally(
|
|
240
|
+
() => clearTimeout(timer)
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
function isRedactedSecret(val) {
|
|
244
|
+
return /^\*+$/.test(val) || val === "REDACTED" || val === "[REDACTED]";
|
|
245
|
+
}
|
|
246
|
+
function resolveTtsConfig(ttsConfig) {
|
|
247
|
+
if (!ttsConfig) return null;
|
|
248
|
+
const preferredProvider = ttsConfig.provider || "elevenlabs";
|
|
249
|
+
const providers = [preferredProvider];
|
|
250
|
+
if (!providers.includes("elevenlabs")) providers.push("elevenlabs");
|
|
251
|
+
if (!providers.includes("openai")) providers.push("openai");
|
|
252
|
+
if (!providers.includes("edge")) providers.push("edge");
|
|
253
|
+
for (const provider of providers) {
|
|
254
|
+
switch (provider) {
|
|
255
|
+
case "elevenlabs": {
|
|
256
|
+
const el = ttsConfig.elevenlabs;
|
|
257
|
+
const apiKey = resolveKey(el?.apiKey, "ELEVENLABS_API_KEY");
|
|
258
|
+
if (apiKey) {
|
|
259
|
+
return {
|
|
260
|
+
provider: "elevenlabs",
|
|
261
|
+
elevenlabs: {
|
|
262
|
+
apiKey,
|
|
263
|
+
voiceId: el?.voiceId || "EXAVITQu4vr4xnSDxMaL",
|
|
264
|
+
modelId: el?.modelId || "eleven_flash_v2_5",
|
|
265
|
+
voiceSettings: el?.voiceSettings ? { ...el.voiceSettings } : void 0
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
case "openai": {
|
|
272
|
+
const oai = ttsConfig.openai;
|
|
273
|
+
const apiKey = resolveKey(oai?.apiKey, "OPENAI_API_KEY");
|
|
274
|
+
if (apiKey) {
|
|
275
|
+
return {
|
|
276
|
+
provider: "openai",
|
|
277
|
+
openai: {
|
|
278
|
+
apiKey,
|
|
279
|
+
model: oai?.model || "tts-1",
|
|
280
|
+
voice: oai?.voice || "alloy"
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case "edge": {
|
|
287
|
+
return {
|
|
288
|
+
provider: "edge",
|
|
289
|
+
edge: {
|
|
290
|
+
voice: ttsConfig.edge?.voice || "en-US-AriaNeural"
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
function resolveKey(configKey, envVar) {
|
|
299
|
+
const ck = configKey?.trim();
|
|
300
|
+
if (ck && !isRedactedSecret(ck)) return ck;
|
|
301
|
+
const ev = process.env[envVar]?.trim();
|
|
302
|
+
if (ev && !isRedactedSecret(ev)) return ev;
|
|
303
|
+
const explicitCloudTts = process.env.ELIZAOS_CLOUD_USE_TTS === "true";
|
|
304
|
+
const legacyCloudTts = process.env.ELIZAOS_CLOUD_USE_TTS === void 0 && process.env.ELIZAOS_CLOUD_ENABLED === "true" && process.env.ELIZA_CLOUD_TTS_DISABLED !== "true";
|
|
305
|
+
if (explicitCloudTts || legacyCloudTts) {
|
|
306
|
+
const cloudKey = process.env.ELIZAOS_CLOUD_API_KEY?.trim();
|
|
307
|
+
if (cloudKey && !isRedactedSecret(cloudKey)) {
|
|
308
|
+
return cloudKey;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
function getTtsProviderStatus(ttsConfig) {
|
|
314
|
+
const resolved = resolveTtsConfig(ttsConfig);
|
|
315
|
+
return {
|
|
316
|
+
configuredProvider: ttsConfig?.provider || null,
|
|
317
|
+
hasApiKey: resolved ? resolved.provider !== "edge" : false,
|
|
318
|
+
resolvedProvider: resolved?.provider || null
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const ttsStreamBridge = new TtsStreamBridge();
|
|
322
|
+
export {
|
|
323
|
+
getTtsProviderStatus,
|
|
324
|
+
resolveTtsConfig,
|
|
325
|
+
ttsStreamBridge
|
|
326
|
+
};
|
|
327
|
+
//# sourceMappingURL=tts-stream-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/services/tts-stream-bridge.ts"],"sourcesContent":["/**\n * TTS Stream Bridge — generates TTS audio server-side and pipes PCM data\n * into FFmpeg's audio track for RTMP streaming.\n *\n * Uses pipe:3 (a 4th stdio fd) as the audio input to FFmpeg. Writes PCM\n * silence when idle and decoded TTS audio when speaking. This avoids FIFO\n * complexity and works cross-platform.\n *\n * @module services/tts-stream-bridge\n */\n\nimport { spawn } from \"node:child_process\";\nimport type { Writable } from \"node:stream\";\nimport { logger, sanitizeSpeechText } from \"@elizaos/core\";\n\nexport type TtsProvider = \"elevenlabs\" | \"openai\" | \"edge\" | (string & {});\n\nexport type TtsConfig = {\n enabled?: boolean;\n provider?: TtsProvider;\n elevenlabs?: {\n apiKey?: string;\n voiceId?: string;\n modelId?: string;\n voiceSettings?: Record<string, unknown>;\n };\n openai?: {\n apiKey?: string;\n model?: string;\n voice?: string;\n };\n edge?: {\n voice?: string;\n };\n};\n\nconst TAG = \"[TtsStreamBridge]\";\n\n// PCM format: signed 16-bit little-endian, 24 kHz, mono\nconst SAMPLE_RATE = 24000;\nconst CHANNELS = 1;\nconst BYTES_PER_SAMPLE = 2;\nconst CHUNK_MS = 50;\n/** Bytes per tick: 24000 * 2 * 1 * 50/1000 = 2400 */\nconst CHUNK_BYTES =\n (SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_MS) / 1000;\n\nconst ELEVENLABS_TIMEOUT_MS = 20_000;\nconst OPENAI_TIMEOUT_MS = 20_000;\n\n/** Resolved TTS configuration for a speak() call. */\nexport interface ResolvedTtsConfig {\n provider: TtsProvider;\n elevenlabs?: {\n apiKey: string;\n voiceId: string;\n modelId: string;\n voiceSettings?: Record<string, unknown>;\n };\n openai?: {\n apiKey: string;\n model: string;\n voice: string;\n };\n edge?: {\n voice: string;\n };\n}\n\n/** Public interface for the TTS stream bridge. */\nexport interface ITtsStreamBridge {\n attach(stream: Writable): void;\n detach(): void;\n isAttached(): boolean;\n isSpeaking(): boolean;\n speak(text: string, config: ResolvedTtsConfig): Promise<boolean>;\n}\n\nclass TtsStreamBridge implements ITtsStreamBridge {\n private writeStream: Writable | null = null;\n private tickTimer: ReturnType<typeof setInterval> | null = null;\n private pcmQueue: Buffer[] = [];\n private _speaking = false;\n private silenceChunk: Buffer;\n\n constructor() {\n // Pre-allocate a silence chunk (all zeros = silence in s16le)\n this.silenceChunk = Buffer.alloc(CHUNK_BYTES, 0);\n }\n\n /** Attach the bridge to an FFmpeg stdio pipe (pipe:3). */\n attach(stream: Writable): void {\n this.detach();\n this.writeStream = stream;\n stream.on(\"error\", (err) => {\n logger.warn(`${TAG} Write stream error: ${err.message}`);\n });\n // Start the tick loop — writes PCM chunks every CHUNK_MS\n this.tickTimer = setInterval(() => this.tick(), CHUNK_MS);\n logger.info(`${TAG} Attached to FFmpeg audio pipe`);\n }\n\n /** Detach from FFmpeg — stops tick loop and clears queue. */\n detach(): void {\n if (this.tickTimer) {\n clearInterval(this.tickTimer);\n this.tickTimer = null;\n }\n this.writeStream = null;\n this.pcmQueue = [];\n this._speaking = false;\n }\n\n /** Whether the bridge is currently attached to an FFmpeg process. */\n isAttached(): boolean {\n return this.writeStream !== null;\n }\n\n /** Whether TTS audio is currently being played. */\n isSpeaking(): boolean {\n return this._speaking;\n }\n\n /**\n * Generate TTS for the given text and queue PCM audio for playback.\n * Non-blocking — queues audio and returns immediately after generation.\n */\n async speak(text: string, config: ResolvedTtsConfig): Promise<boolean> {\n if (!this.writeStream) {\n logger.warn(`${TAG} Cannot speak — not attached to FFmpeg`);\n return false;\n }\n\n const speakableText = sanitizeSpeechText(text);\n if (!speakableText) return false;\n\n try {\n logger.info(\n `${TAG} Generating TTS (${config.provider}, ${speakableText.length} chars)`,\n );\n const mp3 = await this.generateTts(speakableText, config);\n if (!mp3 || mp3.length === 0) {\n logger.warn(`${TAG} TTS returned empty audio`);\n return false;\n }\n\n const pcm = await this.decodeMp3ToPcm(mp3);\n if (!pcm || pcm.length === 0) {\n logger.warn(`${TAG} PCM decode returned empty buffer`);\n return false;\n }\n\n // Split PCM into CHUNK_BYTES-sized buffers for smooth playback\n const chunks: Buffer[] = [];\n for (let i = 0; i < pcm.length; i += CHUNK_BYTES) {\n const end = Math.min(i + CHUNK_BYTES, pcm.length);\n const chunk = Buffer.alloc(CHUNK_BYTES, 0);\n pcm.copy(chunk, 0, i, end);\n chunks.push(chunk);\n }\n\n this.pcmQueue.push(...chunks);\n this._speaking = true;\n logger.info(\n `${TAG} Queued ${chunks.length} PCM chunks (${(pcm.length / SAMPLE_RATE / BYTES_PER_SAMPLE).toFixed(1)}s)`,\n );\n return true;\n } catch (err) {\n logger.error(`${TAG} TTS generation failed: ${String(err)}`);\n return false;\n }\n }\n\n /** Called every CHUNK_MS — writes next PCM chunk (TTS or silence) to FFmpeg. */\n private tick(): void {\n if (!this.writeStream) return;\n\n let chunk: Buffer;\n if (this.pcmQueue.length > 0) {\n chunk = this.pcmQueue.shift() as Buffer;\n if (this.pcmQueue.length === 0) {\n this._speaking = false;\n logger.info(`${TAG} Finished speaking`);\n }\n } else {\n chunk = this.silenceChunk;\n }\n\n try {\n this.writeStream.write(chunk);\n } catch {\n // Stream may have been closed — detach will handle cleanup\n }\n }\n\n // ── TTS generation ─────────────────────────────────────────────────────\n\n private async generateTts(\n text: string,\n config: ResolvedTtsConfig,\n ): Promise<Buffer> {\n switch (config.provider) {\n case \"elevenlabs\":\n return this.generateElevenlabs(text, config);\n case \"openai\":\n return this.generateOpenai(text, config);\n case \"edge\":\n return this.generateEdge(text, config);\n default:\n throw new Error(`Unknown TTS provider: ${config.provider}`);\n }\n }\n\n private async generateElevenlabs(\n text: string,\n config: ResolvedTtsConfig,\n ): Promise<Buffer> {\n const el = config.elevenlabs;\n if (!el?.apiKey) throw new Error(\"ElevenLabs API key not available\");\n\n const voiceId = el.voiceId || \"EXAVITQu4vr4xnSDxMaL\";\n const modelId = el.modelId || \"eleven_flash_v2_5\";\n const url = `https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voiceId)}/stream?output_format=mp3_22050_32`;\n\n const payload: Record<string, unknown> = {\n text,\n model_id: modelId,\n };\n if (el.voiceSettings && Object.keys(el.voiceSettings).length > 0) {\n payload.voice_settings = el.voiceSettings;\n }\n\n const resp = await fetchWithTimeout(\n url,\n {\n method: \"POST\",\n headers: {\n \"xi-api-key\": el.apiKey,\n \"Content-Type\": \"application/json\",\n Accept: \"audio/mpeg\",\n },\n body: JSON.stringify(payload),\n },\n ELEVENLABS_TIMEOUT_MS,\n );\n\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(`ElevenLabs ${resp.status}: ${body.slice(0, 200)}`);\n }\n\n return Buffer.from(await resp.arrayBuffer());\n }\n\n private async generateOpenai(\n text: string,\n config: ResolvedTtsConfig,\n ): Promise<Buffer> {\n const oai = config.openai;\n if (!oai?.apiKey) throw new Error(\"OpenAI API key not available\");\n\n const model = oai.model || \"tts-1\";\n const voice = oai.voice || \"alloy\";\n\n const resp = await fetchWithTimeout(\n \"https://api.openai.com/v1/audio/speech\",\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${oai.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n model,\n input: text,\n voice,\n response_format: \"mp3\",\n }),\n },\n OPENAI_TIMEOUT_MS,\n );\n\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(`OpenAI TTS ${resp.status}: ${body.slice(0, 200)}`);\n }\n\n return Buffer.from(await resp.arrayBuffer());\n }\n\n private async generateEdge(\n text: string,\n config: ResolvedTtsConfig,\n ): Promise<Buffer> {\n // Edge TTS requires node-edge-tts package — optional dependency\n try {\n // Use a variable so Vite's static analysis doesn't try to resolve this optional dep\n const edgeTtsModule = \"node-edge-tts\";\n const { MsEdgeTTS } = await import(edgeTtsModule);\n const tts = new MsEdgeTTS();\n const voice = config.edge?.voice || \"en-US-AriaNeural\";\n await tts.setMetadata(voice, \"audio-24khz-48kbitrate-mono-mp3\");\n const readable = tts.toStream(text);\n\n return new Promise<Buffer>((resolve, reject) => {\n const chunks: Buffer[] = [];\n readable.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n readable.on(\"end\", () => resolve(Buffer.concat(chunks)));\n readable.on(\"error\", reject);\n });\n } catch (err) {\n throw new Error(\n `Edge TTS failed (node-edge-tts may not be installed): ${String(err)}`,\n );\n }\n }\n\n // ── MP3 → PCM decode ──────────────────────────────────────────────────\n\n /** Decode MP3 audio to raw s16le PCM using an FFmpeg subprocess. */\n private decodeMp3ToPcm(mp3: Buffer): Promise<Buffer> {\n return new Promise<Buffer>((resolve, reject) => {\n const proc = spawn(\n \"ffmpeg\",\n [\n \"-i\",\n \"pipe:0\",\n \"-f\",\n \"s16le\",\n \"-ar\",\n String(SAMPLE_RATE),\n \"-ac\",\n String(CHANNELS),\n \"pipe:1\",\n ],\n { stdio: [\"pipe\", \"pipe\", \"ignore\"] },\n );\n\n const chunks: Buffer[] = [];\n proc.stdout?.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n proc.on(\"close\", (code) => {\n if (code === 0) {\n resolve(Buffer.concat(chunks));\n } else {\n reject(new Error(`FFmpeg decode exited with code ${code}`));\n }\n });\n proc.on(\"error\", reject);\n proc.stdin?.write(mp3);\n proc.stdin?.end();\n });\n }\n}\n\n// ── Helper ────────────────────────────────────────────────────────────────\n\nfunction fetchWithTimeout(\n url: string,\n init: RequestInit,\n timeoutMs: number,\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n return fetch(url, { ...init, signal: controller.signal }).finally(() =>\n clearTimeout(timer),\n );\n}\n\n// ── Config resolution ─────────────────────────────────────────────────────\n\n/** Helper to check if a string looks like a redacted secret placeholder. */\nfunction isRedactedSecret(val: string): boolean {\n return /^\\*+$/.test(val) || val === \"REDACTED\" || val === \"[REDACTED]\";\n}\n\n/**\n * Resolve TTS configuration from eliza config, finding the best available\n * provider with valid API keys.\n */\nexport function resolveTtsConfig(\n ttsConfig: TtsConfig | undefined,\n): ResolvedTtsConfig | null {\n if (!ttsConfig) return null;\n\n const preferredProvider = ttsConfig.provider || \"elevenlabs\";\n\n // Try providers in preference order: configured → elevenlabs → openai → edge\n const providers: TtsProvider[] = [preferredProvider];\n if (!providers.includes(\"elevenlabs\")) providers.push(\"elevenlabs\");\n if (!providers.includes(\"openai\")) providers.push(\"openai\");\n if (!providers.includes(\"edge\")) providers.push(\"edge\");\n\n for (const provider of providers) {\n switch (provider) {\n case \"elevenlabs\": {\n const el = ttsConfig.elevenlabs;\n const apiKey = resolveKey(el?.apiKey, \"ELEVENLABS_API_KEY\");\n if (apiKey) {\n return {\n provider: \"elevenlabs\",\n elevenlabs: {\n apiKey,\n voiceId: el?.voiceId || \"EXAVITQu4vr4xnSDxMaL\",\n modelId: el?.modelId || \"eleven_flash_v2_5\",\n voiceSettings: el?.voiceSettings\n ? { ...el.voiceSettings }\n : undefined,\n },\n };\n }\n break;\n }\n case \"openai\": {\n const oai = ttsConfig.openai;\n const apiKey = resolveKey(oai?.apiKey, \"OPENAI_API_KEY\");\n if (apiKey) {\n return {\n provider: \"openai\",\n openai: {\n apiKey,\n model: oai?.model || \"tts-1\",\n voice: oai?.voice || \"alloy\",\n },\n };\n }\n break;\n }\n case \"edge\": {\n // Edge TTS always works (no API key needed)\n return {\n provider: \"edge\",\n edge: {\n voice: ttsConfig.edge?.voice || \"en-US-AriaNeural\",\n },\n };\n }\n }\n }\n\n return null;\n}\n\nfunction resolveKey(\n configKey: string | undefined,\n envVar: string,\n): string | null {\n const ck = configKey?.trim();\n if (ck && !isRedactedSecret(ck)) return ck;\n const ev = process.env[envVar]?.trim();\n if (ev && !isRedactedSecret(ev)) return ev;\n\n const explicitCloudTts = process.env.ELIZAOS_CLOUD_USE_TTS === \"true\";\n const legacyCloudTts =\n process.env.ELIZAOS_CLOUD_USE_TTS === undefined &&\n process.env.ELIZAOS_CLOUD_ENABLED === \"true\" &&\n process.env.ELIZA_CLOUD_TTS_DISABLED !== \"true\";\n if (explicitCloudTts || legacyCloudTts) {\n const cloudKey = process.env.ELIZAOS_CLOUD_API_KEY?.trim();\n if (cloudKey && !isRedactedSecret(cloudKey)) {\n return cloudKey;\n }\n }\n\n return null;\n}\n\n/**\n * Get a summary of available TTS providers and their status.\n */\nexport function getTtsProviderStatus(ttsConfig: TtsConfig | undefined): {\n configuredProvider: string | null;\n hasApiKey: boolean;\n resolvedProvider: string | null;\n} {\n const resolved = resolveTtsConfig(ttsConfig);\n return {\n configuredProvider: ttsConfig?.provider || null,\n hasApiKey: resolved ? resolved.provider !== \"edge\" : false,\n resolvedProvider: resolved?.provider || null,\n };\n}\n\n// Module singleton\nexport const ttsStreamBridge = new TtsStreamBridge();\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elizaos/plugin-streaming",
|
|
3
|
+
"description": "Unified RTMP streaming for elizaOS (Twitch, YouTube, X, pump.fun, custom and named ingest URLs)",
|
|
4
|
+
"version": "2.0.0-beta.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"packageType": "plugin",
|
|
10
|
+
"platform": "node",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"exports": {
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@elizaos/cloud-routing": "2.0.0-beta.1",
|
|
25
|
+
"@elizaos/core": "2.0.0-beta.1",
|
|
26
|
+
"@elizaos/plugin-browser": "2.0.0-beta.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@eslint/js": "^10.0.0",
|
|
30
|
+
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
|
31
|
+
"@typescript-eslint/parser": "^8.22.0",
|
|
32
|
+
"eslint": "^10.0.0",
|
|
33
|
+
"prettier": "^3.8.3",
|
|
34
|
+
"tsup": "^8.5.1",
|
|
35
|
+
"typescript": "^6.0.3",
|
|
36
|
+
"vitest": "^4.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"dev": "tsup --watch",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"lint": "bunx @biomejs/biome check --write --unsafe src",
|
|
43
|
+
"lint:check": "bunx @biomejs/biome check src",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"clean": "rm -rf dist .turbo tsconfig.tsbuildinfo",
|
|
46
|
+
"format": "bunx @biomejs/biome format --write .",
|
|
47
|
+
"format:check": "bunx @biomejs/biome format ."
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/elizaos-plugins/plugin-streaming.git"
|
|
55
|
+
},
|
|
56
|
+
"keywords": [
|
|
57
|
+
"elizaos",
|
|
58
|
+
"elizaos-plugin",
|
|
59
|
+
"streaming",
|
|
60
|
+
"rtmp"
|
|
61
|
+
],
|
|
62
|
+
"agentConfig": {
|
|
63
|
+
"pluginType": "elizaos:plugin:1.0.0",
|
|
64
|
+
"pluginParameters": {
|
|
65
|
+
"TWITCH_STREAM_KEY": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"description": "Twitch RTMP stream key",
|
|
68
|
+
"required": false
|
|
69
|
+
},
|
|
70
|
+
"YOUTUBE_STREAM_KEY": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "YouTube Live stream key",
|
|
73
|
+
"required": false
|
|
74
|
+
},
|
|
75
|
+
"YOUTUBE_RTMP_URL": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Optional YouTube RTMP ingest URL override",
|
|
78
|
+
"required": false
|
|
79
|
+
},
|
|
80
|
+
"X_STREAM_KEY": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"required": false
|
|
83
|
+
},
|
|
84
|
+
"X_RTMP_URL": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"required": false
|
|
87
|
+
},
|
|
88
|
+
"PUMPFUN_STREAM_KEY": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"required": false
|
|
91
|
+
},
|
|
92
|
+
"PUMPFUN_RTMP_URL": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"required": false
|
|
95
|
+
},
|
|
96
|
+
"CUSTOM_RTMP_URL": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"required": false
|
|
99
|
+
},
|
|
100
|
+
"CUSTOM_RTMP_KEY": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"required": false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|