@soniox/speech-to-text-web
Version:
Javascript client library for Soniox Speech-to-Text websocket API
223 lines (222 loc) • 10.1 kB
JavaScript
var l = Object.defineProperty;
var p = (o, e, t) => e in o ? l(o, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : o[e] = t;
var s = (o, e, t) => p(o, typeof e != "symbol" ? e + "" : e, t);
const b = ["Init", "Finished", "Error", "Canceled"], g = [
"RequestingMedia",
"OpeningWebSocket",
"Running",
"FinishingProcessing"
], k = ["OpeningWebSocket", "Running", "FinishingProcessing"];
function f(o) {
return b.includes(o);
}
function S(o) {
return g.includes(o);
}
function m(o) {
return k.includes(o);
}
const R = "wss://stt-rt.soniox.com/transcribe-websocket", _ = 1e3, w = 120, h = '{ "type": "finalize" }', v = () => ({
apiKey: "",
bufferQueueSize: _
}), E = {
echoCancellation: !1,
noiseSuppression: !1,
autoGainControl: !1,
channelCount: 1,
sampleRate: 44100
}, d = class d {
// Queued data (before websocket is opened)
/**
* RecordTranscribe connects to the Soniox Speech-to-Text API for real-time speech-to-text transcription.
* It provides a simple API for starting and stopping the transcription, as well as handling the transcription results.
*
* @example
* const recordTranscribe = new RecordTranscribe({
* apiKey: '<SONIOX_API_KEY>',
* onPartialResult: (result) => {
* console.log('partial result', result.text);
* },
* });
* recordTranscribe.start();
*/
constructor(e) {
s(this, "_state", "Init");
s(this, "_options");
s(this, "_audioOptions");
s(this, "_websocket");
s(this, "_mediaRecorder");
s(this, "_queuedMessages", []);
s(this, "_hasCallback", (e) => {
var t;
return this._options[e] != null || ((t = this._audioOptions) == null ? void 0 : t[e]) != null;
});
s(this, "_callback", (e, ...t) => {
var i, c, n, r;
(c = (i = this._options)[e]) == null || c.call(i, ...t), (r = (n = this._audioOptions) == null ? void 0 : n[e]) == null || r.call(n, ...t);
});
/**
* Start transcription. You can pass options to configure the transcription settings, source and callbacks.
*/
s(this, "start", async (e) => {
if (S(this._state))
throw new Error("RecordTranscribe is already active");
this._audioOptions = { ...e };
let t;
if (e.stream != null)
t = e.stream.clone();
else {
this._setState("RequestingMedia");
try {
t = await navigator.mediaDevices.getUserMedia({
audio: this._audioOptions.audioConstraints ? this._audioOptions.audioConstraints : E
});
} catch (i) {
this._onError("get_user_media_failed", i == null ? void 0 : i.toString());
}
}
if (t == null)
throw new Error("Failed to create stream");
this._mediaRecorder = new MediaRecorder(
t,
e.mediaRecorderOptions ? e.mediaRecorderOptions : {}
), this._queuedMessages = [], this._mediaRecorder.addEventListener("dataavailable", this._onMediaRecorderData), this._mediaRecorder.addEventListener("error", this._onMediaRecorderError), this._mediaRecorder.addEventListener("pause", this._onMediaRecorderPause), this._mediaRecorder.addEventListener("stop", this._onMediaRecorderStop), this._mediaRecorder.start(w), this._setState("OpeningWebSocket"), this._websocket = new WebSocket(this._options.webSocketUri ?? R), this._websocket.addEventListener("open", this._onWebSocketOpen), this._websocket.addEventListener("error", this._onWebSocketError), this._websocket.addEventListener("message", this._onWebSocketMessage);
});
/**
* Stop transcription. Stopping transcription will send stop signal to the API and wait for the final results to be received.
* Only after the final results are received, the transcription will be finished. If you want to cancel the transcription immediately,
* (for example, on component unmount), you should probably use the `cancel()` method instead.
*/
s(this, "stop", () => {
var e;
this._state == "RequestingMedia" || this._state == "OpeningWebSocket" ? (this._closeResources(), this._handleFinished()) : this._state == "Running" && (this._setState("FinishingProcessing"), this._closeSource(), (e = this._websocket) == null || e.send(""));
});
/**
* Cancel transcription. Cancelling transcription will stop the transcription immediately and close the resources.
* For user initiated cancellation, you should probably use the `stop()` method instead.
*/
s(this, "cancel", () => {
f(this._state) || (this._closeResources(), this._setState("Canceled"));
});
/**
* Trigger finalize. This will finalize all non-final tokens.
*/
s(this, "finalize", () => {
var e;
this._state == "RequestingMedia" || this._state == "OpeningWebSocket" ? this._queuedMessages.length < (this._options.bufferQueueSize ?? _) ? this._queuedMessages.push(h) : this._onError("queue_limit_exceeded", "Queue size exceeded before websocket connection was established.") : (this._state == "Running" || this._state == "FinishingProcessing") && ((e = this._websocket) == null || e.send(h));
});
// Media recorder events
s(this, "_onMediaRecorderData", async (e) => {
var t;
if (this._state === "OpeningWebSocket")
this._queuedMessages.length < (this._options.bufferQueueSize ?? _) ? this._queuedMessages.push(e.data) : this._onError("queue_limit_exceeded", "Queue size exceeded before websocket connection was established.");
else if (this._state === "Running") {
const i = await e.data.arrayBuffer();
(t = this._websocket) == null || t.send(i);
}
});
s(this, "_onMediaRecorderError", (e) => {
this._onError("media_recorder_error", e.error ?? "Unknown error");
});
s(this, "_onMediaRecorderPause", (e) => {
this.stop();
});
s(this, "_onMediaRecorderStop", (e) => {
this.stop();
});
// Websocket events
s(this, "_onWebSocketOpen", (e) => {
this._onWebSocketOpenAsync(e);
});
s(this, "_onWebSocketOpenAsync", async (e) => {
var n, r;
if (this._state !== "OpeningWebSocket" || this._audioOptions == null)
return;
const t = this._audioOptions;
let i;
if (typeof this._options.apiKey == "function")
try {
i = await this._options.apiKey();
} catch (a) {
this._onError("api_key_fetch_failed", a == null ? void 0 : a.toString());
return;
}
else
i = this._options.apiKey;
if (this._state !== "OpeningWebSocket")
return;
const c = {
api_key: i,
model: t.model,
audio_format: t.audioFormat ? t.audioFormat : "auto",
sample_rate: t.sampleRate,
num_channels: t.numChannels,
language_hints: t.languageHints,
context: t.context,
enable_speaker_diarization: t.enableSpeakerDiarization,
enable_language_identification: t.enableLanguageIdentification,
enable_endpoint_detection: t.enableEndpointDetection,
translation: t.translation,
max_non_final_tokens_duration_ms: t.maxNonFinalTokensDurationMs,
client_reference_id: t.clientReferenceId
};
(n = this._websocket) == null || n.send(JSON.stringify(c));
for (const a of this._queuedMessages)
(r = this._websocket) == null || r.send(a);
this._queuedMessages = [], this._setState("Running"), this._callback("onStarted");
});
s(this, "_onWebSocketError", (e) => {
m(this._state) && this._onError("websocket_error", "WebSocket error occurred.");
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
s(this, "_onWebSocketMessage", (e) => {
if (this._state != "Running" && this._state != "FinishingProcessing" || this._audioOptions == null)
return;
const t = JSON.parse(e.data);
if (t.error_code != null || t.error_message != null) {
this._onError("api_error", t.error_message, t.error_code);
return;
}
this._callback("onPartialResult", t), t.finished && this._handleFinished();
});
s(this, "_onError", (e, t, i = void 0) => {
if (this._setState("Error"), this._closeResources(), this._hasCallback("onError"))
this._callback("onError", e, t ?? "Unknown error", i);
else
throw new Error(`RecordTranscribe error: ${e}: ${t ?? "Unknown error"}`);
});
s(this, "_closeSource", () => {
this._mediaRecorder != null && (this._mediaRecorder.removeEventListener("dataavailable", this._onMediaRecorderData), this._mediaRecorder.removeEventListener("error", this._onMediaRecorderError), this._mediaRecorder.removeEventListener("pause", this._onMediaRecorderPause), this._mediaRecorder.removeEventListener("stop", this._onMediaRecorderStop), this._mediaRecorder.stop(), this._mediaRecorder.stream.getTracks().forEach((e) => e.stop()), this._mediaRecorder = null);
});
s(this, "_closeResources", () => {
this._queuedMessages = [], this._websocket != null && (this._websocket.removeEventListener("open", this._onWebSocketOpen), this._websocket.removeEventListener("error", this._onWebSocketError), this._websocket.removeEventListener("message", this._onWebSocketMessage), this._websocket.close(), this._websocket = null), this._closeSource();
});
if (!d.isSupported)
throw "Soniox Speech-to-Text is not supported on this browser.";
this._options = {
...v(),
...e
}, this._audioOptions = null, this._websocket = null, this._mediaRecorder = null;
}
_setState(e) {
const t = this._state;
this._state = e, this._callback("onStateChange", {
oldState: t,
newState: e
});
}
get state() {
return this._state;
}
_handleFinished() {
this._closeResources(), this._setState("Finished"), this._callback("onFinished");
}
};
s(d, "isSupported", !!("WebSocket" in window && navigator.mediaDevices && navigator.mediaDevices.getUserMedia));
let u = d;
export {
u as RecordTranscribe,
S as isActiveState,
f as isInactiveState,
m as isWebSocketState
};