@vocalstack/js-sdk 1.1.0 → 2.0.0-alpha.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 -21
- package/README.md +112 -65
- package/SKILL.md +249 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +56 -0
- package/dist/file.d.ts +2 -0
- package/dist/file.js +38 -0
- package/dist/http.d.ts +43 -0
- package/dist/http.js +124 -0
- package/dist/index.d.ts +42 -2
- package/dist/index.js +51 -17
- package/dist/mic.d.ts +27 -0
- package/dist/mic.js +236 -0
- package/dist/realtime.d.ts +77 -0
- package/dist/realtime.js +490 -0
- package/dist/resources.d.ts +374 -0
- package/dist/resources.js +712 -0
- package/dist/subscription.d.ts +38 -0
- package/dist/subscription.js +153 -0
- package/dist/ws.d.ts +9 -0
- package/dist/ws.js +12 -0
- package/package.json +32 -36
- package/skills/vocalstack/SKILL.md +249 -0
- package/dist/ApiVersion.d.ts +0 -2
- package/dist/ApiVersion.js +0 -32
- package/dist/RestApi.d.ts +0 -21
- package/dist/RestApi.js +0 -95
- package/dist/VocalStackApi.d.ts +0 -19
- package/dist/VocalStackApi.js +0 -60
- package/dist/WebsocketApi.d.ts +0 -38
- package/dist/WebsocketApi.js +0 -150
- package/dist/apis/Billing.d.ts +0 -59
- package/dist/apis/Billing.js +0 -21
- package/dist/apis/LiveTranscription.d.ts +0 -58
- package/dist/apis/LiveTranscription.js +0 -55
- package/dist/apis/Polyglot.d.ts +0 -113
- package/dist/apis/Polyglot.js +0 -68
- package/dist/apis/Security.d.ts +0 -25
- package/dist/apis/Security.js +0 -35
- package/dist/apis/Transcriptions.d.ts +0 -60
- package/dist/apis/Transcriptions.js +0 -38
- package/dist/apis/UrlTranscription.d.ts +0 -59
- package/dist/apis/UrlTranscription.js +0 -53
- package/dist/apis/index.d.ts +0 -6
- package/dist/apis/index.js +0 -22
- package/dist/esm/ApiVersion.d.ts +0 -2
- package/dist/esm/ApiVersion.js +0 -28
- package/dist/esm/RestApi.d.ts +0 -21
- package/dist/esm/RestApi.js +0 -91
- package/dist/esm/VocalStackApi.d.ts +0 -19
- package/dist/esm/VocalStackApi.js +0 -56
- package/dist/esm/WebsocketApi.d.ts +0 -38
- package/dist/esm/WebsocketApi.js +0 -146
- package/dist/esm/apis/Billing.d.ts +0 -59
- package/dist/esm/apis/Billing.js +0 -17
- package/dist/esm/apis/LiveTranscription.d.ts +0 -58
- package/dist/esm/apis/LiveTranscription.js +0 -51
- package/dist/esm/apis/Polyglot.d.ts +0 -113
- package/dist/esm/apis/Polyglot.js +0 -64
- package/dist/esm/apis/Security.d.ts +0 -25
- package/dist/esm/apis/Security.js +0 -31
- package/dist/esm/apis/Transcriptions.d.ts +0 -60
- package/dist/esm/apis/Transcriptions.js +0 -34
- package/dist/esm/apis/UrlTranscription.d.ts +0 -59
- package/dist/esm/apis/UrlTranscription.js +0 -49
- package/dist/esm/apis/index.d.ts +0 -6
- package/dist/esm/apis/index.js +0 -6
- package/dist/esm/generics.zod.d.ts +0 -83
- package/dist/esm/generics.zod.js +0 -47
- package/dist/esm/index.d.ts +0 -2
- package/dist/esm/index.js +0 -2
- package/dist/generics.zod.d.ts +0 -83
- package/dist/generics.zod.js +0 -50
package/dist/realtime.js
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { parseServerFrame } from '@vocalstack/contract';
|
|
2
|
+
import { capacityExceeded, fromEnvelope, VocalStackError } from './errors.js';
|
|
3
|
+
import { fileChunks } from './file.js';
|
|
4
|
+
import { openWebSocket, WS_OPEN } from './ws.js';
|
|
5
|
+
const DEFAULT_READY_DEADLINE_MS = 30_000;
|
|
6
|
+
export function engineLost(closeCode) {
|
|
7
|
+
const how = closeCode ? ` (close ${closeCode})` : '';
|
|
8
|
+
return new VocalStackError('engine_lost', `The connection to the transcription engine closed before the realtime session finished${how}; open a new realtime session to continue.`, { retryable: true });
|
|
9
|
+
}
|
|
10
|
+
export function upgradeRefused(status, body) {
|
|
11
|
+
let parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = body ? JSON.parse(body) : undefined;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
parsed = undefined;
|
|
17
|
+
}
|
|
18
|
+
if (status === 503) {
|
|
19
|
+
const code = parsed?.code;
|
|
20
|
+
if (code === 'unavailable')
|
|
21
|
+
return fromEnvelope(parsed, status);
|
|
22
|
+
return capacityExceeded('stream host answered 503 (fleet saturated)');
|
|
23
|
+
}
|
|
24
|
+
const ours = typeof parsed?.code === 'string';
|
|
25
|
+
if ((status === 502 || status === 504) && !ours)
|
|
26
|
+
return capacityExceeded(`stream host answered ${status} (no engine answered in time)`);
|
|
27
|
+
return fromEnvelope(parsed, status);
|
|
28
|
+
}
|
|
29
|
+
export const RESTART_CLOSE = 1012;
|
|
30
|
+
const RESTART_TRIES = 3;
|
|
31
|
+
const MAX_PENDING_BYTES = 4 * 1024 * 1024;
|
|
32
|
+
async function openReady(url, deadlineMs, onLoading, onFrame, attach) {
|
|
33
|
+
const ws = await openWebSocket(url);
|
|
34
|
+
ws.binaryType = 'arraybuffer';
|
|
35
|
+
await new Promise((resolve, reject) => {
|
|
36
|
+
let settled = false;
|
|
37
|
+
let sawFrame = false;
|
|
38
|
+
let rejecting = false;
|
|
39
|
+
let timer;
|
|
40
|
+
const fail = (err) => {
|
|
41
|
+
if (settled)
|
|
42
|
+
return;
|
|
43
|
+
settled = true;
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
try {
|
|
46
|
+
ws.close();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
}
|
|
50
|
+
reject(err);
|
|
51
|
+
};
|
|
52
|
+
const arm = () => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
timer = setTimeout(() => {
|
|
55
|
+
fail(sawFrame
|
|
56
|
+
? new VocalStackError('engine_unavailable', `engine did not become ready within ${deadlineMs}ms`, { retryable: true })
|
|
57
|
+
: capacityExceeded(`no response from the engine within ${deadlineMs}ms — every machine is ` +
|
|
58
|
+
`busy with another session, or one is stuck holding a slot`));
|
|
59
|
+
}, deadlineMs);
|
|
60
|
+
};
|
|
61
|
+
arm();
|
|
62
|
+
ws.addEventListener('message', (ev) => {
|
|
63
|
+
if (settled || typeof ev.data !== 'string')
|
|
64
|
+
return;
|
|
65
|
+
sawFrame = true;
|
|
66
|
+
arm();
|
|
67
|
+
let frame;
|
|
68
|
+
try {
|
|
69
|
+
frame = parseServerFrame(ev.data);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!frame)
|
|
75
|
+
return;
|
|
76
|
+
onFrame(frame);
|
|
77
|
+
if (frame.type === 'session.loading') {
|
|
78
|
+
onLoading?.(frame.stage);
|
|
79
|
+
}
|
|
80
|
+
else if (frame.type === 'session.ready') {
|
|
81
|
+
if (!settled) {
|
|
82
|
+
settled = true;
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
attach(ws);
|
|
85
|
+
resolve();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (frame.type === 'error') {
|
|
89
|
+
fail(new VocalStackError(frame.code, frame.message, {
|
|
90
|
+
retryable: frame.retryable,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const emitterOn = ws
|
|
95
|
+
.on?.bind(ws);
|
|
96
|
+
emitterOn?.('unexpected-response', (_req, res) => {
|
|
97
|
+
rejecting = true;
|
|
98
|
+
const status = res?.statusCode;
|
|
99
|
+
let body = '';
|
|
100
|
+
if (res && typeof res.on === 'function') {
|
|
101
|
+
res.on('data', (c) => (body += c.toString()));
|
|
102
|
+
res.on('end', () => fail(upgradeRefused(status, body)));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
fail(upgradeRefused(status, ''));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
ws.addEventListener('close', () => {
|
|
109
|
+
if (rejecting)
|
|
110
|
+
return;
|
|
111
|
+
fail(new VocalStackError('engine_unavailable', 'connection closed before ready', {
|
|
112
|
+
retryable: true,
|
|
113
|
+
}));
|
|
114
|
+
});
|
|
115
|
+
ws.addEventListener('error', () => {
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
export class RealtimeSession {
|
|
120
|
+
sessionId;
|
|
121
|
+
#ws;
|
|
122
|
+
#queue = [];
|
|
123
|
+
#wake = null;
|
|
124
|
+
#endReason = null;
|
|
125
|
+
#stats = null;
|
|
126
|
+
#error = null;
|
|
127
|
+
#closed = false;
|
|
128
|
+
#closedByUs = false;
|
|
129
|
+
#onFrame;
|
|
130
|
+
#params;
|
|
131
|
+
#reconnecting = false;
|
|
132
|
+
#pending = [];
|
|
133
|
+
#pendingBytes = 0;
|
|
134
|
+
#reconnects = 0;
|
|
135
|
+
constructor(sessionId, ws, params) {
|
|
136
|
+
this.sessionId = sessionId;
|
|
137
|
+
this.#ws = ws;
|
|
138
|
+
this.#onFrame = params.onFrame;
|
|
139
|
+
this.#params = params;
|
|
140
|
+
}
|
|
141
|
+
static async connect(params) {
|
|
142
|
+
const deadlineMs = params.readyDeadlineMs ?? DEFAULT_READY_DEADLINE_MS;
|
|
143
|
+
const early = [];
|
|
144
|
+
let session;
|
|
145
|
+
await openReady(params.streamUrl, deadlineMs, params.onLoading, (f) => early.push(f), (ws) => {
|
|
146
|
+
const s = new RealtimeSession(params.sessionId, ws, params);
|
|
147
|
+
for (const f of early)
|
|
148
|
+
s.#frame(f);
|
|
149
|
+
s.#listen();
|
|
150
|
+
session = s;
|
|
151
|
+
});
|
|
152
|
+
return session;
|
|
153
|
+
}
|
|
154
|
+
async #reconnect(code) {
|
|
155
|
+
this.#reconnecting = true;
|
|
156
|
+
const deadlineMs = this.#params.readyDeadlineMs ?? DEFAULT_READY_DEADLINE_MS;
|
|
157
|
+
while (this.#reconnects < RESTART_TRIES && !this.#closedByUs) {
|
|
158
|
+
await new Promise((r) => setTimeout(r, 1000 * 2 ** this.#reconnects));
|
|
159
|
+
this.#reconnects++;
|
|
160
|
+
if (this.#closedByUs)
|
|
161
|
+
break;
|
|
162
|
+
try {
|
|
163
|
+
await openReady(this.#params.streamUrl, deadlineMs, this.#params.onLoading, (f) => this.#frame(f), (ws) => {
|
|
164
|
+
this.#ws = ws;
|
|
165
|
+
this.#reconnecting = false;
|
|
166
|
+
this.#listen();
|
|
167
|
+
const waiting = this.#pending;
|
|
168
|
+
this.#pending = [];
|
|
169
|
+
this.#pendingBytes = 0;
|
|
170
|
+
for (const d of waiting)
|
|
171
|
+
this.#send(d);
|
|
172
|
+
});
|
|
173
|
+
if (this.#closedByUs)
|
|
174
|
+
this.close();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
if (e instanceof VocalStackError && !e.retryable) {
|
|
179
|
+
this.#fail(e);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
this.#fail(engineLost(code));
|
|
185
|
+
}
|
|
186
|
+
#fail(err) {
|
|
187
|
+
this.#reconnecting = false;
|
|
188
|
+
this.#pending = [];
|
|
189
|
+
this.#closed = true;
|
|
190
|
+
if (!this.#endReason && !this.#error && !this.#closedByUs)
|
|
191
|
+
this.#error = err;
|
|
192
|
+
this.#wake?.();
|
|
193
|
+
}
|
|
194
|
+
#send(data) {
|
|
195
|
+
if (this.#reconnecting) {
|
|
196
|
+
const n = typeof data === 'string' ? data.length : data.byteLength;
|
|
197
|
+
if (this.#pendingBytes + n <= MAX_PENDING_BYTES) {
|
|
198
|
+
this.#pending.push(data);
|
|
199
|
+
this.#pendingBytes += n;
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (this.#ws.readyState !== WS_OPEN)
|
|
204
|
+
return;
|
|
205
|
+
this.#ws.send(data);
|
|
206
|
+
}
|
|
207
|
+
#frame(frame) {
|
|
208
|
+
try {
|
|
209
|
+
this.#onFrame?.(frame);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
#listen() {
|
|
215
|
+
const sock = this.#ws;
|
|
216
|
+
sock.addEventListener('message', (ev) => {
|
|
217
|
+
if (sock !== this.#ws || typeof ev.data !== 'string')
|
|
218
|
+
return;
|
|
219
|
+
let frame;
|
|
220
|
+
try {
|
|
221
|
+
frame = parseServerFrame(ev.data);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (!frame)
|
|
227
|
+
return;
|
|
228
|
+
this.#frame(frame);
|
|
229
|
+
switch (frame.type) {
|
|
230
|
+
case 'transcript.committed':
|
|
231
|
+
case 'transcript.draft':
|
|
232
|
+
case 'transcript.words':
|
|
233
|
+
case 'transcript.translated':
|
|
234
|
+
case 'transcript.amended':
|
|
235
|
+
case 'transcript.latency':
|
|
236
|
+
case 'transcript.speaker':
|
|
237
|
+
case 'transcript.sentence':
|
|
238
|
+
case 'transcript.progress':
|
|
239
|
+
this.#queue.push(frame);
|
|
240
|
+
this.#wake?.();
|
|
241
|
+
break;
|
|
242
|
+
case 'session.done':
|
|
243
|
+
this.#endReason = frame.reason;
|
|
244
|
+
this.#stats = frame.stats;
|
|
245
|
+
this.#wake?.();
|
|
246
|
+
break;
|
|
247
|
+
case 'error':
|
|
248
|
+
this.#error = new VocalStackError(frame.code, frame.message, {
|
|
249
|
+
retryable: frame.retryable,
|
|
250
|
+
});
|
|
251
|
+
this.#wake?.();
|
|
252
|
+
break;
|
|
253
|
+
default:
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
const ws = this.#ws;
|
|
258
|
+
ws.addEventListener('close', (ev) => {
|
|
259
|
+
if (ws !== this.#ws)
|
|
260
|
+
return;
|
|
261
|
+
const code = typeof ev?.code === 'number' ? ev.code : undefined;
|
|
262
|
+
if (code === RESTART_CLOSE && !this.#endReason && !this.#error && !this.#closedByUs) {
|
|
263
|
+
void this.#reconnect(code);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
this.#closed = true;
|
|
267
|
+
if (!this.#endReason && !this.#error && !this.#closedByUs) {
|
|
268
|
+
this.#error = engineLost(code);
|
|
269
|
+
}
|
|
270
|
+
this.#wake?.();
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
sendAudio(chunk) {
|
|
274
|
+
const data = chunk instanceof Float32Array
|
|
275
|
+
? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)
|
|
276
|
+
: chunk;
|
|
277
|
+
this.#send(this.#reconnecting ? (data instanceof Uint8Array ? data.slice() : data.slice(0)) : data);
|
|
278
|
+
}
|
|
279
|
+
async sendFile(source, chunkBytes = 256 * 1024) {
|
|
280
|
+
const MAX_BUFFERED = 4 * 1024 * 1024;
|
|
281
|
+
for await (const chunk of source) {
|
|
282
|
+
for (let off = 0; off < chunk.byteLength; off += chunkBytes) {
|
|
283
|
+
while (this.#reconnecting)
|
|
284
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
285
|
+
if (this.#ws.readyState !== WS_OPEN)
|
|
286
|
+
return;
|
|
287
|
+
while (this.#ws.bufferedAmount > MAX_BUFFERED) {
|
|
288
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
289
|
+
if (this.#ws.readyState !== WS_OPEN && !this.#reconnecting)
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
this.#send(chunk.subarray(off, Math.min(off + chunkBytes, chunk.byteLength)));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
this.eof();
|
|
296
|
+
}
|
|
297
|
+
async pipe(source) {
|
|
298
|
+
const iter = Symbol.asyncIterator in source
|
|
299
|
+
? source
|
|
300
|
+
: readableToAsyncIterable(source);
|
|
301
|
+
for await (const chunk of iter) {
|
|
302
|
+
this.sendAudio(chunk);
|
|
303
|
+
}
|
|
304
|
+
this.eof();
|
|
305
|
+
}
|
|
306
|
+
sendClock(pcm, wall) {
|
|
307
|
+
this.#send(JSON.stringify({ type: 'clock', pcm: Math.floor(pcm), wall }));
|
|
308
|
+
}
|
|
309
|
+
update(changes) {
|
|
310
|
+
this.#send(JSON.stringify({
|
|
311
|
+
type: 'session.update',
|
|
312
|
+
...(changes.latencyMode !== undefined && { latencyMode: changes.latencyMode }),
|
|
313
|
+
...(changes.spokenLanguage !== undefined && { spokenLanguage: changes.spokenLanguage }),
|
|
314
|
+
...(changes.speakerLabels !== undefined && { speakerLabels: changes.speakerLabels }),
|
|
315
|
+
...(changes.translate !== undefined && { translate: changes.translate }),
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
eof() {
|
|
319
|
+
this.#send(JSON.stringify({ type: 'eof' }));
|
|
320
|
+
}
|
|
321
|
+
cancel() {
|
|
322
|
+
this.#send(JSON.stringify({ type: 'cancel' }));
|
|
323
|
+
}
|
|
324
|
+
get doneReason() {
|
|
325
|
+
return this.#endReason;
|
|
326
|
+
}
|
|
327
|
+
get stats() {
|
|
328
|
+
return this.#stats;
|
|
329
|
+
}
|
|
330
|
+
close() {
|
|
331
|
+
this.#closed = true;
|
|
332
|
+
this.#closedByUs = true;
|
|
333
|
+
try {
|
|
334
|
+
this.#ws.close();
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
}
|
|
338
|
+
this.#wake?.();
|
|
339
|
+
}
|
|
340
|
+
async *[Symbol.asyncIterator]() {
|
|
341
|
+
for (;;) {
|
|
342
|
+
const item = this.#queue.shift();
|
|
343
|
+
if (item) {
|
|
344
|
+
yield item;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (this.#error)
|
|
348
|
+
throw this.#error;
|
|
349
|
+
if (this.#endReason || this.#closed)
|
|
350
|
+
return;
|
|
351
|
+
await new Promise((resolve) => {
|
|
352
|
+
this.#wake = resolve;
|
|
353
|
+
});
|
|
354
|
+
this.#wake = null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
[Symbol.dispose]() {
|
|
358
|
+
this.close();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function* readableToAsyncIterable(stream) {
|
|
362
|
+
const reader = stream.getReader();
|
|
363
|
+
try {
|
|
364
|
+
for (;;) {
|
|
365
|
+
const { done, value } = await reader.read();
|
|
366
|
+
if (done)
|
|
367
|
+
return;
|
|
368
|
+
if (value)
|
|
369
|
+
yield value;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
reader.releaseLock();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
export class Realtime {
|
|
377
|
+
#t;
|
|
378
|
+
constructor(t) {
|
|
379
|
+
this.#t = t;
|
|
380
|
+
}
|
|
381
|
+
createSession(options = {}, call = {}) {
|
|
382
|
+
return this.#t.json('POST', '/v1/realtime/sessions', { body: options, ...call });
|
|
383
|
+
}
|
|
384
|
+
async connect(options = {}, connect = {}) {
|
|
385
|
+
const created = await this.createSession(options);
|
|
386
|
+
return RealtimeSession.connect({
|
|
387
|
+
sessionId: created.sessionId,
|
|
388
|
+
streamUrl: created.streamUrl,
|
|
389
|
+
options,
|
|
390
|
+
readyDeadlineMs: connect.readyDeadlineMs ?? this.#t.opts.readyDeadlineMs,
|
|
391
|
+
onLoading: connect.onLoading,
|
|
392
|
+
onFrame: connect.onFrame,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
capabilities() {
|
|
396
|
+
return this.#t.json('GET', '/v1/realtime/capabilities');
|
|
397
|
+
}
|
|
398
|
+
async warm(req) {
|
|
399
|
+
await this.#t.json('POST', '/v1/realtime/warm', { body: req });
|
|
400
|
+
}
|
|
401
|
+
async transcribeFile(file, opts = {}) {
|
|
402
|
+
const { onSession, readyDeadlineMs, handlers, options } = split(opts);
|
|
403
|
+
const session = await this.connect({ ...options, input: { kind: 'file' } }, { readyDeadlineMs });
|
|
404
|
+
onSession?.(session);
|
|
405
|
+
await session.sendFile(fileChunks(file));
|
|
406
|
+
return collectTranscript(session, handlers);
|
|
407
|
+
}
|
|
408
|
+
async transcribeUrl(url, opts = {}) {
|
|
409
|
+
const { onSession, readyDeadlineMs, handlers, options } = split(opts);
|
|
410
|
+
const session = await this.connect({ ...options, input: { kind: 'url', url } }, { readyDeadlineMs });
|
|
411
|
+
onSession?.(session);
|
|
412
|
+
return collectTranscript(session, handlers);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function split(opts) {
|
|
416
|
+
const { onProgress, onCommit, onSession, onTranslated, onAmended, onSpeakerSettled, onSentence, readyDeadlineMs, ...options } = opts;
|
|
417
|
+
const handlers = { onProgress, onCommit, onTranslated, onAmended, onSpeakerSettled, onSentence };
|
|
418
|
+
return { onSession, readyDeadlineMs, handlers, options };
|
|
419
|
+
}
|
|
420
|
+
export function applySentence(list, sentence) {
|
|
421
|
+
const i = list.findIndex((s) => s.sentenceId === sentence.sentenceId);
|
|
422
|
+
if (sentence.status === 'dropped') {
|
|
423
|
+
return i === -1 ? list : [...list.slice(0, i), ...list.slice(i + 1)];
|
|
424
|
+
}
|
|
425
|
+
if (i === -1) {
|
|
426
|
+
const next = [...list, sentence];
|
|
427
|
+
next.sort((a, b) => a.sentenceId - b.sentenceId);
|
|
428
|
+
return next;
|
|
429
|
+
}
|
|
430
|
+
const next = list.slice();
|
|
431
|
+
next[i] = sentence;
|
|
432
|
+
return next;
|
|
433
|
+
}
|
|
434
|
+
async function collectTranscript(session, h) {
|
|
435
|
+
const segments = [];
|
|
436
|
+
let sentences = [];
|
|
437
|
+
for await (const event of session) {
|
|
438
|
+
if (event.type === 'transcript.committed') {
|
|
439
|
+
segments.push(event.segment);
|
|
440
|
+
h.onCommit?.(event.segment);
|
|
441
|
+
}
|
|
442
|
+
else if (event.type === 'transcript.translated') {
|
|
443
|
+
const seg = segments.find((s) => s.seq === event.seq);
|
|
444
|
+
if (seg) {
|
|
445
|
+
seg.translations = event.translations;
|
|
446
|
+
seg.translationSources = event.sources;
|
|
447
|
+
}
|
|
448
|
+
h.onTranslated?.(event.seq, event.translations, event.sources ?? undefined);
|
|
449
|
+
}
|
|
450
|
+
else if (event.type === 'transcript.amended') {
|
|
451
|
+
const seg = segments.find((s) => s.seq === event.seq);
|
|
452
|
+
if (seg) {
|
|
453
|
+
seg.text = event.text;
|
|
454
|
+
if (event.speaker >= 0)
|
|
455
|
+
seg.speaker = event.speaker;
|
|
456
|
+
}
|
|
457
|
+
h.onAmended?.(event.seq, event.text, event.speaker, event.fixes ?? []);
|
|
458
|
+
}
|
|
459
|
+
else if (event.type === 'transcript.latency') {
|
|
460
|
+
const seg = segments.find((s) => s.seq === event.seq);
|
|
461
|
+
if (seg)
|
|
462
|
+
seg.latencyS = event.latencyS;
|
|
463
|
+
}
|
|
464
|
+
else if (event.type === 'transcript.speaker') {
|
|
465
|
+
const seg = segments.find((s) => s.seq === event.seq);
|
|
466
|
+
if (seg) {
|
|
467
|
+
if (event.speaker >= 0)
|
|
468
|
+
seg.speaker = event.speaker;
|
|
469
|
+
if (event.speakerConfidence != null)
|
|
470
|
+
seg.speakerConfidence = event.speakerConfidence;
|
|
471
|
+
seg.speakerProvisional = false;
|
|
472
|
+
}
|
|
473
|
+
h.onSpeakerSettled?.(event.seq, event.speaker, event.verdict, event.speakerConfidence ?? undefined);
|
|
474
|
+
}
|
|
475
|
+
else if (event.type === 'transcript.sentence') {
|
|
476
|
+
sentences = applySentence(sentences, event.sentence);
|
|
477
|
+
h.onSentence?.(event.sentence);
|
|
478
|
+
}
|
|
479
|
+
else if (event.type === 'transcript.progress') {
|
|
480
|
+
h.onProgress?.({ processedS: event.processedS, totalS: event.totalS ?? undefined });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
segments,
|
|
485
|
+
sentences,
|
|
486
|
+
text: (sentences.length ? sentences : segments).map((s) => s.text).join(' '),
|
|
487
|
+
doneReason: session.doneReason,
|
|
488
|
+
stats: session.stats,
|
|
489
|
+
};
|
|
490
|
+
}
|