@tryhakim/voice 1.0.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/LICENSE +21 -0
- package/README.md +188 -0
- package/dist/audio/speech-stream-ws.d.ts +40 -0
- package/dist/audio/speech-stream-ws.d.ts.map +1 -0
- package/dist/audio/speech-stream-ws.js +565 -0
- package/dist/audio/speech-stream-ws.js.map +1 -0
- package/dist/audio/speech.d.ts +46 -0
- package/dist/audio/speech.d.ts.map +1 -0
- package/dist/audio/speech.js +125 -0
- package/dist/audio/speech.js.map +1 -0
- package/dist/audio/stream.d.ts +38 -0
- package/dist/audio/stream.d.ts.map +1 -0
- package/dist/audio/stream.js +452 -0
- package/dist/audio/stream.js.map +1 -0
- package/dist/audio/to-blob.d.ts +10 -0
- package/dist/audio/to-blob.d.ts.map +1 -0
- package/dist/audio/to-blob.js +78 -0
- package/dist/audio/to-blob.js.map +1 -0
- package/dist/audio/transcriptions.d.ts +35 -0
- package/dist/audio/transcriptions.d.ts.map +1 -0
- package/dist/audio/transcriptions.js +105 -0
- package/dist/audio/transcriptions.js.map +1 -0
- package/dist/audio/translate-stream-ws.d.ts +37 -0
- package/dist/audio/translate-stream-ws.d.ts.map +1 -0
- package/dist/audio/translate-stream-ws.js +665 -0
- package/dist/audio/translate-stream-ws.js.map +1 -0
- package/dist/audio/translate.d.ts +36 -0
- package/dist/audio/translate.d.ts.map +1 -0
- package/dist/audio/translate.js +39 -0
- package/dist/audio/translate.js.map +1 -0
- package/dist/audio/voices.d.ts +47 -0
- package/dist/audio/voices.d.ts.map +1 -0
- package/dist/audio/voices.js +91 -0
- package/dist/audio/voices.js.map +1 -0
- package/dist/chat/completions.d.ts +76 -0
- package/dist/chat/completions.d.ts.map +1 -0
- package/dist/chat/completions.js +114 -0
- package/dist/chat/completions.js.map +1 -0
- package/dist/chat/index.d.ts +2 -0
- package/dist/chat/index.d.ts.map +1 -0
- package/dist/chat/index.js +2 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/sse.d.ts +12 -0
- package/dist/chat/sse.d.ts.map +1 -0
- package/dist/chat/sse.js +142 -0
- package/dist/chat/sse.js.map +1 -0
- package/dist/client.d.ts +63 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +80 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +99 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +158 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs.d.ts +34 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +62 -0
- package/dist/jobs.js.map +1 -0
- package/dist/observability.d.ts +28 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +107 -0
- package/dist/observability.js.map +1 -0
- package/dist/settings.d.ts +49 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +84 -0
- package/dist/settings.js.map +1 -0
- package/dist/transport.d.ts +82 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +289 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +969 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/usage.d.ts +43 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +85 -0
- package/dist/usage.js.map +1 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +13 -0
- package/dist/version.js.map +1 -0
- package/dist/webhooks.d.ts +84 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +157 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime translate over WebSocket (`WSS /v1/audio/translate/stream`).
|
|
3
|
+
*
|
|
4
|
+
* The SDK chains caller-supplied source audio (`sendAudio()` → base64
|
|
5
|
+
* `input_audio_buffer.append`) into the proxy and translates server
|
|
6
|
+
* frames into friendly `TranslateStreamEvent`s — STT partials + finals,
|
|
7
|
+
* LLM partials + finals, TTS started + binary audio + done, and a
|
|
8
|
+
* cross-modality `session.usage` rollup.
|
|
9
|
+
*
|
|
10
|
+
* Same dependency posture as the STT/TTS helpers: prefer
|
|
11
|
+
* `globalThis.WebSocket` when present (Node 22+, Bun, Deno, browsers).
|
|
12
|
+
* On Node 18/20 the global isn't there — we dynamically import `ws`
|
|
13
|
+
* at runtime and surface a helpful error if it's missing.
|
|
14
|
+
*
|
|
15
|
+
* The handle is lazy: the socket opens on the first `sendAudio` /
|
|
16
|
+
* iteration / `close`. This keeps the hot path fast and avoids
|
|
17
|
+
* dangling connections when a caller constructs a handle but bails
|
|
18
|
+
* before using it.
|
|
19
|
+
*/
|
|
20
|
+
import { ConnectionError, HakimError } from '../errors.js';
|
|
21
|
+
import { SDK_NAME, SDK_VERSION } from '../version.js';
|
|
22
|
+
export function openTranslateStream(transport, opts = {}) {
|
|
23
|
+
const url = buildWsUrl(transport.baseURL);
|
|
24
|
+
const apiKey = transport.apiKey;
|
|
25
|
+
const pending = [];
|
|
26
|
+
let socket = null;
|
|
27
|
+
let openPromise = null;
|
|
28
|
+
const queue = [];
|
|
29
|
+
let waiter = null;
|
|
30
|
+
const audioQueue = [];
|
|
31
|
+
let audioWaiter = null;
|
|
32
|
+
let streamClosed = false;
|
|
33
|
+
let closedResolve;
|
|
34
|
+
const closedPromise = new Promise((r) => {
|
|
35
|
+
closedResolve = r;
|
|
36
|
+
});
|
|
37
|
+
function emit(ev) {
|
|
38
|
+
if (streamClosed)
|
|
39
|
+
return;
|
|
40
|
+
if (waiter) {
|
|
41
|
+
const w = waiter;
|
|
42
|
+
waiter = null;
|
|
43
|
+
w({ value: ev, done: false });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
queue.push(ev);
|
|
47
|
+
}
|
|
48
|
+
if (ev.type === 'speech.audio') {
|
|
49
|
+
if (audioWaiter) {
|
|
50
|
+
const w = audioWaiter;
|
|
51
|
+
audioWaiter = null;
|
|
52
|
+
w({ value: ev.chunk, done: false });
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
audioQueue.push(ev.chunk);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function finishStream() {
|
|
60
|
+
if (streamClosed)
|
|
61
|
+
return;
|
|
62
|
+
streamClosed = true;
|
|
63
|
+
if (waiter) {
|
|
64
|
+
const w = waiter;
|
|
65
|
+
waiter = null;
|
|
66
|
+
w({ value: undefined, done: true });
|
|
67
|
+
}
|
|
68
|
+
if (audioWaiter) {
|
|
69
|
+
const w = audioWaiter;
|
|
70
|
+
audioWaiter = null;
|
|
71
|
+
w({ value: undefined, done: true });
|
|
72
|
+
}
|
|
73
|
+
closedResolve();
|
|
74
|
+
}
|
|
75
|
+
async function ensureOpen() {
|
|
76
|
+
if (socket)
|
|
77
|
+
return socket;
|
|
78
|
+
if (openPromise)
|
|
79
|
+
return openPromise;
|
|
80
|
+
openPromise = (async () => {
|
|
81
|
+
const Ctor = await resolveSocketCtor();
|
|
82
|
+
const headers = {
|
|
83
|
+
Authorization: `Bearer ${apiKey}`,
|
|
84
|
+
'User-Agent': `${SDK_NAME}/${SDK_VERSION}`,
|
|
85
|
+
};
|
|
86
|
+
const s = new Ctor(url, { headers });
|
|
87
|
+
try {
|
|
88
|
+
s.binaryType = 'arraybuffer';
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
/* soft-fail · some socket impls reject the assignment */
|
|
92
|
+
}
|
|
93
|
+
await new Promise((resolve, reject) => {
|
|
94
|
+
const onOpen = () => {
|
|
95
|
+
detach();
|
|
96
|
+
resolve();
|
|
97
|
+
};
|
|
98
|
+
const onError = (err) => {
|
|
99
|
+
detach();
|
|
100
|
+
reject(new ConnectionError({
|
|
101
|
+
message: 'Hakim SDK: could not open WebSocket to ' +
|
|
102
|
+
`${url}. Is HAKIM_API_KEY valid and the network reachable?` +
|
|
103
|
+
(err instanceof Error ? ` Cause: ${err.message}` : ''),
|
|
104
|
+
requestId: undefined,
|
|
105
|
+
}));
|
|
106
|
+
};
|
|
107
|
+
const detach = () => {
|
|
108
|
+
removeListener(s, 'open', onOpen);
|
|
109
|
+
removeListener(s, 'error', onError);
|
|
110
|
+
};
|
|
111
|
+
addListener(s, 'open', onOpen);
|
|
112
|
+
addListener(s, 'error', onError);
|
|
113
|
+
});
|
|
114
|
+
attachSocketHandlers(s, emit, finishStream);
|
|
115
|
+
socket = s;
|
|
116
|
+
const sessionUpdate = buildSessionUpdate(opts);
|
|
117
|
+
if (sessionUpdate) {
|
|
118
|
+
s.send(JSON.stringify(sessionUpdate));
|
|
119
|
+
}
|
|
120
|
+
for (const frame of pending.splice(0)) {
|
|
121
|
+
s.send(frame);
|
|
122
|
+
}
|
|
123
|
+
return s;
|
|
124
|
+
})();
|
|
125
|
+
return openPromise;
|
|
126
|
+
}
|
|
127
|
+
function enqueueFrame(frame) {
|
|
128
|
+
if (socket && socket.readyState === 1 /* OPEN */) {
|
|
129
|
+
socket.send(frame);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
pending.push(frame);
|
|
133
|
+
ensureOpen().catch((err) => {
|
|
134
|
+
if (err instanceof HakimError || err instanceof ConnectionError) {
|
|
135
|
+
emit({
|
|
136
|
+
type: 'error',
|
|
137
|
+
code: err.code ?? 'connection_error',
|
|
138
|
+
message: err.message,
|
|
139
|
+
retryable: false,
|
|
140
|
+
fatal: true,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
finishStream();
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function sendAudio(chunk) {
|
|
148
|
+
if (streamClosed) {
|
|
149
|
+
throw new HakimError({
|
|
150
|
+
type: 'invalid_request_error',
|
|
151
|
+
code: 'stream_closed',
|
|
152
|
+
message: 'Hakim SDK: cannot send audio; stream is closed.',
|
|
153
|
+
status: 400,
|
|
154
|
+
requestId: undefined,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const bytes = toUint8Array(chunk);
|
|
158
|
+
const frame = {
|
|
159
|
+
type: 'input_audio_buffer.append',
|
|
160
|
+
audio: bufferToBase64(bytes),
|
|
161
|
+
};
|
|
162
|
+
const durationMs = estimateAudioMs(bytes.byteLength, opts.input_sample_rate ?? 16000, opts.input_audio_format ?? 'pcm16');
|
|
163
|
+
if (durationMs !== undefined)
|
|
164
|
+
frame.audio_ms = durationMs;
|
|
165
|
+
enqueueFrame(JSON.stringify(frame));
|
|
166
|
+
}
|
|
167
|
+
function commitAudio() {
|
|
168
|
+
if (streamClosed) {
|
|
169
|
+
throw new HakimError({
|
|
170
|
+
type: 'invalid_request_error',
|
|
171
|
+
code: 'stream_closed',
|
|
172
|
+
message: 'Hakim SDK: cannot commit audio; stream is closed.',
|
|
173
|
+
status: 400,
|
|
174
|
+
requestId: undefined,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
enqueueFrame(JSON.stringify({ type: 'input_audio_buffer.commit' }));
|
|
178
|
+
}
|
|
179
|
+
function updateSession(session) {
|
|
180
|
+
if (streamClosed) {
|
|
181
|
+
throw new HakimError({
|
|
182
|
+
type: 'invalid_request_error',
|
|
183
|
+
code: 'stream_closed',
|
|
184
|
+
message: 'Hakim SDK: cannot update session; stream is closed.',
|
|
185
|
+
status: 400,
|
|
186
|
+
requestId: undefined,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
for (const key of [
|
|
190
|
+
'target_language',
|
|
191
|
+
'source_language',
|
|
192
|
+
'voice',
|
|
193
|
+
'gender',
|
|
194
|
+
'model_stt',
|
|
195
|
+
'model_llm',
|
|
196
|
+
'model_tts',
|
|
197
|
+
'cfg',
|
|
198
|
+
'input_audio_format',
|
|
199
|
+
'input_sample_rate',
|
|
200
|
+
'partials',
|
|
201
|
+
'system_prompt',
|
|
202
|
+
]) {
|
|
203
|
+
const value = session[key];
|
|
204
|
+
if (value !== undefined) {
|
|
205
|
+
opts[key] = value;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const upd = buildSessionUpdate(session);
|
|
209
|
+
if (upd)
|
|
210
|
+
enqueueFrame(JSON.stringify(upd));
|
|
211
|
+
}
|
|
212
|
+
async function close() {
|
|
213
|
+
if (streamClosed)
|
|
214
|
+
return;
|
|
215
|
+
enqueueFrame(JSON.stringify({ type: 'session.close' }));
|
|
216
|
+
await closedPromise;
|
|
217
|
+
}
|
|
218
|
+
if (opts.signal) {
|
|
219
|
+
if (opts.signal.aborted) {
|
|
220
|
+
Promise.resolve()
|
|
221
|
+
.then(() => close())
|
|
222
|
+
.catch(() => undefined);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
opts.signal.addEventListener('abort', () => {
|
|
226
|
+
close().catch(() => undefined);
|
|
227
|
+
}, { once: true });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const events = {
|
|
231
|
+
[Symbol.asyncIterator]() {
|
|
232
|
+
return {
|
|
233
|
+
next() {
|
|
234
|
+
if (queue.length > 0) {
|
|
235
|
+
return Promise.resolve({ value: queue.shift(), done: false });
|
|
236
|
+
}
|
|
237
|
+
if (streamClosed) {
|
|
238
|
+
return Promise.resolve({
|
|
239
|
+
value: undefined,
|
|
240
|
+
done: true,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return new Promise((resolve) => {
|
|
244
|
+
waiter = resolve;
|
|
245
|
+
if (!socket && !openPromise) {
|
|
246
|
+
ensureOpen().catch((err) => {
|
|
247
|
+
if (!streamClosed) {
|
|
248
|
+
emit({
|
|
249
|
+
type: 'error',
|
|
250
|
+
code: err?.code ?? 'connection_error',
|
|
251
|
+
message: err instanceof Error ? err.message : 'connection error',
|
|
252
|
+
retryable: false,
|
|
253
|
+
fatal: true,
|
|
254
|
+
});
|
|
255
|
+
finishStream();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
return() {
|
|
262
|
+
close().catch(() => undefined);
|
|
263
|
+
return Promise.resolve({
|
|
264
|
+
value: undefined,
|
|
265
|
+
done: true,
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
const audio = {
|
|
272
|
+
[Symbol.asyncIterator]() {
|
|
273
|
+
return {
|
|
274
|
+
next() {
|
|
275
|
+
if (audioQueue.length > 0) {
|
|
276
|
+
return Promise.resolve({ value: audioQueue.shift(), done: false });
|
|
277
|
+
}
|
|
278
|
+
if (streamClosed) {
|
|
279
|
+
return Promise.resolve({
|
|
280
|
+
value: undefined,
|
|
281
|
+
done: true,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return new Promise((resolve) => {
|
|
285
|
+
audioWaiter = resolve;
|
|
286
|
+
if (!socket && !openPromise) {
|
|
287
|
+
ensureOpen().catch((err) => {
|
|
288
|
+
if (!streamClosed) {
|
|
289
|
+
emit({
|
|
290
|
+
type: 'error',
|
|
291
|
+
code: err?.code ?? 'connection_error',
|
|
292
|
+
message: err instanceof Error ? err.message : 'connection error',
|
|
293
|
+
retryable: false,
|
|
294
|
+
fatal: true,
|
|
295
|
+
});
|
|
296
|
+
finishStream();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
return() {
|
|
303
|
+
close().catch(() => undefined);
|
|
304
|
+
return Promise.resolve({
|
|
305
|
+
value: undefined,
|
|
306
|
+
done: true,
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
sendAudio,
|
|
314
|
+
commitAudio,
|
|
315
|
+
updateSession,
|
|
316
|
+
events,
|
|
317
|
+
audio,
|
|
318
|
+
close,
|
|
319
|
+
closed: closedPromise,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function attachSocketHandlers(s, emit, finishStream) {
|
|
323
|
+
// Track the last utterance_id seen on the wire so we can attach it
|
|
324
|
+
// to binary audio frames (which carry no envelope of their own).
|
|
325
|
+
// The proxy guarantees `speech.started` always precedes the first
|
|
326
|
+
// audio chunk for an utterance.
|
|
327
|
+
let currentUtteranceId = null;
|
|
328
|
+
const onMessage = (raw, maybeIsBinary) => {
|
|
329
|
+
const binary = decodeBinary(raw, maybeIsBinary);
|
|
330
|
+
if (binary !== null) {
|
|
331
|
+
if (currentUtteranceId === null)
|
|
332
|
+
return;
|
|
333
|
+
emit({ type: 'speech.audio', utterance_id: currentUtteranceId, chunk: binary });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const text = extractMessageText(raw);
|
|
337
|
+
if (text === null)
|
|
338
|
+
return;
|
|
339
|
+
let parsed;
|
|
340
|
+
try {
|
|
341
|
+
parsed = JSON.parse(text);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const translated = translateFrame(parsed);
|
|
347
|
+
if (!translated)
|
|
348
|
+
return;
|
|
349
|
+
if (translated.type === 'speech.started') {
|
|
350
|
+
currentUtteranceId = translated.utterance_id;
|
|
351
|
+
}
|
|
352
|
+
emit(translated);
|
|
353
|
+
};
|
|
354
|
+
const onClose = () => {
|
|
355
|
+
finishStream();
|
|
356
|
+
};
|
|
357
|
+
addListener(s, 'message', onMessage);
|
|
358
|
+
addListener(s, 'close', onClose);
|
|
359
|
+
}
|
|
360
|
+
function decodeBinary(raw, maybeIsBinary) {
|
|
361
|
+
if (maybeIsBinary === true) {
|
|
362
|
+
if (raw instanceof Uint8Array)
|
|
363
|
+
return raw;
|
|
364
|
+
if (raw instanceof ArrayBuffer)
|
|
365
|
+
return new Uint8Array(raw);
|
|
366
|
+
if (Array.isArray(raw)) {
|
|
367
|
+
const total = raw.reduce((n, b) => n + (b?.byteLength ?? 0), 0);
|
|
368
|
+
const out = new Uint8Array(total);
|
|
369
|
+
let off = 0;
|
|
370
|
+
for (const b of raw) {
|
|
371
|
+
out.set(b, off);
|
|
372
|
+
off += b.byteLength;
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (raw && typeof raw === 'object' && 'data' in raw) {
|
|
378
|
+
const d = raw.data;
|
|
379
|
+
if (d instanceof ArrayBuffer)
|
|
380
|
+
return new Uint8Array(d);
|
|
381
|
+
if (ArrayBuffer.isView(d) && d instanceof Uint8Array)
|
|
382
|
+
return d;
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
function extractMessageText(raw) {
|
|
387
|
+
if (typeof raw === 'string')
|
|
388
|
+
return raw;
|
|
389
|
+
if (raw && typeof raw === 'object' && 'data' in raw) {
|
|
390
|
+
const d = raw.data;
|
|
391
|
+
if (typeof d === 'string')
|
|
392
|
+
return d;
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
if (raw instanceof Uint8Array)
|
|
396
|
+
return new TextDecoder().decode(raw);
|
|
397
|
+
if (Array.isArray(raw)) {
|
|
398
|
+
try {
|
|
399
|
+
const parts = raw;
|
|
400
|
+
const total = parts.reduce((n, b) => n + b.byteLength, 0);
|
|
401
|
+
const out = new Uint8Array(total);
|
|
402
|
+
let off = 0;
|
|
403
|
+
for (const b of parts) {
|
|
404
|
+
out.set(b, off);
|
|
405
|
+
off += b.byteLength;
|
|
406
|
+
}
|
|
407
|
+
return new TextDecoder().decode(out);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
export function translateFrame(frame) {
|
|
416
|
+
if (!frame || typeof frame !== 'object')
|
|
417
|
+
return null;
|
|
418
|
+
const f = frame;
|
|
419
|
+
switch (f.type) {
|
|
420
|
+
case 'session.created': {
|
|
421
|
+
const d = frame;
|
|
422
|
+
if (typeof d.session_id !== 'string' ||
|
|
423
|
+
typeof d.voice_id !== 'string' ||
|
|
424
|
+
typeof d.voice_slug !== 'string' ||
|
|
425
|
+
typeof d.model_stt !== 'string' ||
|
|
426
|
+
typeof d.model_llm !== 'string' ||
|
|
427
|
+
typeof d.model_tts !== 'string') {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
type: 'session.created',
|
|
432
|
+
session_id: d.session_id,
|
|
433
|
+
voice_id: d.voice_id,
|
|
434
|
+
voice_slug: d.voice_slug,
|
|
435
|
+
model_stt: d.model_stt,
|
|
436
|
+
model_llm: d.model_llm,
|
|
437
|
+
model_tts: d.model_tts,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
case 'transcription.delta': {
|
|
441
|
+
const d = frame;
|
|
442
|
+
if (typeof d.utterance_id !== 'string' || typeof d.text !== 'string')
|
|
443
|
+
return null;
|
|
444
|
+
return {
|
|
445
|
+
type: 'transcription.delta',
|
|
446
|
+
utterance_id: d.utterance_id,
|
|
447
|
+
text: d.text,
|
|
448
|
+
is_final: d.is_final === true,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
case 'transcription.done': {
|
|
452
|
+
const d = frame;
|
|
453
|
+
if (typeof d.utterance_id !== 'string' || typeof d.text !== 'string')
|
|
454
|
+
return null;
|
|
455
|
+
const out = {
|
|
456
|
+
type: 'transcription.done',
|
|
457
|
+
utterance_id: d.utterance_id,
|
|
458
|
+
text: d.text,
|
|
459
|
+
audio_ms: typeof d.audio_ms === 'number' ? d.audio_ms : 0,
|
|
460
|
+
usage: d.usage,
|
|
461
|
+
};
|
|
462
|
+
if (typeof d.language === 'string') {
|
|
463
|
+
out.language = d.language;
|
|
464
|
+
}
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
467
|
+
case 'translation.delta': {
|
|
468
|
+
const d = frame;
|
|
469
|
+
if (typeof d.utterance_id !== 'string' || typeof d.text !== 'string')
|
|
470
|
+
return null;
|
|
471
|
+
return { type: 'translation.delta', utterance_id: d.utterance_id, text: d.text };
|
|
472
|
+
}
|
|
473
|
+
case 'translation.done': {
|
|
474
|
+
const d = frame;
|
|
475
|
+
if (typeof d.utterance_id !== 'string' || typeof d.text !== 'string')
|
|
476
|
+
return null;
|
|
477
|
+
return {
|
|
478
|
+
type: 'translation.done',
|
|
479
|
+
utterance_id: d.utterance_id,
|
|
480
|
+
text: d.text,
|
|
481
|
+
usage: d.usage,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
case 'speech.started': {
|
|
485
|
+
const d = frame;
|
|
486
|
+
if (typeof d.utterance_id !== 'string')
|
|
487
|
+
return null;
|
|
488
|
+
return {
|
|
489
|
+
type: 'speech.started',
|
|
490
|
+
utterance_id: d.utterance_id,
|
|
491
|
+
characters: typeof d.characters === 'number' ? d.characters : 0,
|
|
492
|
+
sample_rate: typeof d.sample_rate === 'number' ? d.sample_rate : 24000,
|
|
493
|
+
encoding: 'pcm_s16le',
|
|
494
|
+
channels: 1,
|
|
495
|
+
voice_id: typeof d.voice_id === 'string' ? d.voice_id : 'unknown',
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
case 'speech.done': {
|
|
499
|
+
const d = frame;
|
|
500
|
+
if (typeof d.utterance_id !== 'string')
|
|
501
|
+
return null;
|
|
502
|
+
return {
|
|
503
|
+
type: 'speech.done',
|
|
504
|
+
utterance_id: d.utterance_id,
|
|
505
|
+
duration_ms: typeof d.duration_ms === 'number' ? d.duration_ms : 0,
|
|
506
|
+
usage: d.usage,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
case 'session.usage': {
|
|
510
|
+
const d = frame;
|
|
511
|
+
if (typeof d.session_id !== 'string' || !d.totals || typeof d.totals !== 'object') {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
const t = d.totals;
|
|
515
|
+
return {
|
|
516
|
+
type: 'session.usage',
|
|
517
|
+
session_id: d.session_id,
|
|
518
|
+
totals: {
|
|
519
|
+
stt_audio_ms: typeof t.stt_audio_ms === 'number' ? t.stt_audio_ms : 0,
|
|
520
|
+
llm_tokens: typeof t.llm_tokens === 'number' ? t.llm_tokens : 0,
|
|
521
|
+
tts_characters: typeof t.tts_characters === 'number' ? t.tts_characters : 0,
|
|
522
|
+
credits: typeof t.credits === 'number' ? t.credits : 0,
|
|
523
|
+
cost_usd: typeof t.cost_usd === 'string' ? t.cost_usd : '0',
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
case 'error': {
|
|
528
|
+
const d = frame;
|
|
529
|
+
const out = {
|
|
530
|
+
type: 'error',
|
|
531
|
+
code: typeof d.code === 'string' ? d.code : 'unknown_error',
|
|
532
|
+
message: typeof d.message === 'string' ? d.message : 'unknown error',
|
|
533
|
+
retryable: d.retryable === true,
|
|
534
|
+
fatal: d.fatal === true,
|
|
535
|
+
};
|
|
536
|
+
if (typeof d.utterance_id === 'string') {
|
|
537
|
+
out.utterance_id = d.utterance_id;
|
|
538
|
+
}
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
default:
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
export function buildSessionUpdate(session) {
|
|
546
|
+
const out = {};
|
|
547
|
+
if (session.target_language !== undefined)
|
|
548
|
+
out.target_language = session.target_language;
|
|
549
|
+
if (session.source_language !== undefined)
|
|
550
|
+
out.source_language = session.source_language;
|
|
551
|
+
if (session.voice !== undefined)
|
|
552
|
+
out.voice = session.voice;
|
|
553
|
+
if (session.gender !== undefined)
|
|
554
|
+
out.gender = session.gender;
|
|
555
|
+
if (session.model_stt !== undefined)
|
|
556
|
+
out.model_stt = session.model_stt;
|
|
557
|
+
if (session.model_llm !== undefined)
|
|
558
|
+
out.model_llm = session.model_llm;
|
|
559
|
+
if (session.model_tts !== undefined)
|
|
560
|
+
out.model_tts = session.model_tts;
|
|
561
|
+
if (session.cfg !== undefined)
|
|
562
|
+
out.cfg = session.cfg;
|
|
563
|
+
if (session.input_audio_format !== undefined)
|
|
564
|
+
out.input_audio_format = session.input_audio_format;
|
|
565
|
+
if (session.input_sample_rate !== undefined)
|
|
566
|
+
out.input_sample_rate = session.input_sample_rate;
|
|
567
|
+
if (session.partials !== undefined)
|
|
568
|
+
out.partials = session.partials;
|
|
569
|
+
if (session.system_prompt !== undefined)
|
|
570
|
+
out.system_prompt = session.system_prompt;
|
|
571
|
+
if (Object.keys(out).length === 0)
|
|
572
|
+
return null;
|
|
573
|
+
return { type: 'session.update', session: out };
|
|
574
|
+
}
|
|
575
|
+
function buildWsUrl(baseURL) {
|
|
576
|
+
const u = new URL(baseURL);
|
|
577
|
+
u.protocol = u.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
578
|
+
u.pathname = u.pathname.replace(/\/$/, '') + '/v1/audio/translate/stream';
|
|
579
|
+
return u.toString();
|
|
580
|
+
}
|
|
581
|
+
function estimateAudioMs(byteLength, sampleRate, format) {
|
|
582
|
+
if (format === 'pcm16' && sampleRate > 0) {
|
|
583
|
+
return Math.round((byteLength / 2 / sampleRate) * 1000);
|
|
584
|
+
}
|
|
585
|
+
// Opus + mulaw frame sizing is codec-dependent; let the server's
|
|
586
|
+
// byte-derived floor take over rather than guess.
|
|
587
|
+
return undefined;
|
|
588
|
+
}
|
|
589
|
+
function toUint8Array(chunk) {
|
|
590
|
+
if (chunk instanceof Uint8Array)
|
|
591
|
+
return chunk;
|
|
592
|
+
if (chunk instanceof ArrayBuffer)
|
|
593
|
+
return new Uint8Array(chunk);
|
|
594
|
+
if (ArrayBuffer.isView(chunk)) {
|
|
595
|
+
return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
596
|
+
}
|
|
597
|
+
throw new HakimError({
|
|
598
|
+
type: 'invalid_request_error',
|
|
599
|
+
code: 'invalid_audio_chunk',
|
|
600
|
+
message: `Hakim SDK: unsupported audio chunk type (${typeof chunk}). Pass a Uint8Array, Buffer, ArrayBuffer, or ArrayBufferView.`,
|
|
601
|
+
status: 400,
|
|
602
|
+
requestId: undefined,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
function bufferToBase64(bytes) {
|
|
606
|
+
if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
|
|
607
|
+
return Buffer.from(bytes).toString('base64');
|
|
608
|
+
}
|
|
609
|
+
let binary = '';
|
|
610
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
611
|
+
binary += String.fromCharCode(bytes[i]);
|
|
612
|
+
}
|
|
613
|
+
return btoa(binary);
|
|
614
|
+
}
|
|
615
|
+
function addListener(socket, type, listener) {
|
|
616
|
+
if (socket.addEventListener) {
|
|
617
|
+
socket.addEventListener(type, listener);
|
|
618
|
+
}
|
|
619
|
+
else if (socket.on) {
|
|
620
|
+
socket.on(type, listener);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function removeListener(socket, type, listener) {
|
|
624
|
+
const s = socket;
|
|
625
|
+
if (s.removeEventListener)
|
|
626
|
+
s.removeEventListener(type, listener);
|
|
627
|
+
else if (s.off)
|
|
628
|
+
s.off(type, listener);
|
|
629
|
+
}
|
|
630
|
+
let _socketCtor = null;
|
|
631
|
+
let _socketCtorPromise = null;
|
|
632
|
+
async function resolveSocketCtor() {
|
|
633
|
+
if (_socketCtor)
|
|
634
|
+
return _socketCtor;
|
|
635
|
+
if (_socketCtorPromise)
|
|
636
|
+
return _socketCtorPromise;
|
|
637
|
+
const globalWs = globalThis.WebSocket;
|
|
638
|
+
if (globalWs) {
|
|
639
|
+
_socketCtor = globalWs;
|
|
640
|
+
return globalWs;
|
|
641
|
+
}
|
|
642
|
+
_socketCtorPromise = (async () => {
|
|
643
|
+
try {
|
|
644
|
+
const mod = await new Function('s', 'return import(s)')('ws');
|
|
645
|
+
const Ctor = mod.default ?? mod.WebSocket;
|
|
646
|
+
if (!Ctor)
|
|
647
|
+
throw new Error('ws module did not export a WebSocket class');
|
|
648
|
+
_socketCtor = Ctor;
|
|
649
|
+
return Ctor;
|
|
650
|
+
}
|
|
651
|
+
catch (err) {
|
|
652
|
+
throw new HakimError({
|
|
653
|
+
type: 'invalid_request_error',
|
|
654
|
+
code: 'websocket_impl_missing',
|
|
655
|
+
message: 'Hakim SDK: realtime translate requires a WebSocket implementation. On Node 22+ / Bun / Deno / browsers it is built in. On Node 18 / 20, run `npm install ws` alongside @tryhakim/voice. ' +
|
|
656
|
+
(err instanceof Error ? `(cause: ${err.message})` : ''),
|
|
657
|
+
status: 0,
|
|
658
|
+
requestId: undefined,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
})();
|
|
662
|
+
return _socketCtorPromise;
|
|
663
|
+
}
|
|
664
|
+
export const __internals = { translateFrame, buildSessionUpdate, buildWsUrl, estimateAudioMs };
|
|
665
|
+
//# sourceMappingURL=translate-stream-ws.js.map
|