@soniox/speech-to-text-web
Version:
Javascript client library for Soniox Speech-to-Text websocket API
231 lines (230 loc) • 10.5 kB
JavaScript
var p = Object.defineProperty;
var b = (n, e, t) => e in n ? p(n, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : n[e] = t;
var s = (n, e, t) => b(n, typeof e != "symbol" ? e + "" : e, t);
const k = ["Init", "Finished", "Error", "Canceled"], g = [
"RequestingMedia",
"OpeningWebSocket",
"Running",
"FinishingProcessing"
], f = ["OpeningWebSocket", "Running", "FinishingProcessing"];
function S(n) {
return k.includes(n);
}
function v(n) {
return g.includes(n);
}
function R(n) {
return f.includes(n);
}
const m = "wss://stt-rt.soniox.com/transcribe-websocket", d = 1e3, w = 120, u = '{ "type": "finalize" }', E = '{ "type": "keepalive" }', M = () => ({
apiKey: "",
bufferQueueSize: d
}), O = {
echoCancellation: !1,
noiseSuppression: !1,
autoGainControl: !1,
channelCount: 1,
sampleRate: 44100
}, _ = class _ {
/**
* SonioxClient connects to the Soniox Speech-to-Text API for real-time speech-to-text transcription and translation.
* It provides a simple API for starting and stopping the transcription, as well as handling the transcription results.
*
* @example
* const sonioxClient = new SonioxClient({
* apiKey: '<SONIOX_API_KEY>',
* onPartialResult: (result) => {
* console.log('partial result', result.text);
* },
* });
* sonioxClient.start();
*/
constructor(e) {
s(this, "_state", "Init");
s(this, "_options");
s(this, "_audioOptions");
s(this, "_websocket");
s(this, "_mediaRecorder");
s(this, "_queuedMessages", []);
// Queued data (before websocket is opened)
s(this, "_keepAliveInterval", null);
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, o, a;
(c = (i = this._options)[e]) == null || c.call(i, ...t), (a = (o = this._audioOptions) == null ? void 0 : o[e]) == null || a.call(o, ...t);
});
/**
* Start transcription. You can pass options to configure the transcription settings, source and callbacks.
*/
s(this, "start", async (e) => {
if (v(this._state))
throw new Error("SonioxClient 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 : O
});
} 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 ?? m), 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", () => {
S(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 ?? d) ? this._queuedMessages.push(u) : 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(u));
});
// Media recorder events
s(this, "_onMediaRecorderData", async (e) => {
var t;
if (this._state === "OpeningWebSocket")
this._queuedMessages.length < (this._options.bufferQueueSize ?? d) ? 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 o, a;
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 (r) {
this._onError("api_key_fetch_failed", r == null ? void 0 : r.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,
client_reference_id: t.clientReferenceId
};
(o = this._websocket) == null || o.send(JSON.stringify(c));
for (const r of this._queuedMessages)
(a = this._websocket) == null || a.send(r);
if (this._queuedMessages = [], this._setState("Running"), this._callback("onStarted"), this._options.keepAlive) {
const r = this._options.keepAliveInterval ?? 5e3;
this._keepAliveInterval = setInterval(() => {
var l;
(this._state === "Running" || this._state === "FinishingProcessing") && ((l = this._websocket) == null || l.send(E));
}, r);
}
});
s(this, "_onWebSocketError", (e) => {
R(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(`SonioxClient 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._keepAliveInterval != null && (clearInterval(this._keepAliveInterval), this._keepAliveInterval = null), 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 (!_.isSupported)
throw "Soniox Speech-to-Text is not supported on this browser.";
this._options = {
...M(),
...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(_, "isSupported", !!("WebSocket" in window && navigator.mediaDevices && navigator.mediaDevices.getUserMedia));
let h = _;
const y = h;
export {
y as RecordTranscribe,
h as SonioxClient,
v as isActiveState,
S as isInactiveState,
R as isWebSocketState
};