UNPKG

@micdrop/gladia

Version:

Gladia implementation for @micdrop/server

225 lines (223 loc) 8.21 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { GladiaSTT: () => GladiaSTT }); module.exports = __toCommonJS(index_exports); // src/GladiaSTT.ts var import_server = require("@micdrop/server"); var import_ws = __toESM(require("ws")); var SAMPLE_RATE = 16e3; var BIT_DEPTH = 16; var DEFAULT_CONNECTION_TIMEOUT = 5e3; var DEFAULT_TRANSCRIPTION_TIMEOUT = 4e3; var DEFAULT_RETRY_DELAY = 1e3; var DEFAULT_MAX_RETRY = 3; var GladiaSTT = class extends import_server.STT { constructor(options) { super(); this.options = options; this.audioChunksPending = []; // Store audio chunks to send them again if reconnecting this.retryCount = 0; // Register real-time transcription to get a WebSocket URL this.getURL = async () => { const response = await fetch("https://api.gladia.io/v2/live", { method: "POST", headers: { "Content-Type": "application/json", "X-Gladia-Key": this.options.apiKey }, body: JSON.stringify({ encoding: "wav/pcm", sample_rate: SAMPLE_RATE, bit_depth: BIT_DEPTH, channels: 1, messages_config: { receive_final_transcripts: true, receive_speech_events: false, receive_pre_processing_events: false, receive_realtime_processing_events: false, receive_post_processing_events: false, receive_acknowledgments: false, receive_errors: true, receive_lifecycle_events: false }, ...this.options.settings }) }); if (!response.ok) { const status = response.status; if (status >= 400 && status < 500) { throw new Error(`${status}: ${response.statusText}`); } this.log("Error getting URL, retrying...", { status, text: response.statusText }); await new Promise( (resolve) => setTimeout(resolve, this.options.retryDelay ?? DEFAULT_RETRY_DELAY) ); return this.getURL(); } const { url } = await response.json(); return url; }; // Connect to Gladia this.initWS = async (url) => { return new Promise((resolve, reject) => { const socket = new import_ws.default(url); this.socket = socket; const timeout = setTimeout(() => { this.log("Connection timeout"); socket.removeAllListeners(); socket.close(); this.socket = void 0; reject(new Error("WebSocket connection timeout")); }, this.options.connectionTimeout ?? DEFAULT_CONNECTION_TIMEOUT); socket.addEventListener("open", () => { clearTimeout(timeout); this.log("Connection opened"); resolve(); }); socket.addEventListener("error", (error) => { clearTimeout(timeout); this.log("WebSocket error:", error); reject(new Error("WebSocket connection error")); }); socket.addEventListener("close", ({ code, reason }) => { clearTimeout(timeout); this.socket?.removeAllListeners(); this.socket = void 0; if (code !== 1e3) { this.reconnect(); } else { this.log("Connection closed", { code, reason }); } }); socket.addEventListener("message", (event) => { const message = JSON.parse(event.data.toString()); if (message.type === "transcript" && message.data.is_final) { const transcript = message.data.utterance.text; this.log(`Received transcript: "${transcript}"`); this.emit("Transcript", transcript); this.audioChunksPending.length = 0; if (this.transcriptionTimeout) { clearTimeout(this.transcriptionTimeout); this.transcriptionTimeout = void 0; } } }); }); }; this.initPromise = this.getURL().then(this.initWS).catch((error) => { console.error("[GladiaSTT] Connection error:", error); this.reconnect(); }); } transcribe(audioStream) { audioStream.on("data", async (chunk) => { this.audioChunksPending.push(chunk); await this.initPromise; this.socket?.send(chunk); this.log(`Sent audio chunk (${chunk.byteLength} bytes)`); }); audioStream.on("end", async () => { await this.initPromise; if (this.audioChunksPending.length === 0) return; this.sendSilence(2); this.transcriptionTimeout = setTimeout(() => { this.transcriptionTimeout = void 0; this.log(`Transcription timeout`); this.emit("Transcript", ""); this.audioChunksPending.length = 0; }, this.options.transcriptionTimeout || DEFAULT_TRANSCRIPTION_TIMEOUT); }); } destroy() { super.destroy(); if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = void 0; } if (this.transcriptionTimeout) { clearTimeout(this.transcriptionTimeout); this.transcriptionTimeout = void 0; } this.socket?.removeAllListeners(); if (this.socket?.readyState === import_ws.default.OPEN) { this.socket?.close(1e3); } this.socket = void 0; } reconnect() { this.retryCount++; if (this.retryCount > (this.options.maxRetry ?? DEFAULT_MAX_RETRY)) { this.log("Max retries reached, giving up"); this.emit("Failed", this.audioChunksPending); return; } this.initPromise = new Promise((resolve) => { this.log("Reconnecting..."); this.reconnectTimeout = setTimeout(() => { this.reconnectTimeout = void 0; this.getURL().then(this.initWS).then(() => { this.retryCount = 0; if (this.audioChunksPending.length > 0) { this.log("Sending audio chunks again"); this.audioChunksPending.forEach( (chunk) => this.socket?.send(chunk) ); } }).then(resolve).catch((error) => { this.log("Reconnection error:", error); this.reconnect(); }); }, this.options.retryDelay ?? DEFAULT_RETRY_DELAY); }); } sendSilence(durationSeconds) { if (!this.socket) return; const numSamples = Math.round(SAMPLE_RATE * durationSeconds); const bytesPerSample = BIT_DEPTH / 8; const silenceBuffer = Buffer.alloc(numSamples * bytesPerSample); this.socket.send(silenceBuffer); this.audioChunksPending.push(silenceBuffer); this.log( `Sent ${durationSeconds * 1e3}ms of silence (${silenceBuffer.byteLength} bytes) after stream end` ); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { GladiaSTT }); //# sourceMappingURL=index.js.map