@elizaos/plugin-streaming 2.0.0-beta.1 → 2.0.3-beta.3

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.
@@ -1,4 +1,7 @@
1
- import { sanitizeSpeechText } from "@elizaos/core";
1
+ import {
2
+ ModelType,
3
+ sanitizeSpeechText
4
+ } from "@elizaos/core";
2
5
  async function handleTtsRoutes(ctx) {
3
6
  const { req, res, method, pathname, state, json, error, readJsonBody } = ctx;
4
7
  if (method === "GET" && pathname === "/api/tts/config") {
@@ -35,6 +38,56 @@ async function handleTtsRoutes(ctx) {
35
38
  });
36
39
  return true;
37
40
  }
41
+ if (method === "POST" && pathname === "/api/tts/local-inference") {
42
+ const body = await readJsonBody(req, res);
43
+ if (!body) return true;
44
+ const text = typeof body.text === "string" ? sanitizeLocalInferenceSpeechText(body.text) : "";
45
+ if (!text) {
46
+ error(res, "Missing text", 400);
47
+ return true;
48
+ }
49
+ const runtime = state.runtime;
50
+ if (!runtime) {
51
+ error(res, "Local inference TEXT_TO_SPEECH is not available", 503);
52
+ return true;
53
+ }
54
+ try {
55
+ const voice = optionalString(body.voiceId) ?? optionalString(body.voice);
56
+ const model = optionalString(body.model);
57
+ const modelId = optionalString(body.modelId);
58
+ const speed = optionalPositiveNumber(body.speed);
59
+ const sampleRate = optionalPositiveNumber(body.sampleRate);
60
+ const format = optionalAudioFormat(body.format);
61
+ const audio = await useLocalInferenceTts(runtime, {
62
+ text,
63
+ ...voice ? { voice } : {},
64
+ ...model ? { model } : {},
65
+ ...modelId ? { modelId } : {},
66
+ ...speed ? { speed } : {},
67
+ ...sampleRate ? { sampleRate } : {},
68
+ ...format ? { format } : {}
69
+ });
70
+ const bytes = normalizeAudioBytes(audio);
71
+ if (bytes.length === 0) {
72
+ error(res, "Local inference TEXT_TO_SPEECH returned empty audio", 502);
73
+ return true;
74
+ }
75
+ res.writeHead(200, {
76
+ "Content-Type": sniffAudioContentType(bytes),
77
+ "Cache-Control": "no-store",
78
+ "Content-Length": String(bytes.byteLength)
79
+ });
80
+ res.end(Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength));
81
+ return true;
82
+ } catch (err) {
83
+ error(
84
+ res,
85
+ `Local inference TTS error: ${err instanceof Error ? err.message : String(err)}`,
86
+ 502
87
+ );
88
+ return true;
89
+ }
90
+ }
38
91
  if (method === "POST" && pathname === "/api/tts/elevenlabs") {
39
92
  const body = await readJsonBody(req, res);
40
93
  if (!body) return true;
@@ -152,6 +205,79 @@ async function handleTtsRoutes(ctx) {
152
205
  }
153
206
  return false;
154
207
  }
