@tanstack/ai-elevenlabs 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/realtime/adapter.d.ts +22 -0
- package/dist/esm/realtime/adapter.js +185 -0
- package/dist/esm/realtime/adapter.js.map +1 -0
- package/dist/esm/realtime/index.d.ts +3 -0
- package/dist/esm/realtime/token.d.ts +24 -0
- package/dist/esm/realtime/token.js +57 -0
- package/dist/esm/realtime/token.js.map +1 -0
- package/dist/esm/realtime/types.d.ts +51 -0
- package/package.json +55 -0
- package/src/index.ts +13 -0
- package/src/realtime/adapter.ts +300 -0
- package/src/realtime/index.ts +14 -0
- package/src/realtime/token.ts +103 -0
- package/src/realtime/types.ts +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @tanstack/ai-elevenlabs
|
|
2
|
+
|
|
3
|
+
ElevenLabs adapter for TanStack AI realtime voice conversations.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tanstack/ai-elevenlabs @tanstack/ai @tanstack/ai-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Server-Side Token Generation
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { realtimeToken } from '@tanstack/ai'
|
|
17
|
+
import { elevenlabsRealtimeToken } from '@tanstack/ai-elevenlabs'
|
|
18
|
+
|
|
19
|
+
// Generate a signed URL for client use
|
|
20
|
+
const token = await realtimeToken({
|
|
21
|
+
adapter: elevenlabsRealtimeToken({
|
|
22
|
+
agentId: 'your-agent-id',
|
|
23
|
+
}),
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Client-Side Usage
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { RealtimeClient } from '@tanstack/ai-client'
|
|
31
|
+
import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
|
|
32
|
+
|
|
33
|
+
const client = new RealtimeClient({
|
|
34
|
+
getToken: () => fetch('/api/realtime-token').then((r) => r.json()),
|
|
35
|
+
adapter: elevenlabsRealtime(),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await client.connect()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### With React
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { useRealtimeChat } from '@tanstack/ai-react'
|
|
45
|
+
import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
|
|
46
|
+
|
|
47
|
+
function VoiceChat() {
|
|
48
|
+
const { status, mode, messages, connect, disconnect } = useRealtimeChat({
|
|
49
|
+
getToken: () => fetch('/api/realtime-token').then(r => r.json()),
|
|
50
|
+
adapter: elevenlabsRealtime(),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<p>Status: {status}</p>
|
|
56
|
+
<p>Mode: {mode}</p>
|
|
57
|
+
<button onClick={status === 'idle' ? connect : disconnect}>
|
|
58
|
+
{status === 'idle' ? 'Start' : 'Stop'}
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Environment Variables
|
|
66
|
+
|
|
67
|
+
Set `ELEVENLABS_API_KEY` in your environment for server-side token generation.
|
|
68
|
+
|
|
69
|
+
## Requirements
|
|
70
|
+
|
|
71
|
+
- ElevenLabs account with Conversational AI agent configured
|
|
72
|
+
- Agent ID from ElevenLabs dashboard
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { RealtimeAdapter } from '@tanstack/ai-client';
|
|
2
|
+
import { ElevenLabsRealtimeOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates an ElevenLabs realtime adapter for client-side use.
|
|
5
|
+
*
|
|
6
|
+
* Wraps the @11labs/client SDK for voice conversations.
|
|
7
|
+
*
|
|
8
|
+
* @param options - Optional configuration
|
|
9
|
+
* @returns A RealtimeAdapter for use with RealtimeClient
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { RealtimeClient } from '@tanstack/ai-client'
|
|
14
|
+
* import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
|
|
15
|
+
*
|
|
16
|
+
* const client = new RealtimeClient({
|
|
17
|
+
* getToken: () => fetch('/api/realtime-token').then(r => r.json()),
|
|
18
|
+
* adapter: elevenlabsRealtime(),
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function elevenlabsRealtime(options?: ElevenLabsRealtimeOptions): RealtimeAdapter;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Conversation } from "@11labs/client";
|
|
2
|
+
function elevenlabsRealtime(options = {}) {
|
|
3
|
+
return {
|
|
4
|
+
provider: "elevenlabs",
|
|
5
|
+
async connect(token, clientToolDefs) {
|
|
6
|
+
return createElevenLabsConnection(token, options, clientToolDefs);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
async function createElevenLabsConnection(token, _options, clientToolDefs) {
|
|
11
|
+
const eventHandlers = /* @__PURE__ */ new Map();
|
|
12
|
+
let conversation = null;
|
|
13
|
+
let messageIdCounter = 0;
|
|
14
|
+
const emptyFrequencyData = new Uint8Array(128);
|
|
15
|
+
const emptyTimeDomainData = new Uint8Array(128).fill(128);
|
|
16
|
+
function emit(event, payload) {
|
|
17
|
+
const handlers = eventHandlers.get(event);
|
|
18
|
+
if (handlers) {
|
|
19
|
+
for (const handler of handlers) {
|
|
20
|
+
handler(payload);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function generateMessageId() {
|
|
25
|
+
return `el-msg-${Date.now()}-${++messageIdCounter}`;
|
|
26
|
+
}
|
|
27
|
+
const elevenLabsClientTools = {};
|
|
28
|
+
if (clientToolDefs) {
|
|
29
|
+
for (const tool of clientToolDefs) {
|
|
30
|
+
elevenLabsClientTools[tool.name] = {
|
|
31
|
+
handler: async (params) => {
|
|
32
|
+
if (tool.execute) {
|
|
33
|
+
const result = await tool.execute(params);
|
|
34
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
35
|
+
}
|
|
36
|
+
return JSON.stringify({
|
|
37
|
+
error: `No execute function for tool ${tool.name}`
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
description: tool.description,
|
|
41
|
+
parameters: tool.inputSchema ? tool.inputSchema : { type: "object", properties: {} }
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const sessionOptions = {
|
|
46
|
+
signedUrl: token.token,
|
|
47
|
+
onConnect: () => {
|
|
48
|
+
emit("status_change", { status: "connected" });
|
|
49
|
+
emit("mode_change", { mode: "listening" });
|
|
50
|
+
},
|
|
51
|
+
onDisconnect: () => {
|
|
52
|
+
emit("status_change", { status: "idle" });
|
|
53
|
+
emit("mode_change", { mode: "idle" });
|
|
54
|
+
},
|
|
55
|
+
onModeChange: ({ mode }) => {
|
|
56
|
+
const mappedMode = mode === "speaking" ? "speaking" : "listening";
|
|
57
|
+
emit("mode_change", { mode: mappedMode });
|
|
58
|
+
},
|
|
59
|
+
onMessage: ({ message, source }) => {
|
|
60
|
+
const role = source === "user" ? "user" : "assistant";
|
|
61
|
+
emit("transcript", {
|
|
62
|
+
role,
|
|
63
|
+
transcript: message,
|
|
64
|
+
isFinal: true
|
|
65
|
+
});
|
|
66
|
+
const realtimeMessage = {
|
|
67
|
+
id: generateMessageId(),
|
|
68
|
+
role,
|
|
69
|
+
timestamp: Date.now(),
|
|
70
|
+
parts: [{ type: "audio", transcript: message }]
|
|
71
|
+
};
|
|
72
|
+
emit("message_complete", { message: realtimeMessage });
|
|
73
|
+
},
|
|
74
|
+
onError: (error) => {
|
|
75
|
+
emit("error", {
|
|
76
|
+
error: new Error(
|
|
77
|
+
typeof error === "string" ? error : error.message || "Unknown error"
|
|
78
|
+
)
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
if (Object.keys(elevenLabsClientTools).length > 0) {
|
|
83
|
+
sessionOptions.clientTools = elevenLabsClientTools;
|
|
84
|
+
}
|
|
85
|
+
conversation = await Conversation.startSession(
|
|
86
|
+
sessionOptions
|
|
87
|
+
);
|
|
88
|
+
const connection = {
|
|
89
|
+
async disconnect() {
|
|
90
|
+
if (conversation) {
|
|
91
|
+
await conversation.endSession();
|
|
92
|
+
conversation = null;
|
|
93
|
+
}
|
|
94
|
+
emit("status_change", { status: "idle" });
|
|
95
|
+
},
|
|
96
|
+
async startAudioCapture() {
|
|
97
|
+
emit("mode_change", { mode: "listening" });
|
|
98
|
+
},
|
|
99
|
+
stopAudioCapture() {
|
|
100
|
+
emit("mode_change", { mode: "idle" });
|
|
101
|
+
},
|
|
102
|
+
sendText(text) {
|
|
103
|
+
if (!conversation) return;
|
|
104
|
+
conversation.sendUserMessage(text);
|
|
105
|
+
},
|
|
106
|
+
sendImage(_imageData, _mimeType) {
|
|
107
|
+
console.warn(
|
|
108
|
+
"ElevenLabs realtime does not support sending images directly."
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
sendToolResult(_callId, _result) {
|
|
112
|
+
},
|
|
113
|
+
updateSession(_config) {
|
|
114
|
+
console.warn(
|
|
115
|
+
"ElevenLabs does not support runtime session updates. Configure at connection time."
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
interrupt() {
|
|
119
|
+
emit("mode_change", { mode: "listening" });
|
|
120
|
+
emit("interrupted", {});
|
|
121
|
+
},
|
|
122
|
+
on(event, handler) {
|
|
123
|
+
if (!eventHandlers.has(event)) {
|
|
124
|
+
eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
125
|
+
}
|
|
126
|
+
eventHandlers.get(event).add(handler);
|
|
127
|
+
return () => {
|
|
128
|
+
eventHandlers.get(event)?.delete(handler);
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
getAudioVisualization() {
|
|
132
|
+
return {
|
|
133
|
+
get inputLevel() {
|
|
134
|
+
if (!conversation) return 0;
|
|
135
|
+
try {
|
|
136
|
+
return conversation.getInputVolume();
|
|
137
|
+
} catch {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
get outputLevel() {
|
|
142
|
+
if (!conversation) return 0;
|
|
143
|
+
try {
|
|
144
|
+
return conversation.getOutputVolume();
|
|
145
|
+
} catch {
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
getInputFrequencyData() {
|
|
150
|
+
if (!conversation) return emptyFrequencyData;
|
|
151
|
+
try {
|
|
152
|
+
return conversation.getInputByteFrequencyData();
|
|
153
|
+
} catch {
|
|
154
|
+
return emptyFrequencyData;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
getOutputFrequencyData() {
|
|
158
|
+
if (!conversation) return emptyFrequencyData;
|
|
159
|
+
try {
|
|
160
|
+
return conversation.getOutputByteFrequencyData();
|
|
161
|
+
} catch {
|
|
162
|
+
return emptyFrequencyData;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
getInputTimeDomainData() {
|
|
166
|
+
return emptyTimeDomainData;
|
|
167
|
+
},
|
|
168
|
+
getOutputTimeDomainData() {
|
|
169
|
+
return emptyTimeDomainData;
|
|
170
|
+
},
|
|
171
|
+
get inputSampleRate() {
|
|
172
|
+
return 16e3;
|
|
173
|
+
},
|
|
174
|
+
get outputSampleRate() {
|
|
175
|
+
return 16e3;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
return connection;
|
|
181
|
+
}
|
|
182
|
+
export {
|
|
183
|
+
elevenlabsRealtime
|
|
184
|
+
};
|
|
185
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sources":["../../../src/realtime/adapter.ts"],"sourcesContent":["import { Conversation } from '@11labs/client'\nimport type {\n AnyClientTool,\n AudioVisualization,\n RealtimeEvent,\n RealtimeEventHandler,\n RealtimeMessage,\n RealtimeMode,\n RealtimeSessionConfig,\n RealtimeStatus,\n RealtimeToken,\n} from '@tanstack/ai'\nimport type { RealtimeAdapter, RealtimeConnection } from '@tanstack/ai-client'\nimport type { ElevenLabsRealtimeOptions } from './types'\n\n/**\n * Creates an ElevenLabs realtime adapter for client-side use.\n *\n * Wraps the @11labs/client SDK for voice conversations.\n *\n * @param options - Optional configuration\n * @returns A RealtimeAdapter for use with RealtimeClient\n *\n * @example\n * ```typescript\n * import { RealtimeClient } from '@tanstack/ai-client'\n * import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'\n *\n * const client = new RealtimeClient({\n * getToken: () => fetch('/api/realtime-token').then(r => r.json()),\n * adapter: elevenlabsRealtime(),\n * })\n * ```\n */\nexport function elevenlabsRealtime(\n options: ElevenLabsRealtimeOptions = {},\n): RealtimeAdapter {\n return {\n provider: 'elevenlabs',\n\n async connect(\n token: RealtimeToken,\n clientToolDefs?: ReadonlyArray<AnyClientTool>,\n ): Promise<RealtimeConnection> {\n return createElevenLabsConnection(token, options, clientToolDefs)\n },\n }\n}\n\n/**\n * Creates a connection to ElevenLabs conversational AI\n */\nasync function createElevenLabsConnection(\n token: RealtimeToken,\n _options: ElevenLabsRealtimeOptions,\n clientToolDefs?: ReadonlyArray<AnyClientTool>,\n): Promise<RealtimeConnection> {\n const eventHandlers = new Map<RealtimeEvent, Set<RealtimeEventHandler<any>>>()\n let conversation: Awaited<\n ReturnType<typeof Conversation.startSession>\n > | null = null\n let messageIdCounter = 0\n\n // Empty arrays for when visualization isn't available\n const emptyFrequencyData = new Uint8Array(128)\n const emptyTimeDomainData = new Uint8Array(128).fill(128)\n\n // Helper to emit events\n function emit<TEvent extends RealtimeEvent>(\n event: TEvent,\n payload: Parameters<RealtimeEventHandler<TEvent>>[0],\n ) {\n const handlers = eventHandlers.get(event)\n if (handlers) {\n for (const handler of handlers) {\n handler(payload)\n }\n }\n }\n\n function generateMessageId(): string {\n return `el-msg-${Date.now()}-${++messageIdCounter}`\n }\n\n // Convert TanStack tool definitions to ElevenLabs clientTools format\n const elevenLabsClientTools: Record<\n string,\n {\n handler: (params: unknown) => Promise<string>\n description: string\n parameters: Record<string, unknown>\n }\n > = {}\n\n if (clientToolDefs) {\n for (const tool of clientToolDefs) {\n elevenLabsClientTools[tool.name] = {\n handler: async (params: unknown) => {\n if (tool.execute) {\n const result = await tool.execute(params)\n return typeof result === 'string' ? result : JSON.stringify(result)\n }\n return JSON.stringify({\n error: `No execute function for tool ${tool.name}`,\n })\n },\n description: tool.description,\n parameters: tool.inputSchema\n ? (tool.inputSchema as Record<string, unknown>)\n : { type: 'object', properties: {} },\n }\n }\n }\n\n // Build session options\n const sessionOptions: Record<string, unknown> = {\n signedUrl: token.token,\n\n onConnect: () => {\n emit('status_change', { status: 'connected' as RealtimeStatus })\n emit('mode_change', { mode: 'listening' })\n },\n\n onDisconnect: () => {\n emit('status_change', { status: 'idle' as RealtimeStatus })\n emit('mode_change', { mode: 'idle' })\n },\n\n onModeChange: ({ mode }: { mode: string }) => {\n const mappedMode: RealtimeMode =\n mode === 'speaking' ? 'speaking' : 'listening'\n emit('mode_change', { mode: mappedMode })\n },\n\n onMessage: ({ message, source }: { message: string; source: string }) => {\n const role = source === 'user' ? 'user' : 'assistant'\n\n // Emit transcript update\n emit('transcript', {\n role,\n transcript: message,\n isFinal: true,\n })\n\n // Create and emit message\n const realtimeMessage: RealtimeMessage = {\n id: generateMessageId(),\n role,\n timestamp: Date.now(),\n parts: [{ type: 'audio', transcript: message }],\n }\n emit('message_complete', { message: realtimeMessage })\n },\n\n onError: (error: string | Error) => {\n emit('error', {\n error: new Error(\n typeof error === 'string' ? error : error.message || 'Unknown error',\n ),\n })\n },\n }\n\n // Only add clientTools if we have any\n if (Object.keys(elevenLabsClientTools).length > 0) {\n sessionOptions.clientTools = elevenLabsClientTools\n }\n\n // Start the conversation session\n conversation = await Conversation.startSession(\n sessionOptions as Parameters<typeof Conversation.startSession>[0],\n )\n\n // Connection implementation\n const connection: RealtimeConnection = {\n async disconnect() {\n if (conversation) {\n await conversation.endSession()\n conversation = null\n }\n emit('status_change', { status: 'idle' as RealtimeStatus })\n },\n\n async startAudioCapture() {\n // ElevenLabs SDK handles audio capture automatically\n // This is called when the session starts\n emit('mode_change', { mode: 'listening' })\n },\n\n stopAudioCapture() {\n // ElevenLabs SDK handles this\n emit('mode_change', { mode: 'idle' })\n },\n\n sendText(text: string) {\n if (!conversation) return\n conversation.sendUserMessage(text)\n },\n\n sendImage(_imageData: string, _mimeType: string) {\n // ElevenLabs does not support direct image input in the conversation API\n console.warn(\n 'ElevenLabs realtime does not support sending images directly.',\n )\n },\n\n sendToolResult(_callId: string, _result: string) {\n // ElevenLabs client tools are handled via the clientTools handlers\n // registered at session start — results are returned automatically\n },\n\n updateSession(_config: Partial<RealtimeSessionConfig>) {\n // ElevenLabs session config is set at creation time\n console.warn(\n 'ElevenLabs does not support runtime session updates. Configure at connection time.',\n )\n },\n\n interrupt() {\n // ElevenLabs handles interruption automatically via barge-in\n // No explicit API to call\n emit('mode_change', { mode: 'listening' })\n emit('interrupted', {})\n },\n\n on<TEvent extends RealtimeEvent>(\n event: TEvent,\n handler: RealtimeEventHandler<TEvent>,\n ): () => void {\n if (!eventHandlers.has(event)) {\n eventHandlers.set(event, new Set())\n }\n eventHandlers.get(event)!.add(handler)\n\n return () => {\n eventHandlers.get(event)?.delete(handler)\n }\n },\n\n getAudioVisualization(): AudioVisualization {\n return {\n get inputLevel() {\n if (!conversation) return 0\n try {\n return conversation.getInputVolume()\n } catch {\n return 0\n }\n },\n\n get outputLevel() {\n if (!conversation) return 0\n try {\n return conversation.getOutputVolume()\n } catch {\n return 0\n }\n },\n\n getInputFrequencyData() {\n if (!conversation) return emptyFrequencyData\n try {\n return conversation.getInputByteFrequencyData()\n } catch {\n return emptyFrequencyData\n }\n },\n\n getOutputFrequencyData() {\n if (!conversation) return emptyFrequencyData\n try {\n return conversation.getOutputByteFrequencyData()\n } catch {\n return emptyFrequencyData\n }\n },\n\n getInputTimeDomainData() {\n // ElevenLabs SDK doesn't expose time domain data\n return emptyTimeDomainData\n },\n\n getOutputTimeDomainData() {\n // ElevenLabs SDK doesn't expose time domain data\n return emptyTimeDomainData\n },\n\n get inputSampleRate() {\n return 16000\n },\n\n get outputSampleRate() {\n return 16000\n },\n }\n },\n }\n\n return connection\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { RealtimeTokenAdapter } from '@tanstack/ai';
|
|
2
|
+
import { ElevenLabsRealtimeTokenOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates an ElevenLabs realtime token adapter.
|
|
5
|
+
*
|
|
6
|
+
* This adapter generates signed URLs for client-side connections.
|
|
7
|
+
* The signed URL is valid for 30 minutes.
|
|
8
|
+
*
|
|
9
|
+
* @param options - Configuration options including agentId
|
|
10
|
+
* @returns A RealtimeTokenAdapter for use with realtimeToken()
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { realtimeToken } from '@tanstack/ai'
|
|
15
|
+
* import { elevenlabsRealtimeToken } from '@tanstack/ai-elevenlabs'
|
|
16
|
+
*
|
|
17
|
+
* const token = await realtimeToken({
|
|
18
|
+
* adapter: elevenlabsRealtimeToken({
|
|
19
|
+
* agentId: 'your-agent-id',
|
|
20
|
+
* }),
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function elevenlabsRealtimeToken(options: ElevenLabsRealtimeTokenOptions): RealtimeTokenAdapter;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const ELEVENLABS_API_URL = "https://api.elevenlabs.io/v1";
|
|
2
|
+
function getElevenLabsApiKey() {
|
|
3
|
+
if (typeof process !== "undefined" && process.env.ELEVENLABS_API_KEY) {
|
|
4
|
+
return process.env.ELEVENLABS_API_KEY;
|
|
5
|
+
}
|
|
6
|
+
if (typeof window !== "undefined" && window.env?.ELEVENLABS_API_KEY) {
|
|
7
|
+
return window.env.ELEVENLABS_API_KEY;
|
|
8
|
+
}
|
|
9
|
+
throw new Error(
|
|
10
|
+
"ELEVENLABS_API_KEY not found in environment variables. Please set ELEVENLABS_API_KEY in your environment."
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
function elevenlabsRealtimeToken(options) {
|
|
14
|
+
const apiKey = getElevenLabsApiKey();
|
|
15
|
+
return {
|
|
16
|
+
provider: "elevenlabs",
|
|
17
|
+
async generateToken() {
|
|
18
|
+
const { agentId, overrides } = options;
|
|
19
|
+
const response = await fetch(
|
|
20
|
+
`${ELEVENLABS_API_URL}/convai/conversation/get_signed_url?agent_id=${agentId}`,
|
|
21
|
+
{
|
|
22
|
+
method: "GET",
|
|
23
|
+
headers: {
|
|
24
|
+
"xi-api-key": apiKey
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const errorText = await response.text();
|
|
30
|
+
throw new Error(
|
|
31
|
+
`ElevenLabs signed URL request failed: ${response.status} ${errorText}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
const signedUrl = data.signed_url;
|
|
36
|
+
const expiresAt = Date.now() + 30 * 60 * 1e3;
|
|
37
|
+
return {
|
|
38
|
+
provider: "elevenlabs",
|
|
39
|
+
token: signedUrl,
|
|
40
|
+
expiresAt,
|
|
41
|
+
config: {
|
|
42
|
+
voice: overrides?.voiceId,
|
|
43
|
+
instructions: overrides?.systemPrompt,
|
|
44
|
+
providerOptions: {
|
|
45
|
+
agentId,
|
|
46
|
+
firstMessage: overrides?.firstMessage,
|
|
47
|
+
language: overrides?.language
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
elevenlabsRealtimeToken
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.js","sources":["../../../src/realtime/token.ts"],"sourcesContent":["import type { RealtimeToken, RealtimeTokenAdapter } from '@tanstack/ai'\nimport type { ElevenLabsRealtimeTokenOptions } from './types'\n\nconst ELEVENLABS_API_URL = 'https://api.elevenlabs.io/v1'\n\n/**\n * Get ElevenLabs API key from environment\n */\nfunction getElevenLabsApiKey(): string {\n // Check process.env (Node.js)\n if (typeof process !== 'undefined' && process.env.ELEVENLABS_API_KEY) {\n return process.env.ELEVENLABS_API_KEY\n }\n\n // Check window.env (Browser with injected env)\n if (\n typeof window !== 'undefined' &&\n (window as unknown as { env?: { ELEVENLABS_API_KEY?: string } }).env\n ?.ELEVENLABS_API_KEY\n ) {\n return (window as unknown as { env: { ELEVENLABS_API_KEY: string } }).env\n .ELEVENLABS_API_KEY\n }\n\n throw new Error(\n 'ELEVENLABS_API_KEY not found in environment variables. ' +\n 'Please set ELEVENLABS_API_KEY in your environment.',\n )\n}\n\n/**\n * Creates an ElevenLabs realtime token adapter.\n *\n * This adapter generates signed URLs for client-side connections.\n * The signed URL is valid for 30 minutes.\n *\n * @param options - Configuration options including agentId\n * @returns A RealtimeTokenAdapter for use with realtimeToken()\n *\n * @example\n * ```typescript\n * import { realtimeToken } from '@tanstack/ai'\n * import { elevenlabsRealtimeToken } from '@tanstack/ai-elevenlabs'\n *\n * const token = await realtimeToken({\n * adapter: elevenlabsRealtimeToken({\n * agentId: 'your-agent-id',\n * }),\n * })\n * ```\n */\nexport function elevenlabsRealtimeToken(\n options: ElevenLabsRealtimeTokenOptions,\n): RealtimeTokenAdapter {\n const apiKey = getElevenLabsApiKey()\n\n return {\n provider: 'elevenlabs',\n\n async generateToken(): Promise<RealtimeToken> {\n const { agentId, overrides } = options\n\n // Get signed URL from ElevenLabs\n const response = await fetch(\n `${ELEVENLABS_API_URL}/convai/conversation/get_signed_url?agent_id=${agentId}`,\n {\n method: 'GET',\n headers: {\n 'xi-api-key': apiKey,\n },\n },\n )\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(\n `ElevenLabs signed URL request failed: ${response.status} ${errorText}`,\n )\n }\n\n const data = await response.json()\n const signedUrl = data.signed_url as string\n\n // Signed URLs are valid for 30 minutes\n const expiresAt = Date.now() + 30 * 60 * 1000\n\n return {\n provider: 'elevenlabs',\n token: signedUrl,\n expiresAt,\n config: {\n voice: overrides?.voiceId,\n instructions: overrides?.systemPrompt,\n providerOptions: {\n agentId,\n firstMessage: overrides?.firstMessage,\n language: overrides?.language,\n },\n },\n }\n },\n }\n}\n"],"names":[],"mappings":"AAGA,MAAM,qBAAqB;AAK3B,SAAS,sBAA8B;AAErC,MAAI,OAAO,YAAY,eAAe,QAAQ,IAAI,oBAAoB;AACpE,WAAO,QAAQ,IAAI;AAAA,EACrB;AAGA,MACE,OAAO,WAAW,eACjB,OAAgE,KAC7D,oBACJ;AACA,WAAQ,OAA8D,IACnE;AAAA,EACL;AAEA,QAAM,IAAI;AAAA,IACR;AAAA,EAAA;AAGJ;AAuBO,SAAS,wBACd,SACsB;AACtB,QAAM,SAAS,oBAAA;AAEf,SAAO;AAAA,IACL,UAAU;AAAA,IAEV,MAAM,gBAAwC;AAC5C,YAAM,EAAE,SAAS,UAAA,IAAc;AAG/B,YAAM,WAAW,MAAM;AAAA,QACrB,GAAG,kBAAkB,gDAAgD,OAAO;AAAA,QAC5E;AAAA,UACE,QAAQ;AAAA,UACR,SAAS;AAAA,YACP,cAAc;AAAA,UAAA;AAAA,QAChB;AAAA,MACF;AAGF,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,YAAY,MAAM,SAAS,KAAA;AACjC,cAAM,IAAI;AAAA,UACR,yCAAyC,SAAS,MAAM,IAAI,SAAS;AAAA,QAAA;AAAA,MAEzE;AAEA,YAAM,OAAO,MAAM,SAAS,KAAA;AAC5B,YAAM,YAAY,KAAK;AAGvB,YAAM,YAAY,KAAK,IAAA,IAAQ,KAAK,KAAK;AAEzC,aAAO;AAAA,QACL,UAAU;AAAA,QACV,OAAO;AAAA,QACP;AAAA,QACA,QAAQ;AAAA,UACN,OAAO,WAAW;AAAA,UAClB,cAAc,WAAW;AAAA,UACzB,iBAAiB;AAAA,YACf;AAAA,YACA,cAAc,WAAW;AAAA,YACzB,UAAU,WAAW;AAAA,UAAA;AAAA,QACvB;AAAA,MACF;AAAA,IAEJ;AAAA,EAAA;AAEJ;"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for the ElevenLabs realtime token adapter
|
|
3
|
+
*/
|
|
4
|
+
export interface ElevenLabsRealtimeTokenOptions {
|
|
5
|
+
/** Agent ID configured in ElevenLabs dashboard */
|
|
6
|
+
agentId: string;
|
|
7
|
+
/** Optional override values for the agent */
|
|
8
|
+
overrides?: {
|
|
9
|
+
/** Custom voice ID to use */
|
|
10
|
+
voiceId?: string;
|
|
11
|
+
/** Custom system prompt */
|
|
12
|
+
systemPrompt?: string;
|
|
13
|
+
/** First message the agent should speak */
|
|
14
|
+
firstMessage?: string;
|
|
15
|
+
/** Language code (e.g., 'en') */
|
|
16
|
+
language?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Options for the ElevenLabs realtime client adapter
|
|
21
|
+
*/
|
|
22
|
+
export interface ElevenLabsRealtimeOptions {
|
|
23
|
+
/** Connection mode (default: auto-detect) */
|
|
24
|
+
connectionMode?: 'websocket' | 'webrtc';
|
|
25
|
+
/** Enable debug logging */
|
|
26
|
+
debug?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* ElevenLabs conversation mode
|
|
30
|
+
*/
|
|
31
|
+
export type ElevenLabsConversationMode = 'speaking' | 'listening';
|
|
32
|
+
/**
|
|
33
|
+
* ElevenLabs voice activity detection configuration
|
|
34
|
+
*/
|
|
35
|
+
export interface ElevenLabsVADConfig {
|
|
36
|
+
/** VAD threshold (0.1-0.9) */
|
|
37
|
+
vadThreshold?: number;
|
|
38
|
+
/** Silence threshold in seconds (0.3-3.0) */
|
|
39
|
+
vadSilenceThresholdSecs?: number;
|
|
40
|
+
/** Minimum speech duration in ms */
|
|
41
|
+
minSpeechDurationMs?: number;
|
|
42
|
+
/** Minimum silence duration in ms */
|
|
43
|
+
minSilenceDurationMs?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Client tool definition for ElevenLabs
|
|
47
|
+
*/
|
|
48
|
+
export interface ElevenLabsClientTool<TParams = unknown, TResult = unknown> {
|
|
49
|
+
/** Tool handler function */
|
|
50
|
+
handler: (params: TParams) => Promise<TResult> | TResult;
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanstack/ai-elevenlabs",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "ElevenLabs adapter for TanStack AI realtime voice",
|
|
5
|
+
"author": "",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/TanStack/ai.git",
|
|
10
|
+
"directory": "packages/typescript/ai-elevenlabs"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai",
|
|
14
|
+
"elevenlabs",
|
|
15
|
+
"voice",
|
|
16
|
+
"realtime",
|
|
17
|
+
"tanstack",
|
|
18
|
+
"adapter"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"module": "./dist/esm/index.js",
|
|
22
|
+
"types": "./dist/esm/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/esm/index.d.ts",
|
|
26
|
+
"import": "./dist/esm/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "vite build",
|
|
35
|
+
"clean": "premove ./build ./dist",
|
|
36
|
+
"lint:fix": "eslint ./src --fix",
|
|
37
|
+
"test:build": "publint --strict",
|
|
38
|
+
"test:eslint": "eslint ./src",
|
|
39
|
+
"test:lib": "vitest --passWithNoTests",
|
|
40
|
+
"test:lib:dev": "pnpm test:lib --watch",
|
|
41
|
+
"test:types": "tsc"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@11labs/client": "^0.2.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@tanstack/ai": "^0.6.3",
|
|
48
|
+
"@tanstack/ai-client": "^0.6.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@tanstack/ai": "^0.6.3",
|
|
52
|
+
"@tanstack/ai-client": "^0.6.0",
|
|
53
|
+
"@vitest/coverage-v8": "4.0.14"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ElevenLabs Realtime (Voice) Adapters
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
export { elevenlabsRealtimeToken, elevenlabsRealtime } from './realtime/index'
|
|
6
|
+
|
|
7
|
+
export type {
|
|
8
|
+
ElevenLabsRealtimeTokenOptions,
|
|
9
|
+
ElevenLabsRealtimeOptions,
|
|
10
|
+
ElevenLabsConversationMode,
|
|
11
|
+
ElevenLabsVADConfig,
|
|
12
|
+
ElevenLabsClientTool,
|
|
13
|
+
} from './realtime/index'
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { Conversation } from '@11labs/client'
|
|
2
|
+
import type {
|
|
3
|
+
AnyClientTool,
|
|
4
|
+
AudioVisualization,
|
|
5
|
+
RealtimeEvent,
|
|
6
|
+
RealtimeEventHandler,
|
|
7
|
+
RealtimeMessage,
|
|
8
|
+
RealtimeMode,
|
|
9
|
+
RealtimeSessionConfig,
|
|
10
|
+
RealtimeStatus,
|
|
11
|
+
RealtimeToken,
|
|
12
|
+
} from '@tanstack/ai'
|
|
13
|
+
import type { RealtimeAdapter, RealtimeConnection } from '@tanstack/ai-client'
|
|
14
|
+
import type { ElevenLabsRealtimeOptions } from './types'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates an ElevenLabs realtime adapter for client-side use.
|
|
18
|
+
*
|
|
19
|
+
* Wraps the @11labs/client SDK for voice conversations.
|
|
20
|
+
*
|
|
21
|
+
* @param options - Optional configuration
|
|
22
|
+
* @returns A RealtimeAdapter for use with RealtimeClient
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { RealtimeClient } from '@tanstack/ai-client'
|
|
27
|
+
* import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
|
|
28
|
+
*
|
|
29
|
+
* const client = new RealtimeClient({
|
|
30
|
+
* getToken: () => fetch('/api/realtime-token').then(r => r.json()),
|
|
31
|
+
* adapter: elevenlabsRealtime(),
|
|
32
|
+
* })
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function elevenlabsRealtime(
|
|
36
|
+
options: ElevenLabsRealtimeOptions = {},
|
|
37
|
+
): RealtimeAdapter {
|
|
38
|
+
return {
|
|
39
|
+
provider: 'elevenlabs',
|
|
40
|
+
|
|
41
|
+
async connect(
|
|
42
|
+
token: RealtimeToken,
|
|
43
|
+
clientToolDefs?: ReadonlyArray<AnyClientTool>,
|
|
44
|
+
): Promise<RealtimeConnection> {
|
|
45
|
+
return createElevenLabsConnection(token, options, clientToolDefs)
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a connection to ElevenLabs conversational AI
|
|
52
|
+
*/
|
|
53
|
+
async function createElevenLabsConnection(
|
|
54
|
+
token: RealtimeToken,
|
|
55
|
+
_options: ElevenLabsRealtimeOptions,
|
|
56
|
+
clientToolDefs?: ReadonlyArray<AnyClientTool>,
|
|
57
|
+
): Promise<RealtimeConnection> {
|
|
58
|
+
const eventHandlers = new Map<RealtimeEvent, Set<RealtimeEventHandler<any>>>()
|
|
59
|
+
let conversation: Awaited<
|
|
60
|
+
ReturnType<typeof Conversation.startSession>
|
|
61
|
+
> | null = null
|
|
62
|
+
let messageIdCounter = 0
|
|
63
|
+
|
|
64
|
+
// Empty arrays for when visualization isn't available
|
|
65
|
+
const emptyFrequencyData = new Uint8Array(128)
|
|
66
|
+
const emptyTimeDomainData = new Uint8Array(128).fill(128)
|
|
67
|
+
|
|
68
|
+
// Helper to emit events
|
|
69
|
+
function emit<TEvent extends RealtimeEvent>(
|
|
70
|
+
event: TEvent,
|
|
71
|
+
payload: Parameters<RealtimeEventHandler<TEvent>>[0],
|
|
72
|
+
) {
|
|
73
|
+
const handlers = eventHandlers.get(event)
|
|
74
|
+
if (handlers) {
|
|
75
|
+
for (const handler of handlers) {
|
|
76
|
+
handler(payload)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function generateMessageId(): string {
|
|
82
|
+
return `el-msg-${Date.now()}-${++messageIdCounter}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Convert TanStack tool definitions to ElevenLabs clientTools format
|
|
86
|
+
const elevenLabsClientTools: Record<
|
|
87
|
+
string,
|
|
88
|
+
{
|
|
89
|
+
handler: (params: unknown) => Promise<string>
|
|
90
|
+
description: string
|
|
91
|
+
parameters: Record<string, unknown>
|
|
92
|
+
}
|
|
93
|
+
> = {}
|
|
94
|
+
|
|
95
|
+
if (clientToolDefs) {
|
|
96
|
+
for (const tool of clientToolDefs) {
|
|
97
|
+
elevenLabsClientTools[tool.name] = {
|
|
98
|
+
handler: async (params: unknown) => {
|
|
99
|
+
if (tool.execute) {
|
|
100
|
+
const result = await tool.execute(params)
|
|
101
|
+
return typeof result === 'string' ? result : JSON.stringify(result)
|
|
102
|
+
}
|
|
103
|
+
return JSON.stringify({
|
|
104
|
+
error: `No execute function for tool ${tool.name}`,
|
|
105
|
+
})
|
|
106
|
+
},
|
|
107
|
+
description: tool.description,
|
|
108
|
+
parameters: tool.inputSchema
|
|
109
|
+
? (tool.inputSchema as Record<string, unknown>)
|
|
110
|
+
: { type: 'object', properties: {} },
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Build session options
|
|
116
|
+
const sessionOptions: Record<string, unknown> = {
|
|
117
|
+
signedUrl: token.token,
|
|
118
|
+
|
|
119
|
+
onConnect: () => {
|
|
120
|
+
emit('status_change', { status: 'connected' as RealtimeStatus })
|
|
121
|
+
emit('mode_change', { mode: 'listening' })
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
onDisconnect: () => {
|
|
125
|
+
emit('status_change', { status: 'idle' as RealtimeStatus })
|
|
126
|
+
emit('mode_change', { mode: 'idle' })
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
onModeChange: ({ mode }: { mode: string }) => {
|
|
130
|
+
const mappedMode: RealtimeMode =
|
|
131
|
+
mode === 'speaking' ? 'speaking' : 'listening'
|
|
132
|
+
emit('mode_change', { mode: mappedMode })
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
onMessage: ({ message, source }: { message: string; source: string }) => {
|
|
136
|
+
const role = source === 'user' ? 'user' : 'assistant'
|
|
137
|
+
|
|
138
|
+
// Emit transcript update
|
|
139
|
+
emit('transcript', {
|
|
140
|
+
role,
|
|
141
|
+
transcript: message,
|
|
142
|
+
isFinal: true,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Create and emit message
|
|
146
|
+
const realtimeMessage: RealtimeMessage = {
|
|
147
|
+
id: generateMessageId(),
|
|
148
|
+
role,
|
|
149
|
+
timestamp: Date.now(),
|
|
150
|
+
parts: [{ type: 'audio', transcript: message }],
|
|
151
|
+
}
|
|
152
|
+
emit('message_complete', { message: realtimeMessage })
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
onError: (error: string | Error) => {
|
|
156
|
+
emit('error', {
|
|
157
|
+
error: new Error(
|
|
158
|
+
typeof error === 'string' ? error : error.message || 'Unknown error',
|
|
159
|
+
),
|
|
160
|
+
})
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Only add clientTools if we have any
|
|
165
|
+
if (Object.keys(elevenLabsClientTools).length > 0) {
|
|
166
|
+
sessionOptions.clientTools = elevenLabsClientTools
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Start the conversation session
|
|
170
|
+
conversation = await Conversation.startSession(
|
|
171
|
+
sessionOptions as Parameters<typeof Conversation.startSession>[0],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
// Connection implementation
|
|
175
|
+
const connection: RealtimeConnection = {
|
|
176
|
+
async disconnect() {
|
|
177
|
+
if (conversation) {
|
|
178
|
+
await conversation.endSession()
|
|
179
|
+
conversation = null
|
|
180
|
+
}
|
|
181
|
+
emit('status_change', { status: 'idle' as RealtimeStatus })
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async startAudioCapture() {
|
|
185
|
+
// ElevenLabs SDK handles audio capture automatically
|
|
186
|
+
// This is called when the session starts
|
|
187
|
+
emit('mode_change', { mode: 'listening' })
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
stopAudioCapture() {
|
|
191
|
+
// ElevenLabs SDK handles this
|
|
192
|
+
emit('mode_change', { mode: 'idle' })
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
sendText(text: string) {
|
|
196
|
+
if (!conversation) return
|
|
197
|
+
conversation.sendUserMessage(text)
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
sendImage(_imageData: string, _mimeType: string) {
|
|
201
|
+
// ElevenLabs does not support direct image input in the conversation API
|
|
202
|
+
console.warn(
|
|
203
|
+
'ElevenLabs realtime does not support sending images directly.',
|
|
204
|
+
)
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
sendToolResult(_callId: string, _result: string) {
|
|
208
|
+
// ElevenLabs client tools are handled via the clientTools handlers
|
|
209
|
+
// registered at session start — results are returned automatically
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
updateSession(_config: Partial<RealtimeSessionConfig>) {
|
|
213
|
+
// ElevenLabs session config is set at creation time
|
|
214
|
+
console.warn(
|
|
215
|
+
'ElevenLabs does not support runtime session updates. Configure at connection time.',
|
|
216
|
+
)
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
interrupt() {
|
|
220
|
+
// ElevenLabs handles interruption automatically via barge-in
|
|
221
|
+
// No explicit API to call
|
|
222
|
+
emit('mode_change', { mode: 'listening' })
|
|
223
|
+
emit('interrupted', {})
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
on<TEvent extends RealtimeEvent>(
|
|
227
|
+
event: TEvent,
|
|
228
|
+
handler: RealtimeEventHandler<TEvent>,
|
|
229
|
+
): () => void {
|
|
230
|
+
if (!eventHandlers.has(event)) {
|
|
231
|
+
eventHandlers.set(event, new Set())
|
|
232
|
+
}
|
|
233
|
+
eventHandlers.get(event)!.add(handler)
|
|
234
|
+
|
|
235
|
+
return () => {
|
|
236
|
+
eventHandlers.get(event)?.delete(handler)
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
getAudioVisualization(): AudioVisualization {
|
|
241
|
+
return {
|
|
242
|
+
get inputLevel() {
|
|
243
|
+
if (!conversation) return 0
|
|
244
|
+
try {
|
|
245
|
+
return conversation.getInputVolume()
|
|
246
|
+
} catch {
|
|
247
|
+
return 0
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
get outputLevel() {
|
|
252
|
+
if (!conversation) return 0
|
|
253
|
+
try {
|
|
254
|
+
return conversation.getOutputVolume()
|
|
255
|
+
} catch {
|
|
256
|
+
return 0
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
getInputFrequencyData() {
|
|
261
|
+
if (!conversation) return emptyFrequencyData
|
|
262
|
+
try {
|
|
263
|
+
return conversation.getInputByteFrequencyData()
|
|
264
|
+
} catch {
|
|
265
|
+
return emptyFrequencyData
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
getOutputFrequencyData() {
|
|
270
|
+
if (!conversation) return emptyFrequencyData
|
|
271
|
+
try {
|
|
272
|
+
return conversation.getOutputByteFrequencyData()
|
|
273
|
+
} catch {
|
|
274
|
+
return emptyFrequencyData
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
getInputTimeDomainData() {
|
|
279
|
+
// ElevenLabs SDK doesn't expose time domain data
|
|
280
|
+
return emptyTimeDomainData
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
getOutputTimeDomainData() {
|
|
284
|
+
// ElevenLabs SDK doesn't expose time domain data
|
|
285
|
+
return emptyTimeDomainData
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
get inputSampleRate() {
|
|
289
|
+
return 16000
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
get outputSampleRate() {
|
|
293
|
+
return 16000
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return connection
|
|
300
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Token adapter for server-side use
|
|
2
|
+
export { elevenlabsRealtimeToken } from './token'
|
|
3
|
+
|
|
4
|
+
// Client adapter for browser use
|
|
5
|
+
export { elevenlabsRealtime } from './adapter'
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
export type {
|
|
9
|
+
ElevenLabsRealtimeTokenOptions,
|
|
10
|
+
ElevenLabsRealtimeOptions,
|
|
11
|
+
ElevenLabsConversationMode,
|
|
12
|
+
ElevenLabsVADConfig,
|
|
13
|
+
ElevenLabsClientTool,
|
|
14
|
+
} from './types'
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { RealtimeToken, RealtimeTokenAdapter } from '@tanstack/ai'
|
|
2
|
+
import type { ElevenLabsRealtimeTokenOptions } from './types'
|
|
3
|
+
|
|
4
|
+
const ELEVENLABS_API_URL = 'https://api.elevenlabs.io/v1'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get ElevenLabs API key from environment
|
|
8
|
+
*/
|
|
9
|
+
function getElevenLabsApiKey(): string {
|
|
10
|
+
// Check process.env (Node.js)
|
|
11
|
+
if (typeof process !== 'undefined' && process.env.ELEVENLABS_API_KEY) {
|
|
12
|
+
return process.env.ELEVENLABS_API_KEY
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Check window.env (Browser with injected env)
|
|
16
|
+
if (
|
|
17
|
+
typeof window !== 'undefined' &&
|
|
18
|
+
(window as unknown as { env?: { ELEVENLABS_API_KEY?: string } }).env
|
|
19
|
+
?.ELEVENLABS_API_KEY
|
|
20
|
+
) {
|
|
21
|
+
return (window as unknown as { env: { ELEVENLABS_API_KEY: string } }).env
|
|
22
|
+
.ELEVENLABS_API_KEY
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
throw new Error(
|
|
26
|
+
'ELEVENLABS_API_KEY not found in environment variables. ' +
|
|
27
|
+
'Please set ELEVENLABS_API_KEY in your environment.',
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates an ElevenLabs realtime token adapter.
|
|
33
|
+
*
|
|
34
|
+
* This adapter generates signed URLs for client-side connections.
|
|
35
|
+
* The signed URL is valid for 30 minutes.
|
|
36
|
+
*
|
|
37
|
+
* @param options - Configuration options including agentId
|
|
38
|
+
* @returns A RealtimeTokenAdapter for use with realtimeToken()
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { realtimeToken } from '@tanstack/ai'
|
|
43
|
+
* import { elevenlabsRealtimeToken } from '@tanstack/ai-elevenlabs'
|
|
44
|
+
*
|
|
45
|
+
* const token = await realtimeToken({
|
|
46
|
+
* adapter: elevenlabsRealtimeToken({
|
|
47
|
+
* agentId: 'your-agent-id',
|
|
48
|
+
* }),
|
|
49
|
+
* })
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function elevenlabsRealtimeToken(
|
|
53
|
+
options: ElevenLabsRealtimeTokenOptions,
|
|
54
|
+
): RealtimeTokenAdapter {
|
|
55
|
+
const apiKey = getElevenLabsApiKey()
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
provider: 'elevenlabs',
|
|
59
|
+
|
|
60
|
+
async generateToken(): Promise<RealtimeToken> {
|
|
61
|
+
const { agentId, overrides } = options
|
|
62
|
+
|
|
63
|
+
// Get signed URL from ElevenLabs
|
|
64
|
+
const response = await fetch(
|
|
65
|
+
`${ELEVENLABS_API_URL}/convai/conversation/get_signed_url?agent_id=${agentId}`,
|
|
66
|
+
{
|
|
67
|
+
method: 'GET',
|
|
68
|
+
headers: {
|
|
69
|
+
'xi-api-key': apiKey,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const errorText = await response.text()
|
|
76
|
+
throw new Error(
|
|
77
|
+
`ElevenLabs signed URL request failed: ${response.status} ${errorText}`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const data = await response.json()
|
|
82
|
+
const signedUrl = data.signed_url as string
|
|
83
|
+
|
|
84
|
+
// Signed URLs are valid for 30 minutes
|
|
85
|
+
const expiresAt = Date.now() + 30 * 60 * 1000
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
provider: 'elevenlabs',
|
|
89
|
+
token: signedUrl,
|
|
90
|
+
expiresAt,
|
|
91
|
+
config: {
|
|
92
|
+
voice: overrides?.voiceId,
|
|
93
|
+
instructions: overrides?.systemPrompt,
|
|
94
|
+
providerOptions: {
|
|
95
|
+
agentId,
|
|
96
|
+
firstMessage: overrides?.firstMessage,
|
|
97
|
+
language: overrides?.language,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for the ElevenLabs realtime token adapter
|
|
3
|
+
*/
|
|
4
|
+
export interface ElevenLabsRealtimeTokenOptions {
|
|
5
|
+
/** Agent ID configured in ElevenLabs dashboard */
|
|
6
|
+
agentId: string
|
|
7
|
+
/** Optional override values for the agent */
|
|
8
|
+
overrides?: {
|
|
9
|
+
/** Custom voice ID to use */
|
|
10
|
+
voiceId?: string
|
|
11
|
+
/** Custom system prompt */
|
|
12
|
+
systemPrompt?: string
|
|
13
|
+
/** First message the agent should speak */
|
|
14
|
+
firstMessage?: string
|
|
15
|
+
/** Language code (e.g., 'en') */
|
|
16
|
+
language?: string
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Options for the ElevenLabs realtime client adapter
|
|
22
|
+
*/
|
|
23
|
+
export interface ElevenLabsRealtimeOptions {
|
|
24
|
+
/** Connection mode (default: auto-detect) */
|
|
25
|
+
connectionMode?: 'websocket' | 'webrtc'
|
|
26
|
+
/** Enable debug logging */
|
|
27
|
+
debug?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ElevenLabs conversation mode
|
|
32
|
+
*/
|
|
33
|
+
export type ElevenLabsConversationMode = 'speaking' | 'listening'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* ElevenLabs voice activity detection configuration
|
|
37
|
+
*/
|
|
38
|
+
export interface ElevenLabsVADConfig {
|
|
39
|
+
/** VAD threshold (0.1-0.9) */
|
|
40
|
+
vadThreshold?: number
|
|
41
|
+
/** Silence threshold in seconds (0.3-3.0) */
|
|
42
|
+
vadSilenceThresholdSecs?: number
|
|
43
|
+
/** Minimum speech duration in ms */
|
|
44
|
+
minSpeechDurationMs?: number
|
|
45
|
+
/** Minimum silence duration in ms */
|
|
46
|
+
minSilenceDurationMs?: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Client tool definition for ElevenLabs
|
|
51
|
+
*/
|
|
52
|
+
export interface ElevenLabsClientTool<TParams = unknown, TResult = unknown> {
|
|
53
|
+
/** Tool handler function */
|
|
54
|
+
handler: (params: TParams) => Promise<TResult> | TResult
|
|
55
|
+
}
|