uneeq-js 3.13.5 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/363.index.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";(Object("undefined"!=typeof self?self:this).webpackChunkUneeq=Object("undefined"!=typeof self?self:this).webpackChunkUneeq||[]).push([[363],{363(e,t,n){n.d(t,{DeepgramFluxSTT:()=>u});var i=n(514),s=n(838),o=n(33),r=n(388),a=n(58),c=n(260);const h="[Deepgram Flux STT]";var d;!function(e){e.Idle="Idle",e.Connecting="Connecting",e.Connected="Connected",e.Paused="Paused",e.Disconnected="Disconnected"}(d||(d={}));class u{options;connection=null;state=d.Idle;shouldReconnect=!0;stream=null;audioContext=null;workletNode=null;reconnectAttempts=0;reconnectDelay=1e3;reconnectTimeoutId=null;digitalHumanSpeaking=!1;isUserCurrentlySpeaking=!1;isUiShowingSpeaking=!1;eagerPromptSentForTurn=!1;safetyNetTimeoutId=null;audioChunksSent=0;constructor(e){this.options=e,this.options.model=this.options.model||"flux-general-en",this.options.language=this.options.language||"en",this.options.eotThreshold=this.options.eotThreshold??.7,this.options.useEagerEndOfTurn=this.options.useEagerEndOfTurn??!0,this.options.safetyNetTimeoutMs=this.options.safetyNetTimeoutMs??2e3,this.options.echoCancellation=this.options.echoCancellation??!0,this.options.noiseSuppression=this.options.noiseSuppression??!0,this.options.autoGainControl=this.options.autoGainControl??!0,this.handleAppMessages()}async startRecognition(){i.A.info(`${h} Starting speech recognition`),this.shouldReconnect=!0,this.resetReconnectionState(),await this.connect()}async stopRecognition(){i.A.info(`${h} Stopping speech recognition`),this.shouldReconnect=!1,this.clearReconnectTimeout(),await this.disconnect()}async pause(){return i.A.info(`${h} Pausing speech recognition`),this.state=d.Paused,this.clearSafetyNet(),this.resetSpeakingStates(),this.eagerPromptSentForTurn=!1,this.stream&&(this.stream.getTracks().forEach(e=>{e.enabled=!1}),i.A.debug(`${h} Audio tracks disabled`)),!0}async resume(){if(i.A.info(`${h} Resuming speech recognition`),this.state===d.Paused){if(this.stream)return this.state=d.Connected,this.stream.getTracks().forEach(e=>{e.enabled=!0}),i.A.debug(`${h} Audio tracks re-enabled`),!0;if(this.connection)return this.state=d.Connected,await this.startMicrophone(),!0;this.state=d.Disconnected}return i.A.debug(`${h} Initiating connection`),await this.connect(),!0}setChatMetadata(e){this.options.promptMetadata=e}async connect(){if(this.state!==d.Connected)if(this.state!==d.Connecting){this.state=d.Connecting;try{const e=await this.getToken();i.A.info(`${h} Connecting to Deepgram Flux v2 — api_url="${e.api_url}", sdk_version="${e.sdk_version}", token_length=${e.token?.length??0}`);const t=new c.c({accessToken:e.token,baseUrl:e.api_url}),n={model:this.options.model,encoding:"linear16",sample_rate:String(16e3),mip_opt_out:"true",...void 0!==this.options.eotThreshold&&{eot_threshold:this.options.eotThreshold},...void 0!==this.options.eagerEotThreshold&&{eager_eot_threshold:this.options.eagerEotThreshold},...void 0!==this.options.eotTimeoutMs&&{eot_timeout_ms:this.options.eotTimeoutMs},...this.options.keyterms&&this.options.keyterms.length>0&&{keyterm:this.options.keyterms}};if(this.connection=await t.listen.v2.connect(n),this.connection.connect(),await Promise.race([this.connection.waitForOpen(),new Promise((e,t)=>setTimeout(()=>t(new Error("Connection timeout")),1e4))]),this.state!==d.Paused&&(this.state=d.Connected),i.A.info(`${h} Connection opened`),this.setupEventHandlers(),this.state===d.Paused)return i.A.info(`${h} Pause requested during connection — staying paused`),void this.resetReconnectionState();await this.startMicrophone(),i.A.info(`${h} Connected successfully`),this.resetReconnectionState()}catch(e){this.state=d.Disconnected,i.A.error(`${h} Connection error`,i.A.serialiseError(e)),this.shouldReconnect&&(this.emitTransientError(e),this.scheduleReconnect())}}else i.A.warn(`${h} Connection already in progress`);else i.A.warn(`${h} Already connected`)}async disconnect(){if(this.state!==d.Idle&&(this.state!==d.Disconnected||this.connection)){i.A.info(`${h} Disconnecting`);try{if(this.stopMicrophone(),this.connection){try{this.connection.sendCloseStream({type:"CloseStream"})}catch{}this.connection.close(),this.connection=null}}catch(e){i.A.error(`${h} Disconnect error`,i.A.serialiseError(e))}this.clearSafetyNet(),this.resetSpeakingStates(),this.eagerPromptSentForTurn=!1,this.state=d.Disconnected,this.clientMsgSend(new s.WY(!1))}}scheduleReconnect(){if(this.reconnectAttempts>=5)return i.A.error(`${h} Max reconnection attempts (5) reached`),void this.clientMsgSend(new s.Cj("Unable to connect to speech recognition service after 5 attempts"));this.reconnectAttempts++,i.A.info(`${h} Scheduling reconnection attempt ${this.reconnectAttempts}/5 in ${this.reconnectDelay}ms`),this.reconnectTimeoutId=setTimeout(()=>{this.connect()},this.reconnectDelay),this.reconnectDelay=Math.min(2*this.reconnectDelay,3e4)}resetReconnectionState(){this.reconnectAttempts=0,this.reconnectDelay=1e3,this.clearReconnectTimeout()}clearReconnectTimeout(){this.reconnectTimeoutId&&(clearTimeout(this.reconnectTimeoutId),this.reconnectTimeoutId=null)}async getToken(){const e=this.options.model||"flux-general-en",t=`${this.options.connectionUrl}/speech-recognition-service/deepgram/token?model=${encodeURIComponent(e)}`,n=await fetch(t,{method:"GET",headers:{Authorization:`Bearer ${this.options.jwtToken}`,"Content-Type":"application/json"}});if(!n.ok)throw new Error(`Token fetch failed: ${n.status} ${n.statusText}`);return await n.json()}async startMicrophone(){try{if(i.A.info(`${h} Starting microphone`),this.stopMicrophone(),this.stream=await navigator.mediaDevices.getUserMedia({audio:{deviceId:this.options.microphoneDeviceId?{exact:this.options.microphoneDeviceId}:void 0,echoCancellation:this.options.echoCancellation,noiseSuppression:this.options.noiseSuppression,autoGainControl:this.options.autoGainControl}}),this.state===d.Paused)return i.A.info(`${h} Paused during getUserMedia — keeping stream but disabling tracks`),void this.stream.getTracks().forEach(e=>{e.enabled=!1});this.audioContext=new AudioContext({sampleRate:16e3});const e=this.audioContext.createMediaStreamSource(this.stream),t=new Blob(["\nclass PcmCaptureProcessor extends AudioWorkletProcessor {\n constructor() {\n super()\n this._buffer = new Float32Array(1280)\n this._offset = 0\n }\n process(inputs, outputs, parameters) {\n const input = inputs[0]?.[0]\n if (!input) return true\n for (let i = 0; i < input.length; i++) {\n this._buffer[this._offset++] = input[i]\n if (this._offset >= this._buffer.length) {\n const int16 = new Int16Array(this._buffer.length)\n for (let j = 0; j < this._buffer.length; j++) {\n const s = Math.max(-1, Math.min(1, this._buffer[j]))\n int16[j] = s < 0 ? s * 0x8000 : s * 0x7FFF\n }\n this.port.postMessage(int16.buffer, [int16.buffer])\n this._offset = 0\n }\n }\n return true\n }\n}\nregisterProcessor('pcm-capture-processor', PcmCaptureProcessor)\n"],{type:"application/javascript"}),n=URL.createObjectURL(t);await this.audioContext.audioWorklet.addModule(n),URL.revokeObjectURL(n),this.workletNode=new AudioWorkletNode(this.audioContext,"pcm-capture-processor"),this.audioChunksSent=0,this.workletNode.port.onmessage=e=>{this.connection&&this.state===d.Connected&&(this.connection.sendMedia(e.data),this.audioChunksSent++,this.audioChunksSent%50==1&&i.A.info(`${h} Audio chunks sent: ${this.audioChunksSent}, size: ${e.data.byteLength} bytes`))},e.connect(this.workletNode),this.workletNode.connect(this.audioContext.destination),i.A.info(`${h} Microphone started (linear16 PCM @ 16000Hz)`),this.clientMsgSend(new s.WY(!0))}catch(e){i.A.error(`${h} Microphone error`,i.A.serialiseError(e)),this.clientMsgSend(new s.co(new Error(JSON.stringify(e))))}}stopMicrophone(){this.workletNode&&(this.workletNode.port.close(),this.workletNode.disconnect(),this.workletNode=null),this.audioContext&&(this.audioContext.close().catch(()=>{}),this.audioContext=null),this.stream&&(this.stream.getTracks().forEach(e=>{e.stop()}),this.stream=null),i.A.info(`${h} Microphone stopped`)}setupEventHandlers(){this.connection&&(this.connection.on("open",()=>{this.handleConnectionOpen()}),this.connection.on("message",e=>{if(null!==e&&"object"==typeof e&&"type"in e){const t=e;"TurnInfo"===t.type?this.handleTurnInfo(e):"Connected"===t.type?i.A.info(`${h} v2 connection confirmed`):"Error"===t.type?this.handleFatalError(e):i.A.debug(`${h} Unhandled v2 message type: ${t.type}`)}}),this.connection.on("close",e=>{this.handleConnectionClose(e)}),this.connection.on("error",e=>{const t={};e instanceof Event&&(t.type=e.type,t.target=e.target?.url??e.target?.readyState??"unknown"),i.A.error(`${h} WebSocket error event`,e,t),this.emitTransientError(e)}))}handleTurnInfo(e){try{switch(i.A.debug(`${h} TurnInfo event: ${e.event}, transcript="${(e.transcript||"").substring(0,50)}${(e.transcript||"").length>50?"...":""}", turn_index=${e.turn_index}, eot_confidence=${e.end_of_turn_confidence??"n/a"}`),e.event){case"StartOfTurn":this.handleStartOfTurn(e);break;case"Update":this.handleUpdate(e);break;case"EagerEndOfTurn":this.handleEagerEndOfTurn(e);break;case"TurnResumed":this.handleTurnResumed(e);break;case"EndOfTurn":this.handleEndOfTurn(e);break;default:i.A.debug(`${h} Unknown TurnInfo event: ${e.event}`)}}catch(e){i.A.error(`${h} Error processing TurnInfo`,i.A.serialiseError(e))}finally{this.resetSafetyNet()}}handleStartOfTurn(e){i.A.debug(`${h} StartOfTurn: turn_index=${e.turn_index}`),this.eagerPromptSentForTurn=!1}handleUpdate(e){const t=e.transcript||"";if(""===t)return;this.isUiShowingSpeaking||(this.isUiShowingSpeaking=!0,this.clientMsgSend(new s._4)),this.isUserCurrentlySpeaking||(this.isUserCurrentlySpeaking=!0,this.dataChannelMsgSend(new o.A(o.f.Start))),this.digitalHumanSpeaking&&(i.A.info(`${h} User speech detected during avatar speaking — interrupting`),this.dataChannelMsgSend(new a.f),this.clientMsgSend(new s.tc),this.digitalHumanSpeaking=!1);const n={transcript:t,final:!1,confidence:this.calculateWordConfidence(e.words),language_code:this.options.language||""};this.clientMsgSend(new s.Ux(n))}handleEagerEndOfTurn(e){const t=e.transcript||"";i.A.debug(`${h} EagerEndOfTurn: confidence=${e.end_of_turn_confidence}, transcript="${t}"`),this.options.useEagerEndOfTurn?""!==t.trim()&&(this.digitalHumanSpeaking?i.A.debug(`${h} EagerEndOfTurn ignored — avatar is speaking`):(i.A.info(`${h} EagerEndOfTurn: sending prompt early: "${t}"`),this.eagerPromptSentForTurn=!0,this.sendChatPrompt(t))):i.A.debug(`${h} EagerEndOfTurn ignored (useEagerEndOfTurn is disabled)`)}handleTurnResumed(e){i.A.debug(`${h} TurnResumed: turn_index=${e.turn_index}`),this.eagerPromptSentForTurn&&(i.A.warn(`${h} TurnResumed after eager prompt was already sent — cancelling via StopSpeaking`),this.dataChannelMsgSend(new a.f)),this.eagerPromptSentForTurn=!1}handleEndOfTurn(e){const t=e.transcript||"";if(i.A.info(`${h} EndOfTurn: "${t}", confidence=${e.end_of_turn_confidence}`),""!==t.trim()){const n={transcript:t,final:!0,confidence:this.calculateWordConfidence(e.words),language_code:this.options.language||""};this.clientMsgSend(new s.Ux(n)),this.digitalHumanSpeaking?i.A.warn(`${h} EndOfTurn: discarding prompt — avatar is still speaking (no Update interrupted)`):(this.eagerPromptSentForTurn&&i.A.debug(`${h} EndOfTurn: cancelling in-flight eager prompt`),this.dataChannelMsgSend(new a.f),this.sendChatPrompt(t))}this.eagerPromptSentForTurn=!1,this.resetSpeakingStates()}handleFatalError(e){i.A.error(`${h} Fatal error from Deepgram: ${e.code} — ${e.description}`),this.clientMsgSend(new s.Cj(`Deepgram error: ${e.code} — ${e.description}`))}calculateWordConfidence(e){return e&&0!==e.length?e.reduce((e,t)=>e+t.confidence,0)/e.length:1}handleAppMessages(){this.options.messages.subscribe(e=>{switch(e.uneeqMessageType){case s.Yg.AvatarStartedSpeaking:this.digitalHumanSpeaking=!0;break;case s.Yg.PromptResult:e.promptResult.success||this.handleSpeakingEnd();break;case s.Yg.AvatarAnswer:""===e.answerSpeech.replace(/<[^>]*>/g,"")&&this.handleSpeakingEnd();break;case s.Yg.AvatarStoppedSpeaking:this.handleSpeakingEnd();break;case s.Yg.SessionEnded:this.shouldReconnect=!1,this.stopRecognition();break;case s.Yg.SessionReconnecting:this.handleSpeakingEnd(),this.shouldReconnect=!1,this.stopRecognition();break;case s.Yg.CustomMetadataUpdated:this.options.promptMetadata=e.chatMetadata;break;case s.Yg.SessionBackendError:this.handleSpeakingEnd()}})}handleConnectionOpen(){this.state!==d.Paused&&(this.state=d.Connected)}handleConnectionClose(e){const t=e?.code??"unknown",n=e?.reason??"";if(i.A.info(`${h} Connection closed — code=${t}, reason="${n}"`),this.state===d.Paused)return i.A.info(`${h} Connection closed while paused — will reconnect on resume`),this.connection=null,this.stopMicrophone(),this.clearSafetyNet(),void this.resetSpeakingStates();this.state=d.Disconnected,this.clearSafetyNet(),this.resetSpeakingStates(),this.eagerPromptSentForTurn=!1,this.clientMsgSend(new s.WY(!1)),this.shouldReconnect&&(i.A.info(`${h} Unexpected disconnect, attempting reconnection...`),this.scheduleReconnect())}emitTransientError(e){const t=e instanceof Error?e.message:String(e);this.clientMsgSend(new s.fP(t))}sendChatPrompt(e){e&&""!==e.trim()&&(this.options.language&&(this.options.promptMetadata.userSpokenLocale=this.options.language),this.dataChannelMsgSend(new r.D(e,this.options.promptMetadata)))}handleSpeakingEnd(){this.digitalHumanSpeaking=!1}resetSafetyNet(){this.clearSafetyNet(),(this.isUiShowingSpeaking||this.isUserCurrentlySpeaking)&&(this.safetyNetTimeoutId=setTimeout(()=>{i.A.warn(`${h} Safety net: no TurnInfo events for ${this.options.safetyNetTimeoutMs}ms while speaking — resetting`),this.resetSpeakingStates()},this.options.safetyNetTimeoutMs))}clearSafetyNet(){this.safetyNetTimeoutId&&(clearTimeout(this.safetyNetTimeoutId),this.safetyNetTimeoutId=null)}resetSpeakingStates(){this.isUserCurrentlySpeaking&&(this.isUserCurrentlySpeaking=!1,this.dataChannelMsgSend(new o.A(o.f.Stop))),this.isUiShowingSpeaking&&(this.isUiShowingSpeaking=!1,this.clientMsgSend(new s.im))}dataChannelMsgSend(e){this.options.sendMessage(e)}clientMsgSend(e){this.options.messages.next(e)}}}}]);
1
+ "use strict";(Object("undefined"!=typeof self?self:this).webpackChunkUneeq=Object("undefined"!=typeof self?self:this).webpackChunkUneeq||[]).push([[363],{363(e,t,n){n.d(t,{DeepgramFluxSTT:()=>p});var i=n(514),s=n(838),o=n(33),r=n(388),a=n(58),c=n(1),h=n(260);const d="[Deepgram Flux STT]";var u;!function(e){e.Idle="Idle",e.Connecting="Connecting",e.Connected="Connected",e.Paused="Paused",e.Disconnected="Disconnected"}(u||(u={}));class p{options;connection=null;state=u.Idle;shouldReconnect=!0;stream=null;audioContext=null;workletNode=null;reconnectAttempts=0;reconnectDelay=1e3;reconnectTimeoutId=null;digitalHumanSpeaking=!1;pendingPromptRequest=null;isUserCurrentlySpeaking=!1;isUiShowingSpeaking=!1;eagerPromptSentForTurn=!1;turnStartedAt=null;safetyNetTimeoutId=null;audioChunksSent=0;constructor(e){this.options=e,this.options.model=this.options.model||"flux-general-en",this.options.language=this.options.language||"en",this.options.eotThreshold=this.options.eotThreshold??.85,this.options.eagerEotThreshold=this.options.eagerEotThreshold??.5,this.options.eotTimeoutMs=this.options.eotTimeoutMs??3e3,this.options.eagerMaxTurnDurationMs=this.options.eagerMaxTurnDurationMs??5e3,this.options.safetyNetTimeoutMs=this.options.safetyNetTimeoutMs??2e3,this.options.echoCancellation=this.options.echoCancellation??!0,this.options.noiseSuppression=this.options.noiseSuppression??!0,this.options.autoGainControl=this.options.autoGainControl??!0;const t=0===this.options.eagerEotThreshold?"disabled":`${this.options.eagerEotThreshold}`,n=0===this.options.eagerMaxTurnDurationMs?"disabled":`${this.options.eagerMaxTurnDurationMs}ms`;i.A.debug(`${d} init — features: pure-flux-event-flow, eot_threshold=${this.options.eotThreshold}, eager_eot_threshold=${t}, eot_timeout_ms=${this.options.eotTimeoutMs}, eager_max_turn_duration_ms=${n}`),this.handleAppMessages()}async startRecognition(){i.A.info(`${d} Starting speech recognition`),this.shouldReconnect=!0,this.resetReconnectionState(),await this.connect()}async stopRecognition(){i.A.info(`${d} Stopping speech recognition`),this.shouldReconnect=!1,this.clearReconnectTimeout(),await this.disconnect()}async pause(){return i.A.info(`${d} Pausing speech recognition`),this.state=u.Paused,this.clearSafetyNet(),this.resetSpeakingStates(),this.eagerPromptSentForTurn=!1,this.turnStartedAt=null,this.pendingPromptRequest=null,this.stream&&(this.stream.getTracks().forEach(e=>{e.enabled=!1}),i.A.debug(`${d} Audio tracks disabled`)),!0}async resume(){if(i.A.info(`${d} Resuming speech recognition`),this.state===u.Paused){if(this.stream)return this.state=u.Connected,this.stream.getTracks().forEach(e=>{e.enabled=!0}),i.A.debug(`${d} Audio tracks re-enabled`),!0;if(this.connection)return this.state=u.Connected,await this.startMicrophone(),!0;this.state=u.Disconnected}return i.A.debug(`${d} Initiating connection`),await this.connect(),!0}setChatMetadata(e){this.options.promptMetadata=e}async connect(){if(this.state!==u.Connected)if(this.state!==u.Connecting){this.state=u.Connecting;try{const e=await this.getToken();i.A.info(`${d} Connecting to Deepgram Flux v2 — api_url="${e.api_url}", sdk_version="${e.sdk_version}", token_length=${e.token?.length??0}`);const t=new h.c({accessToken:e.token,baseUrl:e.api_url}),n={model:this.options.model,encoding:"linear16",sample_rate:String(16e3),mip_opt_out:"true",...void 0!==this.options.eotThreshold&&{eot_threshold:this.options.eotThreshold},...void 0!==this.options.eagerEotThreshold&&this.options.eagerEotThreshold>0&&{eager_eot_threshold:this.options.eagerEotThreshold},...void 0!==this.options.eotTimeoutMs&&{eot_timeout_ms:this.options.eotTimeoutMs},...this.options.keyterms&&this.options.keyterms.length>0&&{keyterm:this.options.keyterms}};if(this.connection=await t.listen.v2.connect(n),this.connection.connect(),await Promise.race([this.connection.waitForOpen(),new Promise((e,t)=>setTimeout(()=>t(new Error("Connection timeout")),1e4))]),this.state!==u.Paused&&(this.state=u.Connected),i.A.info(`${d} Connection opened`),this.setupEventHandlers(),this.state===u.Paused)return i.A.info(`${d} Pause requested during connection — staying paused`),void this.resetReconnectionState();await this.startMicrophone(),i.A.info(`${d} Connected successfully`),this.resetReconnectionState()}catch(e){this.state=u.Disconnected,i.A.error(`${d} Connection error`,i.A.serialiseError(e)),this.shouldReconnect&&(this.emitTransientError(e),this.scheduleReconnect())}}else i.A.warn(`${d} Connection already in progress`);else i.A.warn(`${d} Already connected`)}async disconnect(){if(this.state!==u.Idle&&(this.state!==u.Disconnected||this.connection)){i.A.info(`${d} Disconnecting`);try{if(this.stopMicrophone(),this.connection){try{this.connection.sendCloseStream({type:"CloseStream"})}catch{}this.connection.close(),this.connection=null}}catch(e){i.A.error(`${d} Disconnect error`,i.A.serialiseError(e))}this.clearSafetyNet(),this.resetSpeakingStates(),this.eagerPromptSentForTurn=!1,this.turnStartedAt=null,this.pendingPromptRequest=null,this.state=u.Disconnected,this.clientMsgSend(new s.WY(!1))}}scheduleReconnect(){if(this.reconnectAttempts>=5)return i.A.error(`${d} Max reconnection attempts (5) reached`),void this.clientMsgSend(new s.Cj("Unable to connect to speech recognition service after 5 attempts"));this.reconnectAttempts++,i.A.info(`${d} Scheduling reconnection attempt ${this.reconnectAttempts}/5 in ${this.reconnectDelay}ms`),this.reconnectTimeoutId=setTimeout(()=>{this.connect()},this.reconnectDelay),this.reconnectDelay=Math.min(2*this.reconnectDelay,3e4)}resetReconnectionState(){this.reconnectAttempts=0,this.reconnectDelay=1e3,this.clearReconnectTimeout()}clearReconnectTimeout(){this.reconnectTimeoutId&&(clearTimeout(this.reconnectTimeoutId),this.reconnectTimeoutId=null)}async getToken(){const e=this.options.model||"flux-general-en",t=`${this.options.connectionUrl}/speech-recognition-service/deepgram/token?model=${encodeURIComponent(e)}`,n=await fetch(t,{method:"GET",headers:{Authorization:`Bearer ${this.options.jwtToken}`,"Content-Type":"application/json"}});if(!n.ok)throw new Error(`Token fetch failed: ${n.status} ${n.statusText}`);return await n.json()}async startMicrophone(){try{if(i.A.info(`${d} Starting microphone`),this.stopMicrophone(),this.stream=await navigator.mediaDevices.getUserMedia({audio:{deviceId:this.options.microphoneDeviceId?{exact:this.options.microphoneDeviceId}:void 0,echoCancellation:this.options.echoCancellation,noiseSuppression:this.options.noiseSuppression,autoGainControl:this.options.autoGainControl}}),this.state===u.Paused)return i.A.info(`${d} Paused during getUserMedia — keeping stream but disabling tracks`),void this.stream.getTracks().forEach(e=>{e.enabled=!1});this.audioContext=new AudioContext({sampleRate:16e3});const e=this.audioContext.createMediaStreamSource(this.stream),t=new Blob(["\nclass PcmCaptureProcessor extends AudioWorkletProcessor {\n constructor() {\n super()\n this._buffer = new Float32Array(1280)\n this._offset = 0\n }\n process(inputs, outputs, parameters) {\n const input = inputs[0]?.[0]\n if (!input) return true\n for (let i = 0; i < input.length; i++) {\n this._buffer[this._offset++] = input[i]\n if (this._offset >= this._buffer.length) {\n const int16 = new Int16Array(this._buffer.length)\n for (let j = 0; j < this._buffer.length; j++) {\n const s = Math.max(-1, Math.min(1, this._buffer[j]))\n int16[j] = s < 0 ? s * 0x8000 : s * 0x7FFF\n }\n this.port.postMessage(int16.buffer, [int16.buffer])\n this._offset = 0\n }\n }\n return true\n }\n}\nregisterProcessor('pcm-capture-processor', PcmCaptureProcessor)\n"],{type:"application/javascript"}),n=URL.createObjectURL(t);await this.audioContext.audioWorklet.addModule(n),URL.revokeObjectURL(n),this.workletNode=new AudioWorkletNode(this.audioContext,"pcm-capture-processor"),this.audioChunksSent=0,this.workletNode.port.onmessage=e=>{this.connection&&this.state===u.Connected&&(this.connection.sendMedia(e.data),this.audioChunksSent++,this.audioChunksSent%50==1&&i.A.debug(`${d} Audio chunks sent: ${this.audioChunksSent}, size: ${e.data.byteLength} bytes`))},e.connect(this.workletNode),this.workletNode.connect(this.audioContext.destination),i.A.info(`${d} Microphone started (linear16 PCM @ 16000Hz)`),this.clientMsgSend(new s.WY(!0))}catch(e){i.A.error(`${d} Microphone error`,i.A.serialiseError(e)),this.clientMsgSend(new s.co(new Error(JSON.stringify(e))))}}stopMicrophone(){this.workletNode&&(this.workletNode.port.close(),this.workletNode.disconnect(),this.workletNode=null),this.audioContext&&(this.audioContext.close().catch(()=>{}),this.audioContext=null),this.stream&&(this.stream.getTracks().forEach(e=>{e.stop()}),this.stream=null),i.A.info(`${d} Microphone stopped`)}setupEventHandlers(){this.connection&&(this.connection.on("open",()=>{this.handleConnectionOpen()}),this.connection.on("message",e=>{if(null!==e&&"object"==typeof e&&"type"in e){const t=e;"TurnInfo"===t.type?this.handleTurnInfo(e):"Connected"===t.type?i.A.info(`${d} v2 connection confirmed`):"Error"===t.type?this.handleFatalError(e):i.A.debug(`${d} Unhandled v2 message type: ${t.type}`)}}),this.connection.on("close",e=>{this.handleConnectionClose(e)}),this.connection.on("error",e=>{const t={};e instanceof Event&&(t.type=e.type,t.target=e.target?.url??e.target?.readyState??"unknown"),i.A.error(`${d} WebSocket error event`,e,t),this.emitTransientError(e)}))}handleTurnInfo(e){try{switch(i.A.debug(`${d} TurnInfo event: ${e.event}, transcript_length=${(e.transcript||"").length}, turn_index=${e.turn_index}, eot_confidence=${e.end_of_turn_confidence??"n/a"}`),e.event){case"StartOfTurn":this.handleStartOfTurn(e);break;case"Update":this.handleUpdate(e);break;case"EagerEndOfTurn":this.handleEagerEndOfTurn(e);break;case"TurnResumed":this.handleTurnResumed(e);break;case"EndOfTurn":this.handleEndOfTurn(e);break;default:i.A.debug(`${d} Unknown TurnInfo event: ${e.event}`)}}catch(e){i.A.error(`${d} Error processing TurnInfo`,i.A.serialiseError(e))}finally{this.resetSafetyNet()}}handleStartOfTurn(e){i.A.debug(`${d} StartOfTurn: turn_index=${e.turn_index}`),this.eagerPromptSentForTurn=!1,this.turnStartedAt=null}handleUpdate(e){const t=e.transcript||"";if(""===t)return;if(this.isUiShowingSpeaking||(this.isUiShowingSpeaking=!0,this.turnStartedAt=Date.now(),this.clientMsgSend(new s._4)),this.isUserCurrentlySpeaking||(this.isUserCurrentlySpeaking=!0,this.dataChannelMsgSend(new o.A(o.f.Start))),this.digitalHumanSpeaking){const e=this.countWords(t);e>=3?(i.A.info(`${d} User speech detected during avatar speaking (${e} words) — interrupting`),this.dataChannelMsgSend(new a.f),this.clientMsgSend(new s.tc),this.digitalHumanSpeaking=!1):i.A.debug(`${d} User speech during avatar speaking is only ${e} word(s) — holding off barge-in (potential backchannel)`)}const n={transcript:t,final:!1,confidence:this.calculateWordConfidence(e.words),language_code:this.options.language||""};this.clientMsgSend(new s.Ux(n))}handleEagerEndOfTurn(e){const t=e.transcript||"";if(i.A.debug(`${d} EagerEndOfTurn: confidence=${e.end_of_turn_confidence}, transcript_length=${t.length}`),""===t.trim())return;if(this.digitalHumanSpeaking&&this.countWords(t)<3)return void i.A.info(`${d} EagerEndOfTurn: dropping ${this.countWords(t)}-word backchannel while avatar speaking`);const n=this.options.eagerMaxTurnDurationMs??0;if(n>0&&null!==this.turnStartedAt){const e=Date.now()-this.turnStartedAt;if(e>n)return void i.A.info(`${d} EagerEndOfTurn: suppressed — turn duration ${e}ms exceeds eagerMaxTurnDurationMs=${n}; deferring to EndOfTurn`)}i.A.info(`${d} EagerEndOfTurn: sending prompt early (${this.countWords(t)} words, ${t.length} chars)`),this.eagerPromptSentForTurn=!0,this.sendChatPromptRaw(t)}handleTurnResumed(e){i.A.debug(`${d} TurnResumed: turn_index=${e.turn_index}`),this.eagerPromptSentForTurn&&(i.A.info(`${d} TurnResumed: cancelling in-flight eager prompt via StopSpeaking`),this.dataChannelMsgSend(new a.f)),this.clearPendingPromptRequest(),this.eagerPromptSentForTurn=!1}handleEndOfTurn(e){const t=e.transcript||"";if(i.A.info(`${d} EndOfTurn: transcript_length=${t.length}, confidence=${e.end_of_turn_confidence}`),""!==t.trim()){const n={transcript:t,final:!0,confidence:this.calculateWordConfidence(e.words),language_code:this.options.language||""};this.clientMsgSend(new s.Ux(n)),this.digitalHumanSpeaking&&this.countWords(t)<3?(i.A.info(`${d} EndOfTurn: dropping ${this.countWords(t)}-word backchannel while avatar speaking`),this.clearPendingPromptRequest()):this.eagerPromptSentForTurn?(i.A.debug(`${d} EndOfTurn: skipping ChatPrompt — eager already fired for this turn`),this.emitPendingPromptRequest()):(this.sendChatPromptRaw(t),this.emitPendingPromptRequest())}this.eagerPromptSentForTurn=!1,this.turnStartedAt=null,this.resetSpeakingStates()}handleFatalError(e){i.A.error(`${d} Fatal error from Deepgram: ${e.code} — ${e.description}`),this.clientMsgSend(new s.Cj(`Deepgram error: ${e.code} — ${e.description}`))}calculateWordConfidence(e){return e&&0!==e.length?e.reduce((e,t)=>e+t.confidence,0)/e.length:1}countWords(e){return e.trim().split(/\s+/).filter(Boolean).length}handleAppMessages(){this.options.messages.subscribe(e=>{switch(e.uneeqMessageType){case s.Yg.AvatarStartedSpeaking:this.digitalHumanSpeaking=!0;break;case s.Yg.PromptResult:e.promptResult.success||this.handleSpeakingEnd();break;case s.Yg.AvatarAnswer:""===e.answerSpeech.replace(/<[^>]*>/g,"")&&this.handleSpeakingEnd();break;case s.Yg.AvatarStoppedSpeaking:this.handleSpeakingEnd();break;case s.Yg.SessionEnded:this.shouldReconnect=!1,this.stopRecognition();break;case s.Yg.SessionReconnecting:this.handleSpeakingEnd(),this.shouldReconnect=!1,this.stopRecognition();break;case s.Yg.CustomMetadataUpdated:this.options.promptMetadata=e.chatMetadata;break;case s.Yg.SessionBackendError:this.handleSpeakingEnd()}})}handleConnectionOpen(){this.state!==u.Paused&&(this.state=u.Connected)}handleConnectionClose(e){const t=e?.code??"unknown",n=e?.reason??"";if(i.A.info(`${d} Connection closed — code=${t}, reason="${n}"`),this.state===u.Paused)return i.A.info(`${d} Connection closed while paused — will reconnect on resume`),this.connection=null,this.stopMicrophone(),this.clearSafetyNet(),void this.resetSpeakingStates();this.state=u.Disconnected,this.clearSafetyNet(),this.resetSpeakingStates(),this.eagerPromptSentForTurn=!1,this.turnStartedAt=null,this.pendingPromptRequest=null,this.clientMsgSend(new s.WY(!1)),this.shouldReconnect&&(i.A.info(`${d} Unexpected disconnect, attempting reconnection...`),this.scheduleReconnect())}emitTransientError(e){const t=e instanceof Error?e.message:String(e);this.clientMsgSend(new s.fP(t))}sendChatPromptRaw(e){if(!e||""===e.trim())return;this.options.language&&(this.options.promptMetadata.userSpokenLocale=this.options.language);const t=(0,c.g)(),n={...this.options.promptMetadata},i=null!==this.turnStartedAt?Date.now()-this.turnStartedAt:0;this.pendingPromptRequest={prompt:e,requestId:t,metadata:n,speakingDurationMs:i},this.dataChannelMsgSend(new r.D(e,this.options.promptMetadata,t,!1))}emitPendingPromptRequest(){const e=this.pendingPromptRequest;null!==e&&(this.clientMsgSend(new s.bS({prompt:e.prompt,requestId:e.requestId,metadata:e.metadata,speakingDurationMs:e.speakingDurationMs})),i.A.debug(`${d} PromptRequest emitted — speakingDurationMs=${e.speakingDurationMs}, requestId=${e.requestId}`),this.pendingPromptRequest=null)}clearPendingPromptRequest(){this.pendingPromptRequest=null}handleSpeakingEnd(){this.digitalHumanSpeaking=!1}resetSafetyNet(){this.clearSafetyNet(),(this.isUiShowingSpeaking||this.isUserCurrentlySpeaking)&&(this.safetyNetTimeoutId=setTimeout(()=>{i.A.warn(`${d} Safety net: no TurnInfo events for ${this.options.safetyNetTimeoutMs}ms while speaking — resetting`),this.resetSpeakingStates()},this.options.safetyNetTimeoutMs))}clearSafetyNet(){this.safetyNetTimeoutId&&(clearTimeout(this.safetyNetTimeoutId),this.safetyNetTimeoutId=null)}resetSpeakingStates(){this.isUserCurrentlySpeaking&&(this.isUserCurrentlySpeaking=!1,this.dataChannelMsgSend(new o.A(o.f.Stop))),this.isUiShowingSpeaking&&(this.isUiShowingSpeaking=!1,this.clientMsgSend(new s.im))}dataChannelMsgSend(e){this.options.sendMessage(e)}clientMsgSend(e){this.options.messages.next(e)}}}}]);
2
2
  //# sourceMappingURL=363.index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"363.index.js","mappings":"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","sources":["webpack://Uneeq/./src/deepgram-flux-stt.ts"],"sourcesContent":["import { type Subject } from 'rxjs'\nimport Logger from './lib/logger'\nimport {\n UserStartedSpeakingMessage,\n UserStoppedSpeakingMessage,\n SpeechTranscriptionMessage,\n EnableMicrophoneUpdatedMessage,\n SessionErrorMessage,\n SpeechRecognitionTransientErrorMessage,\n DeviceErrorMessage,\n AvatarInterruptedMessage,\n type UneeqMessage,\n UneeqMessageType,\n type PromptResultMessage,\n type AvatarAnswerMessage,\n type CustomMetadataUpdated,\n} from './types/UneeqMessages'\nimport { type SpeechTranscriptionResult } from './types/SpeechTranscriptionResult'\nimport { type DataChannelMessage } from './webrtc-data-channel/DataChannelMessage'\nimport { UserSpeaking, UserSpeakingState } from './webrtc-data-channel/messages/UserSpeaking'\nimport { ChatPrompt } from './webrtc-data-channel/messages/ChatPrompt'\nimport { StopSpeaking } from './webrtc-data-channel/messages/StopSpeaking'\nimport { type PromptMetadata } from './types/PromptMetadata'\nimport { DeepgramClient } from '@deepgram/sdk'\nimport { type SpeechRecognitionInterface } from './types/SpeechRecognitionInterface'\n\n// Local interface for the Deepgram v2 connection — duck-typed to avoid coupling to SDK type names\ninterface DeepgramV2Connection {\n on(event: string, handler: (...args: unknown[]) => void): void\n sendMedia(data: ArrayBuffer): void\n sendListenV2Configure(config: Record<string, unknown>): void\n sendCloseStream(message: { type: string }): void\n connect(): void\n waitForOpen(): Promise<unknown>\n close(): void\n}\n\n// Constants\nconst LOG_PREFIX = '[Deepgram Flux STT]'\nconst CONNECTION_TIMEOUT_MS = 10000\n// PCM audio configuration — v2 API requires explicit encoding (no container auto-detection)\nconst PCM_SAMPLE_RATE = 16000\n// Deepgram recommends 80ms audio chunks for optimal Flux latency.\n// At 16kHz mono, 80ms = 1280 samples = 2560 bytes of int16.\n// AudioWorklet processes 128 samples per render quantum, so we accumulate 10 quanta.\nconst PCM_CHUNK_SAMPLES = 1280\n\n// AudioWorklet processor source — runs off the main thread.\n// Inlined as a Blob URL to avoid needing a separate bundled file.\nconst WORKLET_PROCESSOR_SOURCE = `\nclass PcmCaptureProcessor extends AudioWorkletProcessor {\n constructor() {\n super()\n this._buffer = new Float32Array(${PCM_CHUNK_SAMPLES})\n this._offset = 0\n }\n process(inputs, outputs, parameters) {\n const input = inputs[0]?.[0]\n if (!input) return true\n for (let i = 0; i < input.length; i++) {\n this._buffer[this._offset++] = input[i]\n if (this._offset >= this._buffer.length) {\n const int16 = new Int16Array(this._buffer.length)\n for (let j = 0; j < this._buffer.length; j++) {\n const s = Math.max(-1, Math.min(1, this._buffer[j]))\n int16[j] = s < 0 ? s * 0x8000 : s * 0x7FFF\n }\n this.port.postMessage(int16.buffer, [int16.buffer])\n this._offset = 0\n }\n }\n return true\n }\n}\nregisterProcessor('pcm-capture-processor', PcmCaptureProcessor)\n`\n\n// Safety net: if no TurnInfo events arrive while the user is in a speaking state,\n// reset speaking indicators to prevent the UI getting stuck.\nconst SAFETY_NET_TIMEOUT_MS = 2000\n\n// Reconnection constants\nconst INITIAL_RECONNECT_DELAY_MS = 1000\nconst MAX_RECONNECT_DELAY_MS = 30000\nconst RECONNECT_BACKOFF_MULTIPLIER = 2\nconst MAX_RECONNECT_ATTEMPTS = 5\n\n// STT Engine States\nenum STTState {\n Idle = 'Idle',\n Connecting = 'Connecting',\n Connected = 'Connected',\n Paused = 'Paused',\n Disconnected = 'Disconnected'\n}\n\n/**\n * Flux v2 TurnInfo message from unified 'message' event.\n * The v2 API delivers turn lifecycle events via data.type === 'TurnInfo'.\n * Note: all fields are at the top level — there is no nested turn_info property.\n */\ninterface TurnInfoMessage {\n type: 'TurnInfo'\n event: 'StartOfTurn' | 'Update' | 'EagerEndOfTurn' | 'TurnResumed' | 'EndOfTurn'\n transcript: string\n turn_index: number\n end_of_turn_confidence?: number\n words?: Array<{\n word: string\n confidence: number\n }>\n}\n\n/**\n * Flux v2 Error message indicating an unrecoverable error.\n * Note: SDK type is \"Error\" (not \"FatalError\") per ListenV2FatalError.\n */\ninterface FatalErrorMessage {\n type: 'Error'\n code: string\n description: string\n}\n\ninterface DeepgramTokenResponse {\n token: string\n api_url: string\n sdk_version: string\n expires_at: string\n}\n\nexport interface DeepgramFluxSTTOptions {\n // Backend configuration\n connectionUrl: string\n jwtToken: string\n\n // Session information\n sessionId: string\n\n // Deepgram Flux configuration\n model?: string\n language?: string\n\n /** End-of-turn confidence threshold (0.5-0.9). @default 0.7 */\n eotThreshold?: number\n /** Eager end-of-turn threshold (0.3-0.9). */\n eagerEotThreshold?: number\n /** End-of-turn timeout in ms. */\n eotTimeoutMs?: number\n /** Send ChatPrompt on EagerEndOfTurn (lower latency, risk of incomplete). @default true */\n useEagerEndOfTurn?: boolean\n\n /**\n * Safety net timeout in milliseconds. Resets speaking indicators if no TurnInfo\n * events arrive for this duration while in a speaking state. @default 2000\n */\n safetyNetTimeoutMs?: number\n\n /**\n * Keyterms to boost in transcription results.\n */\n keyterms?: string[]\n\n // Microphone configuration\n echoCancellation?: boolean\n noiseSuppression?: boolean\n autoGainControl?: boolean\n microphoneDeviceId?: string\n\n // Metadata and callbacks\n promptMetadata: PromptMetadata\n messages: Subject<UneeqMessage>\n sendMessage: (msg: DataChannelMessage) => void\n}\n\nexport class DeepgramFluxSTT implements SpeechRecognitionInterface {\n private connection: DeepgramV2Connection | null = null\n private state: STTState = STTState.Idle\n private shouldReconnect: boolean = true\n private stream: MediaStream | null = null\n private audioContext: AudioContext | null = null\n private workletNode: AudioWorkletNode | null = null\n\n // Reconnection state\n private reconnectAttempts: number = 0\n private reconnectDelay: number = INITIAL_RECONNECT_DELAY_MS\n private reconnectTimeoutId: NodeJS.Timeout | null = null\n\n // Digital human speaking state\n private digitalHumanSpeaking: boolean = false\n\n // User speaking state (for data channel messages to Renny)\n private isUserCurrentlySpeaking: boolean = false\n\n // UI speaking state (for UI indicator)\n private isUiShowingSpeaking: boolean = false\n\n // Track whether an eager prompt was already sent for the current turn\n private eagerPromptSentForTurn: boolean = false\n\n // Safety net: reset speaking state if Deepgram stalls mid-turn\n private safetyNetTimeoutId: NodeJS.Timeout | null = null\n\n // Debug: track audio chunks sent\n private audioChunksSent: number = 0\n\n constructor(private readonly options: DeepgramFluxSTTOptions) {\n // Apply defaults\n this.options.model = this.options.model || 'flux-general-en'\n this.options.language = this.options.language || 'en'\n this.options.eotThreshold = this.options.eotThreshold ?? 0.7\n this.options.useEagerEndOfTurn = this.options.useEagerEndOfTurn ?? true\n this.options.safetyNetTimeoutMs = this.options.safetyNetTimeoutMs ?? SAFETY_NET_TIMEOUT_MS\n\n this.options.echoCancellation = this.options.echoCancellation ?? true\n this.options.noiseSuppression = this.options.noiseSuppression ?? true\n this.options.autoGainControl = this.options.autoGainControl ?? true\n\n this.handleAppMessages()\n }\n\n // Main lifecycle methods\n public async startRecognition(): Promise<void> {\n Logger.info(`${LOG_PREFIX} Starting speech recognition`)\n this.shouldReconnect = true\n this.resetReconnectionState()\n await this.connect()\n }\n\n public async stopRecognition(): Promise<void> {\n Logger.info(`${LOG_PREFIX} Stopping speech recognition`)\n this.shouldReconnect = false\n this.clearReconnectTimeout()\n await this.disconnect()\n }\n\n public async pause(): Promise<boolean> {\n Logger.info(`${LOG_PREFIX} Pausing speech recognition`)\n this.state = STTState.Paused\n\n // Reset speaking states\n this.clearSafetyNet()\n this.resetSpeakingStates()\n this.eagerPromptSentForTurn = false\n\n // Disable audio tracks to stop sending audio, but keep microphone and connection alive\n if (this.stream) {\n this.stream.getTracks().forEach((track) => { track.enabled = false })\n Logger.debug(`${LOG_PREFIX} Audio tracks disabled`)\n }\n\n return true\n }\n\n public async resume(): Promise<boolean> {\n Logger.info(`${LOG_PREFIX} Resuming speech recognition`)\n\n if (this.state === STTState.Paused) {\n if (this.stream) {\n // Re-enable existing audio tracks (resume from normal pause)\n this.state = STTState.Connected\n this.stream.getTracks().forEach((track) => { track.enabled = true })\n Logger.debug(`${LOG_PREFIX} Audio tracks re-enabled`)\n return true\n }\n // Connection exists but no stream (paused during connect) — start microphone\n if (this.connection) {\n this.state = STTState.Connected\n await this.startMicrophone()\n return true\n }\n // No connection and no stream — reset state so connect() doesn't bail out\n this.state = STTState.Disconnected\n }\n\n // No connection — need full connect\n Logger.debug(`${LOG_PREFIX} Initiating connection`)\n await this.connect()\n return true\n }\n\n // Metadata management\n public setChatMetadata(chatMetadata: PromptMetadata): void {\n this.options.promptMetadata = chatMetadata\n }\n\n // Private methods\n private async connect(): Promise<void> {\n if (this.state === STTState.Connected) {\n Logger.warn(`${LOG_PREFIX} Already connected`)\n return\n }\n\n if (this.state === STTState.Connecting) {\n Logger.warn(`${LOG_PREFIX} Connection already in progress`)\n return\n }\n\n this.state = STTState.Connecting\n\n try {\n const tokenData = await this.getToken()\n Logger.info(`${LOG_PREFIX} Connecting to Deepgram Flux v2 — api_url=\"${tokenData.api_url}\", sdk_version=\"${tokenData.sdk_version}\", token_length=${tokenData.token?.length ?? 0}`)\n\n // CRITICAL: Must use { accessToken: token } format for temporary tokens\n const deepgram = new DeepgramClient({\n accessToken: tokenData.token,\n baseUrl: tokenData.api_url\n })\n\n // Build v2 connection options\n // Note: v2 API does NOT accept 'language' — for Flux, language is embedded in model name (e.g. flux-general-en)\n // Audio format: raw linear16 PCM with explicit encoding+sample_rate (per Flux quickstart docs)\n // Container formats (WebM/Opus) should auto-detect but don't produce TurnInfo events in practice\n const connectionOptions: Record<string, unknown> = {\n model: this.options.model,\n encoding: 'linear16',\n sample_rate: String(PCM_SAMPLE_RATE),\n // Always opt out of Deepgram's Model Improvement Program\n mip_opt_out: 'true',\n // Flux-specific v2 options\n ...(this.options.eotThreshold !== undefined && { eot_threshold: this.options.eotThreshold }),\n ...(this.options.eagerEotThreshold !== undefined && { eager_eot_threshold: this.options.eagerEotThreshold }),\n ...(this.options.eotTimeoutMs !== undefined && { eot_timeout_ms: this.options.eotTimeoutMs }),\n ...(this.options.keyterms && this.options.keyterms.length > 0 && { keyterm: this.options.keyterms }),\n }\n\n // v5 SDK: listen.v2.connect() returns a V2Socket wrapping a WebSocket.\n // Cast because the v5 SDK's TypeScript types don't expose .v2 directly.\n interface DeepgramV2API { connect(options: Record<string, unknown>): Promise<DeepgramV2Connection> }\n this.connection = await (deepgram.listen as unknown as { v2: DeepgramV2API }).v2.connect(connectionOptions)\n\n // Initiate the WebSocket and wait for it to open (with timeout).\n this.connection.connect()\n await Promise.race([\n this.connection.waitForOpen(),\n new Promise<void>((_, reject) =>\n setTimeout(() => reject(new Error('Connection timeout')), CONNECTION_TIMEOUT_MS)\n )\n ])\n\n // Don't overwrite Paused state (user may have paused during async connection)\n if ((this.state as STTState) !== STTState.Paused) {\n this.state = STTState.Connected\n }\n Logger.info(`${LOG_PREFIX} Connection opened`)\n\n // Now set up the persistent event handlers\n this.setupEventHandlers()\n\n // If user paused during async connection, stay paused — don't start microphone\n if ((this.state as STTState) === STTState.Paused) {\n Logger.info(`${LOG_PREFIX} Pause requested during connection — staying paused`)\n this.resetReconnectionState()\n return\n }\n\n // Start the microphone\n await this.startMicrophone()\n\n Logger.info(`${LOG_PREFIX} Connected successfully`)\n\n // Reset reconnection state on successful connection\n this.resetReconnectionState()\n } catch (error) {\n this.state = STTState.Disconnected\n Logger.error(`${LOG_PREFIX} Connection error`, Logger.serialiseError(error))\n\n // Emit a non-fatal transient error so the host can surface a \"reconnecting\"\n // indicator if it wants to. \n if (this.shouldReconnect) {\n this.emitTransientError(error)\n this.scheduleReconnect()\n }\n }\n }\n\n private async disconnect(): Promise<void> {\n if (this.state === STTState.Idle || (this.state === STTState.Disconnected && !this.connection)) {\n return\n }\n\n Logger.info(`${LOG_PREFIX} Disconnecting`)\n\n try {\n this.stopMicrophone()\n\n if (this.connection) {\n // Send CloseStream to let Deepgram flush any in-flight transcription\n try { this.connection.sendCloseStream({ type: 'CloseStream' }) } catch { /* best effort */ }\n this.connection.close()\n this.connection = null\n }\n } catch (error) {\n Logger.error(`${LOG_PREFIX} Disconnect error`, Logger.serialiseError(error))\n }\n\n // Reset speaking states\n this.clearSafetyNet()\n this.resetSpeakingStates()\n this.eagerPromptSentForTurn = false\n\n this.state = STTState.Disconnected\n this.clientMsgSend(new EnableMicrophoneUpdatedMessage(false))\n }\n\n private scheduleReconnect(): void {\n if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n Logger.error(`${LOG_PREFIX} Max reconnection attempts (${MAX_RECONNECT_ATTEMPTS}) reached`)\n this.clientMsgSend(new SessionErrorMessage(\n `Unable to connect to speech recognition service after ${MAX_RECONNECT_ATTEMPTS} attempts`\n ))\n return\n }\n\n this.reconnectAttempts++\n Logger.info(\n `${LOG_PREFIX} Scheduling reconnection attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} ` +\n `in ${this.reconnectDelay}ms`\n )\n\n this.reconnectTimeoutId = setTimeout(() => {\n void this.connect()\n }, this.reconnectDelay)\n\n // Exponential backoff\n this.reconnectDelay = Math.min(\n this.reconnectDelay * RECONNECT_BACKOFF_MULTIPLIER,\n MAX_RECONNECT_DELAY_MS\n )\n }\n\n private resetReconnectionState(): void {\n this.reconnectAttempts = 0\n this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS\n this.clearReconnectTimeout()\n }\n\n private clearReconnectTimeout(): void {\n if (this.reconnectTimeoutId) {\n clearTimeout(this.reconnectTimeoutId)\n this.reconnectTimeoutId = null\n }\n }\n\n private async getToken(): Promise<DeepgramTokenResponse> {\n const model = this.options.model || 'flux-general-en'\n const tokenEndpoint = `${this.options.connectionUrl}/speech-recognition-service/deepgram/token?model=${encodeURIComponent(model)}`\n\n const response = await fetch(tokenEndpoint, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.options.jwtToken}`,\n 'Content-Type': 'application/json'\n }\n })\n\n if (!response.ok) {\n throw new Error(`Token fetch failed: ${response.status} ${response.statusText}`)\n }\n\n return await response.json()\n }\n\n private async startMicrophone(): Promise<void> {\n try {\n Logger.info(`${LOG_PREFIX} Starting microphone`)\n\n // Stop any existing microphone/stream first to prevent orphaned resources\n this.stopMicrophone()\n\n // Get user media\n this.stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n deviceId: this.options.microphoneDeviceId ? { exact: this.options.microphoneDeviceId } : undefined,\n echoCancellation: this.options.echoCancellation,\n noiseSuppression: this.options.noiseSuppression,\n autoGainControl: this.options.autoGainControl\n }\n })\n\n // Check if user paused during the getUserMedia await\n if ((this.state as STTState) === STTState.Paused) {\n Logger.info(`${LOG_PREFIX} Paused during getUserMedia — keeping stream but disabling tracks`)\n this.stream.getTracks().forEach((track) => { track.enabled = false })\n return\n }\n\n // AudioWorklet captures raw linear16 PCM off the main thread.\n // The processor is inlined as a Blob URL to avoid a separate bundled file.\n this.audioContext = new AudioContext({ sampleRate: PCM_SAMPLE_RATE })\n const source = this.audioContext.createMediaStreamSource(this.stream)\n\n const blob = new Blob([WORKLET_PROCESSOR_SOURCE], { type: 'application/javascript' })\n const processorUrl = URL.createObjectURL(blob)\n await this.audioContext.audioWorklet.addModule(processorUrl)\n URL.revokeObjectURL(processorUrl)\n\n this.workletNode = new AudioWorkletNode(this.audioContext, 'pcm-capture-processor')\n this.audioChunksSent = 0\n\n this.workletNode.port.onmessage = (event: MessageEvent) => {\n if (!this.connection || this.state !== STTState.Connected) {\n return\n }\n this.connection.sendMedia(event.data as ArrayBuffer)\n this.audioChunksSent++\n if (this.audioChunksSent % 50 === 1) {\n Logger.info(`${LOG_PREFIX} Audio chunks sent: ${this.audioChunksSent}, size: ${(event.data as ArrayBuffer).byteLength} bytes`)\n }\n }\n\n source.connect(this.workletNode)\n this.workletNode.connect(this.audioContext.destination)\n\n Logger.info(`${LOG_PREFIX} Microphone started (linear16 PCM @ ${PCM_SAMPLE_RATE}Hz)`)\n\n // Notify that microphone is enabled\n this.clientMsgSend(new EnableMicrophoneUpdatedMessage(true))\n } catch (error) {\n Logger.error(`${LOG_PREFIX} Microphone error`, Logger.serialiseError(error))\n this.clientMsgSend(new DeviceErrorMessage(new Error(JSON.stringify(error))))\n }\n }\n\n private stopMicrophone(): void {\n if (this.workletNode) {\n this.workletNode.port.close()\n this.workletNode.disconnect()\n this.workletNode = null\n }\n\n if (this.audioContext) {\n void this.audioContext.close().catch(() => {})\n this.audioContext = null\n }\n\n if (this.stream) {\n this.stream.getTracks().forEach((track) => {\n track.stop()\n })\n this.stream = null\n }\n\n Logger.info(`${LOG_PREFIX} Microphone stopped`)\n }\n\n private setupEventHandlers(): void {\n if (!this.connection) {\n return\n }\n\n this.connection.on('open', () => {\n this.handleConnectionOpen()\n })\n\n // v2: all messages are unified under a single 'message' event,\n // discriminated by data.type ('TurnInfo', 'Connected', 'FatalError', etc.)\n this.connection.on('message', (data: unknown) => {\n if (data !== null && typeof data === 'object' && 'type' in data) {\n const typed = data as { type: string }\n if (typed.type === 'TurnInfo') {\n this.handleTurnInfo(data as unknown as TurnInfoMessage)\n } else if (typed.type === 'Connected') {\n Logger.info(`${LOG_PREFIX} v2 connection confirmed`)\n } else if (typed.type === 'Error') {\n this.handleFatalError(data as unknown as FatalErrorMessage)\n } else {\n Logger.debug(`${LOG_PREFIX} Unhandled v2 message type: ${typed.type}`)\n }\n }\n })\n\n this.connection.on('close', (event: unknown) => {\n this.handleConnectionClose(event)\n })\n\n this.connection.on('error', (error: unknown) => {\n // Emit a non-fatal transient error. A 'close' event will follow and drive\n // the reconnect machinery via handleConnectionClose() → scheduleReconnect().\n // SessionErrorMessage would be fatal in hosted-experience and is reserved\n // for MAX_RECONNECT_ATTEMPTS exhaustion. WebSocket error events are opaque\n // by design — extract what we can for logging.\n const detail: Record<string, unknown> = {}\n if (error instanceof Event) {\n detail['type'] = error.type\n detail['target'] = (error.target as { url?: string; readyState?: number } | null)?.url\n ?? (error.target as { url?: string; readyState?: number } | null)?.readyState\n ?? 'unknown'\n }\n Logger.error(`${LOG_PREFIX} WebSocket error event`, error, detail)\n this.emitTransientError(error)\n })\n }\n\n /**\n * Primary handler for Flux v2 TurnInfo events.\n * Routes to specific handlers based on the turn event type.\n */\n private handleTurnInfo(data: TurnInfoMessage): void {\n try {\n Logger.debug(`${LOG_PREFIX} TurnInfo event: ${data.event}, ` +\n `transcript=\"${(data.transcript || '').substring(0, 50)}${(data.transcript || '').length > 50 ? '...' : ''}\", ` +\n `turn_index=${data.turn_index}, eot_confidence=${data.end_of_turn_confidence ?? 'n/a'}`)\n\n switch (data.event) {\n case 'StartOfTurn':\n this.handleStartOfTurn(data)\n break\n case 'Update':\n this.handleUpdate(data)\n break\n case 'EagerEndOfTurn':\n this.handleEagerEndOfTurn(data)\n break\n case 'TurnResumed':\n this.handleTurnResumed(data)\n break\n case 'EndOfTurn':\n this.handleEndOfTurn(data)\n break\n default:\n Logger.debug(`${LOG_PREFIX} Unknown TurnInfo event: ${(data as { event?: string }).event}`)\n }\n\n } catch (error) {\n Logger.error(`${LOG_PREFIX} Error processing TurnInfo`, Logger.serialiseError(error))\n } finally {\n // Always rearm the safety net — even if a handler threw.\n // Without this, an exception prevents rearming and the mic gets stuck.\n this.resetSafetyNet()\n }\n }\n\n /**\n * StartOfTurn: User has begun speaking.\n * Emit UserStartedSpeaking and send UserSpeaking(Start) to data channel.\n */\n private handleStartOfTurn(turnInfo: TurnInfoMessage): void {\n Logger.debug(`${LOG_PREFIX} StartOfTurn: turn_index=${turnInfo.turn_index}`)\n\n this.eagerPromptSentForTurn = false\n\n // Don't send speaking signals yet — wait for first Update with actual transcript.\n // This prevents background noise from interrupting the digital human.\n }\n\n /**\n * Update: Interim transcript during the current turn.\n *\n * This is also the interruption trigger: the first Update with real transcript\n * while the avatar is speaking sends StopSpeaking immediately. No word thresholds —\n * Flux's own turn detection is the source of truth.\n */\n private handleUpdate(turnInfo: TurnInfoMessage): void {\n const transcript = turnInfo.transcript || ''\n if (transcript === '') {\n return\n }\n\n // First non-empty transcript in this turn — now signal that user is speaking.\n // Deferred from StartOfTurn so background noise doesn't interrupt the digital human.\n if (!this.isUiShowingSpeaking) {\n this.isUiShowingSpeaking = true\n this.clientMsgSend(new UserStartedSpeakingMessage())\n }\n\n if (!this.isUserCurrentlySpeaking) {\n this.isUserCurrentlySpeaking = true\n this.dataChannelMsgSend(new UserSpeaking(UserSpeakingState.Start))\n }\n\n // If the avatar is speaking, interrupt immediately.\n // Flux always allows interruption — it's a conversational model.\n // We only interrupt here — EagerEndOfTurn/EndOfTurn just check the resulting state.\n if (this.digitalHumanSpeaking) {\n Logger.info(`${LOG_PREFIX} User speech detected during avatar speaking — interrupting`)\n this.dataChannelMsgSend(new StopSpeaking())\n this.clientMsgSend(new AvatarInterruptedMessage())\n this.digitalHumanSpeaking = false\n }\n\n // Emit interim transcription for closed captions\n const result: SpeechTranscriptionResult = {\n transcript,\n final: false,\n confidence: this.calculateWordConfidence(turnInfo.words),\n language_code: this.options.language || ''\n }\n this.clientMsgSend(new SpeechTranscriptionMessage(result))\n }\n\n /**\n * EagerEndOfTurn: Model predicts the user might be done speaking.\n * If useEagerEndOfTurn is enabled, send ChatPrompt immediately for lower latency.\n *\n * If the avatar is still speaking (interruption disabled or Update hasn't fired yet),\n * the prompt is discarded — we can't send competing prompts while the avatar talks.\n */\n private handleEagerEndOfTurn(turnInfo: TurnInfoMessage): void {\n const transcript = turnInfo.transcript || ''\n Logger.debug(`${LOG_PREFIX} EagerEndOfTurn: confidence=${turnInfo.end_of_turn_confidence}, transcript=\"${transcript}\"`)\n\n if (!this.options.useEagerEndOfTurn) {\n Logger.debug(`${LOG_PREFIX} EagerEndOfTurn ignored (useEagerEndOfTurn is disabled)`)\n return\n }\n\n if (transcript.trim() === '') {\n return\n }\n\n if (this.digitalHumanSpeaking) {\n Logger.debug(`${LOG_PREFIX} EagerEndOfTurn ignored — avatar is speaking`)\n return\n }\n\n Logger.info(`${LOG_PREFIX} EagerEndOfTurn: sending prompt early: \"${transcript}\"`)\n this.eagerPromptSentForTurn = true\n this.sendChatPrompt(transcript)\n }\n\n /**\n * TurnResumed: Model detects the user continued speaking after an EagerEndOfTurn.\n * If an eager prompt was already sent, log a warning (can't unsend).\n */\n private handleTurnResumed(turnInfo: TurnInfoMessage): void {\n Logger.debug(`${LOG_PREFIX} TurnResumed: turn_index=${turnInfo.turn_index}`)\n\n if (this.eagerPromptSentForTurn) {\n Logger.warn(`${LOG_PREFIX} TurnResumed after eager prompt was already sent — cancelling via StopSpeaking`)\n this.dataChannelMsgSend(new StopSpeaking())\n }\n\n this.eagerPromptSentForTurn = false\n }\n\n /**\n * EndOfTurn: Model has determined the user has finished speaking.\n *\n * If the avatar is still speaking (interruption disabled), the prompt is discarded.\n * Otherwise, cancel any in-flight eager prompt and send the definitive transcript.\n */\n private handleEndOfTurn(turnInfo: TurnInfoMessage): void {\n const transcript = turnInfo.transcript || ''\n Logger.info(`${LOG_PREFIX} EndOfTurn: \"${transcript}\", confidence=${turnInfo.end_of_turn_confidence}`)\n\n if (transcript.trim() !== '') {\n // Emit final transcription for closed captions (always, even if prompt is discarded)\n const result: SpeechTranscriptionResult = {\n transcript,\n final: true,\n confidence: this.calculateWordConfidence(turnInfo.words),\n language_code: this.options.language || ''\n }\n this.clientMsgSend(new SpeechTranscriptionMessage(result))\n\n if (this.digitalHumanSpeaking) {\n Logger.warn(`${LOG_PREFIX} EndOfTurn: discarding prompt — avatar is still speaking (no Update interrupted)`)\n } else {\n // Normal flow: cancel any in-flight eager prompt, send definitive transcript.\n // StopSpeaking is idempotent — safe to send even if nothing is in-flight.\n if (this.eagerPromptSentForTurn) {\n Logger.debug(`${LOG_PREFIX} EndOfTurn: cancelling in-flight eager prompt`)\n }\n this.dataChannelMsgSend(new StopSpeaking())\n this.sendChatPrompt(transcript)\n }\n }\n\n // Reset turn state\n this.eagerPromptSentForTurn = false\n this.resetSpeakingStates()\n }\n\n /**\n * Handle FatalError from v2 API.\n */\n private handleFatalError(data: FatalErrorMessage): void {\n Logger.error(`${LOG_PREFIX} Fatal error from Deepgram: ${data.code} — ${data.description}`)\n this.clientMsgSend(new SessionErrorMessage(`Deepgram error: ${data.code} — ${data.description}`))\n }\n\n /**\n * Calculate average confidence from word-level data, or return a default.\n */\n private calculateWordConfidence(words?: Array<{ confidence: number }>): number {\n if (!words || words.length === 0) {\n return 1.0\n }\n const sum = words.reduce((acc, w) => acc + w.confidence, 0)\n return sum / words.length\n }\n\n private handleAppMessages(): void {\n this.options.messages.subscribe((msg) => {\n switch (msg.uneeqMessageType) {\n case UneeqMessageType.AvatarStartedSpeaking:\n this.digitalHumanSpeaking = true\n break\n\n case UneeqMessageType.PromptResult: {\n const promptResultMessage = msg as PromptResultMessage\n if (!promptResultMessage.promptResult.success) {\n this.handleSpeakingEnd()\n }\n break\n }\n\n case UneeqMessageType.AvatarAnswer: {\n const answer = msg as AvatarAnswerMessage\n if (answer.answerSpeech.replace(/<[^>]*>/g, '') === '') {\n this.handleSpeakingEnd()\n }\n break\n }\n\n case UneeqMessageType.AvatarStoppedSpeaking: {\n this.handleSpeakingEnd()\n break\n }\n\n case UneeqMessageType.SessionEnded: {\n this.shouldReconnect = false\n void this.stopRecognition()\n break\n }\n\n case UneeqMessageType.SessionReconnecting: {\n this.handleSpeakingEnd()\n this.shouldReconnect = false\n void this.stopRecognition()\n break\n }\n\n case UneeqMessageType.CustomMetadataUpdated: {\n this.options.promptMetadata = (msg as CustomMetadataUpdated).chatMetadata\n break\n }\n\n case UneeqMessageType.SessionBackendError: {\n this.handleSpeakingEnd()\n break\n }\n\n default:\n }\n })\n }\n\n private handleConnectionOpen(): void {\n if (this.state !== STTState.Paused) {\n this.state = STTState.Connected\n }\n }\n\n private handleConnectionClose(event?: unknown): void {\n const code = (event as { code?: number })?.code ?? 'unknown'\n const reason = (event as { reason?: string })?.reason ?? ''\n Logger.info(`${LOG_PREFIX} Connection closed — code=${code}, reason=\"${reason}\"`)\n\n if (this.state === STTState.Paused) {\n Logger.info(`${LOG_PREFIX} Connection closed while paused — will reconnect on resume`)\n this.connection = null\n this.stopMicrophone()\n this.clearSafetyNet()\n this.resetSpeakingStates()\n return\n }\n\n this.state = STTState.Disconnected\n this.clearSafetyNet()\n this.resetSpeakingStates()\n this.eagerPromptSentForTurn = false\n this.clientMsgSend(new EnableMicrophoneUpdatedMessage(false))\n\n if (this.shouldReconnect) {\n Logger.info(`${LOG_PREFIX} Unexpected disconnect, attempting reconnection...`)\n this.scheduleReconnect()\n }\n }\n\n /**\n * Emit a non-fatal transient error to the host. Used when a single\n * connect/reconnect attempt fails or a recoverable WebSocket error fires.\n * The reconnect machinery (scheduleReconnect) will continue retrying; the\n * host receives this as an informational signal, not a fatal one.\n * Compare with the fatal `SessionErrorMessage` emitted from\n * scheduleReconnect() only when MAX_RECONNECT_ATTEMPTS is exhausted, and\n * with `handleFatalError` for Deepgram-protocol-level Error messages.\n */\n private emitTransientError(error: unknown): void {\n const message = error instanceof Error ? error.message : String(error)\n this.clientMsgSend(new SpeechRecognitionTransientErrorMessage(message))\n }\n\n private sendChatPrompt(transcript: string): void {\n if (transcript && transcript.trim() !== '') {\n if (this.options.language) {\n this.options.promptMetadata.userSpokenLocale = this.options.language\n }\n\n this.dataChannelMsgSend(new ChatPrompt(transcript, this.options.promptMetadata))\n }\n }\n\n private handleSpeakingEnd(): void {\n this.digitalHumanSpeaking = false\n }\n\n /**\n * Reset the safety net timer. Called on every TurnInfo event.\n * If the user is in a speaking state and no events arrive for SAFETY_NET_TIMEOUT_MS,\n * the speaking indicators are reset to prevent the UI getting stuck.\n */\n private resetSafetyNet(): void {\n this.clearSafetyNet()\n\n if (this.isUiShowingSpeaking || this.isUserCurrentlySpeaking) {\n this.safetyNetTimeoutId = setTimeout(() => {\n Logger.warn(`${LOG_PREFIX} Safety net: no TurnInfo events for ${this.options.safetyNetTimeoutMs}ms while speaking — resetting`)\n this.resetSpeakingStates()\n }, this.options.safetyNetTimeoutMs)\n }\n }\n\n private clearSafetyNet(): void {\n if (this.safetyNetTimeoutId) {\n clearTimeout(this.safetyNetTimeoutId)\n this.safetyNetTimeoutId = null\n }\n }\n\n /**\n * Reset speaking states and send appropriate stop messages.\n */\n private resetSpeakingStates(): void {\n if (this.isUserCurrentlySpeaking) {\n this.isUserCurrentlySpeaking = false\n this.dataChannelMsgSend(new UserSpeaking(UserSpeakingState.Stop))\n }\n\n if (this.isUiShowingSpeaking) {\n this.isUiShowingSpeaking = false\n this.clientMsgSend(new UserStoppedSpeakingMessage())\n }\n }\n\n // Send a message on the data channel to renderer\n private dataChannelMsgSend(msg: DataChannelMessage): void {\n this.options.sendMessage(msg)\n }\n\n // Send a message to the client implementation, i.e. hosted experience\n private clientMsgSend(msg: UneeqMessage): void {\n this.options.messages.next(msg)\n }\n}\n"],"names":["LOG_PREFIX","STTState","DeepgramFluxSTT","options","connection","state","Idle","shouldReconnect","stream","audioContext","workletNode","reconnectAttempts","reconnectDelay","reconnectTimeoutId","digitalHumanSpeaking","isUserCurrentlySpeaking","isUiShowingSpeaking","eagerPromptSentForTurn","safetyNetTimeoutId","audioChunksSent","constructor","this","model","language","eotThreshold","useEagerEndOfTurn","safetyNetTimeoutMs","echoCancellation","noiseSuppression","autoGainControl","handleAppMessages","startRecognition","info","resetReconnectionState","connect","stopRecognition","clearReconnectTimeout","disconnect","pause","Paused","clearSafetyNet","resetSpeakingStates","getTracks","forEach","track","enabled","debug","resume","Connected","startMicrophone","Disconnected","setChatMetadata","chatMetadata","promptMetadata","Connecting","tokenData","getToken","api_url","sdk_version","token","length","deepgram","accessToken","baseUrl","connectionOptions","encoding","sample_rate","String","mip_opt_out","undefined","eot_threshold","eagerEotThreshold","eager_eot_threshold","eotTimeoutMs","eot_timeout_ms","keyterms","keyterm","listen","v2","Promise","race","waitForOpen","_","reject","setTimeout","Error","setupEventHandlers","error","serialiseError","emitTransientError","scheduleReconnect","warn","stopMicrophone","sendCloseStream","type","close","clientMsgSend","Math","min","clearTimeout","tokenEndpoint","connectionUrl","encodeURIComponent","response","fetch","method","headers","Authorization","jwtToken","ok","status","statusText","json","navigator","mediaDevices","getUserMedia","audio","deviceId","microphoneDeviceId","exact","AudioContext","sampleRate","source","createMediaStreamSource","blob","Blob","processorUrl","URL","createObjectURL","audioWorklet","addModule","revokeObjectURL","AudioWorkletNode","port","onmessage","event","sendMedia","data","byteLength","destination","JSON","stringify","catch","stop","on","handleConnectionOpen","typed","handleTurnInfo","handleFatalError","handleConnectionClose","detail","Event","target","url","readyState","transcript","substring","turn_index","end_of_turn_confidence","handleStartOfTurn","handleUpdate","handleEagerEndOfTurn","handleTurnResumed","handleEndOfTurn","resetSafetyNet","turnInfo","dataChannelMsgSend","Start","result","final","confidence","calculateWordConfidence","words","language_code","trim","sendChatPrompt","code","description","reduce","acc","w","messages","subscribe","msg","uneeqMessageType","AvatarStartedSpeaking","PromptResult","promptResult","success","handleSpeakingEnd","AvatarAnswer","answerSpeech","replace","AvatarStoppedSpeaking","SessionEnded","SessionReconnecting","CustomMetadataUpdated","SessionBackendError","reason","message","userSpokenLocale","Stop","sendMessage","next"],"sourceRoot":""}