208
+ const LOCAL_TTS_PROVIDER_IDS = [
209
+ "eliza-local-inference",
210
+ "capacitor-llama",
211
+ "eliza-device-bridge",
212
+ "eliza-aosp-llama"
213
+ ];
214
+ async function useLocalInferenceTts(runtime, request) {
215
+ let lastError;
216
+ for (const provider of LOCAL_TTS_PROVIDER_IDS) {
217
+ try {
218
+ return await runtime.useModel(
219
+ ModelType.TEXT_TO_SPEECH,
220
+ request,
221
+ provider
222
+ );
223
+ } catch (err) {
224
+ lastError = err;
225
+ if (!isMissingProviderError(err)) {
226
+ throw err;
227
+ }
228
+ }
229
+ }
230
+ if (lastError instanceof Error) {
231
+ throw lastError;
232
+ }
233
+ throw new Error("No local-inference TEXT_TO_SPEECH provider is registered");
234
+ }
235
+ const ALLOWED_TTS_FORMATS = /* @__PURE__ */ new Set(["wav", "mp3", "ogg", "flac", "pcm"]);
236
+ function optionalString(value) {
237
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
238
+ }
239
+ function optionalAudioFormat(value) {
240
+ const s = optionalString(value);
241
+ return s && ALLOWED_TTS_FORMATS.has(s) ? s : void 0;
242
+ }
243
+ function optionalPositiveNumber(value) {
244
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
245
+ }
246
+ function isMissingProviderError(error) {
247
+ return error instanceof Error && /No handler found for delegate type: TEXT_TO_SPEECH/.test(error.message);
248
+ }
249
+ function normalizeAudioBytes(value) {
250
+ if (value instanceof Uint8Array) {
251
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
252
+ }
253
+ return new Uint8Array(value);
254
+ }
255
+ function sniffAudioContentType(bytes) {
256
+ if (bytes.length >= 12 && bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 65 && bytes[10] === 86 && bytes[11] === 69) {
257
+ return "audio/wav";
258
+ }
259
+ if (bytes.length >= 3 && bytes[0] === 73 && bytes[1] === 68 && bytes[2] === 51) {
260
+ return "audio/mpeg";
261
+ }
262
+ if (bytes.length >= 2 && bytes[0] === 255 && (bytes[1] & 224) === 224) {
263
+ return "audio/mpeg";
264
+ }
265
+ return "application/octet-stream";
266
+ }
267
+ function sanitizeLocalInferenceSpeechText(input) {
268
+ let text = input.normalize("NFKC");
269
+ text = text.replace(/<think\b[^>]*>[\s\S]*?(?:<\/think>|$)/gi, " ");
270
+ text = text.replace(
271
+ /<(analysis|reasoning|tool_calls?|tools?)\b[^>]*>[\s\S]*?(?:<\/\1>|$)/gi,
272
+ " "
273
+ );
274
+ text = text.replace(/```[\s\S]*?```/g, " ");
275
+ text = text.replace(/`([^`]+)`/g, "$1");
276
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
277
+ text = text.replace(/<[^>\n]+>/g, " ");
278
+ text = text.replace(/\bhttps?:\/\/\S+/gi, " ");
279
+ return text.replace(/\s+/g, " ").trim();
280
+ }
155
281
  export {
156
282
  handleTtsRoutes
157
283
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/api/tts-routes.ts"],"sourcesContent":["import type http from \"node:http\";\nimport { type ReadJsonBodyOptions, sanitizeSpeechText } from \"@elizaos/core\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TtsRouteContext {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n method: string;\n pathname: string;\n state: { config: Record<string, unknown> };\n json: (res: http.ServerResponse, data: unknown, status?: number) => void;\n error: (res: http.ServerResponse, message: string, status?: number) => void;\n readJsonBody: <T extends object>(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n options?: ReadJsonBodyOptions,\n ) => Promise<T | null>;\n isRedactedSecretValue: (value: unknown) => boolean;\n fetchWithTimeoutGuard: (\n url: string,\n init: RequestInit,\n timeoutMs: number,\n ) => Promise<Response>;\n streamResponseBodyWithByteLimit: (\n upstream: Response,\n res: http.ServerResponse,\n maxBytes: number,\n timeoutMs: number,\n ) => Promise<void>;\n responseContentLength: (headers: Pick<Headers, \"get\">) => number | null;\n isAbortError: (error: unknown) => boolean;\n ELEVENLABS_FETCH_TIMEOUT_MS: number;\n ELEVENLABS_AUDIO_MAX_BYTES: number;\n}\n\n// ---------------------------------------------------------------------------\n// Route handler\n// ---------------------------------------------------------------------------\n\nexport async function handleTtsRoutes(ctx: TtsRouteContext): Promise<boolean> {\n const { req, res, method, pathname, state, json, error, readJsonBody } = ctx;\n\n // ── GET /api/tts/config ───────────────────────────────────────────────\n if (method === \"GET\" && pathname === \"/api/tts/config\") {\n const messages =\n state.config && typeof state.config === \"object\"\n ? ((state.config as Record<string, unknown>).messages as\n | Record<string, unknown>\n | undefined)\n : undefined;\n const tts =\n messages && typeof messages === \"object\"\n ? ((messages.tts as Record<string, unknown>) ?? undefined)\n : undefined;\n\n const elevenlabs =\n tts && typeof tts === \"object\"\n ? ((tts.elevenlabs as Record<string, unknown>) ?? undefined)\n : undefined;\n const edge =\n tts && typeof tts === \"object\"\n ? ((tts.edge as Record<string, unknown>) ?? undefined)\n : undefined;\n const openai =\n tts && typeof tts === \"object\"\n ? ((tts.openai as Record<string, unknown>) ?? undefined)\n : undefined;\n\n json(res, {\n provider: typeof tts?.provider === \"string\" ? tts.provider : undefined,\n mode: typeof tts?.mode === \"string\" ? tts.mode : undefined,\n auto: typeof tts?.auto === \"string\" ? tts.auto : undefined,\n enabled: tts?.enabled === true,\n elevenlabs: elevenlabs\n ? {\n apiKey:\n typeof elevenlabs.apiKey === \"string\" &&\n elevenlabs.apiKey.trim() &&\n !ctx.isRedactedSecretValue(elevenlabs.apiKey)\n ? \"[REDACTED]\"\n : undefined,\n voiceId:\n typeof elevenlabs.voiceId === \"string\"\n ? elevenlabs.voiceId\n : undefined,\n modelId:\n typeof elevenlabs.modelId === \"string\"\n ? elevenlabs.modelId\n : undefined,\n stability:\n typeof (\n elevenlabs.voiceSettings as Record<string, unknown> | undefined\n )?.stability === \"number\"\n ? ((elevenlabs.voiceSettings as Record<string, unknown>)\n .stability as number)\n : undefined,\n similarityBoost:\n typeof (\n elevenlabs.voiceSettings as Record<string, unknown> | undefined\n )?.similarityBoost === \"number\"\n ? ((elevenlabs.voiceSettings as Record<string, unknown>)\n .similarityBoost as number)\n : undefined,\n speed:\n typeof (\n elevenlabs.voiceSettings as Record<string, unknown> | undefined\n )?.speed === \"number\"\n ? ((elevenlabs.voiceSettings as Record<string, unknown>)\n .speed as number)\n : undefined,\n }\n : undefined,\n edge: edge\n ? {\n voice: typeof edge.voice === \"string\" ? edge.voice : undefined,\n lang: typeof edge.lang === \"string\" ? edge.lang : undefined,\n rate: typeof edge.rate === \"string\" ? edge.rate : undefined,\n pitch: typeof edge.pitch === \"string\" ? edge.pitch : undefined,\n volume: typeof edge.volume === \"string\" ? edge.volume : undefined,\n }\n : undefined,\n openai: openai\n ? {\n apiKey:\n typeof openai.apiKey === \"string\" &&\n openai.apiKey.trim() &&\n !ctx.isRedactedSecretValue(openai.apiKey)\n ? \"[REDACTED]\"\n : undefined,\n model: typeof openai.model === \"string\" ? openai.model : undefined,\n voice: typeof openai.voice === \"string\" ? openai.voice : undefined,\n }\n : undefined,\n });\n return true;\n }\n\n // ── POST /api/tts/elevenlabs ─────────────────────────────────────────\n if (method === \"POST\" && pathname === \"/api/tts/elevenlabs\") {\n const body = await readJsonBody<{\n text?: string;\n voiceId?: string;\n modelId?: string;\n outputFormat?: string;\n apiKey?: string;\n apply_text_normalization?: \"auto\" | \"on\" | \"off\";\n voice_settings?: {\n stability?: number;\n similarity_boost?: number;\n speed?: number;\n };\n }>(req, res);\n if (!body) return true;\n\n const text =\n typeof body.text === \"string\" ? sanitizeSpeechText(body.text) : \"\";\n if (!text) {\n error(res, \"Missing text\", 400);\n return true;\n }\n\n const messages =\n state.config && typeof state.config === \"object\"\n ? ((state.config as Record<string, unknown>).messages as\n | Record<string, unknown>\n | undefined)\n : undefined;\n const tts =\n messages && typeof messages === \"object\"\n ? ((messages.tts as Record<string, unknown>) ?? undefined)\n : undefined;\n const eleven =\n tts && typeof tts === \"object\"\n ? ((tts.elevenlabs as Record<string, unknown>) ?? undefined)\n : undefined;\n\n const requestedApiKey =\n typeof body.apiKey === \"string\" ? body.apiKey.trim() : \"\";\n const configuredApiKey =\n typeof eleven?.apiKey === \"string\" ? eleven.apiKey.trim() : \"\";\n const envApiKey =\n typeof process.env.ELEVENLABS_API_KEY === \"string\"\n ? process.env.ELEVENLABS_API_KEY.trim()\n : \"\";\n\n const resolvedApiKey =\n requestedApiKey && !ctx.isRedactedSecretValue(requestedApiKey)\n ? requestedApiKey\n : configuredApiKey && !ctx.isRedactedSecretValue(configuredApiKey)\n ? configuredApiKey\n : envApiKey && !ctx.isRedactedSecretValue(envApiKey)\n ? envApiKey\n : \"\";\n\n if (!resolvedApiKey) {\n error(\n res,\n \"ElevenLabs API key is not available. Set ELEVENLABS_API_KEY in Secrets.\",\n 400,\n );\n return true;\n }\n\n const voiceId =\n (typeof body.voiceId === \"string\" && body.voiceId.trim()) ||\n (typeof eleven?.voiceId === \"string\" && eleven.voiceId.trim()) ||\n \"EXAVITQu4vr4xnSDxMaL\";\n const modelId =\n (typeof body.modelId === \"string\" && body.modelId.trim()) ||\n (typeof eleven?.modelId === \"string\" && eleven.modelId.trim()) ||\n \"eleven_flash_v2_5\";\n const outputFormat =\n (typeof body.outputFormat === \"string\" && body.outputFormat.trim()) ||\n \"mp3_22050_32\";\n\n const requestedVoiceSettings =\n body.voice_settings &&\n typeof body.voice_settings === \"object\" &&\n !Array.isArray(body.voice_settings)\n ? body.voice_settings\n : undefined;\n\n const voiceSettings: Record<string, number> = {};\n const stability = requestedVoiceSettings?.stability;\n if (typeof stability === \"number\" && stability >= 0 && stability <= 1) {\n voiceSettings.stability = stability;\n }\n const similarityBoost = requestedVoiceSettings?.similarity_boost;\n if (\n typeof similarityBoost === \"number\" &&\n similarityBoost >= 0 &&\n similarityBoost <= 1\n ) {\n voiceSettings.similarity_boost = similarityBoost;\n }\n const speed = requestedVoiceSettings?.speed;\n if (typeof speed === \"number\" && speed >= 0.5 && speed <= 2) {\n voiceSettings.speed = speed;\n }\n\n const payload: Record<string, unknown> = {\n text,\n model_id: modelId,\n apply_text_normalization:\n body.apply_text_normalization === \"on\" ||\n body.apply_text_normalization === \"off\"\n ? body.apply_text_normalization\n : \"auto\",\n };\n if (Object.keys(voiceSettings).length > 0) {\n payload.voice_settings = voiceSettings;\n }\n\n try {\n const upstreamUrl = new URL(\n `https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voiceId)}/stream`,\n );\n upstreamUrl.searchParams.set(\"output_format\", outputFormat);\n\n const upstream = await ctx.fetchWithTimeoutGuard(\n upstreamUrl.toString(),\n {\n method: \"POST\",\n headers: {\n \"xi-api-key\": resolvedApiKey,\n \"Content-Type\": \"application/json\",\n Accept: \"audio/mpeg\",\n },\n body: JSON.stringify(payload),\n },\n ctx.ELEVENLABS_FETCH_TIMEOUT_MS,\n );\n\n if (!upstream.ok) {\n const upstreamBody = await upstream.text().catch(() => \"\");\n error(\n res,\n `ElevenLabs request failed (${upstream.status}): ${upstreamBody.slice(0, 240)}`,\n upstream.status === 429 ? 429 : 502,\n );\n return true;\n }\n\n const contentType = upstream.headers.get(\"content-type\") || \"audio/mpeg\";\n const contentLength = ctx.responseContentLength(upstream.headers);\n if (\n contentLength !== null &&\n contentLength > ctx.ELEVENLABS_AUDIO_MAX_BYTES\n ) {\n error(\n res,\n `ElevenLabs response exceeds maximum size of ${ctx.ELEVENLABS_AUDIO_MAX_BYTES} bytes`,\n 502,\n );\n return true;\n }\n\n res.writeHead(200, {\n \"Content-Type\": contentType,\n \"Cache-Control\": \"no-store\",\n ...(contentLength !== null\n ? { \"Content-Length\": String(contentLength) }\n : {}),\n });\n\n await ctx.streamResponseBodyWithByteLimit(\n upstream,\n res,\n ctx.ELEVENLABS_AUDIO_MAX_BYTES,\n ctx.ELEVENLABS_FETCH_TIMEOUT_MS,\n );\n res.end();\n return true;\n } catch (err) {\n if (res.headersSent) {\n res.destroy(\n err instanceof Error\n ? err\n : new Error(\n `ElevenLabs proxy error: ${typeof err === \"string\" ? err : String(err)}`,\n ),\n );\n return true;\n }\n error(\n res,\n `ElevenLabs proxy error: ${err instanceof Error ? err.message : String(err)}`,\n ctx.isAbortError(err) ? 504 : 502,\n );\n return true;\n }\n }\n\n return false;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/api/tts-routes.ts"],"sourcesContent":["import type http from \"node:http\";\nimport {\n type IAgentRuntime,\n ModelType,\n type ReadJsonBodyOptions,\n sanitizeSpeechText,\n} from \"@elizaos/core\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TtsRouteContext {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n method: string;\n pathname: string;\n state: { config: Record<string, unknown>; runtime?: IAgentRuntime | null };\n json: (res: http.ServerResponse, data: unknown, status?: number) => void;\n error: (res: http.ServerResponse, message: string, status?: number) => void;\n readJsonBody: <T extends object>(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n options?: ReadJsonBodyOptions,\n ) => Promise<T | null>;\n isRedactedSecretValue: (value: unknown) => boolean;\n fetchWithTimeoutGuard: (\n url: string,\n init: RequestInit,\n timeoutMs: number,\n ) => Promise<Response>;\n streamResponseBodyWithByteLimit: (\n upstream: Response,\n res: http.ServerResponse,\n maxBytes: number,\n timeoutMs: number,\n ) => Promise<void>;\n responseContentLength: (headers: Pick<Headers, \"get\">) => number | null;\n isAbortError: (error: unknown) => boolean;\n ELEVENLABS_FETCH_TIMEOUT_MS: number;\n ELEVENLABS_AUDIO_MAX_BYTES: number;\n}\n\n// ---------------------------------------------------------------------------\n// Route handler\n// ---------------------------------------------------------------------------\n\nexport async function handleTtsRoutes(ctx: TtsRouteContext): Promise<boolean> {\n const { req, res, method, pathname, state, json, error, readJsonBody } = ctx;\n\n // ── GET /api/tts/config ───────────────────────────────────────────────\n if (method === \"GET\" && pathname === \"/api/tts/config\") {\n const messages =\n state.config && typeof state.config === \"object\"\n ? ((state.config as Record<string, unknown>).messages as\n | Record<string, unknown>\n | undefined)\n : undefined;\n const tts =\n messages && typeof messages === \"object\"\n ? ((messages.tts as Record<string, unknown>) ?? undefined)\n : undefined;\n\n const elevenlabs =\n tts && typeof tts === \"object\"\n ? ((tts.elevenlabs as Record<string, unknown>) ?? undefined)\n : undefined;\n const edge =\n tts && typeof tts === \"object\"\n ? ((tts.edge as Record<string, unknown>) ?? undefined)\n : undefined;\n const openai =\n tts && typeof tts === \"object\"\n ? ((tts.openai as Record<string, unknown>) ?? undefined)\n : undefined;\n\n json(res, {\n provider: typeof tts?.provider === \"string\" ? tts.provider : undefined,\n mode: typeof tts?.mode === \"string\" ? tts.mode : undefined,\n auto: typeof tts?.auto === \"string\" ? tts.auto : undefined,\n enabled: tts?.enabled === true,\n elevenlabs: elevenlabs\n ? {\n apiKey:\n typeof elevenlabs.apiKey === \"string\" &&\n elevenlabs.apiKey.trim() &&\n !ctx.isRedactedSecretValue(elevenlabs.apiKey)\n ? \"[REDACTED]\"\n : undefined,\n voiceId:\n typeof elevenlabs.voiceId === \"string\"\n ? elevenlabs.voiceId\n : undefined,\n modelId:\n typeof elevenlabs.modelId === \"string\"\n ? elevenlabs.modelId\n : undefined,\n stability:\n typeof (\n elevenlabs.voiceSettings as Record<string, unknown> | undefined\n )?.stability === \"number\"\n ? ((elevenlabs.voiceSettings as Record<string, unknown>)\n .stability as number)\n : undefined,\n similarityBoost:\n typeof (\n elevenlabs.voiceSettings as Record<string, unknown> | undefined\n )?.similarityBoost === \"number\"\n ? ((elevenlabs.voiceSettings as Record<string, unknown>)\n .similarityBoost as number)\n : undefined,\n speed:\n typeof (\n elevenlabs.voiceSettings as Record<string, unknown> | undefined\n )?.speed === \"number\"\n ? ((elevenlabs.voiceSettings as Record<string, unknown>)\n .speed as number)\n : undefined,\n }\n : undefined,\n edge: edge\n ? {\n voice: typeof edge.voice === \"string\" ? edge.voice : undefined,\n lang: typeof edge.lang === \"string\" ? edge.lang : undefined,\n rate: typeof edge.rate === \"string\" ? edge.rate : undefined,\n pitch: typeof edge.pitch === \"string\" ? edge.pitch : undefined,\n volume: typeof edge.volume === \"string\" ? edge.volume : undefined,\n }\n : undefined,\n openai: openai\n ? {\n apiKey:\n typeof openai.apiKey === \"string\" &&\n openai.apiKey.trim() &&\n !ctx.isRedactedSecretValue(openai.apiKey)\n ? \"[REDACTED]\"\n : undefined,\n model: typeof openai.model === \"string\" ? openai.model : undefined,\n voice: typeof openai.voice === \"string\" ? openai.voice : undefined,\n }\n : undefined,\n });\n return true;\n }\n\n // ── POST /api/tts/local-inference ──────────────────────────────────────\n if (method === \"POST\" && pathname === \"/api/tts/local-inference\") {\n const body = await readJsonBody<LocalInferenceTtsRequestBody>(req, res);\n if (!body) return true;\n\n const text =\n typeof body.text === \"string\"\n ? sanitizeLocalInferenceSpeechText(body.text)\n : \"\";\n if (!text) {\n error(res, \"Missing text\", 400);\n return true;\n }\n\n const runtime = state.runtime;\n if (!runtime) {\n error(res, \"Local inference TEXT_TO_SPEECH is not available\", 503);\n return true;\n }\n\n try {\n const voice = optionalString(body.voiceId) ?? optionalString(body.voice);\n const model = optionalString(body.model);\n const modelId = optionalString(body.modelId);\n const speed = optionalPositiveNumber(body.speed);\n const sampleRate = optionalPositiveNumber(body.sampleRate);\n const format = optionalAudioFormat(body.format);\n const audio = await useLocalInferenceTts(runtime, {\n text,\n ...(voice ? { voice } : {}),\n ...(model ? { model } : {}),\n ...(modelId ? { modelId } : {}),\n ...(speed ? { speed } : {}),\n ...(sampleRate ? { sampleRate } : {}),\n ...(format ? { format } : {}),\n });\n const bytes = normalizeAudioBytes(audio);\n if (bytes.length === 0) {\n error(res, \"Local inference TEXT_TO_SPEECH returned empty audio\", 502);\n return true;\n }\n res.writeHead(200, {\n \"Content-Type\": sniffAudioContentType(bytes),\n \"Cache-Control\": \"no-store\",\n \"Content-Length\": String(bytes.byteLength),\n });\n res.end(Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength));\n return true;\n } catch (err) {\n error(\n res,\n `Local inference TTS error: ${err instanceof Error ? err.message : String(err)}`,\n 502,\n );\n return true;\n }\n }\n\n // ── POST /api/tts/elevenlabs ─────────────────────────────────────────\n if (method === \"POST\" && pathname === \"/api/tts/elevenlabs\") {\n const body = await readJsonBody<{\n text?: string;\n voiceId?: string;\n modelId?: string;\n outputFormat?: string;\n apiKey?: string;\n apply_text_normalization?: \"auto\" | \"on\" | \"off\";\n voice_settings?: {\n stability?: number;\n similarity_boost?: number;\n speed?: number;\n };\n }>(req, res);\n if (!body) return true;\n\n const text =\n typeof body.text === \"string\" ? sanitizeSpeechText(body.text) : \"\";\n if (!text) {\n error(res, \"Missing text\", 400);\n return true;\n }\n\n const messages =\n state.config && typeof state.config === \"object\"\n ? ((state.config as Record<string, unknown>).messages as\n | Record<string, unknown>\n | undefined)\n : undefined;\n const tts =\n messages && typeof messages === \"object\"\n ? ((messages.tts as Record<string, unknown>) ?? undefined)\n : undefined;\n const eleven =\n tts && typeof tts === \"object\"\n ? ((tts.elevenlabs as Record<string, unknown>) ?? undefined)\n : undefined;\n\n const requestedApiKey =\n typeof body.apiKey === \"string\" ? body.apiKey.trim() : \"\";\n const configuredApiKey =\n typeof eleven?.apiKey === \"string\" ? eleven.apiKey.trim() : \"\";\n const envApiKey =\n typeof process.env.ELEVENLABS_API_KEY === \"string\"\n ? process.env.ELEVENLABS_API_KEY.trim()\n : \"\";\n\n const resolvedApiKey =\n requestedApiKey && !ctx.isRedactedSecretValue(requestedApiKey)\n ? requestedApiKey\n : configuredApiKey && !ctx.isRedactedSecretValue(configuredApiKey)\n ? configuredApiKey\n : envApiKey && !ctx.isRedactedSecretValue(envApiKey)\n ? envApiKey\n : \"\";\n\n if (!resolvedApiKey) {\n error(\n res,\n \"ElevenLabs API key is not available. Set ELEVENLABS_API_KEY in Secrets.\",\n 400,\n );\n return true;\n }\n\n const voiceId =\n (typeof body.voiceId === \"string\" && body.voiceId.trim()) ||\n (typeof eleven?.voiceId === \"string\" && eleven.voiceId.trim()) ||\n \"EXAVITQu4vr4xnSDxMaL\";\n const modelId =\n (typeof body.modelId === \"string\" && body.modelId.trim()) ||\n (typeof eleven?.modelId === \"string\" && eleven.modelId.trim()) ||\n \"eleven_flash_v2_5\";\n const outputFormat =\n (typeof body.outputFormat === \"string\" && body.outputFormat.trim()) ||\n \"mp3_22050_32\";\n\n const requestedVoiceSettings =\n body.voice_settings &&\n typeof body.voice_settings === \"object\" &&\n !Array.isArray(body.voice_settings)\n ? body.voice_settings\n : undefined;\n\n const voiceSettings: Record<string, number> = {};\n const stability = requestedVoiceSettings?.stability;\n if (typeof stability === \"number\" && stability >= 0 && stability <= 1) {\n voiceSettings.stability = stability;\n }\n const similarityBoost = requestedVoiceSettings?.similarity_boost;\n if (\n typeof similarityBoost === \"number\" &&\n similarityBoost >= 0 &&\n similarityBoost <= 1\n ) {\n voiceSettings.similarity_boost = similarityBoost;\n }\n const speed = requestedVoiceSettings?.speed;\n if (typeof speed === \"number\" && speed >= 0.5 && speed <= 2) {\n voiceSettings.speed = speed;\n }\n\n const payload: Record<string, unknown> = {\n text,\n model_id: modelId,\n apply_text_normalization:\n body.apply_text_normalization === \"on\" ||\n body.apply_text_normalization === \"off\"\n ? body.apply_text_normalization\n : \"auto\",\n };\n if (Object.keys(voiceSettings).length > 0) {\n payload.voice_settings = voiceSettings;\n }\n\n try {\n const upstreamUrl = new URL(\n `https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voiceId)}/stream`,\n );\n upstreamUrl.searchParams.set(\"output_format\", outputFormat);\n\n const upstream = await ctx.fetchWithTimeoutGuard(\n upstreamUrl.toString(),\n {\n method: \"POST\",\n headers: {\n \"xi-api-key\": resolvedApiKey,\n \"Content-Type\": \"application/json\",\n Accept: \"audio/mpeg\",\n },\n body: JSON.stringify(payload),\n },\n ctx.ELEVENLABS_FETCH_TIMEOUT_MS,\n );\n\n if (!upstream.ok) {\n const upstreamBody = await upstream.text().catch(() => \"\");\n error(\n res,\n `ElevenLabs request failed (${upstream.status}): ${upstreamBody.slice(0, 240)}`,\n upstream.status === 429 ? 429 : 502,\n );\n return true;\n }\n\n const contentType = upstream.headers.get(\"content-type\") || \"audio/mpeg\";\n const contentLength = ctx.responseContentLength(upstream.headers);\n if (\n contentLength !== null &&\n contentLength > ctx.ELEVENLABS_AUDIO_MAX_BYTES\n ) {\n error(\n res,\n `ElevenLabs response exceeds maximum size of ${ctx.ELEVENLABS_AUDIO_MAX_BYTES} bytes`,\n 502,\n );\n return true;\n }\n\n res.writeHead(200, {\n \"Content-Type\": contentType,\n \"Cache-Control\": \"no-store\",\n ...(contentLength !== null\n ? { \"Content-Length\": String(contentLength) }\n : {}),\n });\n\n await ctx.streamResponseBodyWithByteLimit(\n upstream,\n res,\n ctx.ELEVENLABS_AUDIO_MAX_BYTES,\n ctx.ELEVENLABS_FETCH_TIMEOUT_MS,\n );\n res.end();\n return true;\n } catch (err) {\n if (res.headersSent) {\n res.destroy(\n err instanceof Error\n ? err\n : new Error(\n `ElevenLabs proxy error: ${typeof err === \"string\" ? err : String(err)}`,\n ),\n );\n return true;\n }\n error(\n res,\n `ElevenLabs proxy error: ${err instanceof Error ? err.message : String(err)}`,\n ctx.isAbortError(err) ? 504 : 502,\n );\n return true;\n }\n }\n\n return false;\n}\n\nconst LOCAL_TTS_PROVIDER_IDS = [\n \"eliza-local-inference\",\n \"capacitor-llama\",\n \"eliza-device-bridge\",\n \"eliza-aosp-llama\",\n] as const;\n\ninterface LocalInferenceTtsRequestBody {\n text?: string;\n voice?: string;\n voiceId?: string;\n model?: string;\n modelId?: string;\n speed?: number;\n sampleRate?: number;\n format?: string;\n}\n\ninterface LocalInferenceTtsRequest {\n text: string;\n voice?: string;\n model?: string;\n modelId?: string;\n speed?: number;\n sampleRate?: number;\n format?: string;\n}\n\nasync function useLocalInferenceTts(\n runtime: IAgentRuntime,\n request: LocalInferenceTtsRequest,\n): Promise<Buffer | Uint8Array | ArrayBuffer> {\n let lastError: unknown;\n for (const provider of LOCAL_TTS_PROVIDER_IDS) {\n try {\n return await runtime.useModel(\n ModelType.TEXT_TO_SPEECH,\n request,\n provider,\n );\n } catch (err) {\n lastError = err;\n if (!isMissingProviderError(err)) {\n throw err;\n }\n }\n }\n if (lastError instanceof Error) {\n throw lastError;\n }\n throw new Error(\"No local-inference TEXT_TO_SPEECH provider is registered\");\n}\n\nconst ALLOWED_TTS_FORMATS = new Set([\"wav\", \"mp3\", \"ogg\", \"flac\", \"pcm\"]);\n\nfunction optionalString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction optionalAudioFormat(value: unknown): string | undefined {\n const s = optionalString(value);\n return s && ALLOWED_TTS_FORMATS.has(s) ? s : undefined;\n}\n\nfunction optionalPositiveNumber(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value) && value > 0\n ? value\n : undefined;\n}\n\nfunction isMissingProviderError(error: unknown): boolean {\n return (\n error instanceof Error &&\n /No handler found for delegate type: TEXT_TO_SPEECH/.test(error.message)\n );\n}\n\nfunction normalizeAudioBytes(\n value: Buffer | Uint8Array | ArrayBuffer,\n): Uint8Array {\n if (value instanceof Uint8Array) {\n return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);\n }\n return new Uint8Array(value);\n}\n\nfunction sniffAudioContentType(bytes: Uint8Array): string {\n if (\n bytes.length >= 12 &&\n bytes[0] === 0x52 &&\n bytes[1] === 0x49 &&\n bytes[2] === 0x46 &&\n bytes[3] === 0x46 &&\n bytes[8] === 0x57 &&\n bytes[9] === 0x41 &&\n bytes[10] === 0x56 &&\n bytes[11] === 0x45\n ) {\n return \"audio/wav\";\n }\n if (\n bytes.length >= 3 &&\n bytes[0] === 0x49 &&\n bytes[1] === 0x44 &&\n bytes[2] === 0x33\n ) {\n return \"audio/mpeg\";\n }\n if (bytes.length >= 2 && bytes[0] === 0xff && (bytes[1] & 0xe0) === 0xe0) {\n return \"audio/mpeg\";\n }\n return \"application/octet-stream\";\n}\n\nfunction sanitizeLocalInferenceSpeechText(input: string): string {\n let text = input.normalize(\"NFKC\");\n text = text.replace(/<think\\b[^>]*>[\\s\\S]*?(?:<\\/think>|$)/gi, \" \");\n text = text.replace(\n /<(analysis|reasoning|tool_calls?|tools?)\\b[^>]*>[\\s\\S]*?(?:<\\/\\1>|$)/gi,\n \" \",\n );\n text = text.replace(/```[\\s\\S]*?```/g, \" \");\n text = text.replace(/`([^`]+)`/g, \"$1\");\n text = text.replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, \"$1\");\n text = text.replace(/<[^>\\n]+>/g, \" \");\n text = text.replace(/\\bhttps?:\\/\\/\\S+/gi, \" \");\n return text.replace(/\\s+/g, \" \").trim();\n}\n"],"mappings":"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","names":[]}
package/dist/core.d.ts CHANGED
@@ -1,11 +1,9 @@
1
- import { IAgentRuntime, Plugin, Action, Provider } from '@elizaos/core';
2
-
3
1
  /**
4
2
  * Shared RTMP streaming utilities: destinations, cloud relay, overlay presets,
5
3
  * and pipeline control actions (local FFmpeg via dashboard API).
6
4
  */
