@jchaffin/voicekit 0.2.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/README.md +369 -0
- package/dist/adapters/deepgram.d.mts +43 -0
- package/dist/adapters/deepgram.d.ts +43 -0
- package/dist/adapters/deepgram.js +216 -0
- package/dist/adapters/deepgram.mjs +162 -0
- package/dist/adapters/elevenlabs.d.mts +41 -0
- package/dist/adapters/elevenlabs.d.ts +41 -0
- package/dist/adapters/elevenlabs.js +304 -0
- package/dist/adapters/elevenlabs.mjs +250 -0
- package/dist/adapters/livekit.d.mts +44 -0
- package/dist/adapters/livekit.d.ts +44 -0
- package/dist/adapters/livekit.js +225 -0
- package/dist/adapters/livekit.mjs +161 -0
- package/dist/adapters/openai.d.mts +41 -0
- package/dist/adapters/openai.d.ts +41 -0
- package/dist/adapters/openai.js +350 -0
- package/dist/adapters/openai.mjs +294 -0
- package/dist/chunk-22WLZIXO.mjs +33 -0
- package/dist/chunk-T3II3DRG.mjs +178 -0
- package/dist/chunk-UZ2VGPZD.mjs +33 -0
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/index.d.mts +693 -0
- package/dist/index.d.ts +693 -0
- package/dist/index.js +1838 -0
- package/dist/index.mjs +1593 -0
- package/dist/server.d.mts +80 -0
- package/dist/server.d.ts +80 -0
- package/dist/server.js +147 -0
- package/dist/server.mjs +119 -0
- package/dist/types-DY31oVB1.d.mts +150 -0
- package/dist/types-DY31oVB1.d.ts +150 -0
- package/dist/types-mThnXW9S.d.mts +150 -0
- package/dist/types-mThnXW9S.d.ts +150 -0
- package/dist/types-uLnzb8NE.d.mts +150 -0
- package/dist/types-uLnzb8NE.d.ts +150 -0
- package/package.json +100 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EventEmitter
|
|
3
|
+
} from "../chunk-22WLZIXO.mjs";
|
|
4
|
+
|
|
5
|
+
// src/adapters/elevenlabs.ts
|
|
6
|
+
var ELEVENLABS_WS_BASE = "wss://api.elevenlabs.io/v1/convai/conversation";
|
|
7
|
+
var ElevenLabsSession = class extends EventEmitter {
|
|
8
|
+
constructor(agent, agentId, options) {
|
|
9
|
+
super();
|
|
10
|
+
this.ws = null;
|
|
11
|
+
this.mediaStream = null;
|
|
12
|
+
this.audioContext = null;
|
|
13
|
+
this.scriptProcessor = null;
|
|
14
|
+
this.playbackCtx = null;
|
|
15
|
+
this.agent = agent;
|
|
16
|
+
this.agentId = agentId;
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
async connect(config) {
|
|
20
|
+
const wsUrl = config.authToken?.startsWith("wss://") ? config.authToken : `${ELEVENLABS_WS_BASE}?agent_id=${this.agentId}`;
|
|
21
|
+
this.ws = new WebSocket(wsUrl);
|
|
22
|
+
await new Promise((resolve, reject) => {
|
|
23
|
+
const ws = this.ws;
|
|
24
|
+
ws.onopen = () => resolve();
|
|
25
|
+
ws.onerror = () => reject(new Error("ElevenLabs WebSocket connection failed"));
|
|
26
|
+
});
|
|
27
|
+
this.ws.onmessage = (event) => {
|
|
28
|
+
try {
|
|
29
|
+
const msg = JSON.parse(event.data);
|
|
30
|
+
this.handleMessage(msg, config.audioElement);
|
|
31
|
+
} catch {
|
|
32
|
+
this.emit("raw_event", event.data);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
this.ws.onclose = () => {
|
|
36
|
+
this.emit("status_change", "DISCONNECTED");
|
|
37
|
+
};
|
|
38
|
+
this.ws.onerror = () => {
|
|
39
|
+
this.emit("error", new Error("ElevenLabs WebSocket error"));
|
|
40
|
+
};
|
|
41
|
+
if (this.agent.instructions) {
|
|
42
|
+
this.ws.send(JSON.stringify({
|
|
43
|
+
type: "contextual_update",
|
|
44
|
+
text: this.agent.instructions
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
48
|
+
this.audioContext = new AudioContext({ sampleRate: 16e3 });
|
|
49
|
+
const source = this.audioContext.createMediaStreamSource(this.mediaStream);
|
|
50
|
+
this.scriptProcessor = this.audioContext.createScriptProcessor(4096, 1, 1);
|
|
51
|
+
this.scriptProcessor.onaudioprocess = (e) => {
|
|
52
|
+
if (this.ws?.readyState !== WebSocket.OPEN) return;
|
|
53
|
+
const input = e.inputBuffer.getChannelData(0);
|
|
54
|
+
const pcm16 = new Int16Array(input.length);
|
|
55
|
+
for (let i = 0; i < input.length; i++) {
|
|
56
|
+
const s = Math.max(-1, Math.min(1, input[i]));
|
|
57
|
+
pcm16[i] = s < 0 ? s * 32768 : s * 32767;
|
|
58
|
+
}
|
|
59
|
+
const bytes = new Uint8Array(pcm16.buffer);
|
|
60
|
+
let binary = "";
|
|
61
|
+
for (let j = 0; j < bytes.length; j++) binary += String.fromCharCode(bytes[j]);
|
|
62
|
+
const base64 = btoa(binary);
|
|
63
|
+
this.ws.send(JSON.stringify({ user_audio_chunk: base64 }));
|
|
64
|
+
};
|
|
65
|
+
source.connect(this.scriptProcessor);
|
|
66
|
+
this.scriptProcessor.connect(this.audioContext.destination);
|
|
67
|
+
this.emit("status_change", "CONNECTED");
|
|
68
|
+
}
|
|
69
|
+
async disconnect() {
|
|
70
|
+
this.scriptProcessor?.disconnect();
|
|
71
|
+
this.audioContext?.close();
|
|
72
|
+
this.mediaStream?.getTracks().forEach((t) => t.stop());
|
|
73
|
+
this.playbackCtx?.close();
|
|
74
|
+
this.scriptProcessor = null;
|
|
75
|
+
this.audioContext = null;
|
|
76
|
+
this.mediaStream = null;
|
|
77
|
+
this.playbackCtx = null;
|
|
78
|
+
if (this.ws) {
|
|
79
|
+
this.ws.close();
|
|
80
|
+
this.ws = null;
|
|
81
|
+
}
|
|
82
|
+
this.removeAllListeners();
|
|
83
|
+
}
|
|
84
|
+
sendMessage(text) {
|
|
85
|
+
this.ws?.send(JSON.stringify({ type: "user_message", text }));
|
|
86
|
+
}
|
|
87
|
+
interrupt() {
|
|
88
|
+
this.ws?.send(JSON.stringify({ type: "interrupt" }));
|
|
89
|
+
}
|
|
90
|
+
mute(muted) {
|
|
91
|
+
this.mediaStream?.getAudioTracks().forEach((t) => {
|
|
92
|
+
t.enabled = !muted;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
sendRawEvent(event) {
|
|
96
|
+
this.ws?.send(JSON.stringify(event));
|
|
97
|
+
}
|
|
98
|
+
handleMessage(msg, audioElement) {
|
|
99
|
+
switch (msg.type) {
|
|
100
|
+
case "user_transcript":
|
|
101
|
+
this.emit("user_transcript", {
|
|
102
|
+
itemId: msg.id || "",
|
|
103
|
+
text: msg.user_transcript_event?.user_transcript || msg.text || "",
|
|
104
|
+
isFinal: msg.user_transcript_event?.is_final ?? true
|
|
105
|
+
});
|
|
106
|
+
break;
|
|
107
|
+
case "agent_response":
|
|
108
|
+
this.emit("assistant_transcript", {
|
|
109
|
+
itemId: msg.id || "",
|
|
110
|
+
delta: msg.agent_response_event?.agent_response || msg.delta || "",
|
|
111
|
+
isFinal: false
|
|
112
|
+
});
|
|
113
|
+
break;
|
|
114
|
+
case "agent_response_correction":
|
|
115
|
+
this.emit("assistant_transcript", {
|
|
116
|
+
itemId: msg.id || "",
|
|
117
|
+
text: msg.agent_response_correction_event?.corrected_text || "",
|
|
118
|
+
isFinal: true
|
|
119
|
+
});
|
|
120
|
+
break;
|
|
121
|
+
case "audio": {
|
|
122
|
+
const audioData = msg.audio_event?.audio_base_64 || msg.audio;
|
|
123
|
+
if (audioData) {
|
|
124
|
+
this.emit("audio_delta", msg.id || "", audioData);
|
|
125
|
+
this.playAudioChunk(audioData, audioElement);
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "client_tool_call":
|
|
130
|
+
this.emit("tool_call_start", msg.client_tool_call?.tool_name || "", msg.client_tool_call?.parameters);
|
|
131
|
+
this.executeToolCall(msg);
|
|
132
|
+
break;
|
|
133
|
+
case "vad":
|
|
134
|
+
if (msg.vad_event?.type === "SPEECH_START") {
|
|
135
|
+
this.emit("user_speech_started");
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
case "error":
|
|
139
|
+
this.emit("error", new Error(msg.message || msg.error || "ElevenLabs error"));
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
this.emit("raw_event", msg);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async executeToolCall(msg) {
|
|
147
|
+
const toolCall = msg.client_tool_call;
|
|
148
|
+
if (!toolCall) return;
|
|
149
|
+
const toolDef = this.agent.tools?.find((t) => t.name === toolCall.tool_name);
|
|
150
|
+
if (!toolDef) {
|
|
151
|
+
this.ws?.send(JSON.stringify({
|
|
152
|
+
type: "client_tool_result",
|
|
153
|
+
tool_call_id: toolCall.tool_call_id,
|
|
154
|
+
result: JSON.stringify({ error: `Tool ${toolCall.tool_name} not found` })
|
|
155
|
+
}));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const result = await toolDef.execute(toolCall.parameters || {});
|
|
160
|
+
this.emit("tool_call_end", toolCall.tool_name, toolCall.parameters, result);
|
|
161
|
+
this.ws?.send(JSON.stringify({
|
|
162
|
+
type: "client_tool_result",
|
|
163
|
+
tool_call_id: toolCall.tool_call_id,
|
|
164
|
+
result: JSON.stringify(result)
|
|
165
|
+
}));
|
|
166
|
+
} catch (err) {
|
|
167
|
+
const errorResult = { error: String(err) };
|
|
168
|
+
this.emit("tool_call_end", toolCall.tool_name, toolCall.parameters, errorResult);
|
|
169
|
+
this.ws?.send(JSON.stringify({
|
|
170
|
+
type: "client_tool_result",
|
|
171
|
+
tool_call_id: toolCall.tool_call_id,
|
|
172
|
+
result: JSON.stringify(errorResult)
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
playAudioChunk(base64, audioElement) {
|
|
177
|
+
if (!audioElement) return;
|
|
178
|
+
try {
|
|
179
|
+
if (!this.playbackCtx) {
|
|
180
|
+
this.playbackCtx = new AudioContext({ sampleRate: 22050 });
|
|
181
|
+
}
|
|
182
|
+
const binary = atob(base64);
|
|
183
|
+
const bytes = new Uint8Array(binary.length);
|
|
184
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
185
|
+
const samples = new Float32Array(bytes.length / 2);
|
|
186
|
+
const view = new DataView(bytes.buffer);
|
|
187
|
+
for (let i = 0; i < samples.length; i++) {
|
|
188
|
+
samples[i] = view.getInt16(i * 2, true) / 32768;
|
|
189
|
+
}
|
|
190
|
+
const buffer = this.playbackCtx.createBuffer(1, samples.length, 22050);
|
|
191
|
+
buffer.copyToChannel(samples, 0);
|
|
192
|
+
const src = this.playbackCtx.createBufferSource();
|
|
193
|
+
src.buffer = buffer;
|
|
194
|
+
src.connect(this.playbackCtx.destination);
|
|
195
|
+
src.start();
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
function elevenlabs(options) {
|
|
201
|
+
return {
|
|
202
|
+
name: "elevenlabs",
|
|
203
|
+
createSession(agent, sessionOpts) {
|
|
204
|
+
return new ElevenLabsSession(agent, options.agentId, { ...options, ...sessionOpts });
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function elevenlabsServer(config = {}) {
|
|
209
|
+
const getSessionToken = async (overrides = {}) => {
|
|
210
|
+
const merged = { ...config, ...overrides };
|
|
211
|
+
const apiKey = merged.apiKey || process.env.ELEVENLABS_API_KEY;
|
|
212
|
+
const agentId = merged.agentId || process.env.ELEVENLABS_AGENT_ID;
|
|
213
|
+
if (!apiKey) return { error: "ElevenLabs API key not configured" };
|
|
214
|
+
if (!agentId) return { error: "ElevenLabs agent ID not configured" };
|
|
215
|
+
try {
|
|
216
|
+
const res = await fetch(
|
|
217
|
+
`https://api.elevenlabs.io/v1/convai/conversation/get_signed_url?agent_id=${agentId}`,
|
|
218
|
+
{
|
|
219
|
+
method: "GET",
|
|
220
|
+
headers: { "xi-api-key": apiKey }
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
if (!res.ok) {
|
|
224
|
+
return { error: `ElevenLabs API error: ${res.status}` };
|
|
225
|
+
}
|
|
226
|
+
const data = await res.json();
|
|
227
|
+
return { token: data.signed_url || "" };
|
|
228
|
+
} catch (err) {
|
|
229
|
+
return { error: String(err) };
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
getSessionToken,
|
|
234
|
+
createSessionHandler(overrides) {
|
|
235
|
+
return async (_request) => {
|
|
236
|
+
const result = await getSessionToken(overrides);
|
|
237
|
+
if (result.error) {
|
|
238
|
+
return Response.json({ error: result.error }, { status: 500 });
|
|
239
|
+
}
|
|
240
|
+
return Response.json({ ephemeralKey: result.token });
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
var elevenlabs_default = elevenlabs;
|
|
246
|
+
export {
|
|
247
|
+
elevenlabs_default as default,
|
|
248
|
+
elevenlabs,
|
|
249
|
+
elevenlabsServer
|
|
250
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { i as SessionOptions, g as ServerSessionConfig, e as VoiceAdapter, S as ServerAdapter } from '../types-DY31oVB1.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LiveKit adapter for VoiceKit.
|
|
5
|
+
*
|
|
6
|
+
* Peer dependency: livekit-client (>= 2.0.0)
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { livekit } from '@jchaffin/voicekit/livekit';
|
|
11
|
+
*
|
|
12
|
+
* <VoiceProvider adapter={livekit({ serverUrl: 'wss://my-livekit.example.com' })} agent={agent}>
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Server-side: generate an access token containing room name + participant identity
|
|
16
|
+
* and return it from your session endpoint.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface LiveKitAdapterOptions extends SessionOptions {
|
|
20
|
+
/** LiveKit server WebSocket URL (e.g. wss://my-app.livekit.cloud) */
|
|
21
|
+
serverUrl: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a LiveKit adapter.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { livekit } from '@jchaffin/voicekit/livekit';
|
|
28
|
+
* <VoiceProvider adapter={livekit({ serverUrl: 'wss://...' })} agent={agent} />
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
declare function livekit(options: LiveKitAdapterOptions): VoiceAdapter;
|
|
32
|
+
interface LiveKitServerConfig extends ServerSessionConfig {
|
|
33
|
+
apiKey?: string;
|
|
34
|
+
apiSecret?: string;
|
|
35
|
+
/** Room name to grant access to */
|
|
36
|
+
roomName?: string;
|
|
37
|
+
/** Participant identity */
|
|
38
|
+
identity?: string;
|
|
39
|
+
/** Token TTL in seconds (default 600) */
|
|
40
|
+
ttl?: number;
|
|
41
|
+
}
|
|
42
|
+
declare function livekitServer(config?: LiveKitServerConfig): ServerAdapter;
|
|
43
|
+
|
|
44
|
+
export { type LiveKitAdapterOptions, type LiveKitServerConfig, livekit as default, livekit, livekitServer };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { i as SessionOptions, g as ServerSessionConfig, e as VoiceAdapter, S as ServerAdapter } from '../types-DY31oVB1.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LiveKit adapter for VoiceKit.
|
|
5
|
+
*
|
|
6
|
+
* Peer dependency: livekit-client (>= 2.0.0)
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { livekit } from '@jchaffin/voicekit/livekit';
|
|
11
|
+
*
|
|
12
|
+
* <VoiceProvider adapter={livekit({ serverUrl: 'wss://my-livekit.example.com' })} agent={agent}>
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Server-side: generate an access token containing room name + participant identity
|
|
16
|
+
* and return it from your session endpoint.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface LiveKitAdapterOptions extends SessionOptions {
|
|
20
|
+
/** LiveKit server WebSocket URL (e.g. wss://my-app.livekit.cloud) */
|
|
21
|
+
serverUrl: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a LiveKit adapter.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { livekit } from '@jchaffin/voicekit/livekit';
|
|
28
|
+
* <VoiceProvider adapter={livekit({ serverUrl: 'wss://...' })} agent={agent} />
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
declare function livekit(options: LiveKitAdapterOptions): VoiceAdapter;
|
|
32
|
+
interface LiveKitServerConfig extends ServerSessionConfig {
|
|
33
|
+
apiKey?: string;
|
|
34
|
+
apiSecret?: string;
|
|
35
|
+
/** Room name to grant access to */
|
|
36
|
+
roomName?: string;
|
|
37
|
+
/** Participant identity */
|
|
38
|
+
identity?: string;
|
|
39
|
+
/** Token TTL in seconds (default 600) */
|
|
40
|
+
ttl?: number;
|
|
41
|
+
}
|
|
42
|
+
declare function livekitServer(config?: LiveKitServerConfig): ServerAdapter;
|
|
43
|
+
|
|
44
|
+
export { type LiveKitAdapterOptions, type LiveKitServerConfig, livekit as default, livekit, livekitServer };
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/adapters/livekit.ts
|
|
31
|
+
var livekit_exports = {};
|
|
32
|
+
__export(livekit_exports, {
|
|
33
|
+
default: () => livekit_default,
|
|
34
|
+
livekit: () => livekit,
|
|
35
|
+
livekitServer: () => livekitServer
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(livekit_exports);
|
|
38
|
+
|
|
39
|
+
// src/core/EventEmitter.ts
|
|
40
|
+
var EventEmitter = class {
|
|
41
|
+
constructor() {
|
|
42
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
43
|
+
}
|
|
44
|
+
on(event, handler) {
|
|
45
|
+
let set = this.handlers.get(event);
|
|
46
|
+
if (!set) {
|
|
47
|
+
set = /* @__PURE__ */ new Set();
|
|
48
|
+
this.handlers.set(event, set);
|
|
49
|
+
}
|
|
50
|
+
set.add(handler);
|
|
51
|
+
}
|
|
52
|
+
off(event, handler) {
|
|
53
|
+
this.handlers.get(event)?.delete(handler);
|
|
54
|
+
}
|
|
55
|
+
emit(event, ...args) {
|
|
56
|
+
this.handlers.get(event)?.forEach((fn) => {
|
|
57
|
+
try {
|
|
58
|
+
fn(...args);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error(`EventEmitter error in "${event}":`, e);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
removeAllListeners() {
|
|
65
|
+
this.handlers.clear();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/adapters/livekit.ts
|
|
70
|
+
var LiveKitSession = class extends EventEmitter {
|
|
71
|
+
constructor(agent, serverUrl, options) {
|
|
72
|
+
super();
|
|
73
|
+
this.room = null;
|
|
74
|
+
this.agent = agent;
|
|
75
|
+
this.serverUrl = serverUrl;
|
|
76
|
+
this.options = options;
|
|
77
|
+
}
|
|
78
|
+
async connect(config) {
|
|
79
|
+
const { Room, RoomEvent, Track } = await import("livekit-client");
|
|
80
|
+
this.room = new Room();
|
|
81
|
+
this.room.on(RoomEvent.TrackSubscribed, (track, _pub, participant) => {
|
|
82
|
+
if (track.kind === Track.Kind.Audio) {
|
|
83
|
+
const el = config.audioElement || document.createElement("audio");
|
|
84
|
+
track.attach(el);
|
|
85
|
+
if (!config.audioElement) {
|
|
86
|
+
el.autoplay = true;
|
|
87
|
+
el.style.display = "none";
|
|
88
|
+
document.body.appendChild(el);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
this.room.on(RoomEvent.DataReceived, (payload, participant, kind) => {
|
|
93
|
+
try {
|
|
94
|
+
const msg = JSON.parse(new TextDecoder().decode(payload));
|
|
95
|
+
this.handleDataMessage(msg, participant);
|
|
96
|
+
} catch {
|
|
97
|
+
this.emit("raw_event", { payload, participant, kind });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
this.room.on(RoomEvent.Disconnected, () => {
|
|
101
|
+
this.emit("status_change", "DISCONNECTED");
|
|
102
|
+
});
|
|
103
|
+
this.room.on(RoomEvent.Reconnecting, () => {
|
|
104
|
+
this.emit("status_change", "CONNECTING");
|
|
105
|
+
});
|
|
106
|
+
this.room.on(RoomEvent.Reconnected, () => {
|
|
107
|
+
this.emit("status_change", "CONNECTED");
|
|
108
|
+
});
|
|
109
|
+
await this.room.connect(this.serverUrl, config.authToken);
|
|
110
|
+
await this.room.localParticipant.setMicrophoneEnabled(true);
|
|
111
|
+
this.emit("status_change", "CONNECTED");
|
|
112
|
+
}
|
|
113
|
+
async disconnect() {
|
|
114
|
+
if (this.room) {
|
|
115
|
+
await this.room.disconnect();
|
|
116
|
+
this.room = null;
|
|
117
|
+
}
|
|
118
|
+
this.removeAllListeners();
|
|
119
|
+
}
|
|
120
|
+
sendMessage(text) {
|
|
121
|
+
if (!this.room) throw new Error("Not connected");
|
|
122
|
+
const data = new TextEncoder().encode(JSON.stringify({ type: "user_message", text }));
|
|
123
|
+
this.room.localParticipant.publishData(data, { reliable: true });
|
|
124
|
+
}
|
|
125
|
+
interrupt() {
|
|
126
|
+
if (!this.room) return;
|
|
127
|
+
const data = new TextEncoder().encode(JSON.stringify({ type: "interrupt" }));
|
|
128
|
+
this.room.localParticipant.publishData(data, { reliable: true });
|
|
129
|
+
}
|
|
130
|
+
mute(muted) {
|
|
131
|
+
this.room?.localParticipant?.setMicrophoneEnabled(!muted);
|
|
132
|
+
}
|
|
133
|
+
sendRawEvent(event) {
|
|
134
|
+
if (!this.room) return;
|
|
135
|
+
const data = new TextEncoder().encode(JSON.stringify(event));
|
|
136
|
+
this.room.localParticipant.publishData(data, { reliable: true });
|
|
137
|
+
}
|
|
138
|
+
handleDataMessage(msg, _participant) {
|
|
139
|
+
switch (msg.type) {
|
|
140
|
+
case "user_transcript":
|
|
141
|
+
this.emit("user_transcript", {
|
|
142
|
+
itemId: msg.itemId || msg.id || "",
|
|
143
|
+
delta: msg.delta,
|
|
144
|
+
text: msg.text,
|
|
145
|
+
isFinal: msg.isFinal ?? !!msg.text
|
|
146
|
+
});
|
|
147
|
+
break;
|
|
148
|
+
case "assistant_transcript":
|
|
149
|
+
this.emit("assistant_transcript", {
|
|
150
|
+
itemId: msg.itemId || msg.id || "",
|
|
151
|
+
delta: msg.delta,
|
|
152
|
+
text: msg.text,
|
|
153
|
+
isFinal: msg.isFinal ?? !!msg.text
|
|
154
|
+
});
|
|
155
|
+
break;
|
|
156
|
+
case "tool_call_start":
|
|
157
|
+
this.emit("tool_call_start", msg.name, msg.input);
|
|
158
|
+
break;
|
|
159
|
+
case "tool_call_end":
|
|
160
|
+
this.emit("tool_call_end", msg.name, msg.input, msg.output);
|
|
161
|
+
break;
|
|
162
|
+
case "agent_handoff":
|
|
163
|
+
this.emit("agent_handoff", msg.from || "", msg.to || "");
|
|
164
|
+
break;
|
|
165
|
+
case "error":
|
|
166
|
+
this.emit("error", new Error(msg.message || "LiveKit error"));
|
|
167
|
+
break;
|
|
168
|
+
case "speech_started":
|
|
169
|
+
this.emit("user_speech_started");
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
this.emit("raw_event", msg);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
function livekit(options) {
|
|
178
|
+
return {
|
|
179
|
+
name: "livekit",
|
|
180
|
+
createSession(agent, sessionOpts) {
|
|
181
|
+
return new LiveKitSession(agent, options.serverUrl, { ...options, ...sessionOpts });
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function livekitServer(config = {}) {
|
|
186
|
+
const getSessionToken = async (overrides = {}) => {
|
|
187
|
+
const merged = { ...config, ...overrides };
|
|
188
|
+
const apiKey = merged.apiKey || process.env.LIVEKIT_API_KEY;
|
|
189
|
+
const apiSecret = merged.apiSecret || process.env.LIVEKIT_API_SECRET;
|
|
190
|
+
if (!apiKey || !apiSecret) {
|
|
191
|
+
return { error: "LiveKit API key and secret are required" };
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const { AccessToken } = await import("livekit-server-sdk");
|
|
195
|
+
const roomName = merged.roomName || `room-${Date.now()}`;
|
|
196
|
+
const identity = merged.identity || `user-${Date.now()}`;
|
|
197
|
+
const at = new AccessToken(apiKey, apiSecret, {
|
|
198
|
+
identity,
|
|
199
|
+
ttl: (merged.ttl || 600).toString() + "s"
|
|
200
|
+
});
|
|
201
|
+
at.addGrant({ roomJoin: true, room: roomName });
|
|
202
|
+
return { token: await at.toJwt() };
|
|
203
|
+
} catch (err) {
|
|
204
|
+
return { error: String(err) };
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
getSessionToken,
|
|
209
|
+
createSessionHandler(overrides) {
|
|
210
|
+
return async (_request) => {
|
|
211
|
+
const result = await getSessionToken(overrides);
|
|
212
|
+
if (result.error) {
|
|
213
|
+
return Response.json({ error: result.error }, { status: 500 });
|
|
214
|
+
}
|
|
215
|
+
return Response.json({ ephemeralKey: result.token });
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
var livekit_default = livekit;
|
|
221
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
222
|
+
0 && (module.exports = {
|
|
223
|
+
livekit,
|
|
224
|
+
livekitServer
|
|
225
|
+
});
|