assemblyai 4.34.6 → 4.35.3
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 +16 -0
- package/README.md +7 -8
- package/dist/assemblyai.streaming.umd.js +150 -10
- package/dist/assemblyai.streaming.umd.min.js +1 -1
- package/dist/assemblyai.umd.js +150 -10
- package/dist/assemblyai.umd.min.js +1 -1
- package/dist/browser.mjs +148 -12
- package/dist/bun.mjs +148 -12
- package/dist/deno.mjs +148 -12
- package/dist/exports/streaming.d.ts +1 -1
- package/dist/index.cjs +150 -10
- package/dist/index.mjs +150 -10
- package/dist/node.cjs +148 -12
- package/dist/node.mjs +148 -12
- package/dist/services/streaming/service.d.ts +22 -0
- package/dist/streaming.browser.mjs +148 -12
- package/dist/streaming.cjs +149 -9
- package/dist/streaming.mjs +149 -9
- package/dist/types/asyncapi.generated.d.ts +1 -1
- package/dist/types/streaming/index.d.ts +38 -1
- package/dist/workerd.mjs +148 -12
- package/package.json +1 -1
- package/src/exports/streaming.ts +1 -1
- package/src/services/streaming/service.ts +177 -11
- package/src/types/asyncapi.generated.ts +1 -1
- package/src/types/streaming/index.ts +39 -0
|
@@ -531,6 +531,24 @@ function toInt16View(audio) {
|
|
|
531
531
|
}
|
|
532
532
|
const defaultStreamingUrl = "wss://streaming.assemblyai.com/v3/ws";
|
|
533
533
|
const terminateSessionMessage = `{"type":"Terminate"}`;
|
|
534
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1000;
|
|
535
|
+
const DEFAULT_MAX_CONNECTION_RETRIES = 2;
|
|
536
|
+
const DEFAULT_CONNECTION_RETRY_DELAY_MS = 500;
|
|
537
|
+
/**
|
|
538
|
+
* Close/error codes that signal a permanent client-side problem (auth,
|
|
539
|
+
* billing, malformed config). A retry would hit the same failure, so the
|
|
540
|
+
* connection is never retried on these.
|
|
541
|
+
*/
|
|
542
|
+
const NON_RETRYABLE_CLOSE_CODES = new Set([
|
|
543
|
+
StreamingErrorType.BadSampleRate,
|
|
544
|
+
StreamingErrorType.AuthFailed,
|
|
545
|
+
StreamingErrorType.InsufficientFunds,
|
|
546
|
+
StreamingErrorType.FreeTierUser,
|
|
547
|
+
StreamingErrorType.BadSchema,
|
|
548
|
+
]);
|
|
549
|
+
function isRetryableCloseCode(code) {
|
|
550
|
+
return code !== 1000 && !NON_RETRYABLE_CLOSE_CODES.has(code);
|
|
551
|
+
}
|
|
534
552
|
/**
|
|
535
553
|
* Per-send chunk cap in milliseconds for the dual-channel mixer. The streaming
|
|
536
554
|
* server rejects audio messages longer than 1000 ms (`Input Duration Error`).
|
|
@@ -664,8 +682,12 @@ class StreamingTranscriber {
|
|
|
664
682
|
searchParams.set("speech_model", this.params.speechModel.toString());
|
|
665
683
|
}
|
|
666
684
|
if (this.params.languageCode !== undefined) {
|
|
685
|
+
console.warn("[Deprecation Warning] `languageCode` is deprecated and will be removed in a future release. Please use `languageCodes` instead.");
|
|
667
686
|
searchParams.set("language_code", this.params.languageCode);
|
|
668
687
|
}
|
|
688
|
+
if (this.params.languageCodes !== undefined) {
|
|
689
|
+
searchParams.set("language_codes", JSON.stringify(this.params.languageCodes));
|
|
690
|
+
}
|
|
669
691
|
if (this.params.languageDetection !== undefined) {
|
|
670
692
|
searchParams.set("language_detection", this.params.languageDetection.toString());
|
|
671
693
|
}
|
|
@@ -739,12 +761,81 @@ class StreamingTranscriber {
|
|
|
739
761
|
on(event, listener) {
|
|
740
762
|
this.listeners[event] = listener;
|
|
741
763
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
764
|
+
/**
|
|
765
|
+
* Open the streaming session.
|
|
766
|
+
*
|
|
767
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
768
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
769
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
770
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
771
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
772
|
+
* funds, malformed config) are not retried.
|
|
773
|
+
*
|
|
774
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
775
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
776
|
+
* the retry loop) to observe the failure.
|
|
777
|
+
*/
|
|
778
|
+
async connect() {
|
|
779
|
+
if (this.socket) {
|
|
780
|
+
throw new Error("Already connected");
|
|
781
|
+
}
|
|
782
|
+
const maxRetries = this.params.maxConnectionRetries ?? DEFAULT_MAX_CONNECTION_RETRIES;
|
|
783
|
+
const retryDelay = this.params.connectionRetryDelay ?? DEFAULT_CONNECTION_RETRY_DELAY_MS;
|
|
784
|
+
let lastError;
|
|
785
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
786
|
+
try {
|
|
787
|
+
return await this.connectOnce();
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
lastError = err;
|
|
791
|
+
const retryable = err.retryable === true;
|
|
792
|
+
if (!retryable || attempt === maxRetries) {
|
|
793
|
+
throw err;
|
|
794
|
+
}
|
|
795
|
+
console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
|
|
796
|
+
if (retryDelay > 0) {
|
|
797
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
798
|
+
}
|
|
746
799
|
}
|
|
800
|
+
}
|
|
801
|
+
// The loop above always returns or throws; this only satisfies the type
|
|
802
|
+
// checker that a value is produced on every path.
|
|
803
|
+
throw lastError ?? new Error("Failed to connect to streaming server");
|
|
804
|
+
}
|
|
805
|
+
connectOnce() {
|
|
806
|
+
return new Promise((resolve, reject) => {
|
|
747
807
|
const url = this.connectionUrl();
|
|
808
|
+
const timeoutMs = this.params.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
809
|
+
// `settled` flips once this attempt has resolved (`Begin`) or rejected
|
|
810
|
+
// (timeout / pre-`Begin` close / error). Before it flips the socket
|
|
811
|
+
// handlers drive this promise; after it flips they revert to normal
|
|
812
|
+
// runtime dispatch (close / error / message listeners).
|
|
813
|
+
let settled = false;
|
|
814
|
+
let timer;
|
|
815
|
+
const failAttempt = (error) => {
|
|
816
|
+
if (settled)
|
|
817
|
+
return;
|
|
818
|
+
settled = true;
|
|
819
|
+
if (timer)
|
|
820
|
+
clearTimeout(timer);
|
|
821
|
+
this.discardPendingSocket();
|
|
822
|
+
reject(error);
|
|
823
|
+
};
|
|
824
|
+
const succeed = (begin) => {
|
|
825
|
+
if (settled)
|
|
826
|
+
return;
|
|
827
|
+
settled = true;
|
|
828
|
+
if (timer)
|
|
829
|
+
clearTimeout(timer);
|
|
830
|
+
resolve(begin);
|
|
831
|
+
};
|
|
832
|
+
if (timeoutMs > 0) {
|
|
833
|
+
timer = setTimeout(() => {
|
|
834
|
+
const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
|
|
835
|
+
err.retryable = true;
|
|
836
|
+
failAttempt(err);
|
|
837
|
+
}, timeoutMs);
|
|
838
|
+
}
|
|
748
839
|
if (this.token) {
|
|
749
840
|
this.socket = factory(url.toString());
|
|
750
841
|
}
|
|
@@ -765,6 +856,15 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
|
|
|
765
856
|
reason = StreamingErrorMessages[code];
|
|
766
857
|
}
|
|
767
858
|
}
|
|
859
|
+
// A close before `Begin` is a failed connection attempt — reject so
|
|
860
|
+
// connect() can retry (or surface a permanent failure).
|
|
861
|
+
if (!settled) {
|
|
862
|
+
const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
|
|
863
|
+
err.code = code;
|
|
864
|
+
err.retryable = isRetryableCloseCode(code);
|
|
865
|
+
failAttempt(err);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
768
868
|
// Stop the flush timer when the socket is gone (server-initiated close,
|
|
769
869
|
// network drop, etc.) — otherwise subsequent ticks call send() on a
|
|
770
870
|
// closed socket and spam the error listener.
|
|
@@ -775,25 +875,37 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
|
|
|
775
875
|
this.listeners.close?.(code, reason);
|
|
776
876
|
};
|
|
777
877
|
this.socket.onerror = (event) => {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
878
|
+
const error = event.error ?? new Error(event.message);
|
|
879
|
+
// A socket error before `Begin` is a failed attempt → reject/retry.
|
|
880
|
+
if (!settled) {
|
|
881
|
+
error.retryable = true;
|
|
882
|
+
failAttempt(error);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
this.listeners.error?.(error);
|
|
782
886
|
};
|
|
783
887
|
this.socket.onmessage = ({ data }) => {
|
|
784
888
|
const message = JSON.parse(data.toString());
|
|
785
889
|
if ("error" in message) {
|
|
786
890
|
const err = new StreamingError(message.error);
|
|
787
891
|
if ("error_code" in message) {
|
|
788
|
-
err.code =
|
|
789
|
-
|
|
892
|
+
err.code = message.error_code;
|
|
893
|
+
}
|
|
894
|
+
// A server error frame before `Begin` fails the attempt; the code
|
|
895
|
+
// decides whether a retry is worthwhile.
|
|
896
|
+
if (!settled) {
|
|
897
|
+
const attemptErr = err;
|
|
898
|
+
attemptErr.retryable =
|
|
899
|
+
err.code === undefined ? true : isRetryableCloseCode(err.code);
|
|
900
|
+
failAttempt(attemptErr);
|
|
901
|
+
return;
|
|
790
902
|
}
|
|
791
903
|
this.listeners.error?.(err);
|
|
792
904
|
return;
|
|
793
905
|
}
|
|
794
906
|
switch (message.type) {
|
|
795
907
|
case "Begin": {
|
|
796
|
-
|
|
908
|
+
succeed(message);
|
|
797
909
|
this.listeners.open?.(message);
|
|
798
910
|
break;
|
|
799
911
|
}
|
|
@@ -840,6 +952,20 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
|
|
|
840
952
|
};
|
|
841
953
|
});
|
|
842
954
|
}
|
|
955
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
956
|
+
discardPendingSocket() {
|
|
957
|
+
if (!this.socket)
|
|
958
|
+
return;
|
|
959
|
+
try {
|
|
960
|
+
if (this.socket.removeAllListeners)
|
|
961
|
+
this.socket.removeAllListeners();
|
|
962
|
+
this.socket.close();
|
|
963
|
+
}
|
|
964
|
+
catch {
|
|
965
|
+
// Best-effort cleanup; a half-open socket may throw on close.
|
|
966
|
+
}
|
|
967
|
+
this.socket = undefined;
|
|
968
|
+
}
|
|
843
969
|
/**
|
|
844
970
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
845
971
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -1113,6 +1239,16 @@ Learn more at https://github.com/AssemblyAI/assemblyai-node-sdk/blob/main/docs/c
|
|
|
1113
1239
|
};
|
|
1114
1240
|
this.send(JSON.stringify(message));
|
|
1115
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
1244
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
1245
|
+
*/
|
|
1246
|
+
keepAlive() {
|
|
1247
|
+
const message = {
|
|
1248
|
+
type: "KeepAlive",
|
|
1249
|
+
};
|
|
1250
|
+
this.send(JSON.stringify(message));
|
|
1251
|
+
}
|
|
1116
1252
|
send(data) {
|
|
1117
1253
|
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
|
|
1118
1254
|
throw new Error("Socket is not open for communication");
|
|
@@ -1168,7 +1304,7 @@ if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
|
1168
1304
|
defaultUserAgentString += navigator.userAgent;
|
|
1169
1305
|
}
|
|
1170
1306
|
const defaultUserAgent = {
|
|
1171
|
-
sdk: { name: "JavaScript", version: "4.
|
|
1307
|
+
sdk: { name: "JavaScript", version: "4.35.3" },
|
|
1172
1308
|
};
|
|
1173
1309
|
if (typeof process !== "undefined") {
|
|
1174
1310
|
if (process.versions.node && defaultUserAgentString.indexOf("Node") === -1) {
|
package/dist/streaming.cjs
CHANGED
|
@@ -579,6 +579,24 @@ function toInt16View(audio) {
|
|
|
579
579
|
}
|
|
580
580
|
const defaultStreamingUrl = "wss://streaming.assemblyai.com/v3/ws";
|
|
581
581
|
const terminateSessionMessage = `{"type":"Terminate"}`;
|
|
582
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1000;
|
|
583
|
+
const DEFAULT_MAX_CONNECTION_RETRIES = 2;
|
|
584
|
+
const DEFAULT_CONNECTION_RETRY_DELAY_MS = 500;
|
|
585
|
+
/**
|
|
586
|
+
* Close/error codes that signal a permanent client-side problem (auth,
|
|
587
|
+
* billing, malformed config). A retry would hit the same failure, so the
|
|
588
|
+
* connection is never retried on these.
|
|
589
|
+
*/
|
|
590
|
+
const NON_RETRYABLE_CLOSE_CODES = new Set([
|
|
591
|
+
StreamingErrorType.BadSampleRate,
|
|
592
|
+
StreamingErrorType.AuthFailed,
|
|
593
|
+
StreamingErrorType.InsufficientFunds,
|
|
594
|
+
StreamingErrorType.FreeTierUser,
|
|
595
|
+
StreamingErrorType.BadSchema,
|
|
596
|
+
]);
|
|
597
|
+
function isRetryableCloseCode(code) {
|
|
598
|
+
return code !== 1000 && !NON_RETRYABLE_CLOSE_CODES.has(code);
|
|
599
|
+
}
|
|
582
600
|
/**
|
|
583
601
|
* Per-send chunk cap in milliseconds for the dual-channel mixer. The streaming
|
|
584
602
|
* server rejects audio messages longer than 1000 ms (`Input Duration Error`).
|
|
@@ -710,8 +728,12 @@ class StreamingTranscriber {
|
|
|
710
728
|
searchParams.set("speech_model", this.params.speechModel.toString());
|
|
711
729
|
}
|
|
712
730
|
if (this.params.languageCode !== undefined) {
|
|
731
|
+
console.warn("[Deprecation Warning] `languageCode` is deprecated and will be removed in a future release. Please use `languageCodes` instead.");
|
|
713
732
|
searchParams.set("language_code", this.params.languageCode);
|
|
714
733
|
}
|
|
734
|
+
if (this.params.languageCodes !== undefined) {
|
|
735
|
+
searchParams.set("language_codes", JSON.stringify(this.params.languageCodes));
|
|
736
|
+
}
|
|
715
737
|
if (this.params.languageDetection !== undefined) {
|
|
716
738
|
searchParams.set("language_detection", this.params.languageDetection.toString());
|
|
717
739
|
}
|
|
@@ -785,12 +807,85 @@ class StreamingTranscriber {
|
|
|
785
807
|
on(event, listener) {
|
|
786
808
|
this.listeners[event] = listener;
|
|
787
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Open the streaming session.
|
|
812
|
+
*
|
|
813
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
814
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
815
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
816
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
817
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
818
|
+
* funds, malformed config) are not retried.
|
|
819
|
+
*
|
|
820
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
821
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
822
|
+
* the retry loop) to observe the failure.
|
|
823
|
+
*/
|
|
788
824
|
connect() {
|
|
789
|
-
return
|
|
825
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
826
|
+
var _a, _b;
|
|
790
827
|
if (this.socket) {
|
|
791
828
|
throw new Error("Already connected");
|
|
792
829
|
}
|
|
830
|
+
const maxRetries = (_a = this.params.maxConnectionRetries) !== null && _a !== void 0 ? _a : DEFAULT_MAX_CONNECTION_RETRIES;
|
|
831
|
+
const retryDelay = (_b = this.params.connectionRetryDelay) !== null && _b !== void 0 ? _b : DEFAULT_CONNECTION_RETRY_DELAY_MS;
|
|
832
|
+
let lastError;
|
|
833
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
834
|
+
try {
|
|
835
|
+
return yield this.connectOnce();
|
|
836
|
+
}
|
|
837
|
+
catch (err) {
|
|
838
|
+
lastError = err;
|
|
839
|
+
const retryable = err.retryable === true;
|
|
840
|
+
if (!retryable || attempt === maxRetries) {
|
|
841
|
+
throw err;
|
|
842
|
+
}
|
|
843
|
+
console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
|
|
844
|
+
if (retryDelay > 0) {
|
|
845
|
+
yield new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// The loop above always returns or throws; this only satisfies the type
|
|
850
|
+
// checker that a value is produced on every path.
|
|
851
|
+
throw lastError !== null && lastError !== void 0 ? lastError : new Error("Failed to connect to streaming server");
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
connectOnce() {
|
|
855
|
+
return new Promise((resolve, reject) => {
|
|
856
|
+
var _a;
|
|
793
857
|
const url = this.connectionUrl();
|
|
858
|
+
const timeoutMs = (_a = this.params.connectTimeout) !== null && _a !== void 0 ? _a : DEFAULT_CONNECT_TIMEOUT_MS;
|
|
859
|
+
// `settled` flips once this attempt has resolved (`Begin`) or rejected
|
|
860
|
+
// (timeout / pre-`Begin` close / error). Before it flips the socket
|
|
861
|
+
// handlers drive this promise; after it flips they revert to normal
|
|
862
|
+
// runtime dispatch (close / error / message listeners).
|
|
863
|
+
let settled = false;
|
|
864
|
+
let timer;
|
|
865
|
+
const failAttempt = (error) => {
|
|
866
|
+
if (settled)
|
|
867
|
+
return;
|
|
868
|
+
settled = true;
|
|
869
|
+
if (timer)
|
|
870
|
+
clearTimeout(timer);
|
|
871
|
+
this.discardPendingSocket();
|
|
872
|
+
reject(error);
|
|
873
|
+
};
|
|
874
|
+
const succeed = (begin) => {
|
|
875
|
+
if (settled)
|
|
876
|
+
return;
|
|
877
|
+
settled = true;
|
|
878
|
+
if (timer)
|
|
879
|
+
clearTimeout(timer);
|
|
880
|
+
resolve(begin);
|
|
881
|
+
};
|
|
882
|
+
if (timeoutMs > 0) {
|
|
883
|
+
timer = setTimeout(() => {
|
|
884
|
+
const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
|
|
885
|
+
err.retryable = true;
|
|
886
|
+
failAttempt(err);
|
|
887
|
+
}, timeoutMs);
|
|
888
|
+
}
|
|
794
889
|
if (this.token) {
|
|
795
890
|
this.socket = factory(url.toString());
|
|
796
891
|
}
|
|
@@ -808,6 +903,15 @@ class StreamingTranscriber {
|
|
|
808
903
|
reason = StreamingErrorMessages[code];
|
|
809
904
|
}
|
|
810
905
|
}
|
|
906
|
+
// A close before `Begin` is a failed connection attempt — reject so
|
|
907
|
+
// connect() can retry (or surface a permanent failure).
|
|
908
|
+
if (!settled) {
|
|
909
|
+
const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
|
|
910
|
+
err.code = code;
|
|
911
|
+
err.retryable = isRetryableCloseCode(code);
|
|
912
|
+
failAttempt(err);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
811
915
|
// Stop the flush timer when the socket is gone (server-initiated close,
|
|
812
916
|
// network drop, etc.) — otherwise subsequent ticks call send() on a
|
|
813
917
|
// closed socket and spam the error listener.
|
|
@@ -818,11 +922,15 @@ class StreamingTranscriber {
|
|
|
818
922
|
(_b = (_a = this.listeners).close) === null || _b === void 0 ? void 0 : _b.call(_a, code, reason);
|
|
819
923
|
};
|
|
820
924
|
this.socket.onerror = (event) => {
|
|
821
|
-
var _a, _b, _c
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
925
|
+
var _a, _b, _c;
|
|
926
|
+
const error = (_a = event.error) !== null && _a !== void 0 ? _a : new Error(event.message);
|
|
927
|
+
// A socket error before `Begin` is a failed attempt → reject/retry.
|
|
928
|
+
if (!settled) {
|
|
929
|
+
error.retryable = true;
|
|
930
|
+
failAttempt(error);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
(_c = (_b = this.listeners).error) === null || _c === void 0 ? void 0 : _c.call(_b, error);
|
|
826
934
|
};
|
|
827
935
|
this.socket.onmessage = ({ data }) => {
|
|
828
936
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
@@ -830,15 +938,23 @@ class StreamingTranscriber {
|
|
|
830
938
|
if ("error" in message) {
|
|
831
939
|
const err = new StreamingError(message.error);
|
|
832
940
|
if ("error_code" in message) {
|
|
833
|
-
err.code =
|
|
834
|
-
|
|
941
|
+
err.code = message.error_code;
|
|
942
|
+
}
|
|
943
|
+
// A server error frame before `Begin` fails the attempt; the code
|
|
944
|
+
// decides whether a retry is worthwhile.
|
|
945
|
+
if (!settled) {
|
|
946
|
+
const attemptErr = err;
|
|
947
|
+
attemptErr.retryable =
|
|
948
|
+
err.code === undefined ? true : isRetryableCloseCode(err.code);
|
|
949
|
+
failAttempt(attemptErr);
|
|
950
|
+
return;
|
|
835
951
|
}
|
|
836
952
|
(_b = (_a = this.listeners).error) === null || _b === void 0 ? void 0 : _b.call(_a, err);
|
|
837
953
|
return;
|
|
838
954
|
}
|
|
839
955
|
switch (message.type) {
|
|
840
956
|
case "Begin": {
|
|
841
|
-
|
|
957
|
+
succeed(message);
|
|
842
958
|
(_d = (_c = this.listeners).open) === null || _d === void 0 ? void 0 : _d.call(_c, message);
|
|
843
959
|
break;
|
|
844
960
|
}
|
|
@@ -885,6 +1001,20 @@ class StreamingTranscriber {
|
|
|
885
1001
|
};
|
|
886
1002
|
});
|
|
887
1003
|
}
|
|
1004
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
1005
|
+
discardPendingSocket() {
|
|
1006
|
+
if (!this.socket)
|
|
1007
|
+
return;
|
|
1008
|
+
try {
|
|
1009
|
+
if (this.socket.removeAllListeners)
|
|
1010
|
+
this.socket.removeAllListeners();
|
|
1011
|
+
this.socket.close();
|
|
1012
|
+
}
|
|
1013
|
+
catch (_a) {
|
|
1014
|
+
// Best-effort cleanup; a half-open socket may throw on close.
|
|
1015
|
+
}
|
|
1016
|
+
this.socket = undefined;
|
|
1017
|
+
}
|
|
888
1018
|
/**
|
|
889
1019
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
890
1020
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -1158,6 +1288,16 @@ class StreamingTranscriber {
|
|
|
1158
1288
|
};
|
|
1159
1289
|
this.send(JSON.stringify(message));
|
|
1160
1290
|
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
1293
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
1294
|
+
*/
|
|
1295
|
+
keepAlive() {
|
|
1296
|
+
const message = {
|
|
1297
|
+
type: "KeepAlive",
|
|
1298
|
+
};
|
|
1299
|
+
this.send(JSON.stringify(message));
|
|
1300
|
+
}
|
|
1161
1301
|
send(data) {
|
|
1162
1302
|
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
|
|
1163
1303
|
throw new Error("Socket is not open for communication");
|
package/dist/streaming.mjs
CHANGED
|
@@ -577,6 +577,24 @@ function toInt16View(audio) {
|
|
|
577
577
|
}
|
|
578
578
|
const defaultStreamingUrl = "wss://streaming.assemblyai.com/v3/ws";
|
|
579
579
|
const terminateSessionMessage = `{"type":"Terminate"}`;
|
|
580
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1000;
|
|
581
|
+
const DEFAULT_MAX_CONNECTION_RETRIES = 2;
|
|
582
|
+
const DEFAULT_CONNECTION_RETRY_DELAY_MS = 500;
|
|
583
|
+
/**
|
|
584
|
+
* Close/error codes that signal a permanent client-side problem (auth,
|
|
585
|
+
* billing, malformed config). A retry would hit the same failure, so the
|
|
586
|
+
* connection is never retried on these.
|
|
587
|
+
*/
|
|
588
|
+
const NON_RETRYABLE_CLOSE_CODES = new Set([
|
|
589
|
+
StreamingErrorType.BadSampleRate,
|
|
590
|
+
StreamingErrorType.AuthFailed,
|
|
591
|
+
StreamingErrorType.InsufficientFunds,
|
|
592
|
+
StreamingErrorType.FreeTierUser,
|
|
593
|
+
StreamingErrorType.BadSchema,
|
|
594
|
+
]);
|
|
595
|
+
function isRetryableCloseCode(code) {
|
|
596
|
+
return code !== 1000 && !NON_RETRYABLE_CLOSE_CODES.has(code);
|
|
597
|
+
}
|
|
580
598
|
/**
|
|
581
599
|
* Per-send chunk cap in milliseconds for the dual-channel mixer. The streaming
|
|
582
600
|
* server rejects audio messages longer than 1000 ms (`Input Duration Error`).
|
|
@@ -708,8 +726,12 @@ class StreamingTranscriber {
|
|
|
708
726
|
searchParams.set("speech_model", this.params.speechModel.toString());
|
|
709
727
|
}
|
|
710
728
|
if (this.params.languageCode !== undefined) {
|
|
729
|
+
console.warn("[Deprecation Warning] `languageCode` is deprecated and will be removed in a future release. Please use `languageCodes` instead.");
|
|
711
730
|
searchParams.set("language_code", this.params.languageCode);
|
|
712
731
|
}
|
|
732
|
+
if (this.params.languageCodes !== undefined) {
|
|
733
|
+
searchParams.set("language_codes", JSON.stringify(this.params.languageCodes));
|
|
734
|
+
}
|
|
713
735
|
if (this.params.languageDetection !== undefined) {
|
|
714
736
|
searchParams.set("language_detection", this.params.languageDetection.toString());
|
|
715
737
|
}
|
|
@@ -783,12 +805,85 @@ class StreamingTranscriber {
|
|
|
783
805
|
on(event, listener) {
|
|
784
806
|
this.listeners[event] = listener;
|
|
785
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* Open the streaming session.
|
|
810
|
+
*
|
|
811
|
+
* Resolves with the server's `Begin` event once the handshake completes. A
|
|
812
|
+
* single attempt is bounded by `connectTimeout` (default 1000ms); transient
|
|
813
|
+
* failures (timeout, network drop, unexpected close) are retried up to
|
|
814
|
+
* `maxConnectionRetries` times (default 2), waiting `connectionRetryDelay`
|
|
815
|
+
* (default 500ms) between attempts. Permanent failures (auth, insufficient
|
|
816
|
+
* funds, malformed config) are not retried.
|
|
817
|
+
*
|
|
818
|
+
* Unlike previously, a failed connection now rejects this promise rather
|
|
819
|
+
* than only invoking the `error` listener — necessary for the caller (and
|
|
820
|
+
* the retry loop) to observe the failure.
|
|
821
|
+
*/
|
|
786
822
|
connect() {
|
|
787
|
-
return
|
|
823
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
824
|
+
var _a, _b;
|
|
788
825
|
if (this.socket) {
|
|
789
826
|
throw new Error("Already connected");
|
|
790
827
|
}
|
|
828
|
+
const maxRetries = (_a = this.params.maxConnectionRetries) !== null && _a !== void 0 ? _a : DEFAULT_MAX_CONNECTION_RETRIES;
|
|
829
|
+
const retryDelay = (_b = this.params.connectionRetryDelay) !== null && _b !== void 0 ? _b : DEFAULT_CONNECTION_RETRY_DELAY_MS;
|
|
830
|
+
let lastError;
|
|
831
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
832
|
+
try {
|
|
833
|
+
return yield this.connectOnce();
|
|
834
|
+
}
|
|
835
|
+
catch (err) {
|
|
836
|
+
lastError = err;
|
|
837
|
+
const retryable = err.retryable === true;
|
|
838
|
+
if (!retryable || attempt === maxRetries) {
|
|
839
|
+
throw err;
|
|
840
|
+
}
|
|
841
|
+
console.warn(`Streaming connect attempt ${attempt + 1}/${maxRetries + 1} failed (${err.message}); retrying`);
|
|
842
|
+
if (retryDelay > 0) {
|
|
843
|
+
yield new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
// The loop above always returns or throws; this only satisfies the type
|
|
848
|
+
// checker that a value is produced on every path.
|
|
849
|
+
throw lastError !== null && lastError !== void 0 ? lastError : new Error("Failed to connect to streaming server");
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
connectOnce() {
|
|
853
|
+
return new Promise((resolve, reject) => {
|
|
854
|
+
var _a;
|
|
791
855
|
const url = this.connectionUrl();
|
|
856
|
+
const timeoutMs = (_a = this.params.connectTimeout) !== null && _a !== void 0 ? _a : DEFAULT_CONNECT_TIMEOUT_MS;
|
|
857
|
+
// `settled` flips once this attempt has resolved (`Begin`) or rejected
|
|
858
|
+
// (timeout / pre-`Begin` close / error). Before it flips the socket
|
|
859
|
+
// handlers drive this promise; after it flips they revert to normal
|
|
860
|
+
// runtime dispatch (close / error / message listeners).
|
|
861
|
+
let settled = false;
|
|
862
|
+
let timer;
|
|
863
|
+
const failAttempt = (error) => {
|
|
864
|
+
if (settled)
|
|
865
|
+
return;
|
|
866
|
+
settled = true;
|
|
867
|
+
if (timer)
|
|
868
|
+
clearTimeout(timer);
|
|
869
|
+
this.discardPendingSocket();
|
|
870
|
+
reject(error);
|
|
871
|
+
};
|
|
872
|
+
const succeed = (begin) => {
|
|
873
|
+
if (settled)
|
|
874
|
+
return;
|
|
875
|
+
settled = true;
|
|
876
|
+
if (timer)
|
|
877
|
+
clearTimeout(timer);
|
|
878
|
+
resolve(begin);
|
|
879
|
+
};
|
|
880
|
+
if (timeoutMs > 0) {
|
|
881
|
+
timer = setTimeout(() => {
|
|
882
|
+
const err = new StreamingError(`Streaming connection timed out after ${timeoutMs}ms`);
|
|
883
|
+
err.retryable = true;
|
|
884
|
+
failAttempt(err);
|
|
885
|
+
}, timeoutMs);
|
|
886
|
+
}
|
|
792
887
|
if (this.token) {
|
|
793
888
|
this.socket = factory(url.toString());
|
|
794
889
|
}
|
|
@@ -806,6 +901,15 @@ class StreamingTranscriber {
|
|
|
806
901
|
reason = StreamingErrorMessages[code];
|
|
807
902
|
}
|
|
808
903
|
}
|
|
904
|
+
// A close before `Begin` is a failed connection attempt — reject so
|
|
905
|
+
// connect() can retry (or surface a permanent failure).
|
|
906
|
+
if (!settled) {
|
|
907
|
+
const err = new StreamingError(reason || `Streaming connection closed (code=${code})`);
|
|
908
|
+
err.code = code;
|
|
909
|
+
err.retryable = isRetryableCloseCode(code);
|
|
910
|
+
failAttempt(err);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
809
913
|
// Stop the flush timer when the socket is gone (server-initiated close,
|
|
810
914
|
// network drop, etc.) — otherwise subsequent ticks call send() on a
|
|
811
915
|
// closed socket and spam the error listener.
|
|
@@ -816,11 +920,15 @@ class StreamingTranscriber {
|
|
|
816
920
|
(_b = (_a = this.listeners).close) === null || _b === void 0 ? void 0 : _b.call(_a, code, reason);
|
|
817
921
|
};
|
|
818
922
|
this.socket.onerror = (event) => {
|
|
819
|
-
var _a, _b, _c
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
923
|
+
var _a, _b, _c;
|
|
924
|
+
const error = (_a = event.error) !== null && _a !== void 0 ? _a : new Error(event.message);
|
|
925
|
+
// A socket error before `Begin` is a failed attempt → reject/retry.
|
|
926
|
+
if (!settled) {
|
|
927
|
+
error.retryable = true;
|
|
928
|
+
failAttempt(error);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
(_c = (_b = this.listeners).error) === null || _c === void 0 ? void 0 : _c.call(_b, error);
|
|
824
932
|
};
|
|
825
933
|
this.socket.onmessage = ({ data }) => {
|
|
826
934
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
@@ -828,15 +936,23 @@ class StreamingTranscriber {
|
|
|
828
936
|
if ("error" in message) {
|
|
829
937
|
const err = new StreamingError(message.error);
|
|
830
938
|
if ("error_code" in message) {
|
|
831
|
-
err.code =
|
|
832
|
-
|
|
939
|
+
err.code = message.error_code;
|
|
940
|
+
}
|
|
941
|
+
// A server error frame before `Begin` fails the attempt; the code
|
|
942
|
+
// decides whether a retry is worthwhile.
|
|
943
|
+
if (!settled) {
|
|
944
|
+
const attemptErr = err;
|
|
945
|
+
attemptErr.retryable =
|
|
946
|
+
err.code === undefined ? true : isRetryableCloseCode(err.code);
|
|
947
|
+
failAttempt(attemptErr);
|
|
948
|
+
return;
|
|
833
949
|
}
|
|
834
950
|
(_b = (_a = this.listeners).error) === null || _b === void 0 ? void 0 : _b.call(_a, err);
|
|
835
951
|
return;
|
|
836
952
|
}
|
|
837
953
|
switch (message.type) {
|
|
838
954
|
case "Begin": {
|
|
839
|
-
|
|
955
|
+
succeed(message);
|
|
840
956
|
(_d = (_c = this.listeners).open) === null || _d === void 0 ? void 0 : _d.call(_c, message);
|
|
841
957
|
break;
|
|
842
958
|
}
|
|
@@ -883,6 +999,20 @@ class StreamingTranscriber {
|
|
|
883
999
|
};
|
|
884
1000
|
});
|
|
885
1001
|
}
|
|
1002
|
+
/** Tear down a half-open socket from a failed connection attempt. */
|
|
1003
|
+
discardPendingSocket() {
|
|
1004
|
+
if (!this.socket)
|
|
1005
|
+
return;
|
|
1006
|
+
try {
|
|
1007
|
+
if (this.socket.removeAllListeners)
|
|
1008
|
+
this.socket.removeAllListeners();
|
|
1009
|
+
this.socket.close();
|
|
1010
|
+
}
|
|
1011
|
+
catch (_a) {
|
|
1012
|
+
// Best-effort cleanup; a half-open socket may throw on close.
|
|
1013
|
+
}
|
|
1014
|
+
this.socket = undefined;
|
|
1015
|
+
}
|
|
886
1016
|
/**
|
|
887
1017
|
* Returns a WritableStream that pumps PCM chunks into `sendAudio`. Single-channel
|
|
888
1018
|
* only — in dual-channel mode use `sendAudio(pcm, { channel })` directly, since
|
|
@@ -1156,6 +1286,16 @@ class StreamingTranscriber {
|
|
|
1156
1286
|
};
|
|
1157
1287
|
this.send(JSON.stringify(message));
|
|
1158
1288
|
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Reset the server's inactivity timer. Only needed when the session was
|
|
1291
|
+
* created with `inactivityTimeout` and no audio is being sent.
|
|
1292
|
+
*/
|
|
1293
|
+
keepAlive() {
|
|
1294
|
+
const message = {
|
|
1295
|
+
type: "KeepAlive",
|
|
1296
|
+
};
|
|
1297
|
+
this.send(JSON.stringify(message));
|
|
1298
|
+
}
|
|
1159
1299
|
send(data) {
|
|
1160
1300
|
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
|
|
1161
1301
|
throw new Error("Socket is not open for communication");
|