1
+ {"version":3,"file":"363.index.js","mappings":"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","sources":["webpack://Uneeq/./src/deepgram-flux-stt.ts"],"sourcesContent":["import { type Subject } from 'rxjs'\nimport Logger from './lib/logger'\nimport {\n UserStartedSpeakingMessage,\n UserStoppedSpeakingMessage,\n SpeechTranscriptionMessage,\n EnableMicrophoneUpdatedMessage,\n SessionErrorMessage,\n SpeechRecognitionTransientErrorMessage,\n DeviceErrorMessage,\n AvatarInterruptedMessage,\n PromptRequestMessage,\n type UneeqMessage,\n UneeqMessageType,\n type PromptResultMessage,\n type AvatarAnswerMessage,\n type CustomMetadataUpdated,\n} from './types/UneeqMessages'\nimport { type SpeechTranscriptionResult } from './types/SpeechTranscriptionResult'\nimport { type DataChannelMessage } from './webrtc-data-channel/DataChannelMessage'\nimport { UserSpeaking, UserSpeakingState } from './webrtc-data-channel/messages/UserSpeaking'\nimport { ChatPrompt } from './webrtc-data-channel/messages/ChatPrompt'\nimport { StopSpeaking } from './webrtc-data-channel/messages/StopSpeaking'\nimport { type PromptMetadata } from './types/PromptMetadata'\nimport { type PromptRequest } from './types/PromptRequest'\nimport { uuidv4 } from './lib/uuid'\nimport { DeepgramClient } from '@deepgram/sdk'\nimport { type SpeechRecognitionInterface } from './types/SpeechRecognitionInterface'\n\n// Local interface for the Deepgram v2 connection — duck-typed to avoid coupling to SDK type names\ninterface DeepgramV2Connection {\n on(event: string, handler: (...args: unknown[]) => void): void\n sendMedia(data: ArrayBuffer): void\n sendListenV2Configure(config: Record<string, unknown>): void\n sendCloseStream(message: { type: string }): void\n connect(): void\n waitForOpen(): Promise<unknown>\n close(): void\n}\n\n// Constants\nconst LOG_PREFIX = '[Deepgram Flux STT]'\nconst CONNECTION_TIMEOUT_MS = 10000\n// PCM audio configuration — v2 API requires explicit encoding (no container auto-detection)\nconst PCM_SAMPLE_RATE = 16000\n// Deepgram recommends 80ms audio chunks for optimal Flux latency.\n// At 16kHz mono, 80ms = 1280 samples = 2560 bytes of int16.\n// AudioWorklet processes 128 samples per render quantum, so we accumulate 10 quanta.\nconst PCM_CHUNK_SAMPLES = 1280\n\n// AudioWorklet processor source — runs off the main thread.\n// Inlined as a Blob URL to avoid needing a separate bundled file.\nconst WORKLET_PROCESSOR_SOURCE = `\nclass PcmCaptureProcessor extends AudioWorkletProcessor {\n constructor() {\n super()\n this._buffer = new Float32Array(${PCM_CHUNK_SAMPLES})\n this._offset = 0\n }\n process(inputs, outputs, parameters) {\n const input = inputs[0]?.[0]\n if (!input) return true\n for (let i = 0; i < input.length; i++) {\n this._buffer[this._offset++] = input[i]\n if (this._offset >= this._buffer.length) {\n const int16 = new Int16Array(this._buffer.length)\n for (let j = 0; j < this._buffer.length; j++) {\n const s = Math.max(-1, Math.min(1, this._buffer[j]))\n int16[j] = s < 0 ? s * 0x8000 : s * 0x7FFF\n }\n this.port.postMessage(int16.buffer, [int16.buffer])\n this._offset = 0\n }\n }\n return true\n }\n}\nregisterProcessor('pcm-capture-processor', PcmCaptureProcessor)\n`\n\n// Safety net: if no TurnInfo events arrive while the user is in a speaking state,\n// reset speaking indicators to prevent the UI getting stuck.\nconst SAFETY_NET_TIMEOUT_MS = 2000\n\n// Reconnection constants\nconst INITIAL_RECONNECT_DELAY_MS = 1000\nconst MAX_RECONNECT_DELAY_MS = 30000\nconst RECONNECT_BACKOFF_MULTIPLIER = 2\nconst MAX_RECONNECT_ATTEMPTS = 5\n\n// Backchannel filter: while the avatar is speaking, a transcript of\n// fewer than this many words (\"yeah\", \"uh-huh\", \"I see\") is treated as\n// an acknowledgment — we don't barge in and we don't forward it as a\n// ChatPrompt. Filter is scoped to during-avatar-speech only; short\n// utterances at other times are processed normally. See\n// docs/DEEPGRAM_FLUX.md § \"Barge-in (backchannel filter)\".\nconst BARGE_IN_WORD_THRESHOLD = 3\n\n// STT Engine States\nenum STTState {\n Idle = 'Idle',\n Connecting = 'Connecting',\n Connected = 'Connected',\n Paused = 'Paused',\n Disconnected = 'Disconnected'\n}\n\n/**\n * Flux v2 TurnInfo message from unified 'message' event.\n * The v2 API delivers turn lifecycle events via data.type === 'TurnInfo'.\n * Note: all fields are at the top level — there is no nested turn_info property.\n */\ninterface TurnInfoMessage {\n type: 'TurnInfo'\n event: 'StartOfTurn' | 'Update' | 'EagerEndOfTurn' | 'TurnResumed' | 'EndOfTurn'\n transcript: string\n turn_index: number\n end_of_turn_confidence?: number\n words?: Array<{\n word: string\n confidence: number\n }>\n}\n\n/**\n * Flux v2 Error message indicating an unrecoverable error.\n * Note: SDK type is \"Error\" (not \"FatalError\") per ListenV2FatalError.\n */\ninterface FatalErrorMessage {\n type: 'Error'\n code: string\n description: string\n}\n\ninterface DeepgramTokenResponse {\n token: string\n api_url: string\n sdk_version: string\n expires_at: string\n}\n\nexport interface DeepgramFluxSTTOptions {\n // Backend configuration\n connectionUrl: string\n jwtToken: string\n\n // Session information\n sessionId: string\n\n // Deepgram Flux configuration\n model?: string\n language?: string\n\n /** End-of-turn confidence threshold (0.5-0.9). @default 0.85 */\n eotThreshold?: number\n /**\n * Eager end-of-turn threshold (0.3-0.9). When > 0, Flux emits\n * EagerEndOfTurn / TurnResumed events for early prompt prep. Set\n * to `0` to disable eager mode entirely — the threshold isn't sent\n * to Flux, so no EagerEndOfTurn events fire and EndOfTurn becomes\n * the sole commit point.\n * See docs/DEEPGRAM_FLUX.md for the event-handling contract.\n * @default 0.5\n */\n eagerEotThreshold?: number\n /** End-of-turn timeout in ms (silence backstop for stuck turns). @default 3000 */\n eotTimeoutMs?: number\n\n /**\n * Maximum elapsed turn time (from first non-empty Update) before\n * EagerEndOfTurn stops firing chat_prompt. Long turns then defer\n * to the canonical EndOfTurn commit. Set to `0` to disable.\n * See docs/DEEPGRAM_FLUX.md § \"Why the eager-EOT duration gate?\".\n * @default 5000\n */\n eagerMaxTurnDurationMs?: number\n\n /**\n * Safety net timeout in milliseconds. Resets speaking indicators if no TurnInfo\n * events arrive for this duration while in a speaking state. @default 2000\n */\n safetyNetTimeoutMs?: number\n\n /**\n * Keyterms to boost in transcription results.\n */\n keyterms?: string[]\n\n // Microphone configuration\n echoCancellation?: boolean\n noiseSuppression?: boolean\n autoGainControl?: boolean\n microphoneDeviceId?: string\n\n // Metadata and callbacks\n promptMetadata: PromptMetadata\n messages: Subject<UneeqMessage>\n sendMessage: (msg: DataChannelMessage) => void\n}\n\nexport class DeepgramFluxSTT implements SpeechRecognitionInterface {\n private connection: DeepgramV2Connection | null = null\n private state: STTState = STTState.Idle\n private shouldReconnect: boolean = true\n private stream: MediaStream | null = null\n private audioContext: AudioContext | null = null\n private workletNode: AudioWorkletNode | null = null\n\n // Reconnection state\n private reconnectAttempts: number = 0\n private reconnectDelay: number = INITIAL_RECONNECT_DELAY_MS\n private reconnectTimeoutId: NodeJS.Timeout | null = null\n\n // Audio-playback marker (true while Renny's audio track is producing\n // sound). NOT a request-state marker — Renny can be in LLM/TTS for\n // seconds without this flipping. Only used as a hint for the\n // backchannel filter. See AUDIO_PIPELINE.md §6.\n private digitalHumanSpeaking: boolean = false\n\n // Pending chat_prompt awaiting deferred PromptRequest emission at the\n // canonical commit point. See docs/DEEPGRAM_FLUX.md § \"Why only one\n // host-facing PromptRequest per turn?\".\n private pendingPromptRequest: {\n prompt: string\n requestId: string\n metadata: PromptMetadata\n speakingDurationMs: number\n } | null = null\n\n // User speaking state (for data channel messages to Renny)\n private isUserCurrentlySpeaking: boolean = false\n\n // UI speaking state (for UI indicator)\n private isUiShowingSpeaking: boolean = false\n\n // Track whether an eager prompt was already sent for the current turn\n private eagerPromptSentForTurn: boolean = false\n\n // Wall-clock timestamp (ms) of the first non-empty transcript Update in\n // the current turn. Anchors the eagerMaxTurnDurationMs gate. Null between\n // turns and during lifecycle resets. Anchored on first real speech rather\n // than StartOfTurn so VAD-only / throat-clear preludes don't pre-burn\n // the budget.\n private turnStartedAt: number | null = null\n\n // Safety net: reset speaking state if Deepgram stalls mid-turn\n private safetyNetTimeoutId: NodeJS.Timeout | null = null\n\n // Debug: track audio chunks sent\n private audioChunksSent: number = 0\n\n constructor(private readonly options: DeepgramFluxSTTOptions) {\n // Apply defaults\n this.options.model = this.options.model || 'flux-general-en'\n this.options.language = this.options.language || 'en'\n this.options.eotThreshold = this.options.eotThreshold ?? 0.85\n this.options.eagerEotThreshold = this.options.eagerEotThreshold ?? 0.5\n this.options.eotTimeoutMs = this.options.eotTimeoutMs ?? 3000\n this.options.eagerMaxTurnDurationMs = this.options.eagerMaxTurnDurationMs ?? 5000\n this.options.safetyNetTimeoutMs = this.options.safetyNetTimeoutMs ?? SAFETY_NET_TIMEOUT_MS\n\n this.options.echoCancellation = this.options.echoCancellation ?? true\n this.options.noiseSuppression = this.options.noiseSuppression ?? true\n this.options.autoGainControl = this.options.autoGainControl ?? true\n\n const eagerLabel = this.options.eagerEotThreshold === 0 ? 'disabled' : `${this.options.eagerEotThreshold}`\n const maxDurLabel = this.options.eagerMaxTurnDurationMs === 0 ? 'disabled' : `${this.options.eagerMaxTurnDurationMs}ms`\n Logger.debug(`${LOG_PREFIX} init — features: pure-flux-event-flow, eot_threshold=${this.options.eotThreshold}, eager_eot_threshold=${eagerLabel}, eot_timeout_ms=${this.options.eotTimeoutMs}, eager_max_turn_duration_ms=${maxDurLabel}`)\n\n this.handleAppMessages()\n }\n\n // Main lifecycle methods\n public async startRecognition(): Promise<void> {\n Logger.info(`${LOG_PREFIX} Starting speech recognition`)\n this.shouldReconnect = true\n this.resetReconnectionState()\n await this.connect()\n }\n\n public async stopRecognition(): Promise<void> {\n Logger.info(`${LOG_PREFIX} Stopping speech recognition`)\n this.shouldReconnect = false\n this.clearReconnectTimeout()\n await this.disconnect()\n }\n\n public async pause(): Promise<boolean> {\n Logger.info(`${LOG_PREFIX} Pausing speech recognition`)\n this.state = STTState.Paused\n\n // Reset speaking states\n this.clearSafetyNet()\n this.resetSpeakingStates()\n this.eagerPromptSentForTurn = false\n this.turnStartedAt = null\n this.pendingPromptRequest = null\n\n // Disable audio tracks to stop sending audio, but keep microphone and connection alive\n if (this.stream) {\n this.stream.getTracks().forEach((track) => { track.enabled = false })\n Logger.debug(`${LOG_PREFIX} Audio tracks disabled`)\n }\n\n return true\n }\n\n public async resume(): Promise<boolean> {\n Logger.info(`${LOG_PREFIX} Resuming speech recognition`)\n\n if (this.state === STTState.Paused) {\n if (this.stream) {\n // Re-enable existing audio tracks (resume from normal pause)\n this.state = STTState.Connected\n this.stream.getTracks().forEach((track) => { track.enabled = true })\n Logger.debug(`${LOG_PREFIX} Audio tracks re-enabled`)\n return true\n }\n // Connection exists but no stream (paused during connect) — start microphone\n if (this.connection) {\n this.state = STTState.Connected\n await this.startMicrophone()\n return true\n }\n // No connection and no stream — reset state so connect() doesn't bail out\n this.state = STTState.Disconnected\n }\n\n // No connection — need full connect\n Logger.debug(`${LOG_PREFIX} Initiating connection`)\n await this.connect()\n return true\n }\n\n // Metadata management\n public setChatMetadata(chatMetadata: PromptMetadata): void {\n this.options.promptMetadata = chatMetadata\n }\n\n // Private methods\n private async connect(): Promise<void> {\n if (this.state === STTState.Connected) {\n Logger.warn(`${LOG_PREFIX} Already connected`)\n return\n }\n\n if (this.state === STTState.Connecting) {\n Logger.warn(`${LOG_PREFIX} Connection already in progress`)\n return\n }\n\n this.state = STTState.Connecting\n\n try {\n const tokenData = await this.getToken()\n Logger.info(`${LOG_PREFIX} Connecting to Deepgram Flux v2 — api_url=\"${tokenData.api_url}\", sdk_version=\"${tokenData.sdk_version}\", token_length=${tokenData.token?.length ?? 0}`)\n\n // CRITICAL: Must use { accessToken: token } format for temporary tokens\n const deepgram = new DeepgramClient({\n accessToken: tokenData.token,\n baseUrl: tokenData.api_url\n })\n\n // Build v2 connection options\n // Note: v2 API does NOT accept 'language' — for Flux, language is embedded in model name (e.g. flux-general-en)\n // Audio format: raw linear16 PCM with explicit encoding+sample_rate (per Flux quickstart docs)\n // Container formats (WebM/Opus) should auto-detect but don't produce TurnInfo events in practice\n const connectionOptions: Record<string, unknown> = {\n model: this.options.model,\n encoding: 'linear16',\n sample_rate: String(PCM_SAMPLE_RATE),\n // Always opt out of Deepgram's Model Improvement Program\n mip_opt_out: 'true',\n // Flux-specific v2 options\n ...(this.options.eotThreshold !== undefined && { eot_threshold: this.options.eotThreshold }),\n // 0 disables eager mode entirely (don't send the threshold to\n // Flux; no EagerEndOfTurn / TurnResumed events will fire).\n ...(this.options.eagerEotThreshold !== undefined && this.options.eagerEotThreshold > 0 && { eager_eot_threshold: this.options.eagerEotThreshold }),\n ...(this.options.eotTimeoutMs !== undefined && { eot_timeout_ms: this.options.eotTimeoutMs }),\n ...(this.options.keyterms && this.options.keyterms.length > 0 && { keyterm: this.options.keyterms }),\n }\n\n // v5 SDK: listen.v2.connect() returns a V2Socket wrapping a WebSocket.\n // Cast because the v5 SDK's TypeScript types don't expose .v2 directly.\n interface DeepgramV2API { connect(options: Record<string, unknown>): Promise<DeepgramV2Connection> }\n this.connection = await (deepgram.listen as unknown as { v2: DeepgramV2API }).v2.connect(connectionOptions)\n\n // Initiate the WebSocket and wait for it to open (with timeout).\n this.connection.connect()\n await Promise.race([\n this.connection.waitForOpen(),\n new Promise<void>((_, reject) =>\n setTimeout(() => reject(new Error('Connection timeout')), CONNECTION_TIMEOUT_MS)\n )\n ])\n\n // Don't overwrite Paused state (user may have paused during async connection)\n if ((this.state as STTState) !== STTState.Paused) {\n this.state = STTState.Connected\n }\n Logger.info(`${LOG_PREFIX} Connection opened`)\n\n // Now set up the persistent event handlers\n this.setupEventHandlers()\n\n // If user paused during async connection, stay paused — don't start microphone\n if ((this.state as STTState) === STTState.Paused) {\n Logger.info(`${LOG_PREFIX} Pause requested during connection — staying paused`)\n this.resetReconnectionState()\n return\n }\n\n // Start the microphone\n await this.startMicrophone()\n\n Logger.info(`${LOG_PREFIX} Connected successfully`)\n\n // Reset reconnection state on successful connection\n this.resetReconnectionState()\n } catch (error) {\n this.state = STTState.Disconnected\n Logger.error(`${LOG_PREFIX} Connection error`, Logger.serialiseError(error))\n\n // Emit a non-fatal transient error so the host can surface a \"reconnecting\"\n // indicator if it wants to. \n if (this.shouldReconnect) {\n this.emitTransientError(error)\n this.scheduleReconnect()\n }\n }\n }\n\n private async disconnect(): Promise<void> {\n if (this.state === STTState.Idle || (this.state === STTState.Disconnected && !this.connection)) {\n return\n }\n\n Logger.info(`${LOG_PREFIX} Disconnecting`)\n\n try {\n this.stopMicrophone()\n\n if (this.connection) {\n // Send CloseStream to let Deepgram flush any in-flight transcription\n try { this.connection.sendCloseStream({ type: 'CloseStream' }) } catch { /* best effort */ }\n this.connection.close()\n this.connection = null\n }\n } catch (error) {\n Logger.error(`${LOG_PREFIX} Disconnect error`, Logger.serialiseError(error))\n }\n\n // Reset speaking states\n this.clearSafetyNet()\n this.resetSpeakingStates()\n this.eagerPromptSentForTurn = false\n this.turnStartedAt = null\n this.pendingPromptRequest = null\n\n this.state = STTState.Disconnected\n this.clientMsgSend(new EnableMicrophoneUpdatedMessage(false))\n }\n\n private scheduleReconnect(): void {\n if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n Logger.error(`${LOG_PREFIX} Max reconnection attempts (${MAX_RECONNECT_ATTEMPTS}) reached`)\n this.clientMsgSend(new SessionErrorMessage(\n `Unable to connect to speech recognition service after ${MAX_RECONNECT_ATTEMPTS} attempts`\n ))\n return\n }\n\n this.reconnectAttempts++\n Logger.info(\n `${LOG_PREFIX} Scheduling reconnection attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} ` +\n `in ${this.reconnectDelay}ms`\n )\n\n this.reconnectTimeoutId = setTimeout(() => {\n void this.connect()\n }, this.reconnectDelay)\n\n // Exponential backoff\n this.reconnectDelay = Math.min(\n this.reconnectDelay * RECONNECT_BACKOFF_MULTIPLIER,\n MAX_RECONNECT_DELAY_MS\n )\n }\n\n private resetReconnectionState(): void {\n this.reconnectAttempts = 0\n this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS\n this.clearReconnectTimeout()\n }\n\n private clearReconnectTimeout(): void {\n if (this.reconnectTimeoutId) {\n clearTimeout(this.reconnectTimeoutId)\n this.reconnectTimeoutId = null\n }\n }\n\n private async getToken(): Promise<DeepgramTokenResponse> {\n const model = this.options.model || 'flux-general-en'\n const tokenEndpoint = `${this.options.connectionUrl}/speech-recognition-service/deepgram/token?model=${encodeURIComponent(model)}`\n\n const response = await fetch(tokenEndpoint, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.options.jwtToken}`,\n 'Content-Type': 'application/json'\n }\n })\n\n if (!response.ok) {\n throw new Error(`Token fetch failed: ${response.status} ${response.statusText}`)\n }\n\n return await response.json()\n }\n\n private async startMicrophone(): Promise<void> {\n try {\n Logger.info(`${LOG_PREFIX} Starting microphone`)\n\n // Stop any existing microphone/stream first to prevent orphaned resources\n this.stopMicrophone()\n\n // Get user media\n this.stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n deviceId: this.options.microphoneDeviceId ? { exact: this.options.microphoneDeviceId } : undefined,\n echoCancellation: this.options.echoCancellation,\n noiseSuppression: this.options.noiseSuppression,\n autoGainControl: this.options.autoGainControl\n }\n })\n\n // Check if user paused during the getUserMedia await\n if ((this.state as STTState) === STTState.Paused) {\n Logger.info(`${LOG_PREFIX} Paused during getUserMedia — keeping stream but disabling tracks`)\n this.stream.getTracks().forEach((track) => { track.enabled = false })\n return\n }\n\n // AudioWorklet captures raw linear16 PCM off the main thread.\n // The processor is inlined as a Blob URL to avoid a separate bundled file.\n this.audioContext = new AudioContext({ sampleRate: PCM_SAMPLE_RATE })\n const source = this.audioContext.createMediaStreamSource(this.stream)\n\n const blob = new Blob([WORKLET_PROCESSOR_SOURCE], { type: 'application/javascript' })\n const processorUrl = URL.createObjectURL(blob)\n await this.audioContext.audioWorklet.addModule(processorUrl)\n URL.revokeObjectURL(processorUrl)\n\n this.workletNode = new AudioWorkletNode(this.audioContext, 'pcm-capture-processor')\n this.audioChunksSent = 0\n\n this.workletNode.port.onmessage = (event: MessageEvent) => {\n if (!this.connection || this.state !== STTState.Connected) {\n return\n }\n this.connection.sendMedia(event.data as ArrayBuffer)\n this.audioChunksSent++\n if (this.audioChunksSent % 50 === 1) {\n Logger.debug(`${LOG_PREFIX} Audio chunks sent: ${this.audioChunksSent}, size: ${(event.data as ArrayBuffer).byteLength} bytes`)\n }\n }\n\n source.connect(this.workletNode)\n this.workletNode.connect(this.audioContext.destination)\n\n Logger.info(`${LOG_PREFIX} Microphone started (linear16 PCM @ ${PCM_SAMPLE_RATE}Hz)`)\n\n // Notify that microphone is enabled\n this.clientMsgSend(new EnableMicrophoneUpdatedMessage(true))\n } catch (error) {\n Logger.error(`${LOG_PREFIX} Microphone error`, Logger.serialiseError(error))\n this.clientMsgSend(new DeviceErrorMessage(new Error(JSON.stringify(error))))\n }\n }\n\n private stopMicrophone(): void {\n if (this.workletNode) {\n this.workletNode.port.close()\n this.workletNode.disconnect()\n this.workletNode = null\n }\n\n if (this.audioContext) {\n void this.audioContext.close().catch(() => {})\n this.audioContext = null\n }\n\n if (this.stream) {\n this.stream.getTracks().forEach((track) => {\n track.stop()\n })\n this.stream = null\n }\n\n Logger.info(`${LOG_PREFIX} Microphone stopped`)\n }\n\n private setupEventHandlers(): void {\n if (!this.connection) {\n return\n }\n\n this.connection.on('open', () => {\n this.handleConnectionOpen()\n })\n\n // v2: all messages are unified under a single 'message' event,\n // discriminated by data.type ('TurnInfo', 'Connected', 'FatalError', etc.)\n this.connection.on('message', (data: unknown) => {\n if (data !== null && typeof data === 'object' && 'type' in data) {\n const typed = data as { type: string }\n if (typed.type === 'TurnInfo') {\n this.handleTurnInfo(data as unknown as TurnInfoMessage)\n } else if (typed.type === 'Connected') {\n Logger.info(`${LOG_PREFIX} v2 connection confirmed`)\n } else if (typed.type === 'Error') {\n this.handleFatalError(data as unknown as FatalErrorMessage)\n } else {\n Logger.debug(`${LOG_PREFIX} Unhandled v2 message type: ${typed.type}`)\n }\n }\n })\n\n this.connection.on('close', (event: unknown) => {\n this.handleConnectionClose(event)\n })\n\n this.connection.on('error', (error: unknown) => {\n // Emit a non-fatal transient error. A 'close' event will follow and drive\n // the reconnect machinery via handleConnectionClose() → scheduleReconnect().\n // SessionErrorMessage would be fatal in hosted-experience and is reserved\n // for MAX_RECONNECT_ATTEMPTS exhaustion. WebSocket error events are opaque\n // by design — extract what we can for logging.\n const detail: Record<string, unknown> = {}\n if (error instanceof Event) {\n detail['type'] = error.type\n detail['target'] = (error.target as { url?: string; readyState?: number } | null)?.url\n ?? (error.target as { url?: string; readyState?: number } | null)?.readyState\n ?? 'unknown'\n }\n Logger.error(`${LOG_PREFIX} WebSocket error event`, error, detail)\n this.emitTransientError(error)\n })\n }\n\n /**\n * Primary handler for Flux v2 TurnInfo events.\n * Routes to specific handlers based on the turn event type.\n */\n private handleTurnInfo(data: TurnInfoMessage): void {\n try {\n Logger.debug(`${LOG_PREFIX} TurnInfo event: ${data.event}, ` +\n `transcript_length=${(data.transcript || '').length}, ` +\n `turn_index=${data.turn_index}, eot_confidence=${data.end_of_turn_confidence ?? 'n/a'}`)\n\n switch (data.event) {\n case 'StartOfTurn':\n this.handleStartOfTurn(data)\n break\n case 'Update':\n this.handleUpdate(data)\n break\n case 'EagerEndOfTurn':\n this.handleEagerEndOfTurn(data)\n break\n case 'TurnResumed':\n this.handleTurnResumed(data)\n break\n case 'EndOfTurn':\n this.handleEndOfTurn(data)\n break\n default:\n Logger.debug(`${LOG_PREFIX} Unknown TurnInfo event: ${(data as { event?: string }).event}`)\n }\n\n } catch (error) {\n Logger.error(`${LOG_PREFIX} Error processing TurnInfo`, Logger.serialiseError(error))\n } finally {\n // Always rearm the safety net — even if a handler threw.\n // Without this, an exception prevents rearming and the mic gets stuck.\n this.resetSafetyNet()\n }\n }\n\n /**\n * StartOfTurn: User has begun speaking.\n * Emit UserStartedSpeaking and send UserSpeaking(Start) to data channel.\n */\n private handleStartOfTurn(turnInfo: TurnInfoMessage): void {\n Logger.debug(`${LOG_PREFIX} StartOfTurn: turn_index=${turnInfo.turn_index}`)\n\n this.eagerPromptSentForTurn = false\n // turnStartedAt is anchored on first non-empty Update, not here —\n // StartOfTurn can fire on VAD-only signals before real speech.\n this.turnStartedAt = null\n\n // Don't send speaking signals yet — wait for first Update with actual transcript.\n // This prevents background noise from interrupting the digital human.\n }\n\n /**\n * Update: Interim transcript during the current turn.\n *\n * This is also the interruption trigger: the first Update with real transcript\n * while the avatar is speaking sends StopSpeaking immediately. No word thresholds —\n * Flux's own turn detection is the source of truth.\n */\n private handleUpdate(turnInfo: TurnInfoMessage): void {\n const transcript = turnInfo.transcript || ''\n if (transcript === '') {\n return\n }\n\n // First non-empty transcript in this turn — now signal that user is speaking.\n // Deferred from StartOfTurn so background noise doesn't interrupt the digital human.\n if (!this.isUiShowingSpeaking) {\n this.isUiShowingSpeaking = true\n this.turnStartedAt = Date.now()\n this.clientMsgSend(new UserStartedSpeakingMessage())\n }\n\n if (!this.isUserCurrentlySpeaking) {\n this.isUserCurrentlySpeaking = true\n this.dataChannelMsgSend(new UserSpeaking(UserSpeakingState.Start))\n }\n\n // Barge-in: if the avatar is audibly speaking, interrupt — but only\n // once the user has said enough words to look like a real turn, not\n // a backchannel acknowledgment (\"yeah\", \"uh-huh\", \"I see\", \"sure\n // yes\"). The BARGE_IN_WORD_THRESHOLD gate prevents short\n // acknowledgments from cutting the avatar off mid-sentence.\n if (this.digitalHumanSpeaking) {\n const wordCount = this.countWords(transcript)\n if (wordCount >= BARGE_IN_WORD_THRESHOLD) {\n Logger.info(`${LOG_PREFIX} User speech detected during avatar speaking (${wordCount} words) — interrupting`)\n this.dataChannelMsgSend(new StopSpeaking())\n this.clientMsgSend(new AvatarInterruptedMessage())\n this.digitalHumanSpeaking = false\n } else {\n Logger.debug(`${LOG_PREFIX} User speech during avatar speaking is only ${wordCount} word(s) — holding off barge-in (potential backchannel)`)\n }\n }\n\n // Emit interim transcription for closed captions\n const result: SpeechTranscriptionResult = {\n transcript,\n final: false,\n confidence: this.calculateWordConfidence(turnInfo.words),\n language_code: this.options.language || ''\n }\n this.clientMsgSend(new SpeechTranscriptionMessage(result))\n }\n\n /**\n * EagerEndOfTurn: fire ChatPrompt early so Renny can begin preparing\n * the reply. Subject to the backchannel filter and the duration gate.\n */\n private handleEagerEndOfTurn(turnInfo: TurnInfoMessage): void {\n const transcript = turnInfo.transcript || ''\n Logger.debug(`${LOG_PREFIX} EagerEndOfTurn: confidence=${turnInfo.end_of_turn_confidence}, transcript_length=${transcript.length}`)\n\n if (transcript.trim() === '') {\n return\n }\n\n // Backchannel filter — see BARGE_IN_WORD_THRESHOLD doc.\n if (this.digitalHumanSpeaking && this.countWords(transcript) < BARGE_IN_WORD_THRESHOLD) {\n Logger.info(`${LOG_PREFIX} EagerEndOfTurn: dropping ${this.countWords(transcript)}-word backchannel while avatar speaking`)\n return\n }\n\n // Duration gate — `eagerMaxTurnDurationMs === 0` disables.\n // See docs/DEEPGRAM_FLUX.md § \"Why the eager-EOT duration gate?\".\n const maxDuration = this.options.eagerMaxTurnDurationMs ?? 0\n if (maxDuration > 0 && this.turnStartedAt !== null) {\n const elapsed = Date.now() - this.turnStartedAt\n if (elapsed > maxDuration) {\n Logger.info(`${LOG_PREFIX} EagerEndOfTurn: suppressed — turn duration ${elapsed}ms exceeds eagerMaxTurnDurationMs=${maxDuration}; deferring to EndOfTurn`)\n return\n }\n }\n\n Logger.info(`${LOG_PREFIX} EagerEndOfTurn: sending prompt early (${this.countWords(transcript)} words, ${transcript.length} chars)`)\n this.eagerPromptSentForTurn = true\n this.sendChatPromptRaw(transcript)\n }\n\n /**\n * TurnResumed: the user kept talking after an Eager. Cancel the\n * in-flight prompt and drop the pending PromptRequest so it never\n * surfaces to the host as a committed turn.\n */\n private handleTurnResumed(turnInfo: TurnInfoMessage): void {\n Logger.debug(`${LOG_PREFIX} TurnResumed: turn_index=${turnInfo.turn_index}`)\n\n if (this.eagerPromptSentForTurn) {\n Logger.info(`${LOG_PREFIX} TurnResumed: cancelling in-flight eager prompt via StopSpeaking`)\n this.dataChannelMsgSend(new StopSpeaking())\n }\n\n this.clearPendingPromptRequest()\n this.eagerPromptSentForTurn = false\n }\n\n /**\n * EndOfTurn: Flux's high-confidence commit. At most ONE ChatPrompt\n * per turn reaches Renny (Option A dedup):\n * - Eager already fired → skip ChatPrompt, emit deferred PromptRequest\n * - Otherwise → send ChatPrompt now, then emit PromptRequest\n * - Backchannel branch (sub-threshold + avatar speaking) → drop both\n *\n * Rationale + trade-offs: docs/DEEPGRAM_FLUX.md § \"Why skip ChatPrompt\n * at EndOfTurn after Eager?\".\n */\n private handleEndOfTurn(turnInfo: TurnInfoMessage): void {\n const transcript = turnInfo.transcript || ''\n Logger.info(`${LOG_PREFIX} EndOfTurn: transcript_length=${transcript.length}, confidence=${turnInfo.end_of_turn_confidence}`)\n\n if (transcript.trim() !== '') {\n // Emit final transcription for closed captions (always, even if no ChatPrompt fires).\n const result: SpeechTranscriptionResult = {\n transcript,\n final: true,\n confidence: this.calculateWordConfidence(turnInfo.words),\n language_code: this.options.language || ''\n }\n this.clientMsgSend(new SpeechTranscriptionMessage(result))\n\n if (this.digitalHumanSpeaking && this.countWords(transcript) < BARGE_IN_WORD_THRESHOLD) {\n // Backchannel — drop both chat_prompt and PromptRequest.\n Logger.info(`${LOG_PREFIX} EndOfTurn: dropping ${this.countWords(transcript)}-word backchannel while avatar speaking`)\n this.clearPendingPromptRequest()\n } else if (this.eagerPromptSentForTurn) {\n // Eager was the canonical commit — surface the deferred PromptRequest now.\n Logger.debug(`${LOG_PREFIX} EndOfTurn: skipping ChatPrompt — eager already fired for this turn`)\n this.emitPendingPromptRequest()\n } else {\n // No eager fired — send chat_prompt and surface PromptRequest.\n this.sendChatPromptRaw(transcript)\n this.emitPendingPromptRequest()\n }\n }\n\n // Reset turn state\n this.eagerPromptSentForTurn = false\n this.turnStartedAt = null\n this.resetSpeakingStates()\n }\n\n /**\n * Handle FatalError from v2 API.\n */\n private handleFatalError(data: FatalErrorMessage): void {\n Logger.error(`${LOG_PREFIX} Fatal error from Deepgram: ${data.code} — ${data.description}`)\n this.clientMsgSend(new SessionErrorMessage(`Deepgram error: ${data.code} — ${data.description}`))\n }\n\n /**\n * Calculate average confidence from word-level data, or return a default.\n */\n private calculateWordConfidence(words?: Array<{ confidence: number }>): number {\n if (!words || words.length === 0) {\n return 1.0\n }\n const sum = words.reduce((acc, w) => acc + w.confidence, 0)\n return sum / words.length\n }\n\n /**\n * Whitespace-tokenised word count. Drives the backchannel filter — see\n * BARGE_IN_WORD_THRESHOLD. Punctuation glued to words counts with the\n * word; \"uh-huh\" counts as 1 (no internal whitespace).\n */\n private countWords(transcript: string): number {\n return transcript.trim().split(/\\s+/).filter(Boolean).length\n }\n\n private handleAppMessages(): void {\n this.options.messages.subscribe((msg) => {\n switch (msg.uneeqMessageType) {\n case UneeqMessageType.AvatarStartedSpeaking:\n this.digitalHumanSpeaking = true\n break\n\n case UneeqMessageType.PromptResult: {\n const promptResultMessage = msg as PromptResultMessage\n if (!promptResultMessage.promptResult.success) {\n this.handleSpeakingEnd()\n }\n break\n }\n\n case UneeqMessageType.AvatarAnswer: {\n const answer = msg as AvatarAnswerMessage\n if (answer.answerSpeech.replace(/<[^>]*>/g, '') === '') {\n this.handleSpeakingEnd()\n }\n break\n }\n\n case UneeqMessageType.AvatarStoppedSpeaking: {\n this.handleSpeakingEnd()\n break\n }\n\n case UneeqMessageType.SessionEnded: {\n this.shouldReconnect = false\n void this.stopRecognition()\n break\n }\n\n case UneeqMessageType.SessionReconnecting: {\n this.handleSpeakingEnd()\n this.shouldReconnect = false\n void this.stopRecognition()\n break\n }\n\n case UneeqMessageType.CustomMetadataUpdated: {\n this.options.promptMetadata = (msg as CustomMetadataUpdated).chatMetadata\n break\n }\n\n case UneeqMessageType.SessionBackendError: {\n this.handleSpeakingEnd()\n break\n }\n\n default:\n }\n })\n }\n\n private handleConnectionOpen(): void {\n if (this.state !== STTState.Paused) {\n this.state = STTState.Connected\n }\n }\n\n private handleConnectionClose(event?: unknown): void {\n const code = (event as { code?: number })?.code ?? 'unknown'\n const reason = (event as { reason?: string })?.reason ?? ''\n Logger.info(`${LOG_PREFIX} Connection closed — code=${code}, reason=\"${reason}\"`)\n\n if (this.state === STTState.Paused) {\n Logger.info(`${LOG_PREFIX} Connection closed while paused — will reconnect on resume`)\n this.connection = null\n this.stopMicrophone()\n this.clearSafetyNet()\n this.resetSpeakingStates()\n return\n }\n\n this.state = STTState.Disconnected\n this.clearSafetyNet()\n this.resetSpeakingStates()\n this.eagerPromptSentForTurn = false\n this.turnStartedAt = null\n this.pendingPromptRequest = null\n this.clientMsgSend(new EnableMicrophoneUpdatedMessage(false))\n\n if (this.shouldReconnect) {\n Logger.info(`${LOG_PREFIX} Unexpected disconnect, attempting reconnection...`)\n this.scheduleReconnect()\n }\n }\n\n /**\n * Emit a non-fatal transient error to the host. Used when a single\n * connect/reconnect attempt fails or a recoverable WebSocket error fires.\n * The reconnect machinery (scheduleReconnect) will continue retrying; the\n * host receives this as an informational signal, not a fatal one.\n * Compare with the fatal `SessionErrorMessage` emitted from\n * scheduleReconnect() only when MAX_RECONNECT_ATTEMPTS is exhausted, and\n * with `handleFatalError` for Deepgram-protocol-level Error messages.\n */\n private emitTransientError(error: unknown): void {\n const message = error instanceof Error ? error.message : String(error)\n this.clientMsgSend(new SpeechRecognitionTransientErrorMessage(message))\n }\n\n /**\n * Send chat_prompt to Renny and stash a pending PromptRequest for\n * the host. The chat_prompt is sent with shouldEmitPromptRequest=false\n * so signaling doesn't fire PromptRequest immediately; the STT\n * surfaces it later via emitPendingPromptRequest at the commit point.\n */\n private sendChatPromptRaw(transcript: string): void {\n if (!transcript || transcript.trim() === '') {\n return\n }\n if (this.options.language) {\n this.options.promptMetadata.userSpokenLocale = this.options.language\n }\n const requestId = uuidv4()\n const metadataSnapshot = { ...this.options.promptMetadata }\n // turnStartedAt is anchored on first non-empty Update; guard for null\n // is defensive — Flux's state machine guarantees Update before Eager.\n const speakingDurationMs = this.turnStartedAt !== null ? Date.now() - this.turnStartedAt : 0\n this.pendingPromptRequest = { prompt: transcript, requestId, metadata: metadataSnapshot, speakingDurationMs }\n this.dataChannelMsgSend(new ChatPrompt(transcript, this.options.promptMetadata, requestId, false))\n }\n\n /** Surface the pending PromptRequest to the host and clear the slot. */\n private emitPendingPromptRequest(): void {\n const pending = this.pendingPromptRequest\n if (pending === null) return\n // Cast through unknown — PromptRequest declares metadata as\n // Record<string, unknown>; ours is the structured PromptMetadata.\n this.clientMsgSend(new PromptRequestMessage({\n prompt: pending.prompt,\n requestId: pending.requestId,\n metadata: pending.metadata,\n speakingDurationMs: pending.speakingDurationMs,\n } as unknown as PromptRequest))\n Logger.debug(`${LOG_PREFIX} PromptRequest emitted — speakingDurationMs=${pending.speakingDurationMs}, requestId=${pending.requestId}`)\n this.pendingPromptRequest = null\n }\n\n /** Drop the pending PromptRequest without emitting (e.g. on TurnResumed). */\n private clearPendingPromptRequest(): void {\n this.pendingPromptRequest = null\n }\n\n private handleSpeakingEnd(): void {\n this.digitalHumanSpeaking = false\n }\n\n /**\n * Reset the safety net timer. Called on every TurnInfo event.\n * If the user is in a speaking state and no events arrive for SAFETY_NET_TIMEOUT_MS,\n * the speaking indicators are reset to prevent the UI getting stuck.\n */\n private resetSafetyNet(): void {\n this.clearSafetyNet()\n\n if (this.isUiShowingSpeaking || this.isUserCurrentlySpeaking) {\n this.safetyNetTimeoutId = setTimeout(() => {\n Logger.warn(`${LOG_PREFIX} Safety net: no TurnInfo events for ${this.options.safetyNetTimeoutMs}ms while speaking — resetting`)\n this.resetSpeakingStates()\n }, this.options.safetyNetTimeoutMs)\n }\n }\n\n private clearSafetyNet(): void {\n if (this.safetyNetTimeoutId) {\n clearTimeout(this.safetyNetTimeoutId)\n this.safetyNetTimeoutId = null\n }\n }\n\n /**\n * Reset speaking states and send appropriate stop messages.\n */\n private resetSpeakingStates(): void {\n if (this.isUserCurrentlySpeaking) {\n this.isUserCurrentlySpeaking = false\n this.dataChannelMsgSend(new UserSpeaking(UserSpeakingState.Stop))\n }\n\n if (this.isUiShowingSpeaking) {\n this.isUiShowingSpeaking = false\n this.clientMsgSend(new UserStoppedSpeakingMessage())\n }\n }\n\n // Send a message on the data channel to renderer\n private dataChannelMsgSend(msg: DataChannelMessage): void {\n this.options.sendMessage(msg)\n }\n\n // Send a message to the client implementation, i.e. hosted experience\n private clientMsgSend(msg: UneeqMessage): void {\n this.options.messages.next(msg)\n }\n}\n"],"names":["LOG_PREFIX","STTState","DeepgramFluxSTT","options","connection","state","Idle","shouldReconnect","stream","audioContext","workletNode","reconnectAttempts","reconnectDelay","reconnectTimeoutId","digitalHumanSpeaking","pendingPromptRequest","isUserCurrentlySpeaking","isUiShowingSpeaking","eagerPromptSentForTurn","turnStartedAt","safetyNetTimeoutId","audioChunksSent","constructor","this","model","language","eotThreshold","eagerEotThreshold","eotTimeoutMs","eagerMaxTurnDurationMs","safetyNetTimeoutMs","echoCancellation","noiseSuppression","autoGainControl","eagerLabel","maxDurLabel","debug","handleAppMessages","startRecognition","info","resetReconnectionState","connect","stopRecognition","clearReconnectTimeout","disconnect","pause","Paused","clearSafetyNet","resetSpeakingStates","getTracks","forEach","track","enabled","resume","Connected","startMicrophone","Disconnected","setChatMetadata","chatMetadata","promptMetadata","Connecting","tokenData","getToken","api_url","sdk_version","token","length","deepgram","accessToken","baseUrl","connectionOptions","encoding","sample_rate","String","mip_opt_out","undefined","eot_threshold","eager_eot_threshold","eot_timeout_ms","keyterms","keyterm","listen","v2","Promise","race","waitForOpen","_","reject","setTimeout","Error","setupEventHandlers","error","serialiseError","emitTransientError","scheduleReconnect","warn","stopMicrophone","sendCloseStream","type","close","clientMsgSend","Math","min","clearTimeout","tokenEndpoint","connectionUrl","encodeURIComponent","response","fetch","method","headers","Authorization","jwtToken","ok","status","statusText","json","navigator","mediaDevices","getUserMedia","audio","deviceId","microphoneDeviceId","exact","AudioContext","sampleRate","source","createMediaStreamSource","blob","Blob","processorUrl","URL","createObjectURL","audioWorklet","addModule","revokeObjectURL","AudioWorkletNode","port","onmessage","event","sendMedia","data","byteLength","destination","JSON","stringify","catch","stop","on","handleConnectionOpen","typed","handleTurnInfo","handleFatalError","handleConnectionClose","detail","Event","target","url","readyState","transcript","turn_index","end_of_turn_confidence","handleStartOfTurn","handleUpdate","handleEagerEndOfTurn","handleTurnResumed","handleEndOfTurn","resetSafetyNet","turnInfo","Date","now","dataChannelMsgSend","Start","wordCount","countWords","result","final","confidence","calculateWordConfidence","words","language_code","trim","maxDuration","elapsed","sendChatPromptRaw","clearPendingPromptRequest","emitPendingPromptRequest","code","description","reduce","acc","w","split","filter","Boolean","messages","subscribe","msg","uneeqMessageType","AvatarStartedSpeaking","PromptResult","promptResult","success","handleSpeakingEnd","AvatarAnswer","answerSpeech","replace","AvatarStoppedSpeaking","SessionEnded","SessionReconnecting","CustomMetadataUpdated","SessionBackendError","reason","message","userSpokenLocale","requestId","metadataSnapshot","speakingDurationMs","prompt","metadata","pending","Stop","sendMessage","next"],"sourceRoot":""}
@@ -9,14 +9,28 @@ export interface DeepgramFluxSTTOptions {
9
9
  sessionId: string;
10
10
  model?: string;
11
11
  language?: string;
12
- /** End-of-turn confidence threshold (0.5-0.9). @default 0.7 */
12
+ /** End-of-turn confidence threshold (0.5-0.9). @default 0.85 */
13
13
  eotThreshold?: number;
14
- /** Eager end-of-turn threshold (0.3-0.9). */
14
+ /**
15
+ * Eager end-of-turn threshold (0.3-0.9). When > 0, Flux emits
16
+ * EagerEndOfTurn / TurnResumed events for early prompt prep. Set
17
+ * to `0` to disable eager mode entirely — the threshold isn't sent
18
+ * to Flux, so no EagerEndOfTurn events fire and EndOfTurn becomes
19
+ * the sole commit point.
20
+ * See docs/DEEPGRAM_FLUX.md for the event-handling contract.
21
+ * @default 0.5
22
+ */
15
23
  eagerEotThreshold?: number;
16
- /** End-of-turn timeout in ms. */
24
+ /** End-of-turn timeout in ms (silence backstop for stuck turns). @default 3000 */
17
25
  eotTimeoutMs?: number;
18
- /** Send ChatPrompt on EagerEndOfTurn (lower latency, risk of incomplete). @default true */
19
- useEagerEndOfTurn?: boolean;
26
+ /**
27
+ * Maximum elapsed turn time (from first non-empty Update) before
28
+ * EagerEndOfTurn stops firing chat_prompt. Long turns then defer
29
+ * to the canonical EndOfTurn commit. Set to `0` to disable.
30
+ * See docs/DEEPGRAM_FLUX.md § "Why the eager-EOT duration gate?".
31
+ * @default 5000
32
+ */
33
+ eagerMaxTurnDurationMs?: number;
20
34
  /**
21
35
  * Safety net timeout in milliseconds. Resets speaking indicators if no TurnInfo
22
36
  * events arrive for this duration while in a speaking state. @default 2000
@@ -46,9 +60,11 @@ export declare class DeepgramFluxSTT implements SpeechRecognitionInterface {
46
60
  private reconnectDelay;
47
61
  private reconnectTimeoutId;
48
62
  private digitalHumanSpeaking;
63
+ private pendingPromptRequest;
49
64
  private isUserCurrentlySpeaking;
50
65
  private isUiShowingSpeaking;
51
66
  private eagerPromptSentForTurn;
67
+ private turnStartedAt;
52
68
  private safetyNetTimeoutId;
53
69
  private audioChunksSent;
54
70
  constructor(options: DeepgramFluxSTTOptions);
@@ -85,23 +101,25 @@ export declare class DeepgramFluxSTT implements SpeechRecognitionInterface {
85
101
  */
86
102
  private handleUpdate;
87
103
  /**
88
- * EagerEndOfTurn: Model predicts the user might be done speaking.
89
- * If useEagerEndOfTurn is enabled, send ChatPrompt immediately for lower latency.
90
- *
91
- * If the avatar is still speaking (interruption disabled or Update hasn't fired yet),
92
- * the prompt is discarded — we can't send competing prompts while the avatar talks.
104
+ * EagerEndOfTurn: fire ChatPrompt early so Renny can begin preparing
105
+ * the reply. Subject to the backchannel filter and the duration gate.
93
106
  */
94
107
  private handleEagerEndOfTurn;
95
108
  /**
96
- * TurnResumed: Model detects the user continued speaking after an EagerEndOfTurn.
97
- * If an eager prompt was already sent, log a warning (can't unsend).
109
+ * TurnResumed: the user kept talking after an Eager. Cancel the
110
+ * in-flight prompt and drop the pending PromptRequest so it never
111
+ * surfaces to the host as a committed turn.
98
112
  */
99
113
  private handleTurnResumed;
100
114
  /**
101
- * EndOfTurn: Model has determined the user has finished speaking.
115
+ * EndOfTurn: Flux's high-confidence commit. At most ONE ChatPrompt
116
+ * per turn reaches Renny (Option A dedup):
117
+ * - Eager already fired → skip ChatPrompt, emit deferred PromptRequest
118
+ * - Otherwise → send ChatPrompt now, then emit PromptRequest
119
+ * - Backchannel branch (sub-threshold + avatar speaking) → drop both
102
120
  *
103
- * If the avatar is still speaking (interruption disabled), the prompt is discarded.
104
- * Otherwise, cancel any in-flight eager prompt and send the definitive transcript.
121
+ * Rationale + trade-offs: docs/DEEPGRAM_FLUX.md § "Why skip ChatPrompt
122
+ * at EndOfTurn after Eager?".
105
123
  */
106
124
  private handleEndOfTurn;
107
125
  /**
@@ -112,6 +130,12 @@ export declare class DeepgramFluxSTT implements SpeechRecognitionInterface {
112
130
  * Calculate average confidence from word-level data, or return a default.
113
131
  */
114
132
  private calculateWordConfidence;
133
+ /**
134
+ * Whitespace-tokenised word count. Drives the backchannel filter — see
135
+ * BARGE_IN_WORD_THRESHOLD. Punctuation glued to words counts with the
136
+ * word; "uh-huh" counts as 1 (no internal whitespace).
137
+ */
138
+ private countWords;
115
139
  private handleAppMessages;
116
140
  private handleConnectionOpen;
117
141
  private handleConnectionClose;
@@ -125,7 +149,17 @@ export declare class DeepgramFluxSTT implements SpeechRecognitionInterface {
125
149
  * with `handleFatalError` for Deepgram-protocol-level Error messages.
126
150
  */
127
151
  private emitTransientError;
128
- private sendChatPrompt;
152
+ /**
153
+ * Send chat_prompt to Renny and stash a pending PromptRequest for
154
+ * the host. The chat_prompt is sent with shouldEmitPromptRequest=false
155
+ * so signaling doesn't fire PromptRequest immediately; the STT
156
+ * surfaces it later via emitPendingPromptRequest at the commit point.
157
+ */
158
+ private sendChatPromptRaw;
159
+ /** Surface the pending PromptRequest to the host and clear the slot. */
160
+ private emitPendingPromptRequest;
161
+ /** Drop the pending PromptRequest without emitting (e.g. on TurnResumed). */
162
+ private clearPendingPromptRequest;
129
163
  private handleSpeakingEnd;
130
164
  /**
131
165
  * Reset the safety net timer. Called on every TurnInfo event.
@@ -0,0 +1 @@
1
+ var _=Object.create;var p=Object.defineProperty;var ee=Object.getOwnPropertyDescriptor;var te=Object.getOwnPropertyNames;var ne=Object.getPrototypeOf,se=Object.prototype.hasOwnProperty;var oe=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,s)=>(typeof require<"u"?require:t)[s]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var ie=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),pe=(e,t)=>{for(var s in t)p(e,s,{get:t[s],enumerable:!0})},re=(e,t,s,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of te(t))!se.call(e,r)&&r!==s&&p(e,r,{get:()=>t[r],enumerable:!(i=ee(t,r))||i.enumerable});return e};var ce=(e,t,s)=>(s=e!=null?_(ne(e)):{},re(t||!e||!e.__esModule?p(s,"default",{value:e,enumerable:!0}):s,e));var c=(a=>(a.Fatal="fatal",a.Error="error",a.Warn="warn",a.Info="info",a.Debug="debug",a.Trace="trace",a))(c||{});var u="UneeQ SDK: ",g={fatal:0,error:1,warn:2,info:3,debug:4,trace:5},l=class e{static currentLevel="info";static shouldLog(t){return g[t]<=g[e.currentLevel]}static formatLogMessage(t,s){return s.length===0?u+t:u+t+" | "+JSON.stringify(s)}static fatal(t,...s){e.shouldLog("fatal")&&console.error(e.formatLogMessage(t,s))}static error(t,...s){e.shouldLog("error")&&console.error(e.formatLogMessage(t,s))}static warn(t,...s){e.shouldLog("warn")&&console.warn(e.formatLogMessage(t,s))}static info(t,...s){e.shouldLog("info")&&console.info(e.formatLogMessage(t,s))}static debug(t,...s){e.shouldLog("debug")&&console.debug(e.formatLogMessage(t,s))}static trace(t,...s){e.shouldLog("trace")&&console.debug(e.formatLogMessage(t,s))}static setLevel(t){Object.values(c).includes(t)?e.currentLevel=t:e.currentLevel="info"}static serialiseError(t){return JSON.stringify(t,Object.getOwnPropertyNames(t))}};var ae=(n=>(n.AvatarAnswer="AvatarAnswer",n.AvatarInterrupted="AvatarInterrupted",n.AvatarStartedSpeaking="AvatarStartedSpeaking",n.AvatarStoppedSpeaking="AvatarStoppedSpeaking",n.CustomerConcurrencyLimitReached="CustomerConcurrencyLimitReached",n.CustomMetadataUpdated="CustomMetadataUpdated",n.DeviceError="DeviceError",n.DigitalHumanMuted="DigitalHumanMuted",n.DigitalHumanPlayedInMutedModeSuccess="DigitalHumanPlayedInMutedModeSuccess",n.DigitalHumanUnmuted="DigitalHumanUnmuted",n.EnableMicrophoneUpdated="EnableMicrophoneUpdated",n.NetworkQuality="NetworkQuality",n.PromptRequest="PromptRequest",n.PromptResult="PromptResult",n.RendererError="RendererError",n.SceneReady="SceneReady",n.SessionBackendError="SessionBackendError",n.SessionConnecting="SessionConnecting",n.SessionDisconnected="SessionDisconnected",n.SessionEnded="SessionEnded",n.SessionError="SessionError",n.SessionLive="SessionLive",n.SessionReconnecting="SessionReconnecting",n.SessionReconnectingFinished="SessionReconnectingFinished",n.SoftSwitchFinished="SoftSwitchFinished",n.SoftSwitchStarting="SoftSwitchStarting",n.SpeechEvent="SpeechEvent",n.SpeechRecognitionTransientError="SpeechRecognitionTransientError",n.SpeechTranscription="SpeechTranscription",n.UserStartedSpeaking="UserStartedSpeaking",n.UserStoppedSpeaking="UserStoppedSpeaking",n.VadInterruptionAllowed="VadInterruptionAllowed",n.VideoAutoplayBlocked="VideoAutoplayBlocked",n.VideoLayoutConfiguring="VideoLayoutConfiguring",n.WaitingInQueue="WaitingInQueue",n.WebRtcConnected="webRtcConnected",n.WebRtcStats="WebRtcStats",n))(ae||{}),m=class{constructor(t){this.error=t}error;uneeqMessageType="DeviceError"},d=class{constructor(t,s,i,r){this.answer=t;this.answerAvatar=s;this.answerSpeech=i;this.transcriptId=r}answer;answerAvatar;answerSpeech;transcriptId;uneeqMessageType="AvatarAnswer"},y=class{constructor(t){this.speechTranscription=t}speechTranscription;uneeqMessageType="SpeechTranscription"},q=class{constructor(t){this.interruptionAllowed=t}interruptionAllowed;uneeqMessageType="VadInterruptionAllowed"},M=class{uneeqMessageType="UserStartedSpeaking"},b=class{uneeqMessageType="UserStoppedSpeaking"},U=class{uneeqMessageType="AvatarStartedSpeaking"},T=class{uneeqMessageType="AvatarStoppedSpeaking"},x=class{constructor(t){this.transcriptId=t}transcriptId;uneeqMessageType="AvatarInterrupted"},S=class{constructor(t){this.sessionId=t}sessionId;uneeqMessageType="SessionLive"},f=class{constructor(t){this.position=t}position;uneeqMessageType="WaitingInQueue"},h=class{constructor(t){this.stats=t}stats;uneeqMessageType="WebRtcStats"},v=class{constructor(t){this.error=t}error;uneeqMessageType="SessionError"},D=class{constructor(t){this.error=t}error;uneeqMessageType="SpeechRecognitionTransientError"},I=class{constructor(t){this.error=t}error;uneeqMessageType="SessionBackendError"},w=class{constructor(t){this.speechEvent=t}speechEvent;uneeqMessageType="SpeechEvent"},R=class{constructor(t){this.reason=t}reason;uneeqMessageType="SessionEnded"},C=class{uneeqMessageType="DigitalHumanPlayedInMutedModeSuccess";msg="Digital Human was successfully played in muted mode. After the user interacts with the page you will need to call unmuteDigitalHuman() to unmute the video."},k=class{uneeqMessageType="DigitalHumanUnmuted"},A=class{uneeqMessageType="DigitalHumanMuted"},E=class{constructor(t){this.enabled=t}enabled;uneeqMessageType="EnableMicrophoneUpdated"},L=class{constructor(t){this.chatMetadata=t}chatMetadata;uneeqMessageType="CustomMetadataUpdated"},P=class{constructor(t){this.promptRequest=t}promptRequest;uneeqMessageType="PromptRequest"},N=class{constructor(t){this.promptResult=t}promptResult;uneeqMessageType="PromptResult"},O=class{uneeqMessageType="SceneReady"},F=class{constructor(t){this.rendererId=t}rendererId;uneeqMessageType="SessionConnecting"},H=class{uneeqMessageType="SessionDisconnected"},W=class{uneeqMessageType="SessionReconnecting"},Q=class{uneeqMessageType="SessionReconnectingFinished"},V=class{uneeqMessageType="SoftSwitchStarting"},B=class{uneeqMessageType="SoftSwitchFinished"},J=class{uneeqMessageType="VideoLayoutConfiguring"},j=class{uneeqMessageType="webRtcConnected"},G=class{uneeqMessageType="CustomerConcurrencyLimitReached"},K=class{constructor(t){this.metrics=t}metrics;uneeqMessageType="NetworkQuality"},X=class{constructor(t,s){this.code=t;this.error=s}code;error;uneeqMessageType="RendererError"},Y=class{uneeqMessageType="VideoAutoplayBlocked"};function o(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}var z=class{constructor(t,s,i=o()){this.prompt=t;this.metadata=s;this.requestId=i}prompt;metadata;requestId;toJSON(){return{action:"chat_prompt",data:{requestId:this.requestId,prompt:this.prompt,metadata:this.metadata}}}};var Z=class{constructor(t,s=o()){this.state=t;this.requestId=s}state;requestId;toJSON(){return{action:"user_speaking",data:{requestId:this.requestId,event:this.state}}}};var $=class{constructor(t=o()){this.requestId=t}requestId;toJSON(){return{action:"stop_speaking",data:{requestId:this.requestId}}}};export{oe as a,ie as b,pe as c,ce as d,c as e,l as f,ae as g,m as h,d as i,y as j,q as k,M as l,b as m,U as n,T as o,x as p,S as q,f as r,h as s,v as t,D as u,I as v,w,R as x,C as y,k as z,A,E as B,L as C,P as D,N as E,O as F,F as G,H,W as I,Q as J,V as K,B as L,J as M,j as N,G as O,K as P,X as Q,Y as R,o as S,z as T,Z as U,$ as V};