@speechmatics/real-time-client
Version:
Client for the Speechmatics real-time API
243 lines (239 loc) • 7.36 kB
JavaScript
import { TypedEventTarget } from 'typescript-event-target';
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
class SocketStateChangeEvent extends Event {
constructor(socketState) {
super("socketStateChange");
this.socketState = socketState;
}
}
class ReceiveMessageEvent extends Event {
constructor(data) {
super("receiveMessage");
this.data = data;
}
}
class SendMessageEvent extends Event {
constructor(data) {
super("sendMessage");
this.data = data;
}
}
class RealtimeClient extends TypedEventTarget {
constructor(config = {}) {
super();
__publicField(this, "url");
__publicField(this, "appId");
__publicField(this, "enableLegacy");
__publicField(this, "timeout");
__publicField(this, "socket");
// Track the last AudioAdded sequence number, used when stopping transcription to avoid missing audio
// https://docs.speechmatics.com/rt-api-ref#audioadded
__publicField(this, "lastAudioAddedSeqNo", 0);
this.url = config.url ?? "wss://eu2.rt.speechmatics.com/v2";
this.appId = config.appId;
this.enableLegacy = config.enableLegacy ?? false;
this.timeout = config.connectionTimeout ?? 1e4;
}
get socketState() {
if (!this.socket) return void 0;
return {
[WebSocket.CONNECTING]: "connecting",
[WebSocket.OPEN]: "open",
[WebSocket.CLOSING]: "closing",
[WebSocket.CLOSED]: "closed"
}[this.socket.readyState];
}
async connect(jwt) {
return new Promise((resolve, reject) => {
const url = new URL(this.url);
url.searchParams.append("jwt", jwt);
if (this.appId) {
url.searchParams.append("sm-app", this.appId);
}
if (this.enableLegacy) {
url.searchParams.append("sm-enable-legacy-rt", "true");
}
this.socket = new WebSocket(url.toString());
this.dispatchTypedEvent(
"socketStateChange",
new SocketStateChangeEvent(this.socketState)
);
this.socket.addEventListener(
"open",
() => {
resolve();
},
{ once: true }
);
this.socket.addEventListener("error", (error) => {
this.dispatchTypedEvent(
"socketStateChange",
new SocketStateChangeEvent(this.socketState)
);
reject(error);
});
this.socket.addEventListener("close", () => {
this.dispatchTypedEvent(
"socketStateChange",
new SocketStateChangeEvent(this.socketState)
);
});
this.socket.addEventListener("message", (socketMessage) => {
const data = JSON.parse(socketMessage.data);
if (!dataIsRealtimeTranscriptionMessage(data)) {
console.warn(
"message does not look like a valid message: ",
JSON.stringify(data)
);
return;
}
if (data.message === "AudioAdded") {
this.lastAudioAddedSeqNo = data.seq_no;
}
this.dispatchTypedEvent(
"receiveMessage",
new ReceiveMessageEvent(data)
);
});
});
}
sendMessage(message) {
if (!this.socket) {
throw new SpeechmaticsRealtimeError("Client socket not initialized");
}
this.socket.send(JSON.stringify(message));
this.dispatchTypedEvent("sendMessage", new SendMessageEvent(message));
}
sendAudio(data) {
if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
throw new SpeechmaticsRealtimeError("Socket not ready to receive audio");
}
this.socket.send(data);
}
async getSpeakers(options = {}) {
this.sendMessage({
message: "GetSpeakers",
final: options.final
});
const waitForSpeakers = new Promise((resolve, reject) => {
this.addEventListener("receiveMessage", ({ data }) => {
if (data.message === "SpeakersResult") {
resolve(data);
} else if (data.message === "Error") {
reject(new Error(data.type));
}
});
this.addEventListener("socketStateChange", (state) => {
state.socketState === "closed" && reject(new Error("Socket closed"));
});
});
if (options.timeout) {
return Promise.race([
waitForSpeakers,
rejectAfter(options.timeout, "SpeakersResult")
]);
}
return waitForSpeakers;
}
async start(jwt, config) {
await this.connect(jwt);
const waitForRecognitionStarted = new Promise(
(resolve, reject) => {
this.addEventListener("receiveMessage", ({ data }) => {
if (data.message === "RecognitionStarted") {
resolve(data);
} else if (data.message === "Error") {
reject(new Error(data.type));
}
});
const startRecognitionMessage = {
audio_format: defaultAudioFormat,
...config,
message: "StartRecognition"
};
this.sendMessage(startRecognitionMessage);
}
);
return Promise.race([
waitForRecognitionStarted,
rejectAfter(this.timeout, "RecognitionStarted")
]);
}
/** Sends an `"EndOfStream"` message, resolving if acknowledged by an `"EndOfTranscript"` from server, rejecting if not received */
async stopRecognition({ noTimeout } = {}) {
const waitForEndOfTranscript = new Promise((resolve) => {
this.addEventListener("receiveMessage", ({ data }) => {
if (data.message === "EndOfTranscript") {
this.socket?.close();
resolve();
}
});
this.sendMessage({
message: "EndOfStream",
last_seq_no: this.lastAudioAddedSeqNo
});
});
if (noTimeout) {
return;
}
return Promise.race([
waitForEndOfTranscript,
rejectAfter(this.timeout, "EndOfTranscript")
]);
}
setRecognitionConfig(config) {
this.sendMessage({
message: "SetRecognitionConfig",
transcription_config: config
});
}
forceEndOfUtterance(channel) {
this.sendMessage({
message: "ForceEndOfUtterance",
channel
});
}
}
function dataIsRealtimeTranscriptionMessage(data) {
if (typeof data !== "object" || data === null) {
return false;
}
if (!("message" in data)) {
return false;
}
if (typeof data.message !== "string") {
return false;
}
return true;
}
const defaultAudioFormat = {
type: "file"
};
class SpeechmaticsRealtimeError extends Error {
constructor(message, options) {
super(message, options);
this.name = "SpeechmaticsRealtimeError";
}
}
function rejectAfter(timeoutMs, key) {
return new Promise((_, reject) => {
setTimeout(
() => reject(
new SpeechmaticsRealtimeError(
`Timed out after ${timeoutMs}ms waiting for ${key}`
)
),
timeoutMs
);
});
}
async function getFeatures(region = "eu2") {
const resp = await fetch(
`https://${region}.rt.speechmatics.com/v1/discovery/features`
);
return resp.json();
}
export { RealtimeClient, ReceiveMessageEvent, SendMessageEvent, SocketStateChangeEvent, SpeechmaticsRealtimeError, getFeatures };
//# sourceMappingURL=index.browser.js.map