@livekit/agents-plugin-cartesia 1.5.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/tts.cjs +120 -38
- package/dist/tts.cjs.map +1 -1
- package/dist/tts.d.cts +7 -0
- package/dist/tts.d.ts +7 -0
- package/dist/tts.d.ts.map +1 -1
- package/dist/tts.js +121 -38
- package/dist/tts.js.map +1 -1
- package/dist/tts.test.cjs +204 -0
- package/dist/tts.test.cjs.map +1 -1
- package/dist/tts.test.js +207 -3
- package/dist/tts.test.js.map +1 -1
- package/package.json +11 -8
- package/src/tts.test.ts +252 -3
- package/src/tts.ts +177 -33
package/dist/tts.test.cjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var import_agents = require("@livekit/agents");
|
|
2
3
|
var import_agents_plugin_openai = require("@livekit/agents-plugin-openai");
|
|
3
4
|
var import_agents_plugins_test = require("@livekit/agents-plugins-test");
|
|
5
|
+
var import_node_events = require("node:events");
|
|
4
6
|
var import_vitest = require("vitest");
|
|
7
|
+
var import_ws = require("ws");
|
|
5
8
|
var import_tts = require("./tts.cjs");
|
|
6
9
|
const hasCartesiaConfig = Boolean(process.env.CARTESIA_API_KEY && process.env.OPENAI_API_KEY);
|
|
7
10
|
if (hasCartesiaConfig) {
|
|
@@ -14,4 +17,205 @@ if (hasCartesiaConfig) {
|
|
|
14
17
|
});
|
|
15
18
|
});
|
|
16
19
|
}
|
|
20
|
+
const CHUNK_BASE64 = Buffer.alloc(4800).toString("base64");
|
|
21
|
+
async function startWebSocketServer() {
|
|
22
|
+
const wss = new import_ws.WebSocketServer({ host: "127.0.0.1", port: 0 });
|
|
23
|
+
await (0, import_node_events.once)(wss, "listening");
|
|
24
|
+
const address = wss.address();
|
|
25
|
+
return { wss, baseURL: `http://127.0.0.1:${address.port}` };
|
|
26
|
+
}
|
|
27
|
+
async function closeWebSocketServer(wss) {
|
|
28
|
+
for (const client of wss.clients) {
|
|
29
|
+
client.close();
|
|
30
|
+
}
|
|
31
|
+
await new Promise((resolve) => wss.close(() => resolve()));
|
|
32
|
+
}
|
|
33
|
+
async function waitFor(promise, timeoutMs = 1e3) {
|
|
34
|
+
let timeout;
|
|
35
|
+
try {
|
|
36
|
+
return await Promise.race([
|
|
37
|
+
promise,
|
|
38
|
+
new Promise((_, reject) => {
|
|
39
|
+
timeout = setTimeout(() => reject(new Error("timed out waiting for promise")), timeoutMs);
|
|
40
|
+
})
|
|
41
|
+
]);
|
|
42
|
+
} finally {
|
|
43
|
+
if (timeout) clearTimeout(timeout);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function serveCartesia(wss, onStop) {
|
|
47
|
+
let connectionCount = 0;
|
|
48
|
+
wss.on("connection", (ws) => {
|
|
49
|
+
connectionCount++;
|
|
50
|
+
const connectionNumber = connectionCount;
|
|
51
|
+
ws.on("message", (raw) => {
|
|
52
|
+
const message = JSON.parse(raw.toString());
|
|
53
|
+
if (message.continue !== false) return;
|
|
54
|
+
const contextId = message.context_id;
|
|
55
|
+
if (onStop && !onStop(ws, contextId, connectionNumber)) return;
|
|
56
|
+
ws.send(
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
type: "chunk",
|
|
59
|
+
data: CHUNK_BASE64,
|
|
60
|
+
done: false,
|
|
61
|
+
status_code: 200,
|
|
62
|
+
step_time: 0,
|
|
63
|
+
context_id: contextId
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
ws.send(
|
|
67
|
+
JSON.stringify({ type: "done", done: true, status_code: 200, context_id: contextId })
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
return { connectionCount: () => connectionCount };
|
|
72
|
+
}
|
|
73
|
+
async function synthesizeTurn(cartesia, text, connOptions) {
|
|
74
|
+
const stream = cartesia.stream({ connOptions });
|
|
75
|
+
stream.pushText(text);
|
|
76
|
+
stream.endInput();
|
|
77
|
+
try {
|
|
78
|
+
const events = [];
|
|
79
|
+
for await (const event of stream) {
|
|
80
|
+
if (event !== import_agents.tts.SynthesizeStream.END_OF_STREAM) events.push(event);
|
|
81
|
+
}
|
|
82
|
+
return events;
|
|
83
|
+
} finally {
|
|
84
|
+
stream.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
(0, import_vitest.describe)("Cartesia streaming pool", () => {
|
|
88
|
+
(0, import_vitest.it)("reuses one websocket across sequential turns", async () => {
|
|
89
|
+
const { wss, baseURL } = await startWebSocketServer();
|
|
90
|
+
const server = serveCartesia(wss);
|
|
91
|
+
const cartesia = new import_tts.TTS({ apiKey: "test-key", baseUrl: baseURL });
|
|
92
|
+
try {
|
|
93
|
+
(0, import_vitest.expect)(await synthesizeTurn(cartesia, "first turn.")).not.toHaveLength(0);
|
|
94
|
+
(0, import_vitest.expect)(await synthesizeTurn(cartesia, "second turn.")).not.toHaveLength(0);
|
|
95
|
+
(0, import_vitest.expect)(server.connectionCount()).toBe(1);
|
|
96
|
+
} finally {
|
|
97
|
+
await cartesia.close();
|
|
98
|
+
await closeWebSocketServer(wss);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
(0, import_vitest.it)("prewarms and reuses the ready websocket", async () => {
|
|
102
|
+
const { wss, baseURL } = await startWebSocketServer();
|
|
103
|
+
const server = serveCartesia(wss);
|
|
104
|
+
const connected = (0, import_node_events.once)(wss, "connection");
|
|
105
|
+
const cartesia = new import_tts.TTS({ apiKey: "test-key", baseUrl: baseURL });
|
|
106
|
+
try {
|
|
107
|
+
cartesia.prewarm();
|
|
108
|
+
await waitFor(connected);
|
|
109
|
+
(0, import_vitest.expect)(await synthesizeTurn(cartesia, "prewarmed turn.")).not.toHaveLength(0);
|
|
110
|
+
(0, import_vitest.expect)(server.connectionCount()).toBe(1);
|
|
111
|
+
} finally {
|
|
112
|
+
await cartesia.close();
|
|
113
|
+
await closeWebSocketServer(wss);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
(0, import_vitest.it)("discards a poisoned websocket after a failure", async () => {
|
|
117
|
+
const { wss, baseURL } = await startWebSocketServer();
|
|
118
|
+
const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {
|
|
119
|
+
if (connectionNumber === 1) {
|
|
120
|
+
ws.close(1011, "provider failure");
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
const cartesia = new import_tts.TTS({ apiKey: "test-key", baseUrl: baseURL });
|
|
126
|
+
try {
|
|
127
|
+
(0, import_vitest.expect)(
|
|
128
|
+
await synthesizeTurn(cartesia, "failing turn.", {
|
|
129
|
+
...import_agents.DEFAULT_API_CONNECT_OPTIONS,
|
|
130
|
+
maxRetry: 0
|
|
131
|
+
})
|
|
132
|
+
).toHaveLength(0);
|
|
133
|
+
(0, import_vitest.expect)(await synthesizeTurn(cartesia, "recovery turn.")).not.toHaveLength(0);
|
|
134
|
+
(0, import_vitest.expect)(server.connectionCount()).toBe(2);
|
|
135
|
+
} finally {
|
|
136
|
+
await cartesia.close();
|
|
137
|
+
await closeWebSocketServer(wss);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
(0, import_vitest.it)("fails over when the socket drops mid-generation instead of ending silently", async () => {
|
|
141
|
+
const { wss, baseURL } = await startWebSocketServer();
|
|
142
|
+
const server = serveCartesia(wss, (ws, contextId, connectionNumber) => {
|
|
143
|
+
if (connectionNumber === 1) {
|
|
144
|
+
ws.send(
|
|
145
|
+
JSON.stringify({
|
|
146
|
+
type: "chunk",
|
|
147
|
+
data: CHUNK_BASE64,
|
|
148
|
+
done: false,
|
|
149
|
+
status_code: 200,
|
|
150
|
+
step_time: 0,
|
|
151
|
+
context_id: contextId
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
setTimeout(() => ws.close(1011, "mid-speech drop"), 5);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
});
|
|
159
|
+
const cartesia = new import_tts.TTS({ apiKey: "test-key", baseUrl: baseURL });
|
|
160
|
+
try {
|
|
161
|
+
(0, import_vitest.expect)(
|
|
162
|
+
await synthesizeTurn(cartesia, "dropping turn.", {
|
|
163
|
+
...import_agents.DEFAULT_API_CONNECT_OPTIONS,
|
|
164
|
+
maxRetry: 0
|
|
165
|
+
})
|
|
166
|
+
).toHaveLength(0);
|
|
167
|
+
(0, import_vitest.expect)(await synthesizeTurn(cartesia, "recovery turn.")).not.toHaveLength(0);
|
|
168
|
+
(0, import_vitest.expect)(server.connectionCount()).toBe(2);
|
|
169
|
+
} finally {
|
|
170
|
+
await cartesia.close();
|
|
171
|
+
await closeWebSocketServer(wss);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
(0, import_vitest.it)("replaces a websocket that closed while idle", async () => {
|
|
175
|
+
const { wss, baseURL } = await startWebSocketServer();
|
|
176
|
+
let firstConnectionClosed;
|
|
177
|
+
const firstClosed = new Promise((resolve) => {
|
|
178
|
+
firstConnectionClosed = resolve;
|
|
179
|
+
});
|
|
180
|
+
const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {
|
|
181
|
+
if (connectionNumber === 1) {
|
|
182
|
+
ws.on("close", () => firstConnectionClosed == null ? void 0 : firstConnectionClosed());
|
|
183
|
+
setTimeout(() => ws.close(), 10);
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
});
|
|
187
|
+
const cartesia = new import_tts.TTS({ apiKey: "test-key", baseUrl: baseURL });
|
|
188
|
+
try {
|
|
189
|
+
(0, import_vitest.expect)(await synthesizeTurn(cartesia, "first turn.")).not.toHaveLength(0);
|
|
190
|
+
await waitFor(firstClosed);
|
|
191
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
192
|
+
(0, import_vitest.expect)(
|
|
193
|
+
await waitFor(
|
|
194
|
+
synthesizeTurn(cartesia, "second turn.", { ...import_agents.DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 })
|
|
195
|
+
)
|
|
196
|
+
).not.toHaveLength(0);
|
|
197
|
+
(0, import_vitest.expect)(server.connectionCount()).toBe(2);
|
|
198
|
+
} finally {
|
|
199
|
+
await cartesia.close();
|
|
200
|
+
await closeWebSocketServer(wss);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
(0, import_vitest.it)("closes the pooled websocket when the TTS closes", async () => {
|
|
204
|
+
const { wss, baseURL } = await startWebSocketServer();
|
|
205
|
+
serveCartesia(wss);
|
|
206
|
+
const cartesia = new import_tts.TTS({ apiKey: "test-key", baseUrl: baseURL });
|
|
207
|
+
try {
|
|
208
|
+
await synthesizeTurn(cartesia, "closing turn.");
|
|
209
|
+
await cartesia.close();
|
|
210
|
+
await waitFor(
|
|
211
|
+
(async () => {
|
|
212
|
+
while (wss.clients.size > 0) await new Promise((r) => setTimeout(r, 5));
|
|
213
|
+
})()
|
|
214
|
+
);
|
|
215
|
+
(0, import_vitest.expect)(wss.clients.size).toBe(0);
|
|
216
|
+
} finally {
|
|
217
|
+
await closeWebSocketServer(wss);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
17
221
|
//# sourceMappingURL=tts.test.cjs.map
|
package/dist/tts.test.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tts.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { STT } from '@livekit/agents-plugin-openai';\nimport { tts } from '@livekit/agents-plugins-test';\nimport { describe, it } from 'vitest';\nimport { TTS } from './tts.js';\n\nconst hasCartesiaConfig = Boolean(process.env.CARTESIA_API_KEY && process.env.OPENAI_API_KEY);\n\nif (hasCartesiaConfig) {\n describe('Cartesia', async () => {\n await tts(new TTS(), new STT());\n });\n} else {\n describe('Cartesia', () => {\n it.skip('requires CARTESIA_API_KEY and OPENAI_API_KEY', () => {});\n });\n}\n"],"mappings":";AAGA,kCAAoB;AACpB,iCAAoB;AACpB,oBAA6B;AAC7B,iBAAoB;AAEpB,MAAM,oBAAoB,QAAQ,QAAQ,IAAI,oBAAoB,QAAQ,IAAI,cAAc;AAE5F,IAAI,mBAAmB;AACrB,8BAAS,YAAY,YAAY;AAC/B,cAAM,gCAAI,IAAI,eAAI,GAAG,IAAI,gCAAI,CAAC;AAAA,EAChC,CAAC;AACH,OAAO;AACL,8BAAS,YAAY,MAAM;AACzB,qBAAG,KAAK,gDAAgD,MAAM;AAAA,IAAC,CAAC;AAAA,EAClE,CAAC;AACH;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/tts.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS, tts } from '@livekit/agents';\nimport { STT } from '@livekit/agents-plugin-openai';\nimport { tts as testTts } from '@livekit/agents-plugins-test';\nimport { once } from 'node:events';\nimport type { AddressInfo } from 'node:net';\nimport { describe, expect, it } from 'vitest';\nimport { type WebSocket, WebSocketServer } from 'ws';\nimport { TTS } from './tts.js';\n\nconst hasCartesiaConfig = Boolean(process.env.CARTESIA_API_KEY && process.env.OPENAI_API_KEY);\n\nif (hasCartesiaConfig) {\n describe('Cartesia', async () => {\n await testTts(new TTS(), new STT());\n });\n} else {\n describe('Cartesia', () => {\n it.skip('requires CARTESIA_API_KEY and OPENAI_API_KEY', () => {});\n });\n}\n\n// A single 24 kHz mono s16le frame's worth of silence, base64-encoded the way\n// Cartesia sends audio chunks.\nconst CHUNK_BASE64 = Buffer.alloc(4800).toString('base64');\n\nasync function startWebSocketServer() {\n const wss = new WebSocketServer({ host: '127.0.0.1', port: 0 });\n await once(wss, 'listening');\n const address = wss.address() as AddressInfo;\n return { wss, baseURL: `http://127.0.0.1:${address.port}` };\n}\n\nasync function closeWebSocketServer(wss: WebSocketServer): Promise<void> {\n for (const client of wss.clients) {\n client.close();\n }\n await new Promise<void>((resolve) => wss.close(() => resolve()));\n}\n\nasync function waitFor<T>(promise: Promise<T>, timeoutMs = 1000): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('timed out waiting for promise')), timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\n// A minimal Cartesia TTS WebSocket server: for every generation it replies with\n// one audio chunk and a done message, echoing the caller's context_id. `onStop`\n// lets a test override the reply (e.g. to simulate a provider failure); return\n// false to suppress the normal chunk/done reply.\nfunction serveCartesia(\n wss: WebSocketServer,\n onStop?: (ws: WebSocket, contextId: string, connectionNumber: number) => boolean,\n): { connectionCount: () => number } {\n let connectionCount = 0;\n wss.on('connection', (ws) => {\n connectionCount++;\n const connectionNumber = connectionCount;\n ws.on('message', (raw) => {\n const message = JSON.parse(raw.toString()) as { context_id: string; continue?: boolean };\n if (message.continue !== false) return; // only reply once the turn is closed\n const contextId = message.context_id;\n if (onStop && !onStop(ws, contextId, connectionNumber)) return;\n ws.send(\n JSON.stringify({\n type: 'chunk',\n data: CHUNK_BASE64,\n done: false,\n status_code: 200,\n step_time: 0,\n context_id: contextId,\n }),\n );\n ws.send(\n JSON.stringify({ type: 'done', done: true, status_code: 200, context_id: contextId }),\n );\n });\n });\n return { connectionCount: () => connectionCount };\n}\n\nasync function synthesizeTurn(\n cartesia: TTS,\n text: string,\n connOptions?: APIConnectOptions,\n): Promise<tts.SynthesizedAudio[]> {\n const stream = cartesia.stream({ connOptions });\n stream.pushText(text);\n stream.endInput();\n\n try {\n const events: tts.SynthesizedAudio[] = [];\n for await (const event of stream) {\n if (event !== tts.SynthesizeStream.END_OF_STREAM) events.push(event);\n }\n return events;\n } finally {\n stream.close();\n }\n}\n\ndescribe('Cartesia streaming pool', () => {\n it('reuses one websocket across sequential turns', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n const server = serveCartesia(wss);\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(await synthesizeTurn(cartesia, 'first turn.')).not.toHaveLength(0);\n expect(await synthesizeTurn(cartesia, 'second turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(1);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('prewarms and reuses the ready websocket', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n const server = serveCartesia(wss);\n const connected = once(wss, 'connection');\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n cartesia.prewarm();\n await waitFor(connected);\n expect(await synthesizeTurn(cartesia, 'prewarmed turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(1);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('discards a poisoned websocket after a failure', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n // The first connection drops the turn; the second serves it normally.\n const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.close(1011, 'provider failure');\n return false;\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(\n await synthesizeTurn(cartesia, 'failing turn.', {\n ...DEFAULT_API_CONNECT_OPTIONS,\n maxRetry: 0,\n }),\n ).toHaveLength(0);\n expect(await synthesizeTurn(cartesia, 'recovery turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('fails over when the socket drops mid-generation instead of ending silently', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n // Connection 1 emits one audio chunk, then drops WITHOUT a done message,\n // i.e. mid-speech. Connection 2 serves the recovery turn normally.\n const server = serveCartesia(wss, (ws, contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.send(\n JSON.stringify({\n type: 'chunk',\n data: CHUNK_BASE64,\n done: false,\n status_code: 200,\n step_time: 0,\n context_id: contextId,\n }),\n );\n setTimeout(() => ws.close(1011, 'mid-speech drop'), 5);\n return false; // suppress the normal chunk/done reply\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n // The dropped turn does not complete successfully (it fails over rather\n // than silently ending); at maxRetry: 0 that surfaces as no audio.\n expect(\n await synthesizeTurn(cartesia, 'dropping turn.', {\n ...DEFAULT_API_CONNECT_OPTIONS,\n maxRetry: 0,\n }),\n ).toHaveLength(0);\n // The dead socket is discarded, so the next turn opens a fresh one.\n expect(await synthesizeTurn(cartesia, 'recovery turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('replaces a websocket that closed while idle', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n let firstConnectionClosed: (() => void) | undefined;\n const firstClosed = new Promise<void>((resolve) => {\n firstConnectionClosed = resolve;\n });\n const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.on('close', () => firstConnectionClosed?.());\n // Serve the turn, then drop the idle socket so the next turn reconnects.\n setTimeout(() => ws.close(), 10);\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(await synthesizeTurn(cartesia, 'first turn.')).not.toHaveLength(0);\n await waitFor(firstClosed);\n // Let the client observe the close so the idle handler removes the socket\n // before the next checkout, making the maxRetry: 0 assertion deterministic.\n await new Promise((resolve) => setTimeout(resolve, 100));\n // maxRetry: 0 proves the idle-closed socket was dropped from the pool, not\n // handed back to burn the turn's only attempt.\n expect(\n await waitFor(\n synthesizeTurn(cartesia, 'second turn.', { ...DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 }),\n ),\n ).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('closes the pooled websocket when the TTS closes', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n serveCartesia(wss);\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n await synthesizeTurn(cartesia, 'closing turn.');\n await cartesia.close();\n // close() drains the pooled socket; give the close frame a beat to land.\n await waitFor(\n (async () => {\n while (wss.clients.size > 0) await new Promise((r) => setTimeout(r, 5));\n })(),\n );\n expect(wss.clients.size).toBe(0);\n } finally {\n await closeWebSocketServer(wss);\n }\n });\n});\n"],"mappings":"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","names":["testTts"]}
|
package/dist/tts.test.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { DEFAULT_API_CONNECT_OPTIONS, tts } from "@livekit/agents";
|
|
1
2
|
import { STT } from "@livekit/agents-plugin-openai";
|
|
2
|
-
import { tts } from "@livekit/agents-plugins-test";
|
|
3
|
-
import {
|
|
3
|
+
import { tts as testTts } from "@livekit/agents-plugins-test";
|
|
4
|
+
import { once } from "node:events";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { WebSocketServer } from "ws";
|
|
4
7
|
import { TTS } from "./tts.js";
|
|
5
8
|
const hasCartesiaConfig = Boolean(process.env.CARTESIA_API_KEY && process.env.OPENAI_API_KEY);
|
|
6
9
|
if (hasCartesiaConfig) {
|
|
7
10
|
describe("Cartesia", async () => {
|
|
8
|
-
await
|
|
11
|
+
await testTts(new TTS(), new STT());
|
|
9
12
|
});
|
|
10
13
|
} else {
|
|
11
14
|
describe("Cartesia", () => {
|
|
@@ -13,4 +16,205 @@ if (hasCartesiaConfig) {
|
|
|
13
16
|
});
|
|
14
17
|
});
|
|
15
18
|
}
|
|
19
|
+
const CHUNK_BASE64 = Buffer.alloc(4800).toString("base64");
|
|
20
|
+
async function startWebSocketServer() {
|
|
21
|
+
const wss = new WebSocketServer({ host: "127.0.0.1", port: 0 });
|
|
22
|
+
await once(wss, "listening");
|
|
23
|
+
const address = wss.address();
|
|
24
|
+
return { wss, baseURL: `http://127.0.0.1:${address.port}` };
|
|
25
|
+
}
|
|
26
|
+
async function closeWebSocketServer(wss) {
|
|
27
|
+
for (const client of wss.clients) {
|
|
28
|
+
client.close();
|
|
29
|
+
}
|
|
30
|
+
await new Promise((resolve) => wss.close(() => resolve()));
|
|
31
|
+
}
|
|
32
|
+
async function waitFor(promise, timeoutMs = 1e3) {
|
|
33
|
+
let timeout;
|
|
34
|
+
try {
|
|
35
|
+
return await Promise.race([
|
|
36
|
+
promise,
|
|
37
|
+
new Promise((_, reject) => {
|
|
38
|
+
timeout = setTimeout(() => reject(new Error("timed out waiting for promise")), timeoutMs);
|
|
39
|
+
})
|
|
40
|
+
]);
|
|
41
|
+
} finally {
|
|
42
|
+
if (timeout) clearTimeout(timeout);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function serveCartesia(wss, onStop) {
|
|
46
|
+
let connectionCount = 0;
|
|
47
|
+
wss.on("connection", (ws) => {
|
|
48
|
+
connectionCount++;
|
|
49
|
+
const connectionNumber = connectionCount;
|
|
50
|
+
ws.on("message", (raw) => {
|
|
51
|
+
const message = JSON.parse(raw.toString());
|
|
52
|
+
if (message.continue !== false) return;
|
|
53
|
+
const contextId = message.context_id;
|
|
54
|
+
if (onStop && !onStop(ws, contextId, connectionNumber)) return;
|
|
55
|
+
ws.send(
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
type: "chunk",
|
|
58
|
+
data: CHUNK_BASE64,
|
|
59
|
+
done: false,
|
|
60
|
+
status_code: 200,
|
|
61
|
+
step_time: 0,
|
|
62
|
+
context_id: contextId
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
ws.send(
|
|
66
|
+
JSON.stringify({ type: "done", done: true, status_code: 200, context_id: contextId })
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
return { connectionCount: () => connectionCount };
|
|
71
|
+
}
|
|
72
|
+
async function synthesizeTurn(cartesia, text, connOptions) {
|
|
73
|
+
const stream = cartesia.stream({ connOptions });
|
|
74
|
+
stream.pushText(text);
|
|
75
|
+
stream.endInput();
|
|
76
|
+
try {
|
|
77
|
+
const events = [];
|
|
78
|
+
for await (const event of stream) {
|
|
79
|
+
if (event !== tts.SynthesizeStream.END_OF_STREAM) events.push(event);
|
|
80
|
+
}
|
|
81
|
+
return events;
|
|
82
|
+
} finally {
|
|
83
|
+
stream.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
describe("Cartesia streaming pool", () => {
|
|
87
|
+
it("reuses one websocket across sequential turns", async () => {
|
|
88
|
+
const { wss, baseURL } = await startWebSocketServer();
|
|
89
|
+
const server = serveCartesia(wss);
|
|
90
|
+
const cartesia = new TTS({ apiKey: "test-key", baseUrl: baseURL });
|
|
91
|
+
try {
|
|
92
|
+
expect(await synthesizeTurn(cartesia, "first turn.")).not.toHaveLength(0);
|
|
93
|
+
expect(await synthesizeTurn(cartesia, "second turn.")).not.toHaveLength(0);
|
|
94
|
+
expect(server.connectionCount()).toBe(1);
|
|
95
|
+
} finally {
|
|
96
|
+
await cartesia.close();
|
|
97
|
+
await closeWebSocketServer(wss);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
it("prewarms and reuses the ready websocket", async () => {
|
|
101
|
+
const { wss, baseURL } = await startWebSocketServer();
|
|
102
|
+
const server = serveCartesia(wss);
|
|
103
|
+
const connected = once(wss, "connection");
|
|
104
|
+
const cartesia = new TTS({ apiKey: "test-key", baseUrl: baseURL });
|
|
105
|
+
try {
|
|
106
|
+
cartesia.prewarm();
|
|
107
|
+
await waitFor(connected);
|
|
108
|
+
expect(await synthesizeTurn(cartesia, "prewarmed turn.")).not.toHaveLength(0);
|
|
109
|
+
expect(server.connectionCount()).toBe(1);
|
|
110
|
+
} finally {
|
|
111
|
+
await cartesia.close();
|
|
112
|
+
await closeWebSocketServer(wss);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
it("discards a poisoned websocket after a failure", async () => {
|
|
116
|
+
const { wss, baseURL } = await startWebSocketServer();
|
|
117
|
+
const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {
|
|
118
|
+
if (connectionNumber === 1) {
|
|
119
|
+
ws.close(1011, "provider failure");
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
});
|
|
124
|
+
const cartesia = new TTS({ apiKey: "test-key", baseUrl: baseURL });
|
|
125
|
+
try {
|
|
126
|
+
expect(
|
|
127
|
+
await synthesizeTurn(cartesia, "failing turn.", {
|
|
128
|
+
...DEFAULT_API_CONNECT_OPTIONS,
|
|
129
|
+
maxRetry: 0
|
|
130
|
+
})
|
|
131
|
+
).toHaveLength(0);
|
|
132
|
+
expect(await synthesizeTurn(cartesia, "recovery turn.")).not.toHaveLength(0);
|
|
133
|
+
expect(server.connectionCount()).toBe(2);
|
|
134
|
+
} finally {
|
|
135
|
+
await cartesia.close();
|
|
136
|
+
await closeWebSocketServer(wss);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
it("fails over when the socket drops mid-generation instead of ending silently", async () => {
|
|
140
|
+
const { wss, baseURL } = await startWebSocketServer();
|
|
141
|
+
const server = serveCartesia(wss, (ws, contextId, connectionNumber) => {
|
|
142
|
+
if (connectionNumber === 1) {
|
|
143
|
+
ws.send(
|
|
144
|
+
JSON.stringify({
|
|
145
|
+
type: "chunk",
|
|
146
|
+
data: CHUNK_BASE64,
|
|
147
|
+
done: false,
|
|
148
|
+
status_code: 200,
|
|
149
|
+
step_time: 0,
|
|
150
|
+
context_id: contextId
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
setTimeout(() => ws.close(1011, "mid-speech drop"), 5);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
});
|
|
158
|
+
const cartesia = new TTS({ apiKey: "test-key", baseUrl: baseURL });
|
|
159
|
+
try {
|
|
160
|
+
expect(
|
|
161
|
+
await synthesizeTurn(cartesia, "dropping turn.", {
|
|
162
|
+
...DEFAULT_API_CONNECT_OPTIONS,
|
|
163
|
+
maxRetry: 0
|
|
164
|
+
})
|
|
165
|
+
).toHaveLength(0);
|
|
166
|
+
expect(await synthesizeTurn(cartesia, "recovery turn.")).not.toHaveLength(0);
|
|
167
|
+
expect(server.connectionCount()).toBe(2);
|
|
168
|
+
} finally {
|
|
169
|
+
await cartesia.close();
|
|
170
|
+
await closeWebSocketServer(wss);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
it("replaces a websocket that closed while idle", async () => {
|
|
174
|
+
const { wss, baseURL } = await startWebSocketServer();
|
|
175
|
+
let firstConnectionClosed;
|
|
176
|
+
const firstClosed = new Promise((resolve) => {
|
|
177
|
+
firstConnectionClosed = resolve;
|
|
178
|
+
});
|
|
179
|
+
const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {
|
|
180
|
+
if (connectionNumber === 1) {
|
|
181
|
+
ws.on("close", () => firstConnectionClosed == null ? void 0 : firstConnectionClosed());
|
|
182
|
+
setTimeout(() => ws.close(), 10);
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
});
|
|
186
|
+
const cartesia = new TTS({ apiKey: "test-key", baseUrl: baseURL });
|
|
187
|
+
try {
|
|
188
|
+
expect(await synthesizeTurn(cartesia, "first turn.")).not.toHaveLength(0);
|
|
189
|
+
await waitFor(firstClosed);
|
|
190
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
191
|
+
expect(
|
|
192
|
+
await waitFor(
|
|
193
|
+
synthesizeTurn(cartesia, "second turn.", { ...DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 })
|
|
194
|
+
)
|
|
195
|
+
).not.toHaveLength(0);
|
|
196
|
+
expect(server.connectionCount()).toBe(2);
|
|
197
|
+
} finally {
|
|
198
|
+
await cartesia.close();
|
|
199
|
+
await closeWebSocketServer(wss);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
it("closes the pooled websocket when the TTS closes", async () => {
|
|
203
|
+
const { wss, baseURL } = await startWebSocketServer();
|
|
204
|
+
serveCartesia(wss);
|
|
205
|
+
const cartesia = new TTS({ apiKey: "test-key", baseUrl: baseURL });
|
|
206
|
+
try {
|
|
207
|
+
await synthesizeTurn(cartesia, "closing turn.");
|
|
208
|
+
await cartesia.close();
|
|
209
|
+
await waitFor(
|
|
210
|
+
(async () => {
|
|
211
|
+
while (wss.clients.size > 0) await new Promise((r) => setTimeout(r, 5));
|
|
212
|
+
})()
|
|
213
|
+
);
|
|
214
|
+
expect(wss.clients.size).toBe(0);
|
|
215
|
+
} finally {
|
|
216
|
+
await closeWebSocketServer(wss);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
|
16
220
|
//# sourceMappingURL=tts.test.js.map
|
package/dist/tts.test.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tts.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { STT } from '@livekit/agents-plugin-openai';\nimport { tts } from '@livekit/agents-plugins-test';\nimport { describe, it } from 'vitest';\nimport { TTS } from './tts.js';\n\nconst hasCartesiaConfig = Boolean(process.env.CARTESIA_API_KEY && process.env.OPENAI_API_KEY);\n\nif (hasCartesiaConfig) {\n describe('Cartesia', async () => {\n await tts(new TTS(), new STT());\n });\n} else {\n describe('Cartesia', () => {\n it.skip('requires CARTESIA_API_KEY and OPENAI_API_KEY', () => {});\n });\n}\n"],"mappings":"AAGA,SAAS,WAAW;AACpB,SAAS,WAAW;AACpB,SAAS,UAAU,UAAU;AAC7B,SAAS,WAAW;AAEpB,MAAM,oBAAoB,QAAQ,QAAQ,IAAI,oBAAoB,QAAQ,IAAI,cAAc;AAE5F,IAAI,mBAAmB;AACrB,WAAS,YAAY,YAAY;AAC/B,UAAM,IAAI,IAAI,IAAI,GAAG,IAAI,IAAI,CAAC;AAAA,EAChC,CAAC;AACH,OAAO;AACL,WAAS,YAAY,MAAM;AACzB,OAAG,KAAK,gDAAgD,MAAM;AAAA,IAAC,CAAC;AAAA,EAClE,CAAC;AACH;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/tts.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS, tts } from '@livekit/agents';\nimport { STT } from '@livekit/agents-plugin-openai';\nimport { tts as testTts } from '@livekit/agents-plugins-test';\nimport { once } from 'node:events';\nimport type { AddressInfo } from 'node:net';\nimport { describe, expect, it } from 'vitest';\nimport { type WebSocket, WebSocketServer } from 'ws';\nimport { TTS } from './tts.js';\n\nconst hasCartesiaConfig = Boolean(process.env.CARTESIA_API_KEY && process.env.OPENAI_API_KEY);\n\nif (hasCartesiaConfig) {\n describe('Cartesia', async () => {\n await testTts(new TTS(), new STT());\n });\n} else {\n describe('Cartesia', () => {\n it.skip('requires CARTESIA_API_KEY and OPENAI_API_KEY', () => {});\n });\n}\n\n// A single 24 kHz mono s16le frame's worth of silence, base64-encoded the way\n// Cartesia sends audio chunks.\nconst CHUNK_BASE64 = Buffer.alloc(4800).toString('base64');\n\nasync function startWebSocketServer() {\n const wss = new WebSocketServer({ host: '127.0.0.1', port: 0 });\n await once(wss, 'listening');\n const address = wss.address() as AddressInfo;\n return { wss, baseURL: `http://127.0.0.1:${address.port}` };\n}\n\nasync function closeWebSocketServer(wss: WebSocketServer): Promise<void> {\n for (const client of wss.clients) {\n client.close();\n }\n await new Promise<void>((resolve) => wss.close(() => resolve()));\n}\n\nasync function waitFor<T>(promise: Promise<T>, timeoutMs = 1000): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('timed out waiting for promise')), timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\n// A minimal Cartesia TTS WebSocket server: for every generation it replies with\n// one audio chunk and a done message, echoing the caller's context_id. `onStop`\n// lets a test override the reply (e.g. to simulate a provider failure); return\n// false to suppress the normal chunk/done reply.\nfunction serveCartesia(\n wss: WebSocketServer,\n onStop?: (ws: WebSocket, contextId: string, connectionNumber: number) => boolean,\n): { connectionCount: () => number } {\n let connectionCount = 0;\n wss.on('connection', (ws) => {\n connectionCount++;\n const connectionNumber = connectionCount;\n ws.on('message', (raw) => {\n const message = JSON.parse(raw.toString()) as { context_id: string; continue?: boolean };\n if (message.continue !== false) return; // only reply once the turn is closed\n const contextId = message.context_id;\n if (onStop && !onStop(ws, contextId, connectionNumber)) return;\n ws.send(\n JSON.stringify({\n type: 'chunk',\n data: CHUNK_BASE64,\n done: false,\n status_code: 200,\n step_time: 0,\n context_id: contextId,\n }),\n );\n ws.send(\n JSON.stringify({ type: 'done', done: true, status_code: 200, context_id: contextId }),\n );\n });\n });\n return { connectionCount: () => connectionCount };\n}\n\nasync function synthesizeTurn(\n cartesia: TTS,\n text: string,\n connOptions?: APIConnectOptions,\n): Promise<tts.SynthesizedAudio[]> {\n const stream = cartesia.stream({ connOptions });\n stream.pushText(text);\n stream.endInput();\n\n try {\n const events: tts.SynthesizedAudio[] = [];\n for await (const event of stream) {\n if (event !== tts.SynthesizeStream.END_OF_STREAM) events.push(event);\n }\n return events;\n } finally {\n stream.close();\n }\n}\n\ndescribe('Cartesia streaming pool', () => {\n it('reuses one websocket across sequential turns', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n const server = serveCartesia(wss);\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(await synthesizeTurn(cartesia, 'first turn.')).not.toHaveLength(0);\n expect(await synthesizeTurn(cartesia, 'second turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(1);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('prewarms and reuses the ready websocket', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n const server = serveCartesia(wss);\n const connected = once(wss, 'connection');\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n cartesia.prewarm();\n await waitFor(connected);\n expect(await synthesizeTurn(cartesia, 'prewarmed turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(1);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('discards a poisoned websocket after a failure', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n // The first connection drops the turn; the second serves it normally.\n const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.close(1011, 'provider failure');\n return false;\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(\n await synthesizeTurn(cartesia, 'failing turn.', {\n ...DEFAULT_API_CONNECT_OPTIONS,\n maxRetry: 0,\n }),\n ).toHaveLength(0);\n expect(await synthesizeTurn(cartesia, 'recovery turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('fails over when the socket drops mid-generation instead of ending silently', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n // Connection 1 emits one audio chunk, then drops WITHOUT a done message,\n // i.e. mid-speech. Connection 2 serves the recovery turn normally.\n const server = serveCartesia(wss, (ws, contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.send(\n JSON.stringify({\n type: 'chunk',\n data: CHUNK_BASE64,\n done: false,\n status_code: 200,\n step_time: 0,\n context_id: contextId,\n }),\n );\n setTimeout(() => ws.close(1011, 'mid-speech drop'), 5);\n return false; // suppress the normal chunk/done reply\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n // The dropped turn does not complete successfully (it fails over rather\n // than silently ending); at maxRetry: 0 that surfaces as no audio.\n expect(\n await synthesizeTurn(cartesia, 'dropping turn.', {\n ...DEFAULT_API_CONNECT_OPTIONS,\n maxRetry: 0,\n }),\n ).toHaveLength(0);\n // The dead socket is discarded, so the next turn opens a fresh one.\n expect(await synthesizeTurn(cartesia, 'recovery turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('replaces a websocket that closed while idle', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n let firstConnectionClosed: (() => void) | undefined;\n const firstClosed = new Promise<void>((resolve) => {\n firstConnectionClosed = resolve;\n });\n const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.on('close', () => firstConnectionClosed?.());\n // Serve the turn, then drop the idle socket so the next turn reconnects.\n setTimeout(() => ws.close(), 10);\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(await synthesizeTurn(cartesia, 'first turn.')).not.toHaveLength(0);\n await waitFor(firstClosed);\n // Let the client observe the close so the idle handler removes the socket\n // before the next checkout, making the maxRetry: 0 assertion deterministic.\n await new Promise((resolve) => setTimeout(resolve, 100));\n // maxRetry: 0 proves the idle-closed socket was dropped from the pool, not\n // handed back to burn the turn's only attempt.\n expect(\n await waitFor(\n synthesizeTurn(cartesia, 'second turn.', { ...DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 }),\n ),\n ).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('closes the pooled websocket when the TTS closes', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n serveCartesia(wss);\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n await synthesizeTurn(cartesia, 'closing turn.');\n await cartesia.close();\n // close() drains the pooled socket; give the close frame a beat to land.\n await waitFor(\n (async () => {\n while (wss.clients.size > 0) await new Promise((r) => setTimeout(r, 5));\n })(),\n );\n expect(wss.clients.size).toBe(0);\n } finally {\n await closeWebSocketServer(wss);\n }\n });\n});\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livekit/agents-plugin-cartesia",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Cartesia plugin for LiveKit Node Agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"require": "dist/index.cjs",
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
},
|
|
18
18
|
"author": "LiveKit",
|
|
19
19
|
"type": "module",
|
|
20
|
-
"repository":
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git@github.com:livekit/agents-js.git"
|
|
23
|
+
},
|
|
21
24
|
"license": "Apache-2.0",
|
|
22
25
|
"files": [
|
|
23
26
|
"dist",
|
|
@@ -30,18 +33,18 @@
|
|
|
30
33
|
"@types/ws": "^8.5.10",
|
|
31
34
|
"tsup": "^8.3.5",
|
|
32
35
|
"typescript": "^5.0.0",
|
|
33
|
-
"@livekit/agents": "1.
|
|
34
|
-
"@livekit/agents-plugin-openai": "1.
|
|
35
|
-
"@livekit/agents-plugin-silero": "1.
|
|
36
|
-
"@livekit/agents-plugins-test": "1.
|
|
36
|
+
"@livekit/agents": "1.6.0",
|
|
37
|
+
"@livekit/agents-plugin-openai": "1.6.0",
|
|
38
|
+
"@livekit/agents-plugin-silero": "1.6.0",
|
|
39
|
+
"@livekit/agents-plugins-test": "1.6.0"
|
|
37
40
|
},
|
|
38
41
|
"dependencies": {
|
|
39
|
-
"ws": "^8.
|
|
42
|
+
"ws": "^8.21.0"
|
|
40
43
|
},
|
|
41
44
|
"peerDependencies": {
|
|
42
45
|
"@livekit/rtc-node": "^0.13.31",
|
|
43
46
|
"zod": "^3.25.76 || ^4.1.8",
|
|
44
|
-
"@livekit/agents": "1.
|
|
47
|
+
"@livekit/agents": "1.6.0"
|
|
45
48
|
},
|
|
46
49
|
"scripts": {
|
|
47
50
|
"build": "tsup --onSuccess \"pnpm build:types\"",
|