UNPKG

@micdrop/gladia

Version:

Gladia implementation for @micdrop/server

1 lines 8.69 kB
{"version":3,"sources":["../src/GladiaSTT.ts"],"sourcesContent":["import { convertToPCM, DeepPartial, STT } from '@micdrop/server'\nimport { Readable } from 'stream'\nimport WebSocket from 'ws'\nimport { GladiaLiveSessionPayload } from './types'\n\n/**\n * Gladia Real-time V2 STT\n *\n * @see https://docs.gladia.io/chapters/live-stt/getting-started\n */\n\nexport interface GladiaSTTOptions {\n apiKey: string\n settings?: DeepPartial<GladiaLiveSessionPayload>\n}\n\nconst SAMPLE_RATE = 16000\nconst BIT_DEPTH = 16\n\nexport class GladiaSTT extends STT {\n private socket?: WebSocket\n private initPromise: Promise<void>\n private reconnectTimeout?: NodeJS.Timeout\n private audioChunksPending: ArrayBuffer[] = [] // Store audio chunks to send them again if reconnecting\n\n constructor(private options: GladiaSTTOptions) {\n super()\n\n // Setup Websocket connection\n this.initPromise = this.getURL()\n .then(this.initWS)\n .catch((error) => {\n console.error('[GladiaSTT] Connection error:', error)\n })\n }\n\n transcribe(audioStream: Readable) {\n const pcmStream = convertToPCM(audioStream, SAMPLE_RATE, BIT_DEPTH)\n\n // Read transformed stream and send to Gladia\n pcmStream.on('data', async (chunk) => {\n await this.initPromise\n this.audioChunksPending.push(chunk)\n this.socket?.send(chunk)\n this.log(`Sent audio chunk (${chunk.byteLength} bytes)`)\n })\n\n // Send silence when the stream ends to force Gladia to transcribe\n pcmStream.on('end', async () => {\n if (this.audioChunksPending.length === 0) return\n await this.initPromise\n this.sendSilence(1)\n })\n }\n\n destroy() {\n super.destroy()\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout)\n this.reconnectTimeout = undefined\n }\n this.socket?.removeAllListeners()\n if (this.socket?.readyState === WebSocket.OPEN) {\n this.socket?.close(1000)\n }\n this.socket = undefined\n }\n\n // Register real-time transcription to get a WebSocket URL\n private getURL = async () => {\n const response = await fetch('https://api.gladia.io/v2/live', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Gladia-Key': this.options.apiKey,\n },\n body: JSON.stringify({\n encoding: 'wav/pcm',\n sample_rate: SAMPLE_RATE,\n bit_depth: BIT_DEPTH,\n channels: 1,\n messages_config: {\n receive_final_transcripts: true,\n receive_speech_events: false,\n receive_pre_processing_events: false,\n receive_realtime_processing_events: false,\n receive_post_processing_events: false,\n receive_acknowledgments: false,\n receive_errors: true,\n receive_lifecycle_events: false,\n },\n ...this.options.settings,\n }),\n })\n\n if (!response.ok) {\n throw new Error(\n `${response.status}: ${(await response.text()) || response.statusText}`\n )\n }\n\n const { url } = (await response.json()) as { url: string }\n return url\n }\n\n // Connect to Gladia\n private initWS = async (url: string) => {\n return new Promise<void>((resolve, reject) => {\n const socket = new WebSocket(url)\n this.socket = socket\n\n socket.addEventListener('open', () => {\n this.log('Connection opened')\n resolve()\n })\n\n socket.addEventListener('error', (error) => {\n reject(error)\n })\n\n socket.addEventListener('close', ({ code, reason }) => {\n this.socket?.removeAllListeners()\n this.socket = undefined\n\n if (code !== 1000) {\n this.reconnect()\n } else {\n this.log('Connection closed', { code, reason })\n }\n })\n\n socket.addEventListener('message', (event) => {\n const message = JSON.parse(event.data.toString())\n if (message.type === 'transcript' && message.data.is_final) {\n const transcript = message.data.utterance.text\n this.log(`Received transcript: \"${transcript}\"`)\n this.emit('Transcript', transcript)\n // Reset audio chunks and transcript flag to avoid sending them again if reconnecting\n this.audioChunksPending.length = 0\n }\n })\n })\n }\n\n private reconnect() {\n this.initPromise = new Promise((resolve, reject) => {\n this.log('Reconnecting...')\n this.reconnectTimeout = setTimeout(() => {\n this.reconnectTimeout = undefined\n this.getURL()\n .then(this.initWS)\n .then(() => {\n // Send audio chunks again if reconnecting during transcription\n if (this.audioChunksPending.length > 0) {\n this.log('Sending audio chunks again')\n this.audioChunksPending.forEach((chunk) =>\n this.socket?.send(chunk)\n )\n }\n })\n .then(resolve)\n .catch((error) => {\n this.log('Reconnection error:', error)\n reject(error)\n })\n }, 1000)\n })\n }\n\n private sendSilence(durationSeconds: number) {\n if (!this.socket) return\n const numSamples = Math.round(SAMPLE_RATE * durationSeconds)\n const bytesPerSample = BIT_DEPTH / 8\n const silenceBuffer = Buffer.alloc(numSamples * bytesPerSample, 0)\n this.socket.send(silenceBuffer)\n this.audioChunksPending.push(silenceBuffer)\n this.log(\n `Sent ${durationSeconds * 1000}ms of silence (${silenceBuffer.byteLength} bytes) after stream end`\n )\n }\n}\n"],"mappings":"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","names":[]}