assemblyai 4.34.6 → 4.35.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/CHANGELOG.md +4 -0
- package/dist/assemblyai.streaming.umd.js +146 -10
- package/dist/assemblyai.streaming.umd.min.js +1 -1
- package/dist/assemblyai.umd.js +146 -10
- package/dist/assemblyai.umd.min.js +1 -1
- package/dist/browser.mjs +144 -12
- package/dist/bun.mjs +144 -12
- package/dist/deno.mjs +144 -12
- package/dist/index.cjs +146 -10
- package/dist/index.mjs +146 -10
- package/dist/node.cjs +144 -12
- package/dist/node.mjs +144 -12
- package/dist/services/streaming/service.d.ts +22 -0
- package/dist/streaming.browser.mjs +144 -12
- package/dist/streaming.cjs +145 -9
- package/dist/streaming.mjs +145 -9
- package/dist/types/streaming/index.d.ts +20 -1
- package/dist/workerd.mjs +144 -12
- package/package.json +1 -1
- package/src/services/streaming/service.ts +167 -11
- package/src/types/streaming/index.ts +21 -0
package/dist/workerd.mjs
CHANGED
|
@@ -28,7 +28,7 @@ if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
|
28
28
|
defaultUserAgentString += navigator.userAgent;
|
|
29
29
|
}
|
|
30
30
|
const defaultUserAgent = {
|
|
31
|
-
sdk: { name: "JavaScript", version: "4.
|
|
31
|
+
sdk: { name: "JavaScript", version: "4.35.0" },
|
|
32
32
|
};
|
|
33
33
|
if (typeof process !== "undefined") {
|
|
34
34
|
if (process.versions.node && defaultUserAgentString.indexOf("Node") === -1) {
|
|
@@ -983,6 +983,24 @@ function toInt16View(audio) {
|
|
|
983
983
|
}
|
|
984
984
|
const defaultStreamingUrl$1 = "wss://streaming.assemblyai.com/v3/ws";
|
|
985
985
|
const terminateSessionMessage = `{"type":"Terminate"}`;
|
|
986
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1000;
|
|
987
|
+
const DEFAULT_MAX_CONNECTION_RETRIES = 2;
|
|
988
|
+
const DEFAULT_CONNECTION_RETRY_DELAY_MS = 500;
|
|
989
|
+
/**
|
|
990
|
+
* Close/error codes that signal a permanent client-side problem (auth,
|
|
991
|
+
* billing, malformed config). A retry would hit the same failure, so the
|
|
992
|
+
* connection is never retried on these.
|
|
993
|
+
*/
|
|
994
|
+
const NON_RETRYABLE_CLOSE_CODES = new Set([
|
|
995
|
+
StreamingErrorType.BadSampleRate,
|
|
996
|
+
StreamingErrorType.AuthFailed,
|
|
997
|
+
StreamingErrorType.InsufficientFunds,
|
|
998
|
+
StreamingErrorType.FreeTierUser,
|
|
999
|
+
StreamingErrorType.BadSchema,
|
|
1000
|
+
]);
|
|
1001
|
+
function isRetryableCloseCode(code) {
|
|
1002
|
+
return code !== 1000 && !NON_RETRYABLE_CLOSE_CODES.has(code);
|
|
1003
|
+
}
|
|
986
1004
|
/**
|
|
987
1005
|
* Per-send chunk cap in milliseconds for the dual-channel mixer. The streaming
|
|
988
1006
|
* server rejects audio messages longer than 1000 ms (`Input Duration Error`).
|
|
@@ -1191,12 +1209,81 @@ class StreamingTranscriber {
|
|
|
1191
1209
|
on(event, listener) {
|
|
1192
1210
|
this.listeners[event] = listener;
|
|
1193
1211
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1212
|
+
/**
|
|
1213
|
+
* Open the streaming session.
|
|
1214
|
+
*
|
|
1215
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
1216
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
1217
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
1218
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
1219
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
1220
|
+
* funds, malformed config) are not retried.
|
|
1221
|
+
*
|
|
1222
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
1223
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
1224
|
+
* the retry loop) to observe the failure.
|
|
1225
|
+
*/
|
|
1226
|
+
async connect() {
|
|
1227
|
+
if (this.socket) {
|
|
1228
|
+
throw new Error("Already connected");
|
|
1229
|
+
}
|
|
1230
|
+
const maxRetries = this.params.maxConnectionRetries ?? DEFAULT_MAX_CONNECTION_RETRIES;
|
|
1231
|
+
const retryDelay = this.params.connectionRetryDelay ?? DEFAULT_CONNECTION_RETRY_DELAY_MS;
|
|
1232
|
+
let lastError;
|
|
1233
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1234
|
+
try {
|
|
1235
|
+
return await this.connectOnce();
|
|
1236
|
+
}
|
|
1237
|
+
catch (err) {
|
|
1238
|
+
lastError = err;
|
|
1239
|
+
const retryable = err.retryable === true;
|
|
1240
|
+
if (!retryable || attempt === maxRetries) {
|
|
1241
|
+
throw err;
|
|
1242
|
+
}
|
|
1243
|
+
console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
|
|
1244
|
+
if (retryDelay > 0) {
|
|
1245
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1246
|
+
}
|
|
1198
1247
|
}
|
|
1248
|
+
}
|
|
1249
|
+
// The loop above always returns or throws; this only satisfies the type
|
|
1250
|
+
// checker that a value is produced on every path.
|
|
1251
|
+
throw lastError ?? new Error("Failed to connect to streaming server");
|
|
1252
|
+
}
|
|
1253
|
+
connectOnce() {
|
|
1254
|
+
return new Promise((resolve, reject) => {
|
|
1199
1255
|
const url = this.connectionUrl();
|
|
1256
|
+
const timeoutMs = this.params.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
1257
|
+
// `settled` flips once this attempt has resolved (`Begin`) or rejected
|
|
1258
|
+
// (timeout / pre-`Begin` close / error). Before it flips the socket
|
|
1259
|
+
// handlers drive this promise; after it flips they revert to normal
|
|
1260
|
+
// runtime dispatch (close / error / message listeners).
|
|
1261
|
+
let settled = false;
|
|
1262
|
+
let timer;
|
|
1263
|
+
const failAttempt = (error) => {
|
|
1264
|
+
if (settled)
|
|
1265
|
+
return;
|
|
1266
|
+
settled = true;
|
|
1267
|
+
if (timer)
|
|
1268
|
+
clearTimeout(timer);
|
|
1269
|
+
this.discardPendingSocket();
|
|
1270
|
+
reject(error);
|
|
1271
|
+
};
|
|
1272
|
+
const succeed = (begin) => {
|
|
1273
|
+
if (settled)
|
|
1274
|
+
return;
|
|
1275
|
+
settled = true;
|
|
1276
|
+
if (timer)
|
|
1277
|
+
clearTimeout(timer);
|
|
1278
|
+
resolve(begin);
|
|
1279
|
+
};
|
|
1280
|
+
if (timeoutMs > 0) {
|
|
1281
|
+
timer = setTimeout(() => {
|
|
1282
|
+
const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
|
|
1283
|
+
err.retryable = true;
|
|
1284
|
+
failAttempt(err);
|
|
1285
|
+
}, timeoutMs);
|
|
1286
|
+
}
|
|
1200
1287
|
if (this.token) {
|
|
1201
1288
|
this.socket = factory(url.toString());
|
|
1202
1289
|
}
|
|
@@ -1213,6 +1300,15 @@ class StreamingTranscriber {
|
|
|
1213
1300
|
reason = StreamingErrorMessages[code];
|
|
1214
1301
|
}
|
|
1215
1302
|
}
|
|
1303
|
+
// A close before `Begin` is a failed connection attempt — reject so
|
|
1304
|
+
// connect() can retry (or surface a permanent failure).
|
|
1305
|
+
if (!settled) {
|
|
1306
|
+
const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
|
|
1307
|
+
err.code = code;
|
|
1308
|
+
err.retryable = isRetryableCloseCode(code);
|
|
1309
|
+
failAttempt(err);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1216
1312
|
// Stop the flush timer when the socket is gone (server-initiated close,
|
|
1217
1313
|
// network drop, etc.) — otherwise subsequent ticks call send() on a
|
|
1218
1314
|
// closed socket and spam the error listener.
|
|
@@ -1223,25 +1319,37 @@ class StreamingTranscriber {
|
|
|
1223
1319
|
this.listeners.close?.(code, reason);
|
|
1224
1320
|
};
|
|
1225
1321
|
this.socket.onerror = (event) => {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1322
|
+
const error = event.error ?? new Error(event.message);
|
|
1323
|
+
// A socket error before `Begin` is a failed attempt → reject/retry.
|
|
1324
|
+
if (!settled) {
|
|
1325
|
+
error.retryable = true;
|
|
1326
|
+
failAttempt(error);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
this.listeners.error?.(error);
|
|
1230
1330
|
};
|
|
1231
1331
|
this.socket.onmessage = ({ data }) => {
|
|
1232
1332
|
const message = JSON.parse(data.toString());
|
|
1233
1333
|
if ("error" in message) {
|
|
1234
1334
|
const err = new StreamingError(message.error);
|
|
1235
1335
|
if ("error_code" in message) {
|
|
1236
|
-
err.code =
|
|
1237
|
-
|
|
1336
|
+
err.code = message.error_code;
|
|
1337
|
+
}
|
|
1338
|
+
// A server error frame before `Begin` fails the attempt; the code
|
|
1339
|
+
// decides whether a retry is worthwhile.
|
|
1340
|
+
if (!settled) {
|
|
1341
|
+
const attemptErr = err;
|
|
1342
|
+
attemptErr.retryable =
|
|
1343
|
+
err.code === undefined ? true : isRetryableCloseCode(err.code);
|
|
1344
|
+
failAttempt(attemptErr);
|
|
1345
|
+
return;
|
|
1238
1346
|
}
|
|
1239
1347
|
this.listeners.error?.(err);
|
|
1240
1348
|
return;
|
|
1241
1349
|
}
|
|
1242
1350
|
switch (message.type) {
|
|
1243
1351
|
case "Begin": {
|
|
1244
|
-
|
|
1352
|
+
succeed(message);
|
|
1245
1353
|
this.listeners.open?.(message);
|
|
1246
1354
|
break;
|
|
1247
1355
|
}
|
|
@@ -1288,6 +1396,20 @@ class StreamingTranscriber {
|
|
|
1288
1396
|
};
|
|
1289
1397
|
});
|
|
1290
1398
|
}
|
|
1399
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
1400
|
+
discardPendingSocket() {
|
|
1401
|
+
if (!this.socket)
|
|
1402
|
+
return;
|
|
1403
|
+
try {
|
|
1404
|
+
if (this.socket.removeAllListeners)
|
|
1405
|
+
this.socket.removeAllListeners();
|
|
1406
|
+
this.socket.close();
|
|
1407
|
+
}
|
|
1408
|
+
catch {
|
|
1409
|
+
// Best-effort cleanup; a half-open socket may throw on close.
|
|
1410
|
+
}
|
|
1411
|
+
this.socket = undefined;
|
|
1412
|
+
}
|
|
1291
1413
|
/**
|
|
1292
1414
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
1293
1415
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -1561,6 +1683,16 @@ class StreamingTranscriber {
|
|
|
1561
1683
|
};
|
|
1562
1684
|
this.send(JSON.stringify(message));
|
|
1563
1685
|
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
1688
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
1689
|
+
*/
|
|
1690
|
+
keepAlive() {
|
|
1691
|
+
const message = {
|
|
1692
|
+
type: "KeepAlive",
|
|
1693
|
+
};
|
|
1694
|
+
this.send(JSON.stringify(message));
|
|
1695
|
+
}
|
|
1564
1696
|
send(data) {
|
|
1565
1697
|
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
|
|
1566
1698
|
throw new Error("Socket is not open for communication");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assemblyai",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.35.0",
|
|
4
4
|
"description": "The AssemblyAI JavaScript SDK provides an easy-to-use interface for interacting with the AssemblyAI API, which supports async and real-time transcription, as well as the latest LeMUR models.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18"
|
|
@@ -18,12 +18,17 @@ import {
|
|
|
18
18
|
SpeakerRevisionEvent,
|
|
19
19
|
StreamingUpdateConfiguration,
|
|
20
20
|
StreamingForceEndpoint,
|
|
21
|
+
StreamingKeepAlive,
|
|
21
22
|
WarningEvent,
|
|
22
23
|
} from "../..";
|
|
23
24
|
import type { VadDetector, VadFrame } from "../../types/streaming/dual-channel";
|
|
24
25
|
import { EnergyVad } from "./energy-vad";
|
|
25
26
|
import { attributeTurn, rollUpTurnChannel, VadTimeline } from "./label-mapper";
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
StreamingError,
|
|
29
|
+
StreamingErrorMessages,
|
|
30
|
+
StreamingErrorType,
|
|
31
|
+
} from "../../utils/errors";
|
|
27
32
|
import { StreamingErrorTypeCodes } from "../../utils/errors/streaming";
|
|
28
33
|
|
|
29
34
|
/**
|
|
@@ -58,6 +63,30 @@ function toInt16View(audio: AudioData): Int16Array {
|
|
|
58
63
|
const defaultStreamingUrl = "wss://streaming.assemblyai.com/v3/ws";
|
|
59
64
|
const terminateSessionMessage = `{"type":"Terminate"}`;
|
|
60
65
|
|
|
66
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1000;
|
|
67
|
+
const DEFAULT_MAX_CONNECTION_RETRIES = 2;
|
|
68
|
+
const DEFAULT_CONNECTION_RETRY_DELAY_MS = 500;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Close/error codes that signal a permanent client-side problem (auth,
|
|
72
|
+
* billing, malformed config). A retry would hit the same failure, so the
|
|
73
|
+
* connection is never retried on these.
|
|
74
|
+
*/
|
|
75
|
+
const NON_RETRYABLE_CLOSE_CODES = new Set<number>([
|
|
76
|
+
StreamingErrorType.BadSampleRate,
|
|
77
|
+
StreamingErrorType.AuthFailed,
|
|
78
|
+
StreamingErrorType.InsufficientFunds,
|
|
79
|
+
StreamingErrorType.FreeTierUser,
|
|
80
|
+
StreamingErrorType.BadSchema,
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/** Error from a single connection attempt, tagged for retry handling. */
|
|
84
|
+
type ConnectionAttemptError = Error & { code?: number; retryable: boolean };
|
|
85
|
+
|
|
86
|
+
function isRetryableCloseCode(code: number): boolean {
|
|
87
|
+
return code !== 1000 && !NON_RETRYABLE_CLOSE_CODES.has(code);
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
/**
|
|
62
91
|
* Per-send chunk cap in milliseconds for the dual-channel mixer. The streaming
|
|
63
92
|
* server rejects audio messages longer than 1000 ms (`Input Duration Error`).
|
|
@@ -432,13 +461,90 @@ export class StreamingTranscriber {
|
|
|
432
461
|
this.listeners[event] = listener;
|
|
433
462
|
}
|
|
434
463
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
464
|
+
/**
|
|
465
|
+
* Open the streaming session.
|
|
466
|
+
*
|
|
467
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
468
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
469
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
470
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
471
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
472
|
+
* funds, malformed config) are not retried.
|
|
473
|
+
*
|
|
474
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
475
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
476
|
+
* the retry loop) to observe the failure.
|
|
477
|
+
*/
|
|
478
|
+
async connect(): Promise<BeginEvent> {
|
|
479
|
+
if (this.socket) {
|
|
480
|
+
throw new Error("Already connected");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const maxRetries =
|
|
484
|
+
this.params.maxConnectionRetries ?? DEFAULT_MAX_CONNECTION_RETRIES;
|
|
485
|
+
const retryDelay =
|
|
486
|
+
this.params.connectionRetryDelay ?? DEFAULT_CONNECTION_RETRY_DELAY_MS;
|
|
487
|
+
|
|
488
|
+
let lastError: Error | undefined;
|
|
489
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
490
|
+
try {
|
|
491
|
+
return await this.connectOnce();
|
|
492
|
+
} catch (err) {
|
|
493
|
+
lastError = err as Error;
|
|
494
|
+
const retryable = (err as ConnectionAttemptError).retryable === true;
|
|
495
|
+
if (!retryable || attempt === maxRetries) {
|
|
496
|
+
throw err;
|
|
497
|
+
}
|
|
498
|
+
console.warn(
|
|
499
|
+
`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${(err as Error).message}); retrying`,
|
|
500
|
+
);
|
|
501
|
+
if (retryDelay > 0) {
|
|
502
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
503
|
+
}
|
|
439
504
|
}
|
|
505
|
+
}
|
|
506
|
+
// The loop above always returns or throws; this only satisfies the type
|
|
507
|
+
// checker that a value is produced on every path.
|
|
508
|
+
throw lastError ?? new Error("Failed to connect to streaming server");
|
|
509
|
+
}
|
|
440
510
|
|
|
511
|
+
private connectOnce(): Promise<BeginEvent> {
|
|
512
|
+
return new Promise<BeginEvent>((resolve, reject) => {
|
|
441
513
|
const url = this.connectionUrl();
|
|
514
|
+
const timeoutMs =
|
|
515
|
+
this.params.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
516
|
+
|
|
517
|
+
// `settled` flips once this attempt has resolved (`Begin`) or rejected
|
|
518
|
+
// (timeout / pre-`Begin` close / error). Before it flips the socket
|
|
519
|
+
// handlers drive this promise; after it flips they revert to normal
|
|
520
|
+
// runtime dispatch (close / error / message listeners).
|
|
521
|
+
let settled = false;
|
|
522
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
523
|
+
|
|
524
|
+
const failAttempt = (error: ConnectionAttemptError) => {
|
|
525
|
+
if (settled) return;
|
|
526
|
+
settled = true;
|
|
527
|
+
if (timer) clearTimeout(timer);
|
|
528
|
+
this.discardPendingSocket();
|
|
529
|
+
reject(error);
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const succeed = (begin: BeginEvent) => {
|
|
533
|
+
if (settled) return;
|
|
534
|
+
settled = true;
|
|
535
|
+
if (timer) clearTimeout(timer);
|
|
536
|
+
resolve(begin);
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
if (timeoutMs > 0) {
|
|
540
|
+
timer = setTimeout(() => {
|
|
541
|
+
const err = new StreamingError(
|
|
542
|
+
`Streaming connection timed out after ${timeoutMs}ms`,
|
|
543
|
+
) as ConnectionAttemptError;
|
|
544
|
+
err.retryable = true;
|
|
545
|
+
failAttempt(err);
|
|
546
|
+
}, timeoutMs);
|
|
547
|
+
}
|
|
442
548
|
|
|
443
549
|
if (this.token) {
|
|
444
550
|
this.socket = polyfillWebSocketFactory(url.toString());
|
|
@@ -465,6 +571,17 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
|
|
|
465
571
|
reason = StreamingErrorMessages[code as StreamingErrorTypeCodes];
|
|
466
572
|
}
|
|
467
573
|
}
|
|
574
|
+
// A close before `Begin` is a failed connection attempt — reject so
|
|
575
|
+
// connect() can retry (or surface a permanent failure).
|
|
576
|
+
if (!settled) {
|
|
577
|
+
const err = new StreamingError(
|
|
578
|
+
reason || `Streaming connection closed (code=${code})`,
|
|
579
|
+
) as ConnectionAttemptError;
|
|
580
|
+
err.code = code;
|
|
581
|
+
err.retryable = isRetryableCloseCode(code);
|
|
582
|
+
failAttempt(err);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
468
585
|
// Stop the flush timer when the socket is gone (server-initiated close,
|
|
469
586
|
// network drop, etc.) — otherwise subsequent ticks call send() on a
|
|
470
587
|
// closed socket and spam the error listener.
|
|
@@ -476,18 +593,34 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
|
|
|
476
593
|
};
|
|
477
594
|
|
|
478
595
|
this.socket.onerror = (event: ErrorEvent) => {
|
|
479
|
-
|
|
480
|
-
|
|
596
|
+
const error = (event.error as Error) ?? new Error(event.message);
|
|
597
|
+
// A socket error before `Begin` is a failed attempt → reject/retry.
|
|
598
|
+
if (!settled) {
|
|
599
|
+
(error as ConnectionAttemptError).retryable = true;
|
|
600
|
+
failAttempt(error as ConnectionAttemptError);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
this.listeners.error?.(error);
|
|
481
604
|
};
|
|
482
605
|
|
|
483
606
|
this.socket.onmessage = ({ data }: MessageEvent) => {
|
|
484
607
|
const message = JSON.parse(data.toString()) as StreamingEventMessage;
|
|
485
608
|
|
|
486
609
|
if ("error" in message) {
|
|
487
|
-
const err = new StreamingError(message.error)
|
|
610
|
+
const err = new StreamingError(message.error) as StreamingError & {
|
|
611
|
+
code?: number;
|
|
612
|
+
};
|
|
488
613
|
if ("error_code" in message) {
|
|
489
|
-
|
|
490
|
-
|
|
614
|
+
err.code = message.error_code;
|
|
615
|
+
}
|
|
616
|
+
// A server error frame before `Begin` fails the attempt; the code
|
|
617
|
+
// decides whether a retry is worthwhile.
|
|
618
|
+
if (!settled) {
|
|
619
|
+
const attemptErr = err as ConnectionAttemptError;
|
|
620
|
+
attemptErr.retryable =
|
|
621
|
+
err.code === undefined ? true : isRetryableCloseCode(err.code);
|
|
622
|
+
failAttempt(attemptErr);
|
|
623
|
+
return;
|
|
491
624
|
}
|
|
492
625
|
this.listeners.error?.(err);
|
|
493
626
|
return;
|
|
@@ -495,7 +628,7 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
|
|
|
495
628
|
|
|
496
629
|
switch (message.type) {
|
|
497
630
|
case "Begin": {
|
|
498
|
-
|
|
631
|
+
succeed(message);
|
|
499
632
|
this.listeners.open?.(message);
|
|
500
633
|
break;
|
|
501
634
|
}
|
|
@@ -548,6 +681,18 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
|
|
|
548
681
|
});
|
|
549
682
|
}
|
|
550
683
|
|
|
684
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
685
|
+
private discardPendingSocket(): void {
|
|
686
|
+
if (!this.socket) return;
|
|
687
|
+
try {
|
|
688
|
+
if (this.socket.removeAllListeners) this.socket.removeAllListeners();
|
|
689
|
+
this.socket.close();
|
|
690
|
+
} catch {
|
|
691
|
+
// Best-effort cleanup; a half-open socket may throw on close.
|
|
692
|
+
}
|
|
693
|
+
this.socket = undefined;
|
|
694
|
+
}
|
|
695
|
+
|
|
551
696
|
/**
|
|
552
697
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
553
698
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -829,6 +974,17 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
|
|
|
829
974
|
this.send(JSON.stringify(message));
|
|
830
975
|
}
|
|
831
976
|
|
|
977
|
+
/**
|
|
978
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
979
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
980
|
+
*/
|
|
981
|
+
keepAlive() {
|
|
982
|
+
const message: StreamingKeepAlive = {
|
|
983
|
+
type: "KeepAlive",
|
|
984
|
+
};
|
|
985
|
+
this.send(JSON.stringify(message));
|
|
986
|
+
}
|
|
987
|
+
|
|
832
988
|
private send(data: BufferLike) {
|
|
833
989
|
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
|
|
834
990
|
throw new Error("Socket is not open for communication");
|
|
@@ -76,6 +76,22 @@ export type StreamingTranscriberParams = {
|
|
|
76
76
|
websocketBaseUrl?: string;
|
|
77
77
|
apiKey?: string;
|
|
78
78
|
token?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Milliseconds to wait for the streaming handshake (socket open + server
|
|
81
|
+
* `Begin`) before treating the attempt as failed. Defaults to 1000.
|
|
82
|
+
*/
|
|
83
|
+
connectTimeout?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Number of additional connection attempts after the first one fails on a
|
|
86
|
+
* transient error (timeout, network drop, unexpected close). 0 disables
|
|
87
|
+
* retries. Permanent failures (auth, insufficient funds, malformed config)
|
|
88
|
+
* are never retried. Defaults to 2.
|
|
89
|
+
*/
|
|
90
|
+
maxConnectionRetries?: number;
|
|
91
|
+
/**
|
|
92
|
+
* Milliseconds to wait between connection attempts. Defaults to 500.
|
|
93
|
+
*/
|
|
94
|
+
connectionRetryDelay?: number;
|
|
79
95
|
sampleRate: number;
|
|
80
96
|
encoding?: AudioEncoding;
|
|
81
97
|
endOfTurnConfidenceThreshold?: number;
|
|
@@ -349,6 +365,10 @@ export type StreamingForceEndpoint = {
|
|
|
349
365
|
type: "ForceEndpoint";
|
|
350
366
|
};
|
|
351
367
|
|
|
368
|
+
export type StreamingKeepAlive = {
|
|
369
|
+
type: "KeepAlive";
|
|
370
|
+
};
|
|
371
|
+
|
|
352
372
|
export type ErrorEvent = {
|
|
353
373
|
type: "Error";
|
|
354
374
|
error_code?: number;
|
|
@@ -405,4 +425,5 @@ export type StreamingEventMessage =
|
|
|
405
425
|
export type StreamingOperationMessage =
|
|
406
426
|
| StreamingUpdateConfiguration
|
|
407
427
|
| StreamingForceEndpoint
|
|
428
|
+
| StreamingKeepAlive
|
|
408
429
|
| StreamingTerminateSession;
|