@micdrop/gladia
Version:
Gladia implementation for @micdrop/server
1 lines • 11.8 kB
Source Map (JSON)
{"version":3,"sources":["../src/GladiaSTT.ts"],"sourcesContent":["import { 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 connectionTimeout?: number\n transcriptionTimeout?: number\n retryDelay?: number\n maxRetry?: number\n}\n\nconst SAMPLE_RATE = 16000\nconst BIT_DEPTH = 16\nconst DEFAULT_CONNECTION_TIMEOUT = 5000\nconst DEFAULT_TRANSCRIPTION_TIMEOUT = 4000\nconst DEFAULT_RETRY_DELAY = 1000\nconst DEFAULT_MAX_RETRY = 3\n\nexport class GladiaSTT extends STT {\n private socket?: WebSocket\n private initPromise: Promise<void>\n private reconnectTimeout?: NodeJS.Timeout\n private transcriptionTimeout?: NodeJS.Timeout\n private audioChunksPending: Buffer[] = [] // Store audio chunks to send them again if reconnecting\n private retryCount = 0\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 this.reconnect()\n })\n }\n\n transcribe(audioStream: Readable) {\n // Read transformed stream and send to Gladia\n audioStream.on('data', async (chunk: Buffer) => {\n this.audioChunksPending.push(chunk)\n await this.initPromise\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 audioStream.on('end', async () => {\n await this.initPromise\n if (this.audioChunksPending.length === 0) return\n this.sendSilence(2)\n\n // Timeout transcription if no transcript is received\n this.transcriptionTimeout = setTimeout(() => {\n this.transcriptionTimeout = undefined\n this.log(`Transcription timeout`)\n this.emit('Transcript', '')\n this.audioChunksPending.length = 0\n }, this.options.transcriptionTimeout || DEFAULT_TRANSCRIPTION_TIMEOUT)\n })\n }\n\n destroy() {\n super.destroy()\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout)\n this.reconnectTimeout = undefined\n }\n if (this.transcriptionTimeout) {\n clearTimeout(this.transcriptionTimeout)\n this.transcriptionTimeout = 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 (): Promise<string> => {\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 const status = response.status\n\n // Don't retry on 4xx errors\n if (status >= 400 && status < 500) {\n throw new Error(`${status}: ${response.statusText}`)\n }\n\n // Retry on other errors\n this.log('Error getting URL, retrying...', {\n status,\n text: response.statusText,\n })\n await new Promise((resolve) =>\n setTimeout(resolve, this.options.retryDelay ?? DEFAULT_RETRY_DELAY)\n )\n return this.getURL()\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 const timeout = setTimeout(() => {\n this.log('Connection timeout')\n socket.removeAllListeners()\n socket.close()\n this.socket = undefined\n reject(new Error('WebSocket connection timeout'))\n }, this.options.connectionTimeout ?? DEFAULT_CONNECTION_TIMEOUT)\n\n socket.addEventListener('open', () => {\n clearTimeout(timeout)\n this.log('Connection opened')\n resolve()\n })\n\n socket.addEventListener('error', (error) => {\n clearTimeout(timeout)\n this.log('WebSocket error:', error)\n reject(new Error('WebSocket connection error'))\n })\n\n socket.addEventListener('close', ({ code, reason }) => {\n clearTimeout(timeout)\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 if (this.transcriptionTimeout) {\n clearTimeout(this.transcriptionTimeout)\n this.transcriptionTimeout = undefined\n }\n }\n })\n })\n }\n\n private reconnect() {\n this.retryCount++\n if (this.retryCount > (this.options.maxRetry ?? DEFAULT_MAX_RETRY)) {\n this.log('Max retries reached, giving up')\n this.emit('Failed', this.audioChunksPending)\n return\n }\n\n this.initPromise = new Promise((resolve) => {\n this.log('Reconnecting...')\n this.reconnectTimeout = setTimeout(() => {\n this.reconnectTimeout = undefined\n this.getURL()\n .then(this.initWS)\n .then(() => {\n this.retryCount = 0\n\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 this.reconnect()\n })\n }, this.options.retryDelay ?? DEFAULT_RETRY_DELAY)\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)\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":[]}