aillom-vox-client 1.0.2 → 2.1.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 +102 -195
- package/dist/AillomVox.d.ts +78 -19
- package/dist/AillomVox.js +345 -60
- package/dist/constants.d.ts +19 -0
- package/dist/constants.js +35 -0
- package/dist/gateway-url.d.ts +12 -0
- package/dist/gateway-url.js +74 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/pricing.d.ts +26 -0
- package/dist/pricing.js +20 -0
- package/dist/types.d.ts +168 -11
- package/package.json +18 -7
- package/docs/ASTERISK.md +0 -411
- package/docs/PROTOCOL.md +0 -156
- package/docs/PROVIDERS.md +0 -40
- package/docs/TOOLS.md +0 -314
- package/docs/TROUBLESHOOTING.md +0 -86
- package/docs/VOICES.md +0 -219
- package/docs/providers/AILLOMVOX.md +0 -185
- package/docs/providers/AWS.md +0 -32
- package/docs/providers/GEMINI.md +0 -33
- package/docs/providers/GROK.md +0 -25
- package/docs/providers/OPENAI.md +0 -39
- package/docs/providers/QWEN.md +0 -27
- package/docs/providers/ULTRAVOX.md +0 -29
- package/examples/01-basic/app.js +0 -196
- package/examples/01-basic/index.html +0 -27
- package/examples/02-advanced-dashboard/app.js +0 -465
- package/examples/02-advanced-dashboard/index.html +0 -200
- package/examples/02-advanced-dashboard/style.css +0 -501
- package/examples/03-smart-home/index.html +0 -377
- package/examples/04-customer-support/index.html +0 -474
- package/examples/sdk-usage.ts +0 -44
- package/integrations/n8n-nodes-aillomvox/README.md +0 -56
- package/integrations/n8n-nodes-aillomvox/credentials/AillomVoxApi.credentials.ts +0 -29
- package/integrations/n8n-nodes-aillomvox/dist/credentials/AillomVoxApi.credentials.js +0 -30
- package/integrations/n8n-nodes-aillomvox/dist/nodes/AillomVox/AillomVox.node.js +0 -219
- package/integrations/n8n-nodes-aillomvox/dist/nodes/AillomVox/aillomvox.svg +0 -6
- package/integrations/n8n-nodes-aillomvox/gulpfile.js +0 -10
- package/integrations/n8n-nodes-aillomvox/nodes/AillomVox/AillomVox.node.ts +0 -229
- package/integrations/n8n-nodes-aillomvox/nodes/AillomVox/aillomvox.svg +0 -6
- package/integrations/n8n-nodes-aillomvox/package-lock.json +0 -11741
- package/integrations/n8n-nodes-aillomvox/package.json +0 -56
- package/integrations/n8n-nodes-aillomvox/tsconfig.json +0 -32
- package/src/AillomVox.ts +0 -172
- package/src/index.ts +0 -2
- package/src/types.ts +0 -51
- package/tsconfig.json +0 -23
package/dist/AillomVox.js
CHANGED
|
@@ -4,28 +4,57 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.AillomVox = void 0;
|
|
7
|
+
const constants_1 = require("./constants");
|
|
8
|
+
const gateway_url_1 = require("./gateway-url");
|
|
7
9
|
const isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
|
|
10
|
+
function isNodeRuntime() {
|
|
11
|
+
return typeof process !== 'undefined' && Boolean(process.versions?.node);
|
|
12
|
+
}
|
|
13
|
+
function resolveHttpOrigin(baseUrl) {
|
|
14
|
+
if (baseUrl?.startsWith('wss://') || baseUrl?.startsWith('ws://')) {
|
|
15
|
+
return (0, gateway_url_1.httpOriginFromGatewayUrl)(baseUrl);
|
|
16
|
+
}
|
|
17
|
+
return baseUrl?.replace(/\/?$/, '') || constants_1.AILLOMVOX_DEFAULT_HTTP_ORIGIN;
|
|
18
|
+
}
|
|
19
|
+
function setSearchParams(url, params) {
|
|
20
|
+
for (const [key, value] of Object.entries(params)) {
|
|
21
|
+
if (value === undefined || value === null || String(value).trim() === '')
|
|
22
|
+
continue;
|
|
23
|
+
url.searchParams.set(key, String(value));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Browser or Node.js WebSocket client for the AillomVox voice gateway.
|
|
28
|
+
*
|
|
29
|
+
* Protocol summary: after `connect()`, the first outbound message must be the JSON `config`
|
|
30
|
+
* handshake (sent automatically). All further outbound binary messages are PCM16 mono chunks.
|
|
31
|
+
*/
|
|
8
32
|
class AillomVox {
|
|
9
33
|
constructor(config) {
|
|
10
34
|
this.ws = null;
|
|
11
35
|
this.eventListeners = new Map();
|
|
12
36
|
this.isConnected = false;
|
|
13
|
-
this.url = 'wss://wss.aillom.com/ws';
|
|
14
37
|
this.config = config;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
if (!this.config.apiKey) {
|
|
38
|
+
this.url = (0, gateway_url_1.normalizeWebSocketUrl)(config.gatewayUrl);
|
|
39
|
+
if (!config.apiKey) {
|
|
19
40
|
throw new Error('AillomVox: apiKey is required');
|
|
20
41
|
}
|
|
21
42
|
}
|
|
43
|
+
/** Resolved WebSocket URL after normalization. */
|
|
44
|
+
get websocketUrl() {
|
|
45
|
+
return this.url;
|
|
46
|
+
}
|
|
47
|
+
/** True when the underlying WebSocket is open. */
|
|
48
|
+
get connected() {
|
|
49
|
+
return Boolean(this.ws && this.ws.readyState === isomorphic_ws_1.default.OPEN);
|
|
50
|
+
}
|
|
22
51
|
/**
|
|
23
|
-
* Connects to the
|
|
52
|
+
* Connects to the gateway and sends the `config` handshake as the first message.
|
|
24
53
|
*/
|
|
25
54
|
connect() {
|
|
26
55
|
return new Promise((resolve, reject) => {
|
|
27
56
|
try {
|
|
28
|
-
this.ws =
|
|
57
|
+
this.ws = this.createWebSocket();
|
|
29
58
|
this.ws.binaryType = 'arraybuffer';
|
|
30
59
|
this.ws.onopen = () => {
|
|
31
60
|
this.isConnected = true;
|
|
@@ -51,29 +80,71 @@ class AillomVox {
|
|
|
51
80
|
}
|
|
52
81
|
});
|
|
53
82
|
}
|
|
83
|
+
createWebSocket() {
|
|
84
|
+
if (this.config.authMode === 'header' && !isNodeRuntime()) {
|
|
85
|
+
throw new Error('AillomVox: authMode "header" requires a Node.js runtime with WebSocket header support');
|
|
86
|
+
}
|
|
87
|
+
if (this.shouldUseHeaderAuth()) {
|
|
88
|
+
const WebSocketWithOptions = isomorphic_ws_1.default;
|
|
89
|
+
return new WebSocketWithOptions(this.url, [], {
|
|
90
|
+
headers: { 'x-api-key': this.config.apiKey },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return new isomorphic_ws_1.default(this.url);
|
|
94
|
+
}
|
|
95
|
+
shouldUseHeaderAuth() {
|
|
96
|
+
const mode = this.config.authMode ?? 'auto';
|
|
97
|
+
if (mode === 'header' || mode === 'both')
|
|
98
|
+
return isNodeRuntime();
|
|
99
|
+
return mode === 'auto' && isNodeRuntime();
|
|
100
|
+
}
|
|
101
|
+
shouldSendHandshakeApiKey() {
|
|
102
|
+
const mode = this.config.authMode ?? 'auto';
|
|
103
|
+
if (mode === 'handshake' || mode === 'both')
|
|
104
|
+
return true;
|
|
105
|
+
if (mode === 'header')
|
|
106
|
+
return false;
|
|
107
|
+
return !this.shouldUseHeaderAuth();
|
|
108
|
+
}
|
|
54
109
|
/**
|
|
55
|
-
*
|
|
110
|
+
* Send microphone capture to the model. PCM16 LE mono at the configured `sampleRate`.
|
|
56
111
|
*/
|
|
57
112
|
sendAudio(chunk) {
|
|
58
113
|
if (!this.ws || this.ws.readyState !== isomorphic_ws_1.default.OPEN)
|
|
59
114
|
return;
|
|
115
|
+
if (chunk instanceof Int16Array) {
|
|
116
|
+
this.ws.send(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
60
119
|
this.ws.send(chunk);
|
|
61
120
|
}
|
|
62
121
|
/**
|
|
63
|
-
*
|
|
122
|
+
* Reply to a `tool_call` event within 15 seconds or the model may stall.
|
|
64
123
|
*/
|
|
65
124
|
sendToolResult(callId, result) {
|
|
66
|
-
|
|
67
|
-
return;
|
|
68
|
-
this.ws.send(JSON.stringify({
|
|
125
|
+
this.sendJson({
|
|
69
126
|
type: 'tool_result',
|
|
70
127
|
call_id: callId,
|
|
71
|
-
result
|
|
72
|
-
})
|
|
128
|
+
result,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Ask the server to end the call (mirrors dashboard playground). */
|
|
132
|
+
sendHangup() {
|
|
133
|
+
this.sendJson({ type: 'hangup' });
|
|
134
|
+
}
|
|
135
|
+
/** Send a text turn over the same WebSocket session. */
|
|
136
|
+
sendText(text) {
|
|
137
|
+
this.sendJson({ type: 'text', data: text });
|
|
138
|
+
}
|
|
139
|
+
/** Send image payload data for gateways with vision support. */
|
|
140
|
+
sendImage(data) {
|
|
141
|
+
this.sendJson({ type: 'image', data });
|
|
142
|
+
}
|
|
143
|
+
sendJson(payload) {
|
|
144
|
+
if (!this.ws || this.ws.readyState !== isomorphic_ws_1.default.OPEN)
|
|
145
|
+
return;
|
|
146
|
+
this.ws.send(JSON.stringify(payload));
|
|
73
147
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Disconnects the session
|
|
76
|
-
*/
|
|
77
148
|
disconnect() {
|
|
78
149
|
if (this.ws) {
|
|
79
150
|
this.ws.close();
|
|
@@ -85,71 +156,285 @@ class AillomVox {
|
|
|
85
156
|
if (!this.eventListeners.has(event)) {
|
|
86
157
|
this.eventListeners.set(event, []);
|
|
87
158
|
}
|
|
88
|
-
this.eventListeners.get(event)
|
|
159
|
+
this.eventListeners.get(event).push(handler);
|
|
160
|
+
}
|
|
161
|
+
off(event, handler) {
|
|
162
|
+
const list = this.eventListeners.get(event);
|
|
163
|
+
if (!list)
|
|
164
|
+
return;
|
|
165
|
+
this.eventListeners.set(event, list.filter((h) => h !== handler));
|
|
89
166
|
}
|
|
90
167
|
sendConfig() {
|
|
91
168
|
if (!this.ws)
|
|
92
169
|
return;
|
|
93
170
|
const payload = {
|
|
94
171
|
type: 'config',
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
system_prompt: this.config.systemPrompt,
|
|
101
|
-
tools: this.config.tools,
|
|
102
|
-
webhook_url: this.config.webhookUrl,
|
|
103
|
-
max_duration: this.config.maxDuration
|
|
172
|
+
provider: (this.config.provider ?? 'aillomvox').toLowerCase(),
|
|
173
|
+
voice: this.config.voice ?? 'Aanya',
|
|
174
|
+
language: this.config.language ?? 'en-US',
|
|
175
|
+
sample_rate: this.config.sampleRate ?? 16000,
|
|
176
|
+
tools: this.config.tools ?? [],
|
|
104
177
|
};
|
|
178
|
+
if (this.shouldSendHandshakeApiKey()) {
|
|
179
|
+
payload.apikey = this.config.apiKey;
|
|
180
|
+
}
|
|
181
|
+
if (this.config.systemPrompt !== undefined) {
|
|
182
|
+
payload.system_prompt = this.config.systemPrompt;
|
|
183
|
+
}
|
|
184
|
+
if (this.config.workspaceId) {
|
|
185
|
+
payload.workspace_id = this.config.workspaceId;
|
|
186
|
+
}
|
|
187
|
+
if (this.config.webhookUrl) {
|
|
188
|
+
payload.webhook_url = this.config.webhookUrl;
|
|
189
|
+
}
|
|
190
|
+
if (this.config.maxDuration !== undefined) {
|
|
191
|
+
payload.max_duration = this.config.maxDuration;
|
|
192
|
+
}
|
|
193
|
+
if (this.config.firstMessage !== undefined) {
|
|
194
|
+
payload.first_message = this.config.firstMessage;
|
|
195
|
+
}
|
|
196
|
+
if (this.config.farewellMessage !== undefined) {
|
|
197
|
+
payload.farewell_message = this.config.farewellMessage;
|
|
198
|
+
}
|
|
199
|
+
if (this.config.qualityProfile)
|
|
200
|
+
payload.quality_profile = this.config.qualityProfile;
|
|
201
|
+
if (this.config.toolTimeout !== undefined)
|
|
202
|
+
payload.tool_timeout = this.config.toolTimeout;
|
|
203
|
+
if (this.config.ttsBufferMs !== undefined)
|
|
204
|
+
payload.tts_buffer_ms = this.config.ttsBufferMs;
|
|
205
|
+
if (this.config.ttsEarlyStartMs !== undefined)
|
|
206
|
+
payload.tts_early_start_ms = this.config.ttsEarlyStartMs;
|
|
207
|
+
if (this.config.ttsMinChunkMs !== undefined)
|
|
208
|
+
payload.tts_min_chunk_ms = this.config.ttsMinChunkMs;
|
|
209
|
+
if (this.config.streamLlmTextToTts !== undefined)
|
|
210
|
+
payload.stream_llm_text_to_tts = this.config.streamLlmTextToTts;
|
|
211
|
+
if (this.config.accumulatorMs !== undefined)
|
|
212
|
+
payload.accumulator_ms = this.config.accumulatorMs;
|
|
213
|
+
if (this.config.extraConfig)
|
|
214
|
+
payload.extra_config = this.config.extraConfig;
|
|
215
|
+
const provider = String(payload.provider);
|
|
216
|
+
if (provider === 'aillomvox') {
|
|
217
|
+
payload.tts_engine = this.config.ttsEngine ?? 'inworld';
|
|
218
|
+
}
|
|
105
219
|
if (this.config.debug) {
|
|
106
|
-
|
|
220
|
+
const redacted = { ...payload };
|
|
221
|
+
if (redacted.apikey)
|
|
222
|
+
redacted.apikey = '[REDACTED]';
|
|
223
|
+
console.log('[AillomVox] config:', JSON.stringify(redacted, null, 2));
|
|
107
224
|
}
|
|
108
225
|
this.ws.send(JSON.stringify(payload));
|
|
109
226
|
}
|
|
110
227
|
handleMessage(event) {
|
|
111
228
|
const data = event.data;
|
|
112
|
-
// Handle Binary Audio
|
|
113
229
|
if (data instanceof ArrayBuffer || (typeof Buffer !== 'undefined' && Buffer.isBuffer(data))) {
|
|
114
230
|
this.emit('audio', data);
|
|
115
231
|
return;
|
|
116
232
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
233
|
+
if (typeof data !== 'string')
|
|
234
|
+
return;
|
|
235
|
+
let msg;
|
|
236
|
+
try {
|
|
237
|
+
msg = JSON.parse(data);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
console.error('[AillomVox] Non-JSON text message');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
this.emit('raw', msg);
|
|
244
|
+
const t = msg.type;
|
|
245
|
+
switch (t) {
|
|
246
|
+
case 'transcript':
|
|
247
|
+
this.emit('transcript', msg);
|
|
248
|
+
break;
|
|
249
|
+
case 'tool_call':
|
|
250
|
+
this.emit('tool_call', msg);
|
|
251
|
+
break;
|
|
252
|
+
case 'error':
|
|
253
|
+
this.emit('error', msg);
|
|
254
|
+
break;
|
|
255
|
+
case 'interruption':
|
|
256
|
+
this.emit('interruption', {});
|
|
257
|
+
break;
|
|
258
|
+
case 'playback_clear_buffer':
|
|
259
|
+
this.emit('playback_clear_buffer', {});
|
|
260
|
+
break;
|
|
261
|
+
case 'state':
|
|
262
|
+
this.emit('state', msg);
|
|
263
|
+
break;
|
|
264
|
+
case 'control':
|
|
265
|
+
this.emit('control', msg);
|
|
266
|
+
break;
|
|
267
|
+
case 'hangup':
|
|
268
|
+
case 'close':
|
|
269
|
+
this.disconnect();
|
|
270
|
+
this.emit('disconnected', { reason: 'server_hangup' });
|
|
271
|
+
break;
|
|
272
|
+
default:
|
|
273
|
+
if (this.config.debug)
|
|
274
|
+
console.log('[AillomVox] message:', msg);
|
|
146
275
|
}
|
|
147
276
|
}
|
|
148
277
|
emit(event, data) {
|
|
149
278
|
const listeners = this.eventListeners.get(event);
|
|
150
|
-
if (listeners)
|
|
151
|
-
|
|
279
|
+
if (!listeners)
|
|
280
|
+
return;
|
|
281
|
+
for (const handler of listeners)
|
|
282
|
+
handler(data);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* `GET /api/providers` — models and nested voices (public; optional auth for workspace scoping).
|
|
286
|
+
*/
|
|
287
|
+
static async fetchProviders(options) {
|
|
288
|
+
const origin = resolveHttpOrigin(options?.baseUrl);
|
|
289
|
+
const url = new URL('/api/providers', origin.endsWith('/') ? origin : `${origin}/`);
|
|
290
|
+
setSearchParams(url, {
|
|
291
|
+
workspace_id: options?.workspaceId,
|
|
292
|
+
include_voices: options?.includeVoices === undefined ? undefined : options.includeVoices,
|
|
293
|
+
});
|
|
294
|
+
const headers = {};
|
|
295
|
+
if (options?.apiKey)
|
|
296
|
+
headers['x-api-key'] = options.apiKey;
|
|
297
|
+
const res = await fetch(url.toString(), { headers });
|
|
298
|
+
if (!res.ok) {
|
|
299
|
+
const body = await res.text();
|
|
300
|
+
throw new Error(`fetchProviders failed (${res.status}): ${body}`);
|
|
301
|
+
}
|
|
302
|
+
return res.json();
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* `GET /api/pricing` — public USD/min rate card from the live gateway.
|
|
306
|
+
*/
|
|
307
|
+
static async fetchPricing(options) {
|
|
308
|
+
const origin = resolveHttpOrigin(options?.baseUrl);
|
|
309
|
+
const url = new URL('/api/pricing', origin.endsWith('/') ? origin : `${origin}/`);
|
|
310
|
+
const res = await fetch(url.toString());
|
|
311
|
+
if (!res.ok) {
|
|
312
|
+
const body = await res.text();
|
|
313
|
+
throw new Error(`fetchPricing failed (${res.status}): ${body}`);
|
|
314
|
+
}
|
|
315
|
+
return res.json();
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* `GET /api/voices` — optional provider filter matches dashboard catalog keys.
|
|
319
|
+
*/
|
|
320
|
+
static async fetchVoices(options) {
|
|
321
|
+
const origin = resolveHttpOrigin(options?.baseUrl);
|
|
322
|
+
const url = new URL('/api/voices', origin.endsWith('/') ? origin : `${origin}/`);
|
|
323
|
+
setSearchParams(url, {
|
|
324
|
+
provider: options.provider,
|
|
325
|
+
workspace_id: options.workspaceId,
|
|
326
|
+
page_size: options.pageSize,
|
|
327
|
+
page_number: options.pageNumber,
|
|
328
|
+
max_pages: options.maxPages,
|
|
329
|
+
q: options.q,
|
|
330
|
+
title: options.title,
|
|
331
|
+
tag: options.tag,
|
|
332
|
+
language: options.language,
|
|
333
|
+
title_language: options.titleLanguage,
|
|
334
|
+
sort_by: options.sortBy,
|
|
335
|
+
preferred_language: options.preferredLanguage,
|
|
336
|
+
type: options.type,
|
|
337
|
+
scope: options.scope,
|
|
338
|
+
visibility: options.visibility,
|
|
339
|
+
});
|
|
340
|
+
const headers = {};
|
|
341
|
+
if (options?.apiKey)
|
|
342
|
+
headers['x-api-key'] = options.apiKey;
|
|
343
|
+
const res = await fetch(url.toString(), { headers });
|
|
344
|
+
if (!res.ok) {
|
|
345
|
+
const body = await res.text();
|
|
346
|
+
throw new Error(`fetchVoices failed (${res.status}): ${body}`);
|
|
347
|
+
}
|
|
348
|
+
return res.json();
|
|
349
|
+
}
|
|
350
|
+
/** Build the public `/api/voices/preview` URL for a provider voice. */
|
|
351
|
+
static buildVoicePreviewUrl(options) {
|
|
352
|
+
const origin = resolveHttpOrigin(options.baseUrl);
|
|
353
|
+
const url = new URL('/api/voices/preview', origin.endsWith('/') ? origin : `${origin}/`);
|
|
354
|
+
setSearchParams(url, {
|
|
355
|
+
provider: options.provider,
|
|
356
|
+
voice: options.voice,
|
|
357
|
+
});
|
|
358
|
+
return url.toString();
|
|
359
|
+
}
|
|
360
|
+
/** `GET /api/voices/preview` — returns an audio Blob for UI preview playback. */
|
|
361
|
+
static async fetchVoicePreview(options) {
|
|
362
|
+
const res = await fetch(AillomVox.buildVoicePreviewUrl(options));
|
|
363
|
+
if (!res.ok) {
|
|
364
|
+
const body = await res.text();
|
|
365
|
+
throw new Error(`fetchVoicePreview failed (${res.status}): ${body}`);
|
|
366
|
+
}
|
|
367
|
+
return res.blob();
|
|
368
|
+
}
|
|
369
|
+
/** `DELETE /api/voices/:id` — removes a workspace-owned cloned voice. */
|
|
370
|
+
static async deleteVoice(voiceId, apiKey, options = {}) {
|
|
371
|
+
const origin = resolveHttpOrigin(options.baseUrl);
|
|
372
|
+
const url = new URL(`/api/voices/${encodeURIComponent(voiceId)}`, origin.endsWith('/') ? origin : `${origin}/`);
|
|
373
|
+
setSearchParams(url, {
|
|
374
|
+
provider: options.provider,
|
|
375
|
+
workspace_id: options.workspaceId,
|
|
376
|
+
});
|
|
377
|
+
const res = await fetch(url.toString(), {
|
|
378
|
+
method: 'DELETE',
|
|
379
|
+
headers: { 'x-api-key': apiKey },
|
|
380
|
+
});
|
|
381
|
+
if (!res.ok) {
|
|
382
|
+
const body = await res.text();
|
|
383
|
+
throw new Error(`deleteVoice failed (${res.status}): ${body}`);
|
|
384
|
+
}
|
|
385
|
+
return res.json();
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Upload a short clean recording to create a cloned voice handle.
|
|
389
|
+
*/
|
|
390
|
+
static async cloneVoice(clip, apiKey, options = {}) {
|
|
391
|
+
const data = await AillomVox.cloneVoiceDetailed(clip, apiKey, options);
|
|
392
|
+
if (!data.voice_id) {
|
|
393
|
+
throw new Error(data.message || 'Voice clone did not return a voice_id');
|
|
394
|
+
}
|
|
395
|
+
return data.voice_id;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Upload a short clean recording and return the full multi-provider clone response.
|
|
399
|
+
*/
|
|
400
|
+
static async cloneVoiceDetailed(clip, apiKey, options = {}) {
|
|
401
|
+
const origin = resolveHttpOrigin(options.baseUrl || options.gatewayUrl);
|
|
402
|
+
const formData = new FormData();
|
|
403
|
+
formData.append('clip', clip, options.filename || 'clone.wav');
|
|
404
|
+
if (options.name)
|
|
405
|
+
formData.append('name', options.name);
|
|
406
|
+
if (options.description)
|
|
407
|
+
formData.append('description', options.description);
|
|
408
|
+
if (options.providers?.length)
|
|
409
|
+
formData.append('providers', options.providers.join(','));
|
|
410
|
+
if (options.workspaceId)
|
|
411
|
+
formData.append('workspace_id', options.workspaceId);
|
|
412
|
+
if (options.language)
|
|
413
|
+
formData.append('language', options.language);
|
|
414
|
+
if (options.transcription || options.transcript)
|
|
415
|
+
formData.append('transcription', options.transcription || options.transcript || '');
|
|
416
|
+
if (options.gender)
|
|
417
|
+
formData.append('gender', options.gender);
|
|
418
|
+
if (options.accent)
|
|
419
|
+
formData.append('accent', options.accent);
|
|
420
|
+
if (options.age)
|
|
421
|
+
formData.append('age', options.age);
|
|
422
|
+
if (options.tone)
|
|
423
|
+
formData.append('tone', options.tone);
|
|
424
|
+
if (options.useCase)
|
|
425
|
+
formData.append('use_case', options.useCase);
|
|
426
|
+
const response = await fetch(`${origin.replace(/\/?$/, '')}/api/voices/clone`, {
|
|
427
|
+
method: 'POST',
|
|
428
|
+
headers: {
|
|
429
|
+
'x-api-key': apiKey,
|
|
430
|
+
},
|
|
431
|
+
body: formData,
|
|
432
|
+
});
|
|
433
|
+
if (!response.ok) {
|
|
434
|
+
const err = (await response.json().catch(() => ({})));
|
|
435
|
+
throw new Error(err.error || `Failed to clone voice: ${response.status}`);
|
|
152
436
|
}
|
|
437
|
+
return (await response.json());
|
|
153
438
|
}
|
|
154
439
|
}
|
|
155
440
|
exports.AillomVox = AillomVox;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical endpoints — keep in sync with `aillom-vox/flow-src/src/constants/publicGateway.ts`.
|
|
3
|
+
*/
|
|
4
|
+
export declare const AILLOMVOX_DEFAULT_WS_URL = "wss://vox.aillom.com/ws";
|
|
5
|
+
/** @deprecated Hostname only — use {@link AILLOMVOX_DEFAULT_WS_URL}. The SDK rewrites this to `vox.aillom.com` in {@link migrateLegacyGatewayUrl}. */
|
|
6
|
+
export declare const AILLOMVOX_LEGACY_WS_URL = "wss://wss.aillom.com/ws";
|
|
7
|
+
export declare const AILLOMVOX_DEFAULT_HTTP_ORIGIN = "https://vox.aillom.com";
|
|
8
|
+
/**
|
|
9
|
+
* Providers exposed in the public playground (`aillom-vox` WebSocket `provider` field).
|
|
10
|
+
* The server accepts aliases (case-insensitive); see docs on the dashboard.
|
|
11
|
+
*/
|
|
12
|
+
export declare const AILLOMVOX_PUBLIC_PROVIDERS: readonly ["aillomvox", "aws", "gemini", "grok", "openai", "qwen", "ultravox"];
|
|
13
|
+
export type AillomVoxPublicProviderId = (typeof AILLOMVOX_PUBLIC_PROVIDERS)[number];
|
|
14
|
+
/**
|
|
15
|
+
* When `provider === "aillomvox"`, the gateway selects a TTS stack via `tts_engine`
|
|
16
|
+
* (see `GET /api/providers` → `aillomvox.tts_options`).
|
|
17
|
+
*/
|
|
18
|
+
export declare const AILLOMVOX_TTS_ENGINES: readonly ["inworld", "xai", "lmnt", "soniox", "rime", "fish"];
|
|
19
|
+
export type AillomVoxTtsEngineId = (typeof AILLOMVOX_TTS_ENGINES)[number];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AILLOMVOX_TTS_ENGINES = exports.AILLOMVOX_PUBLIC_PROVIDERS = exports.AILLOMVOX_DEFAULT_HTTP_ORIGIN = exports.AILLOMVOX_LEGACY_WS_URL = exports.AILLOMVOX_DEFAULT_WS_URL = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Canonical endpoints — keep in sync with `aillom-vox/flow-src/src/constants/publicGateway.ts`.
|
|
6
|
+
*/
|
|
7
|
+
exports.AILLOMVOX_DEFAULT_WS_URL = 'wss://vox.aillom.com/ws';
|
|
8
|
+
/** @deprecated Hostname only — use {@link AILLOMVOX_DEFAULT_WS_URL}. The SDK rewrites this to `vox.aillom.com` in {@link migrateLegacyGatewayUrl}. */
|
|
9
|
+
exports.AILLOMVOX_LEGACY_WS_URL = 'wss://wss.aillom.com/ws';
|
|
10
|
+
exports.AILLOMVOX_DEFAULT_HTTP_ORIGIN = 'https://vox.aillom.com';
|
|
11
|
+
/**
|
|
12
|
+
* Providers exposed in the public playground (`aillom-vox` WebSocket `provider` field).
|
|
13
|
+
* The server accepts aliases (case-insensitive); see docs on the dashboard.
|
|
14
|
+
*/
|
|
15
|
+
exports.AILLOMVOX_PUBLIC_PROVIDERS = [
|
|
16
|
+
'aillomvox',
|
|
17
|
+
'aws',
|
|
18
|
+
'gemini',
|
|
19
|
+
'grok',
|
|
20
|
+
'openai',
|
|
21
|
+
'qwen',
|
|
22
|
+
'ultravox',
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* When `provider === "aillomvox"`, the gateway selects a TTS stack via `tts_engine`
|
|
26
|
+
* (see `GET /api/providers` → `aillomvox.tts_options`).
|
|
27
|
+
*/
|
|
28
|
+
exports.AILLOMVOX_TTS_ENGINES = [
|
|
29
|
+
'inworld',
|
|
30
|
+
'xai',
|
|
31
|
+
'lmnt',
|
|
32
|
+
'soniox',
|
|
33
|
+
'rime',
|
|
34
|
+
'fish',
|
|
35
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrites `wss.aillom.com` (legacy) to `vox.aillom.com` so old configs keep working.
|
|
3
|
+
*/
|
|
4
|
+
export declare function migrateLegacyGatewayUrl(url: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes user input into a WebSocket URL ending with `/ws`.
|
|
7
|
+
* Accepts `wss://…/ws`, `https://vox.aillom.com`, or `wss://vox.aillom.com`.
|
|
8
|
+
* Legacy `https://wss.aillom.com` / `wss://wss.aillom.com/ws` is migrated to **vox**.
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizeWebSocketUrl(gatewayUrl?: string): string;
|
|
11
|
+
/** Converts a gateway WebSocket base URL into `https://` origin for REST (`/api/*`). */
|
|
12
|
+
export declare function httpOriginFromGatewayUrl(gatewayUrl?: string): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.migrateLegacyGatewayUrl = migrateLegacyGatewayUrl;
|
|
4
|
+
exports.normalizeWebSocketUrl = normalizeWebSocketUrl;
|
|
5
|
+
exports.httpOriginFromGatewayUrl = httpOriginFromGatewayUrl;
|
|
6
|
+
const constants_1 = require("./constants");
|
|
7
|
+
/** Query parameter names that must not carry secrets (logs, referrers, browser history). */
|
|
8
|
+
const SENSITIVE_WS_QUERY_KEYS = new Set(['apikey', 'api_key', 'x-api-key', 'token', 'secret']);
|
|
9
|
+
function warnIfWebSocketUrlLeaksCredentials(url) {
|
|
10
|
+
try {
|
|
11
|
+
const { searchParams } = new URL(url);
|
|
12
|
+
for (const name of searchParams.keys()) {
|
|
13
|
+
if (SENSITIVE_WS_QUERY_KEYS.has(name.toLowerCase())) {
|
|
14
|
+
console.warn('[AillomVox] Do not put API keys in the WebSocket URL query string. ' +
|
|
15
|
+
'Use `x-api-key` headers in Node.js or the JSON `config` `apikey` field in browsers. ' +
|
|
16
|
+
'Query credentials appear in logs, proxies, and analytics.');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* invalid URL — caller may pass partial strings; ignore */
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Legacy WebSocket hostname — rewritten to `vox.aillom.com` for all clients. */
|
|
26
|
+
const LEGACY_GATEWAY_HOSTNAME = 'wss.aillom.com';
|
|
27
|
+
const CANONICAL_GATEWAY_HOSTNAME = 'vox.aillom.com';
|
|
28
|
+
/**
|
|
29
|
+
* Rewrites `wss.aillom.com` (legacy) to `vox.aillom.com` so old configs keep working.
|
|
30
|
+
*/
|
|
31
|
+
function migrateLegacyGatewayUrl(url) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = new URL(url);
|
|
34
|
+
if (parsed.hostname === LEGACY_GATEWAY_HOSTNAME) {
|
|
35
|
+
parsed.hostname = CANONICAL_GATEWAY_HOSTNAME;
|
|
36
|
+
return parsed.toString();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* ignore */
|
|
41
|
+
}
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Normalizes user input into a WebSocket URL ending with `/ws`.
|
|
46
|
+
* Accepts `wss://…/ws`, `https://vox.aillom.com`, or `wss://vox.aillom.com`.
|
|
47
|
+
* Legacy `https://wss.aillom.com` / `wss://wss.aillom.com/ws` is migrated to **vox**.
|
|
48
|
+
*/
|
|
49
|
+
function normalizeWebSocketUrl(gatewayUrl) {
|
|
50
|
+
if (!gatewayUrl?.trim()) {
|
|
51
|
+
return constants_1.AILLOMVOX_DEFAULT_WS_URL;
|
|
52
|
+
}
|
|
53
|
+
let u = migrateLegacyGatewayUrl(gatewayUrl.trim());
|
|
54
|
+
if (u.startsWith('https://')) {
|
|
55
|
+
u = `wss://${u.slice('https://'.length)}`;
|
|
56
|
+
}
|
|
57
|
+
else if (u.startsWith('http://')) {
|
|
58
|
+
u = `ws://${u.slice('http://'.length)}`;
|
|
59
|
+
}
|
|
60
|
+
if (!/\/ws\/?($|\?)/i.test(u)) {
|
|
61
|
+
u = `${u.replace(/\/?$/, '')}/ws`;
|
|
62
|
+
}
|
|
63
|
+
warnIfWebSocketUrlLeaksCredentials(u);
|
|
64
|
+
return u;
|
|
65
|
+
}
|
|
66
|
+
/** Converts a gateway WebSocket base URL into `https://` origin for REST (`/api/*`). */
|
|
67
|
+
function httpOriginFromGatewayUrl(gatewayUrl) {
|
|
68
|
+
if (!gatewayUrl?.trim()) {
|
|
69
|
+
return constants_1.AILLOMVOX_DEFAULT_HTTP_ORIGIN;
|
|
70
|
+
}
|
|
71
|
+
const ws = normalizeWebSocketUrl(gatewayUrl);
|
|
72
|
+
const withoutPath = ws.replace(/\/ws\/?$/i, '').replace(/\/ws\?.*$/i, '');
|
|
73
|
+
return withoutPath.replace(/^wss:\/\//i, 'https://').replace(/^ws:\/\//i, 'http://');
|
|
74
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -15,4 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./constants"), exports);
|
|
19
|
+
__exportStar(require("./gateway-url"), exports);
|
|
20
|
+
__exportStar(require("./pricing"), exports);
|
|
18
21
|
__exportStar(require("./AillomVox"), exports);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public rate card (USD per **billed** minute of voice usage), aligned with the
|
|
3
|
+
* marketing table on https://vox.aillom.com and `GET /api/pricing`.
|
|
4
|
+
*
|
|
5
|
+
* **Do not treat this as a legal quote** — always confirm charges in the billing
|
|
6
|
+
* dashboard; `GET /api/providers` may expose model metadata but not necessarily
|
|
7
|
+
* your negotiated rates.
|
|
8
|
+
*/
|
|
9
|
+
export type VoxRateTier = 'Gateway' | 'S2S' | 'Premium';
|
|
10
|
+
export interface VoxProviderRateRow {
|
|
11
|
+
/** Row label on the marketing site */
|
|
12
|
+
label: string;
|
|
13
|
+
/** WebSocket handshake `provider` field */
|
|
14
|
+
provider: string;
|
|
15
|
+
/**
|
|
16
|
+
* When `provider === "aillomvox"`, this hints the currently documented
|
|
17
|
+
* TTS lanes. Omit when not applicable.
|
|
18
|
+
*/
|
|
19
|
+
ttsEngineHint?: string;
|
|
20
|
+
usdPerMinute: number;
|
|
21
|
+
tier: VoxRateTier;
|
|
22
|
+
/** Short model / stack description from the live site */
|
|
23
|
+
modelSummary: string;
|
|
24
|
+
badge?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare const VOX_PROVIDER_RATECARD: readonly VoxProviderRateRow[];
|