@voiceinput/elevenlabs 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/index.cjs +527 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +24 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +525 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +153 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +34 -0
- package/dist/server.d.cts.map +1 -0
- package/dist/server.d.ts +34 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +152 -0
- package/dist/server.js.map +1 -0
- package/dist/session-config-D2GQf8TE.cjs +130 -0
- package/dist/session-config-D2GQf8TE.cjs.map +1 -0
- package/dist/session-config-DEaQLtZY.js +107 -0
- package/dist/session-config-DEaQLtZY.js.map +1 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hirad Arshadiyarahmadi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# `@voiceinput/elevenlabs`
|
|
2
|
+
|
|
3
|
+
ElevenLabs Realtime Scribe adapter for VoiceInput. The root export runs in the
|
|
4
|
+
browser; `@voiceinput/elevenlabs/server` mints single-use tokens with a
|
|
5
|
+
long-lived API key.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @voiceinput/react@next @voiceinput/elevenlabs@next
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Browser adapter
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { elevenlabs } from "@voiceinput/elevenlabs";
|
|
17
|
+
|
|
18
|
+
const provider = elevenlabs({
|
|
19
|
+
tokenEndpoint: "/api/voice-token",
|
|
20
|
+
vadThreshold: 0.35,
|
|
21
|
+
filterBackgroundAudio: true,
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Defaults and shared-option mapping
|
|
26
|
+
|
|
27
|
+
- Model: `scribe_v2_realtime` (`ELEVENLABS_DEFAULT_MODEL`)
|
|
28
|
+
- Audio: mono PCM16 at 16 kHz
|
|
29
|
+
- Omitted language: provider automatic detection
|
|
30
|
+
- `language`: normalized to an ISO 639-1 or ISO 639-3 primary code
|
|
31
|
+
- `vocabulary`: ElevenLabs key terms
|
|
32
|
+
- `endpointing`: VAD with a 650 ms silence threshold when omitted, manual commit
|
|
33
|
+
when `false`, or VAD with a supplied 300–3000 ms silence threshold
|
|
34
|
+
|
|
35
|
+
Vocabulary accepts at most 50 trimmed terms, each at most 20 characters and
|
|
36
|
+
without line breaks. Invalid or unsupported settings fail before microphone
|
|
37
|
+
permission with distinct error codes.
|
|
38
|
+
|
|
39
|
+
### `ElevenLabsVoiceInputProviderOptions`
|
|
40
|
+
|
|
41
|
+
| Option | Purpose |
|
|
42
|
+
| ----------------------------------- | ------------------------------------------------------------- |
|
|
43
|
+
| `tokenEndpoint` | Required same-origin endpoint that returns a single-use token |
|
|
44
|
+
| `model` | Model ID; default `scribe_v2_realtime` |
|
|
45
|
+
| `finishTimeoutMs` | Graceful final-commit deadline; default 4 seconds |
|
|
46
|
+
| `vadThreshold` | VAD threshold from 0.1 to 0.9 |
|
|
47
|
+
| `minSpeechDurationMs` | Integer from 50 to 2000 |
|
|
48
|
+
| `minSilenceDurationMs` | Integer from 50 to 2000 |
|
|
49
|
+
| `noVerbatim` | Provider no-verbatim behavior |
|
|
50
|
+
| `filterBackgroundAudio` | Provider background-audio filtering |
|
|
51
|
+
| `fetch`, `webSocket`, `realtimeUrl` | Transport/endpoint overrides |
|
|
52
|
+
|
|
53
|
+
Provider-only VAD settings customize the VAD behavior used by the portable 650
|
|
54
|
+
ms default.
|
|
55
|
+
|
|
56
|
+
## Server token handler
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { createElevenLabsTokenHandler } from "@voiceinput/elevenlabs/server";
|
|
60
|
+
|
|
61
|
+
export const POST = createElevenLabsTokenHandler({
|
|
62
|
+
apiKey: process.env.ELEVENLABS_API_KEY!,
|
|
63
|
+
authorize: async (request) => {
|
|
64
|
+
const user = await authenticate(request);
|
|
65
|
+
return user ? { subject: user.id } : null;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The handler accepts only `POST`, requires authorization, sets
|
|
71
|
+
`Cache-Control: no-store`, and returns a single-use token rather than the
|
|
72
|
+
long-lived API key. Requests must be JSON and are limited to 16 KiB.
|
|
73
|
+
Authorization and rate-limit callbacks receive independent request bodies. If
|
|
74
|
+
`onTokenIssued` throws, token delivery fails closed.
|
|
75
|
+
|
|
76
|
+
### `CreateElevenLabsTokenHandlerOptions`
|
|
77
|
+
|
|
78
|
+
| Option | Purpose |
|
|
79
|
+
| ------------------------- | -------------------------------------------------------- |
|
|
80
|
+
| `apiKey` | Required server-only ElevenLabs key |
|
|
81
|
+
| `authorize(request)` | Required application authorization |
|
|
82
|
+
| `model` | Default model; default `scribe_v2_realtime` |
|
|
83
|
+
| `allowedModels` | Browser-selectable models; defaults to only `model` |
|
|
84
|
+
| `rateLimit(context)` | Optional application quota check |
|
|
85
|
+
| `onTokenIssued(metadata)` | Metadata-only callback with provider, subject, and model |
|
|
86
|
+
| `fetch`, `tokenUrl` | Transport/endpoint overrides |
|
|
87
|
+
|
|
88
|
+
`ElevenLabsTokenHandlerContext` contains `request`, `subject`, and `model`.
|
|
89
|
+
|
|
90
|
+
## Public API
|
|
91
|
+
|
|
92
|
+
Browser root:
|
|
93
|
+
|
|
94
|
+
- `elevenlabs(options)`
|
|
95
|
+
- `ELEVENLABS_DEFAULT_MODEL`
|
|
96
|
+
- `ElevenLabsVoiceInputProviderOptions`
|
|
97
|
+
|
|
98
|
+
Server-only entry point:
|
|
99
|
+
|
|
100
|
+
- `createElevenLabsTokenHandler(options)`
|
|
101
|
+
- `CreateElevenLabsTokenHandlerOptions`
|
|
102
|
+
- `ElevenLabsAuthorization`
|
|
103
|
+
- `ElevenLabsRateLimitResult`
|
|
104
|
+
- `ElevenLabsTokenHandlerContext`
|
|
105
|
+
- `ElevenLabsTokenIssuedMetadata`
|
|
106
|
+
|
|
107
|
+
## Security
|
|
108
|
+
|
|
109
|
+
Import `/server` only from server code; the export is disabled under the browser
|
|
110
|
+
condition. Never expose `ELEVENLABS_API_KEY` to the client. The browser uses the
|
|
111
|
+
single-use token to stream audio directly to ElevenLabs.
|
|
112
|
+
|
|
113
|
+
See the
|
|
114
|
+
[secure integration guides](https://github.com/VoiceInput/voiceinput/blob/main/docs/nextjs.md).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_session_config = require("./session-config-D2GQf8TE.cjs");
|
|
3
|
+
let _voiceinput_provider_transport = require("@voiceinput/provider/transport");
|
|
4
|
+
let _voiceinput_provider = require("@voiceinput/provider");
|
|
5
|
+
//#region src/index.ts
|
|
6
|
+
const DEFAULT_REALTIME_URL = "wss://api.elevenlabs.io/v1/speech-to-text/realtime";
|
|
7
|
+
const DEFAULT_FINISH_TIMEOUT_MS = 4e3;
|
|
8
|
+
const FINISH_DRAIN_MS = 250;
|
|
9
|
+
function elevenlabs(options) {
|
|
10
|
+
const model = factoryString(options.model ?? "scribe_v2_realtime", "model");
|
|
11
|
+
const tokenEndpoint = factoryString(String(options.tokenEndpoint), "tokenEndpoint");
|
|
12
|
+
const realtimeUrl = factoryString(options.realtimeUrl ?? DEFAULT_REALTIME_URL, "realtimeUrl");
|
|
13
|
+
const finishTimeoutMs = positiveInteger(options.finishTimeoutMs ?? DEFAULT_FINISH_TIMEOUT_MS, "finishTimeoutMs");
|
|
14
|
+
const providerSettings = {
|
|
15
|
+
...options.vadThreshold === void 0 ? {} : { vadThreshold: options.vadThreshold },
|
|
16
|
+
...options.minSpeechDurationMs === void 0 ? {} : { minSpeechDurationMs: options.minSpeechDurationMs },
|
|
17
|
+
...options.minSilenceDurationMs === void 0 ? {} : { minSilenceDurationMs: options.minSilenceDurationMs },
|
|
18
|
+
...options.noVerbatim === void 0 ? {} : { noVerbatim: options.noVerbatim },
|
|
19
|
+
...options.filterBackgroundAudio === void 0 ? {} : { filterBackgroundAudio: options.filterBackgroundAudio }
|
|
20
|
+
};
|
|
21
|
+
const validateProviderOptions = (transcriptionOptions) => {
|
|
22
|
+
try {
|
|
23
|
+
require_session_config.validateElevenLabsConfiguration({
|
|
24
|
+
model,
|
|
25
|
+
...providerSettings,
|
|
26
|
+
...transcriptionOptions
|
|
27
|
+
});
|
|
28
|
+
} catch (cause) {
|
|
29
|
+
if (_voiceinput_provider.VoiceInputError.isInstance(cause)) throw cause;
|
|
30
|
+
throw invalidConfiguration(cause);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
validateProviderOptions({});
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
specificationVersion: "v1",
|
|
36
|
+
provider: "elevenlabs",
|
|
37
|
+
modelId: model,
|
|
38
|
+
sampleRate: require_session_config.ELEVENLABS_SAMPLE_RATE,
|
|
39
|
+
validateOptions: validateProviderOptions,
|
|
40
|
+
async doOpen(callOptions) {
|
|
41
|
+
validateProviderOptions(callOptions);
|
|
42
|
+
const configuration = require_session_config.validateElevenLabsConfiguration({
|
|
43
|
+
model,
|
|
44
|
+
...providerSettings,
|
|
45
|
+
...callOptions
|
|
46
|
+
});
|
|
47
|
+
return await openSession({
|
|
48
|
+
abortSignal: callOptions.abortSignal,
|
|
49
|
+
configuration,
|
|
50
|
+
fetchImplementation: options.fetch ?? globalThis.fetch,
|
|
51
|
+
finishTimeoutMs,
|
|
52
|
+
realtimeUrl,
|
|
53
|
+
tokenEndpoint,
|
|
54
|
+
WebSocketImplementation: options.webSocket ?? globalThis.WebSocket
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function openSession(options) {
|
|
60
|
+
throwIfAborted(options.abortSignal);
|
|
61
|
+
requireBrowserFunction(options.fetchImplementation, "fetch");
|
|
62
|
+
requireBrowserFunction(options.WebSocketImplementation, "WebSocket");
|
|
63
|
+
const token = await requestToken(options.fetchImplementation, options.tokenEndpoint, options.configuration.model, options.abortSignal);
|
|
64
|
+
throwIfAborted(options.abortSignal);
|
|
65
|
+
return await createSession(new options.WebSocketImplementation(require_session_config.createElevenLabsRealtimeUrl(options.realtimeUrl, token, options.configuration)), options.abortSignal, options.finishTimeoutMs);
|
|
66
|
+
}
|
|
67
|
+
async function requestToken(fetchImplementation, tokenEndpoint, model, abortSignal) {
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
response = await fetchImplementation(tokenEndpoint, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify({ model }),
|
|
74
|
+
credentials: "same-origin",
|
|
75
|
+
signal: abortSignal
|
|
76
|
+
});
|
|
77
|
+
} catch (cause) {
|
|
78
|
+
throwIfAborted(abortSignal);
|
|
79
|
+
throw new _voiceinput_provider.VoiceInputError({
|
|
80
|
+
code: "network-error",
|
|
81
|
+
message: "Unable to reach the ElevenLabs token endpoint.",
|
|
82
|
+
provider: "elevenlabs",
|
|
83
|
+
retryable: true,
|
|
84
|
+
cause
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (!response.ok) throw await tokenResponseError(response);
|
|
88
|
+
let value;
|
|
89
|
+
try {
|
|
90
|
+
value = await response.json();
|
|
91
|
+
} catch (cause) {
|
|
92
|
+
throw new _voiceinput_provider.VoiceInputError({
|
|
93
|
+
code: "token-error",
|
|
94
|
+
message: "The ElevenLabs token endpoint returned invalid JSON.",
|
|
95
|
+
provider: "elevenlabs",
|
|
96
|
+
cause
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (!isRecord(value) || !nonEmpty(value["token"])) throw new _voiceinput_provider.VoiceInputError({
|
|
100
|
+
code: "token-error",
|
|
101
|
+
message: "The ElevenLabs token endpoint returned an invalid token.",
|
|
102
|
+
provider: "elevenlabs"
|
|
103
|
+
});
|
|
104
|
+
return value["token"];
|
|
105
|
+
}
|
|
106
|
+
async function createSession(socket, abortSignal, finishTimeoutMs) {
|
|
107
|
+
let controller;
|
|
108
|
+
const stream = new ReadableStream({ start(value) {
|
|
109
|
+
controller = value;
|
|
110
|
+
} });
|
|
111
|
+
if (controller === void 0) throw new _voiceinput_provider.VoiceInputError({
|
|
112
|
+
code: "provider-error",
|
|
113
|
+
message: "Unable to initialize the ElevenLabs transcript stream.",
|
|
114
|
+
provider: "elevenlabs"
|
|
115
|
+
});
|
|
116
|
+
let hasAudio = false;
|
|
117
|
+
let closed = false;
|
|
118
|
+
let failed = false;
|
|
119
|
+
let finishing = false;
|
|
120
|
+
let finishDeferred;
|
|
121
|
+
let finishSettled = false;
|
|
122
|
+
let finishTimer;
|
|
123
|
+
let finishDrainTimer;
|
|
124
|
+
let lastInterim = "";
|
|
125
|
+
let segmentSequence = 0;
|
|
126
|
+
const segmentId = () => `commit:${segmentSequence}`;
|
|
127
|
+
let speechActive = false;
|
|
128
|
+
const closeSocket = (reason) => {
|
|
129
|
+
if (socket.readyState === 0 || socket.readyState === 1) socket.close(1e3, reason);
|
|
130
|
+
};
|
|
131
|
+
const closeStream = () => {
|
|
132
|
+
if (closed) return;
|
|
133
|
+
closed = true;
|
|
134
|
+
abortSignal.removeEventListener("abort", abort);
|
|
135
|
+
controller?.close();
|
|
136
|
+
};
|
|
137
|
+
const settleFinish = (error) => {
|
|
138
|
+
if (finishDeferred === void 0 || finishSettled) return;
|
|
139
|
+
finishSettled = true;
|
|
140
|
+
if (finishTimer !== void 0) {
|
|
141
|
+
clearTimeout(finishTimer);
|
|
142
|
+
finishTimer = void 0;
|
|
143
|
+
}
|
|
144
|
+
if (finishDrainTimer !== void 0) {
|
|
145
|
+
clearTimeout(finishDrainTimer);
|
|
146
|
+
finishDrainTimer = void 0;
|
|
147
|
+
}
|
|
148
|
+
if (error === void 0) finishDeferred.resolve(void 0);
|
|
149
|
+
else finishDeferred.reject(error);
|
|
150
|
+
};
|
|
151
|
+
const finishCleanly = () => {
|
|
152
|
+
if (lastInterim.length > 0) {
|
|
153
|
+
lastInterim = "";
|
|
154
|
+
controller?.enqueue({
|
|
155
|
+
type: "interim",
|
|
156
|
+
text: "",
|
|
157
|
+
segmentId: segmentId()
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
endSpeech();
|
|
161
|
+
settleFinish();
|
|
162
|
+
closeStream();
|
|
163
|
+
closeSocket("finished");
|
|
164
|
+
};
|
|
165
|
+
const scheduleFinishDrain = () => {
|
|
166
|
+
if (!finishing || finishSettled) return;
|
|
167
|
+
if (finishDrainTimer !== void 0) clearTimeout(finishDrainTimer);
|
|
168
|
+
finishDrainTimer = setTimeout(finishCleanly, FINISH_DRAIN_MS);
|
|
169
|
+
};
|
|
170
|
+
const fail = (error) => {
|
|
171
|
+
if (closed || failed) return;
|
|
172
|
+
failed = true;
|
|
173
|
+
settleFinish(error);
|
|
174
|
+
controller?.enqueue({
|
|
175
|
+
type: "error",
|
|
176
|
+
error
|
|
177
|
+
});
|
|
178
|
+
closeStream();
|
|
179
|
+
closeSocket("aborted");
|
|
180
|
+
};
|
|
181
|
+
const startSpeech = () => {
|
|
182
|
+
if (!speechActive) {
|
|
183
|
+
speechActive = true;
|
|
184
|
+
controller?.enqueue({ type: "speech-start" });
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const endSpeech = () => {
|
|
188
|
+
if (speechActive) {
|
|
189
|
+
speechActive = false;
|
|
190
|
+
controller?.enqueue({ type: "speech-end" });
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const emitInterim = (text) => {
|
|
194
|
+
if (text.length === 0 || text === lastInterim) return;
|
|
195
|
+
startSpeech();
|
|
196
|
+
lastInterim = text;
|
|
197
|
+
controller?.enqueue({
|
|
198
|
+
type: "interim",
|
|
199
|
+
text,
|
|
200
|
+
segmentId: segmentId()
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
const commitTranscript = (text) => {
|
|
204
|
+
if (text.length > 0) startSpeech();
|
|
205
|
+
controller?.enqueue({
|
|
206
|
+
type: "final",
|
|
207
|
+
text,
|
|
208
|
+
segmentId: segmentId()
|
|
209
|
+
});
|
|
210
|
+
segmentSequence += 1;
|
|
211
|
+
lastInterim = "";
|
|
212
|
+
if (!finishing) endSpeech();
|
|
213
|
+
};
|
|
214
|
+
const handleMessage = (event) => {
|
|
215
|
+
if (closed) return;
|
|
216
|
+
try {
|
|
217
|
+
const value = JSON.parse(String(event.data));
|
|
218
|
+
if (!isRecord(value) || !nonEmpty(value["message_type"])) throw new TypeError("ElevenLabs sent an invalid Realtime event.");
|
|
219
|
+
const type = value["message_type"];
|
|
220
|
+
if (type === "partial_transcript") {
|
|
221
|
+
emitInterim(readText(value));
|
|
222
|
+
if (finishDrainTimer !== void 0) scheduleFinishDrain();
|
|
223
|
+
} else if (type === "final_transcript") {
|
|
224
|
+
if (finishDrainTimer !== void 0) scheduleFinishDrain();
|
|
225
|
+
} else if (type === "committed_transcript") {
|
|
226
|
+
commitTranscript(readText(value));
|
|
227
|
+
if (finishing) scheduleFinishDrain();
|
|
228
|
+
} else if (elevenLabsErrorTypes.has(type)) fail(normalizeRealtimeError(value));
|
|
229
|
+
} catch (cause) {
|
|
230
|
+
fail(new _voiceinput_provider.VoiceInputError({
|
|
231
|
+
code: "provider-error",
|
|
232
|
+
message: "ElevenLabs sent an invalid Realtime event.",
|
|
233
|
+
provider: "elevenlabs",
|
|
234
|
+
cause
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const handleClose = (event) => {
|
|
239
|
+
if (closed) return;
|
|
240
|
+
if (event.code === 1e3) {
|
|
241
|
+
if (finishing) {
|
|
242
|
+
finishCleanly();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
closeStream();
|
|
246
|
+
} else fail(new _voiceinput_provider.VoiceInputError({
|
|
247
|
+
code: "network-error",
|
|
248
|
+
message: "The ElevenLabs Realtime connection closed unexpectedly.",
|
|
249
|
+
provider: "elevenlabs",
|
|
250
|
+
retryable: true,
|
|
251
|
+
cause: event
|
|
252
|
+
}));
|
|
253
|
+
};
|
|
254
|
+
function abort() {
|
|
255
|
+
if (closed) return;
|
|
256
|
+
settleFinish(abortSignal.reason ?? new _voiceinput_provider.VoiceInputError({
|
|
257
|
+
code: "provider-error",
|
|
258
|
+
message: "The ElevenLabs Realtime session was aborted.",
|
|
259
|
+
provider: "elevenlabs"
|
|
260
|
+
}));
|
|
261
|
+
closeStream();
|
|
262
|
+
closeSocket("aborted");
|
|
263
|
+
}
|
|
264
|
+
socket.addEventListener("message", handleMessage);
|
|
265
|
+
socket.addEventListener("close", handleClose);
|
|
266
|
+
socket.addEventListener("error", () => {
|
|
267
|
+
fail(new _voiceinput_provider.VoiceInputError({
|
|
268
|
+
code: "network-error",
|
|
269
|
+
message: "The ElevenLabs Realtime connection failed.",
|
|
270
|
+
provider: "elevenlabs",
|
|
271
|
+
retryable: true
|
|
272
|
+
}));
|
|
273
|
+
});
|
|
274
|
+
abortSignal.addEventListener("abort", abort, { once: true });
|
|
275
|
+
await waitForOpen(socket, abortSignal);
|
|
276
|
+
return {
|
|
277
|
+
stream,
|
|
278
|
+
sendAudio(chunk) {
|
|
279
|
+
if (closed || finishing || chunk.length === 0) return;
|
|
280
|
+
hasAudio = true;
|
|
281
|
+
return (0, _voiceinput_provider_transport.sendWithBackpressure)(socket, Math.ceil(chunk.byteLength * 4 / 3) + 160, abortSignal, "elevenlabs", () => sendJson(socket, {
|
|
282
|
+
message_type: "input_audio_chunk",
|
|
283
|
+
audio_base_64: encodePcm16(chunk),
|
|
284
|
+
commit: false,
|
|
285
|
+
sample_rate: require_session_config.ELEVENLABS_SAMPLE_RATE
|
|
286
|
+
}));
|
|
287
|
+
},
|
|
288
|
+
finish() {
|
|
289
|
+
if (finishDeferred !== void 0) return finishDeferred.promise;
|
|
290
|
+
if (closed) return;
|
|
291
|
+
finishing = true;
|
|
292
|
+
finishDeferred = createDeferred();
|
|
293
|
+
if (!hasAudio) {
|
|
294
|
+
finishCleanly();
|
|
295
|
+
return finishDeferred.promise;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
sendJson(socket, {
|
|
299
|
+
message_type: "input_audio_chunk",
|
|
300
|
+
audio_base_64: "",
|
|
301
|
+
commit: true,
|
|
302
|
+
sample_rate: require_session_config.ELEVENLABS_SAMPLE_RATE
|
|
303
|
+
});
|
|
304
|
+
} catch (cause) {
|
|
305
|
+
fail(_voiceinput_provider.VoiceInputError.isInstance(cause) ? cause : new _voiceinput_provider.VoiceInputError({
|
|
306
|
+
code: "network-error",
|
|
307
|
+
message: "Unable to commit the ElevenLabs transcript.",
|
|
308
|
+
provider: "elevenlabs",
|
|
309
|
+
retryable: true,
|
|
310
|
+
cause
|
|
311
|
+
}));
|
|
312
|
+
return finishDeferred.promise;
|
|
313
|
+
}
|
|
314
|
+
finishTimer = setTimeout(() => {
|
|
315
|
+
fail(new _voiceinput_provider.VoiceInputError({
|
|
316
|
+
code: "network-error",
|
|
317
|
+
message: `ElevenLabs did not commit the final transcript within ${finishTimeoutMs}ms.`,
|
|
318
|
+
provider: "elevenlabs",
|
|
319
|
+
retryable: true
|
|
320
|
+
}));
|
|
321
|
+
}, finishTimeoutMs);
|
|
322
|
+
return finishDeferred.promise;
|
|
323
|
+
},
|
|
324
|
+
abort
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const elevenLabsErrorTypes = /* @__PURE__ */ new Set([
|
|
328
|
+
"auth_error",
|
|
329
|
+
"quota_exceeded",
|
|
330
|
+
"transcriber_error",
|
|
331
|
+
"input_error",
|
|
332
|
+
"invalid_request",
|
|
333
|
+
"error",
|
|
334
|
+
"commit_throttled",
|
|
335
|
+
"unaccepted_terms",
|
|
336
|
+
"rate_limited",
|
|
337
|
+
"queue_overflow",
|
|
338
|
+
"resource_exhausted",
|
|
339
|
+
"session_time_limit_exceeded",
|
|
340
|
+
"chunk_size_exceeded",
|
|
341
|
+
"insufficient_audio_activity"
|
|
342
|
+
]);
|
|
343
|
+
function normalizeRealtimeError(value) {
|
|
344
|
+
const type = String(value["message_type"]);
|
|
345
|
+
const rateLimited = type === "rate_limited" || type === "quota_exceeded" || type === "commit_throttled";
|
|
346
|
+
const retryable = rateLimited || type === "queue_overflow" || type === "resource_exhausted";
|
|
347
|
+
return new _voiceinput_provider.VoiceInputError({
|
|
348
|
+
code: type === "auth_error" ? "unauthorized" : rateLimited ? "rate-limited" : type === "input_error" || type === "chunk_size_exceeded" ? "audio-error" : "provider-error",
|
|
349
|
+
message: typeof value["error"] === "string" ? value["error"] : `ElevenLabs Realtime reported ${type}.`,
|
|
350
|
+
provider: "elevenlabs",
|
|
351
|
+
retryable,
|
|
352
|
+
cause: value
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function invalidConfiguration(cause) {
|
|
356
|
+
return new _voiceinput_provider.VoiceInputError({
|
|
357
|
+
code: "invalid-configuration",
|
|
358
|
+
message: cause instanceof Error ? cause.message : "Invalid ElevenLabs transcription options.",
|
|
359
|
+
provider: "elevenlabs",
|
|
360
|
+
cause
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function readText(value) {
|
|
364
|
+
if (typeof value["text"] !== "string") throw new TypeError("text must be a string.");
|
|
365
|
+
return value["text"];
|
|
366
|
+
}
|
|
367
|
+
function requireBrowserFunction(value, feature) {
|
|
368
|
+
if (typeof value !== "function") throw new _voiceinput_provider.VoiceInputError({
|
|
369
|
+
code: "unsupported-browser",
|
|
370
|
+
message: `ElevenLabs voice input requires browser ${feature} support.`,
|
|
371
|
+
provider: "elevenlabs"
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
function factoryString(value, name) {
|
|
375
|
+
if (value.trim().length === 0) throw invalidConfiguration(/* @__PURE__ */ new TypeError(`${name} must be non-empty.`));
|
|
376
|
+
return value;
|
|
377
|
+
}
|
|
378
|
+
function positiveInteger(value, name) {
|
|
379
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw invalidConfiguration(/* @__PURE__ */ new TypeError(`${name} must be a positive safe integer.`));
|
|
380
|
+
return value;
|
|
381
|
+
}
|
|
382
|
+
function createDeferred() {
|
|
383
|
+
let resolve = () => {};
|
|
384
|
+
let reject = () => {};
|
|
385
|
+
return {
|
|
386
|
+
promise: new Promise((resolve_, reject_) => {
|
|
387
|
+
resolve = resolve_;
|
|
388
|
+
reject = reject_;
|
|
389
|
+
}),
|
|
390
|
+
resolve,
|
|
391
|
+
reject
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
async function tokenResponseError(response) {
|
|
395
|
+
const retryAfterMs = parseRetryAfter(response.headers.get("Retry-After"));
|
|
396
|
+
if (response.status === 401 || response.status === 403) return new _voiceinput_provider.VoiceInputError({
|
|
397
|
+
code: "unauthorized",
|
|
398
|
+
message: "The ElevenLabs token endpoint rejected this request.",
|
|
399
|
+
provider: "elevenlabs"
|
|
400
|
+
});
|
|
401
|
+
if (response.status === 429) return new _voiceinput_provider.VoiceInputError({
|
|
402
|
+
code: "rate-limited",
|
|
403
|
+
message: "The ElevenLabs token endpoint rate limit was exceeded.",
|
|
404
|
+
provider: "elevenlabs",
|
|
405
|
+
retryable: true,
|
|
406
|
+
...retryAfterMs === void 0 ? {} : { retryAfterMs }
|
|
407
|
+
});
|
|
408
|
+
const safeError = await readSafeTokenError(response);
|
|
409
|
+
if (safeError !== void 0) return new _voiceinput_provider.VoiceInputError({
|
|
410
|
+
...safeError,
|
|
411
|
+
provider: "elevenlabs"
|
|
412
|
+
});
|
|
413
|
+
return new _voiceinput_provider.VoiceInputError({
|
|
414
|
+
code: "token-error",
|
|
415
|
+
message: "The ElevenLabs token endpoint did not issue a token.",
|
|
416
|
+
provider: "elevenlabs",
|
|
417
|
+
retryable: response.status >= 500
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
async function readSafeTokenError(response) {
|
|
421
|
+
if (response.status !== 400 || response.headers.get("X-VoiceInput-Error") !== "1" || response.headers.get("Content-Type")?.split(";", 1)[0]?.trim() !== "application/json") return;
|
|
422
|
+
const text = await readBoundedErrorText(response);
|
|
423
|
+
if (text === void 0) return void 0;
|
|
424
|
+
try {
|
|
425
|
+
const value = JSON.parse(text);
|
|
426
|
+
const error = isRecord(value) ? value["error"] : void 0;
|
|
427
|
+
if (!isRecord(error) || error["code"] !== "invalid-configuration" && error["code"] !== "unsupported-feature" || typeof error["message"] !== "string" || error["message"].length === 0 || error["message"].length > 1e3) return;
|
|
428
|
+
return {
|
|
429
|
+
code: error["code"],
|
|
430
|
+
message: error["message"]
|
|
431
|
+
};
|
|
432
|
+
} catch {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function readBoundedErrorText(response) {
|
|
437
|
+
const reader = response.body?.getReader();
|
|
438
|
+
if (reader === void 0) return void 0;
|
|
439
|
+
const decoder = new TextDecoder();
|
|
440
|
+
let bytesRead = 0;
|
|
441
|
+
let text = "";
|
|
442
|
+
try {
|
|
443
|
+
while (true) {
|
|
444
|
+
const { done, value } = await reader.read();
|
|
445
|
+
if (done) return text + decoder.decode();
|
|
446
|
+
bytesRead += value.byteLength;
|
|
447
|
+
if (bytesRead > 4096) {
|
|
448
|
+
await reader.cancel().catch(() => {});
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
text += decoder.decode(value, { stream: true });
|
|
452
|
+
}
|
|
453
|
+
} catch {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function waitForOpen(socket, abortSignal) {
|
|
458
|
+
if (socket.readyState === 1) return Promise.resolve();
|
|
459
|
+
return new Promise((resolve, reject) => {
|
|
460
|
+
const cleanup = () => {
|
|
461
|
+
socket.removeEventListener("open", handleOpen);
|
|
462
|
+
socket.removeEventListener("error", handleError);
|
|
463
|
+
abortSignal.removeEventListener("abort", handleAbort);
|
|
464
|
+
};
|
|
465
|
+
const handleOpen = () => {
|
|
466
|
+
cleanup();
|
|
467
|
+
resolve();
|
|
468
|
+
};
|
|
469
|
+
const handleError = (event) => {
|
|
470
|
+
cleanup();
|
|
471
|
+
reject(new _voiceinput_provider.VoiceInputError({
|
|
472
|
+
code: "network-error",
|
|
473
|
+
message: "Unable to open the ElevenLabs Realtime connection.",
|
|
474
|
+
provider: "elevenlabs",
|
|
475
|
+
retryable: true,
|
|
476
|
+
cause: event
|
|
477
|
+
}));
|
|
478
|
+
};
|
|
479
|
+
const handleAbort = () => {
|
|
480
|
+
cleanup();
|
|
481
|
+
reject(abortSignal.reason);
|
|
482
|
+
};
|
|
483
|
+
socket.addEventListener("open", handleOpen, { once: true });
|
|
484
|
+
socket.addEventListener("error", handleError, { once: true });
|
|
485
|
+
abortSignal.addEventListener("abort", handleAbort, { once: true });
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
function sendJson(socket, value) {
|
|
489
|
+
try {
|
|
490
|
+
socket.send(JSON.stringify(value));
|
|
491
|
+
} catch (cause) {
|
|
492
|
+
throw new _voiceinput_provider.VoiceInputError({
|
|
493
|
+
code: "network-error",
|
|
494
|
+
message: "Unable to send data to ElevenLabs Realtime.",
|
|
495
|
+
provider: "elevenlabs",
|
|
496
|
+
retryable: true,
|
|
497
|
+
cause
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function encodePcm16(chunk) {
|
|
502
|
+
const bytes = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
503
|
+
let binary = "";
|
|
504
|
+
for (let offset = 0; offset < bytes.length; offset += 8192) binary += String.fromCharCode(...bytes.subarray(offset, offset + 8192));
|
|
505
|
+
return btoa(binary);
|
|
506
|
+
}
|
|
507
|
+
function parseRetryAfter(value) {
|
|
508
|
+
if (value === null) return;
|
|
509
|
+
const seconds = Number(value);
|
|
510
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds * 1e3);
|
|
511
|
+
const date = Date.parse(value);
|
|
512
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
|
|
513
|
+
}
|
|
514
|
+
function throwIfAborted(signal) {
|
|
515
|
+
if (signal.aborted) throw signal.reason;
|
|
516
|
+
}
|
|
517
|
+
function nonEmpty(value) {
|
|
518
|
+
return typeof value === "string" && value.length > 0;
|
|
519
|
+
}
|
|
520
|
+
function isRecord(value) {
|
|
521
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
522
|
+
}
|
|
523
|
+
//#endregion
|
|
524
|
+
exports.ELEVENLABS_DEFAULT_MODEL = require_session_config.ELEVENLABS_DEFAULT_MODEL;
|
|
525
|
+
exports.elevenlabs = elevenlabs;
|
|
526
|
+
|
|
527
|
+
//# sourceMappingURL=index.cjs.map
|