7
-
8
- interface OverlayWidgetInstance {
5
+ import type { Action, IAgentRuntime, Plugin, Provider } from "@elizaos/core";
6
+ export interface OverlayWidgetInstance {
9
7
  id: string;
10
8
  type: string;
11
9
  enabled: boolean;
@@ -18,12 +16,12 @@ interface OverlayWidgetInstance {
18
16
  zIndex: number;
19
17
  config: Record<string, unknown>;
20
18
  }
21
- interface OverlayLayoutData {
19
+ export interface OverlayLayoutData {
22
20
  version: 1;
23
21
  name: string;
24
22
  widgets: OverlayWidgetInstance[];
25
23
  }
26
- interface StreamingDestination {
24
+ export interface StreamingDestination {
27
25
  id: string;
28
26
  name: string;
29
27
  getCredentials(): Promise<{
@@ -35,7 +33,7 @@ interface StreamingDestination {
35
33
  /** Per-destination default overlay layout, seeded on first stream start. */
36
34
  defaultOverlayLayout?: OverlayLayoutData;
37
35
  }
38
- interface StreamingPluginConfig {
36
+ export interface StreamingPluginConfig {
39
37
  /** Short lowercase identifier, e.g. "twitch" or "youtube" */
40
38
  platformId: string;
41
39
  /** Display name, e.g. "Twitch" or "YouTube" */
@@ -71,18 +69,18 @@ interface StreamingPluginConfig {
71
69
  * Build a preset overlay layout with the given widget types enabled.
72
70
  * Widget types not listed in `enabledTypes` are included but disabled.
73
71
  */
74
- declare function buildPresetLayout(name: string, enabledTypes: string[]): OverlayLayoutData;
75
- declare function createNamedRtmpDestination(params: {
72
+ export declare function buildPresetLayout(name: string, enabledTypes: string[]): OverlayLayoutData;
73
+ export declare function createNamedRtmpDestination(params: {
76
74
  id: string;
77
75
  name?: string;
78
76
  rtmpUrl: string;
79
77
  rtmpKey: string;
80
78
  }): StreamingDestination;
81
- declare function createCustomRtmpDestination(config?: {
79
+ export declare function createCustomRtmpDestination(config?: {
82
80
  rtmpUrl?: string;
83
81
  rtmpKey?: string;
84
82
  }): StreamingDestination;
85
- declare function createStreamingDestination(cfg: StreamingPluginConfig, overrides?: {
83
+ export declare function createStreamingDestination(cfg: StreamingPluginConfig, overrides?: {
86
84
  streamKey?: string;
87
85
  rtmpUrl?: string;
88
86
  }): StreamingDestination;
@@ -93,7 +91,7 @@ declare function createStreamingDestination(cfg: StreamingPluginConfig, override
93
91
  * per-session ingest URL + stream key. The cloud forwards the inbound
94
92
  * stream to the user's stored destinations for `platformId`.
95
93
  */
96
- interface CloudRelayDestinationCfg {
94
+ export interface CloudRelayDestinationCfg {
97
95
  /** Short lowercase platform identifier — e.g. "twitch", "youtube". */
98
96
  platformId: string;
99
97
  /** Display name — e.g. "Twitch", "YouTube". */
@@ -117,8 +115,8 @@ interface CloudRelayDestinationCfg {
117
115
  *
118
116
  * Throws if Eliza Cloud is not connected.
119
117
  */
120
- declare function createCloudRelayDestination(cfg: CloudRelayDestinationCfg): StreamingDestination;
121
- type StreamingBackend = "direct" | "cloud" | "auto";
118
+ export declare function createCloudRelayDestination(cfg: CloudRelayDestinationCfg): StreamingDestination;
119
+ export type StreamingBackend = "direct" | "cloud" | "auto";
122
120
  /**
123
121
  * Resolve which streaming backend to use for a given platform at runtime.
124
122
  *
@@ -128,22 +126,22 @@ type StreamingBackend = "direct" | "cloud" | "auto";
128
126
  * `auto` picks `cloud` iff Eliza Cloud is connected AND no local stream key
129
127
  * is set in `cfg.streamKeyEnvVar`. Otherwise it picks `direct`.
130
128
  */
131
- declare function resolveStreamingBackend(runtime: IAgentRuntime, cfg: StreamingPluginConfig): "direct" | "cloud";
132
- declare function streamingPipelineLocalPort(): number;
133
- declare const STREAMING_PLATFORMS: readonly ["twitch", "youtube", "x", "pumpfun"];
134
- type StreamingPlatform = (typeof STREAMING_PLATFORMS)[number];
135
- type StreamingOp = "start" | "stop" | "status";
136
- interface BuildStreamOpActionParams {
129
+ export declare function resolveStreamingBackend(runtime: IAgentRuntime, cfg: StreamingPluginConfig): "direct" | "cloud";
130
+ export declare function streamingPipelineLocalPort(): number;
131
+ export declare const STREAMING_PLATFORMS: readonly ["twitch", "youtube", "x", "pumpfun"];
132
+ export type StreamingPlatform = (typeof STREAMING_PLATFORMS)[number];
133
+ export type StreamingOp = "start" | "stop" | "status";
134
+ export interface BuildStreamOpActionParams {
137
135
  validate?: () => Promise<boolean>;
138
136
  }
139
- declare function buildStreamOpAction(params?: BuildStreamOpActionParams): Action;
137
+ export declare function buildStreamOpAction(params?: BuildStreamOpActionParams): Action;
140
138
  /**
141
139
  * Provider that renders the live status of every supported streaming platform
142
140
  * as JSON context. The pipeline currently exposes a single shared
143
141
  * `/api/stream/status` endpoint, so each platform row reflects that same
144
142
  * snapshot tagged with its destination label.
145
143
  */
146
- declare const streamStatusProvider: Provider;
144
+ export declare const streamStatusProvider: Provider;
147
145
  /**
148
146
  * Build a complete elizaOS Plugin for a streaming destination.
149
147
  *
@@ -152,13 +150,11 @@ declare const streamStatusProvider: Provider;
152
150
  * - `createDestination` -- the destination factory (for the streaming pipeline)
153
151
  */
154
152
  /** Result of {@link createStreamingPlugin} — plugin + a backend-aware destination factory. */
155
- interface CreatedStreamingPlugin {
153
+ export interface CreatedStreamingPlugin {
156
154
  plugin: Plugin;
157
155
  createDestination: (runtime?: IAgentRuntime, overrides?: {
158
156
  streamKey?: string;
159
157
  rtmpUrl?: string;
160
158
  }) => StreamingDestination;
161
159
  }
162
- declare function createStreamingPlugin(cfg: StreamingPluginConfig): CreatedStreamingPlugin;
163
-
164
- export { type BuildStreamOpActionParams, type CloudRelayDestinationCfg, type CreatedStreamingPlugin, type OverlayLayoutData, type OverlayWidgetInstance, STREAMING_PLATFORMS, type StreamingBackend, type StreamingDestination, type StreamingOp, type StreamingPlatform, type StreamingPluginConfig, buildPresetLayout, buildStreamOpAction, createCloudRelayDestination, createCustomRtmpDestination, createNamedRtmpDestination, createStreamingDestination, createStreamingPlugin, resolveStreamingBackend, streamStatusProvider, streamingPipelineLocalPort };
160
+ export declare function createStreamingPlugin(cfg: StreamingPluginConfig): CreatedStreamingPlugin;
package/dist/core.js CHANGED
@@ -259,26 +259,6 @@ async function fetchStreamStatus(platform) {
259
259
  }
260
260
  function buildStreamOpAction(params = {}) {
261
261
  const validate = params.validate ?? (async () => true);
262
- const platformParam = {
263
- name: "platform",
264
- description: "Streaming destination platform: twitch, youtube, x, or pumpfun.",
265
- descriptionCompressed: "Platform: twitch|youtube|x|pumpfun.",
266
- required: true,
267
- schema: {
268
- type: "string",
269
- enum: [...STREAMING_PLATFORMS]
270
- }
271
- };
272
- const opParam = {
273
- name: "subaction",
274
- description: "Operation to perform: start (go live), stop (go offline), or status.",
275
- descriptionCompressed: "Op: start|stop|status.",
276
- required: true,
277
- schema: {
278
- type: "string",
279
- enum: ["start", "stop", "status"]
280
- }
281
- };
282
262
  return {
283
263
  name: "STREAM",
284
264
  contexts: ["media", "automation", "connectors"],
@@ -295,18 +275,49 @@ function buildStreamOpAction(params = {}) {
295
275
  "STREAM_STATUS",
296
276
  "IS_LIVE"
297
277
  ],
298
- parameters: [platformParam, opParam],
278
+ parameters: [
279
+ {
280
+ name: "platform",
281
+ description: "Streaming destination platform: twitch, youtube, x, or pumpfun.",
282
+ descriptionCompressed: "Platform: twitch|youtube|x|pumpfun.",
283
+ required: true,
284
+ schema: {
285
+ type: "string",
286
+ enum: [...STREAMING_PLATFORMS]
287
+ }
288
+ },
289
+ {
290
+ name: "action",
291
+ description: "Operation to perform: start (go live), stop (go offline), or status.",
292
+ descriptionCompressed: "Op: start|stop|status.",
293
+ required: true,
294
+ schema: {
295
+ type: "string",
296
+ enum: ["start", "stop", "status"]
297
+ }
298
+ },
299
+ {
300
+ name: "subaction",
301
+ description: "Legacy alias for action.",
302
+ descriptionCompressed: "Legacy action alias.",
303
+ required: false,
304
+ schema: {
305
+ type: "string",
306
+ enum: ["start", "stop", "status"]
307
+ }
308
+ }
309
+ ],
299
310
  validate,
300
311
  handler: async (_runtime, _message, _state, options, callback) => {
301
312
  const platformRaw = readParam(options, "platform");
302
- const opRaw = readParam(options, "op");
313
+ const opRaw = readParam(options, "action") ?? readParam(options, "subaction") ?? readParam(options, "op") ?? readParam(options, "operation");
303
314
  if (!isStreamingPlatform(platformRaw)) {
304
315
  const text = `STREAM_OP requires platform in {${STREAMING_PLATFORMS.join(", ")}}, got ${String(platformRaw)}`;
305
316
  if (callback) await callback({ text, actions: [] });
306
317
  return { success: false, error: text };
307
318
  }
308
319
  if (!isStreamingOp(opRaw)) {
309
- const text = `STREAM_OP requires op in {start, stop, status}, got ${String(opRaw)}`;
320
+ const text = `STREAM requires action in {start, stop, status}, got ${String(opRaw)}`;
310
321
  if (callback) await callback({ text, actions: [] });
311
322
  return { success: false, error: text };
312
323
  }