uneeq-js 3.15.0 → 3.15.1
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:()=>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)}}}}]);
|
|
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:()=>l});var s=n(514),i=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 l{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;cancelledEagerRequestId=void 0;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`;s.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(){s.A.info(`${d} Starting speech recognition`),this.shouldReconnect=!0,this.resetReconnectionState(),await this.connect()}async stopRecognition(){s.A.info(`${d} Stopping speech recognition`),this.shouldReconnect=!1,this.clearReconnectTimeout(),await this.disconnect()}async pause(){return s.A.info(`${d} Pausing speech recognition`),this.state=u.Paused,this.clearSafetyNet(),this.resetSpeakingStates(),this.eagerPromptSentForTurn=!1,this.turnStartedAt=null,this.pendingPromptRequest=null,this.cancelledEagerRequestId=void 0,this.stream&&(this.stream.getTracks().forEach(e=>{e.enabled=!1}),s.A.debug(`${d} Audio tracks disabled`)),!0}async resume(){if(s.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}),s.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 s.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();s.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),s.A.info(`${d} Connection opened`),this.setupEventHandlers(),this.state===u.Paused)return s.A.info(`${d} Pause requested during connection — staying paused`),void this.resetReconnectionState();await this.startMicrophone(),s.A.info(`${d} Connected successfully`),this.resetReconnectionState()}catch(e){this.state=u.Disconnected,s.A.error(`${d} Connection error`,s.A.serialiseError(e)),this.shouldReconnect&&(this.emitTransientError(e),this.scheduleReconnect())}}else s.A.warn(`${d} Connection already in progress`);else s.A.warn(`${d} Already connected`)}async disconnect(){if(this.state!==u.Idle&&(this.state!==u.Disconnected||this.connection)){s.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){s.A.error(`${d} Disconnect error`,s.A.serialiseError(e))}this.clearSafetyNet(),this.resetSpeakingStates(),this.eagerPromptSentForTurn=!1,this.turnStartedAt=null,this.pendingPromptRequest=null,this.cancelledEagerRequestId=void 0,this.state=u.Disconnected,this.clientMsgSend(new i.WY(!1))}}scheduleReconnect(){if(this.reconnectAttempts>=5)return s.A.error(`${d} Max reconnection attempts (5) reached`),void this.clientMsgSend(new i.Cj("Unable to connect to speech recognition service after 5 attempts"));this.reconnectAttempts++,s.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(s.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 s.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&&s.A.debug(`${d} Audio chunks sent: ${this.audioChunksSent}, size: ${e.data.byteLength} bytes`))},e.connect(this.workletNode),this.workletNode.connect(this.audioContext.destination),s.A.info(`${d} Microphone started (linear16 PCM @ 16000Hz)`),this.clientMsgSend(new i.WY(!0))}catch(e){s.A.error(`${d} Microphone error`,s.A.serialiseError(e)),this.clientMsgSend(new i.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),s.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?s.A.info(`${d} v2 connection confirmed`):"Error"===t.type?this.handleFatalError(e):s.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"),s.A.error(`${d} WebSocket error event`,e,t),this.emitTransientError(e)}))}handleTurnInfo(e){try{switch(s.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:s.A.debug(`${d} Unknown TurnInfo event: ${e.event}`)}}catch(e){s.A.error(`${d} Error processing TurnInfo`,s.A.serialiseError(e))}finally{this.resetSafetyNet()}}handleStartOfTurn(e){s.A.debug(`${d} StartOfTurn: turn_index=${e.turn_index}`),this.eagerPromptSentForTurn=!1,this.turnStartedAt=null,this.cancelledEagerRequestId=void 0}handleUpdate(e){const t=e.transcript||"";if(""===t)return;if(this.isUiShowingSpeaking||(this.isUiShowingSpeaking=!0,this.turnStartedAt=Date.now(),this.clientMsgSend(new i._4)),this.isUserCurrentlySpeaking||(this.isUserCurrentlySpeaking=!0,this.dataChannelMsgSend(new o.A(o.f.Start))),this.digitalHumanSpeaking){const e=this.countWords(t);e>=3?(s.A.info(`${d} User speech detected during avatar speaking (${e} words) — interrupting`),this.dataChannelMsgSend(new a.f),this.clientMsgSend(new i.tc),this.digitalHumanSpeaking=!1):s.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 i.Ux(n))}handleEagerEndOfTurn(e){const t=e.transcript||"";if(s.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 s.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 s.A.info(`${d} EagerEndOfTurn: suppressed — turn duration ${e}ms exceeds eagerMaxTurnDurationMs=${n}; deferring to EndOfTurn`)}s.A.info(`${d} EagerEndOfTurn: sending prompt early (${this.countWords(t)} words, ${t.length} chars)`),this.eagerPromptSentForTurn=!0,this.sendChatPromptRaw(t,this.takeCancelledEagerRequestId())}takeCancelledEagerRequestId(){const e=this.cancelledEagerRequestId;return this.cancelledEagerRequestId=void 0,e}handleTurnResumed(e){s.A.debug(`${d} TurnResumed: turn_index=${e.turn_index}`),null!==this.pendingPromptRequest&&(this.cancelledEagerRequestId=this.pendingPromptRequest.requestId),this.eagerPromptSentForTurn&&(s.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(s.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 i.Ux(n)),this.digitalHumanSpeaking&&this.countWords(t)<3?(s.A.info(`${d} EndOfTurn: dropping ${this.countWords(t)}-word backchannel while avatar speaking`),this.clearPendingPromptRequest()):this.eagerPromptSentForTurn?(s.A.debug(`${d} EndOfTurn: skipping ChatPrompt — eager already fired for this turn`),this.emitPendingPromptRequest()):(this.sendChatPromptRaw(t,this.takeCancelledEagerRequestId()),this.emitPendingPromptRequest())}this.eagerPromptSentForTurn=!1,this.turnStartedAt=null,this.cancelledEagerRequestId=void 0,this.resetSpeakingStates()}handleFatalError(e){s.A.error(`${d} Fatal error from Deepgram: ${e.code} — ${e.description}`),this.clientMsgSend(new i.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 i.Yg.AvatarStartedSpeaking:this.digitalHumanSpeaking=!0;break;case i.Yg.PromptResult:e.promptResult.success||this.handleSpeakingEnd();break;case i.Yg.AvatarAnswer:""===e.answerSpeech.replace(/<[^>]*>/g,"")&&this.handleSpeakingEnd();break;case i.Yg.AvatarStoppedSpeaking:this.handleSpeakingEnd();break;case i.Yg.SessionEnded:this.shouldReconnect=!1,this.stopRecognition();break;case i.Yg.SessionReconnecting:this.handleSpeakingEnd(),this.shouldReconnect=!1,this.stopRecognition();break;case i.Yg.CustomMetadataUpdated:this.options.promptMetadata=e.chatMetadata;break;case i.Yg.SessionBackendError:this.handleSpeakingEnd()}})}handleConnectionOpen(){this.state!==u.Paused&&(this.state=u.Connected)}handleConnectionClose(e){const t=e?.code??"unknown",n=e?.reason??"";if(s.A.info(`${d} Connection closed — code=${t}, reason="${n}"`),this.state===u.Paused)return s.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.cancelledEagerRequestId=void 0,this.clientMsgSend(new i.WY(!1)),this.shouldReconnect&&(s.A.info(`${d} Unexpected disconnect, attempting reconnection...`),this.scheduleReconnect())}emitTransientError(e){const t=e instanceof Error?e.message:String(e);this.clientMsgSend(new i.fP(t))}sendChatPromptRaw(e,t){if(!e||""===e.trim())return;this.options.language&&(this.options.promptMetadata.userSpokenLocale=this.options.language);const n=t??(0,c.g)();void 0!==t&&s.A.info(`${d} sendChatPromptRaw: reusing requestId=${n} from cancelled eager prompt`);const i={...this.options.promptMetadata},o=null!==this.turnStartedAt?Date.now()-this.turnStartedAt:0;this.pendingPromptRequest={prompt:e,requestId:n,metadata:i,speakingDurationMs:o},this.dataChannelMsgSend(new r.D(e,this.options.promptMetadata,n,!1))}emitPendingPromptRequest(){const e=this.pendingPromptRequest;null!==e&&(this.clientMsgSend(new i.bS({prompt:e.prompt,requestId:e.requestId,metadata:e.metadata,speakingDurationMs:e.speakingDurationMs})),s.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(()=>{s.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 i.im))}dataChannelMsgSend(e){this.options.sendMessage(e)}clientMsgSend(e){this.options.messages.next(e)}}}}]);
|
|
2
2
|
//# sourceMappingURL=363.index.js.map
|
package/dist/363.index.js.map
CHANGED
|
@@ -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 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":""}
|
|
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 // Cancelled eager prompt id, reused by the next ChatPrompt in the same turn.\n private cancelledEagerRequestId: string | undefined = undefined\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 this.cancelledEagerRequestId = undefined\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 this.cancelledEagerRequestId = undefined\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 this.cancelledEagerRequestId = undefined\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, this.takeCancelledEagerRequestId())\n }\n\n /** Read and clear the cancelled-eager requestId in one shot. */\n private takeCancelledEagerRequestId(): string | undefined {\n const id = this.cancelledEagerRequestId\n this.cancelledEagerRequestId = undefined\n return id\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. Stash the cancelled\n * eager's requestId for reuse by the next ChatPrompt in this turn.\n */\n private handleTurnResumed(turnInfo: TurnInfoMessage): void {\n Logger.debug(`${LOG_PREFIX} TurnResumed: turn_index=${turnInfo.turn_index}`)\n\n // Capture before clearPendingPromptRequest wipes it.\n if (this.pendingPromptRequest !== null) {\n this.cancelledEagerRequestId = this.pendingPromptRequest.requestId\n }\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 (or every eager was cancelled by TurnResumed —\n // in which case takeCancelledEagerRequestId() returns the carried id).\n this.sendChatPromptRaw(transcript, this.takeCancelledEagerRequestId())\n this.emitPendingPromptRequest()\n }\n }\n\n // Reset turn state\n this.eagerPromptSentForTurn = false\n this.turnStartedAt = null\n this.cancelledEagerRequestId = undefined\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.cancelledEagerRequestId = undefined\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 * Pass `reuseRequestId` to bind this ChatPrompt to a previous one;\n * otherwise a fresh uuid is generated.\n */\n private sendChatPromptRaw(transcript: string, reuseRequestId?: 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 = reuseRequestId ?? uuidv4()\n if (reuseRequestId !== undefined) {\n Logger.info(`${LOG_PREFIX} sendChatPromptRaw: reusing requestId=${requestId} from cancelled eager prompt`)\n }\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","cancelledEagerRequestId","undefined","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","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","takeCancelledEagerRequestId","id","requestId","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","reuseRequestId","userSpokenLocale","metadataSnapshot","speakingDurationMs","prompt","metadata","pending","Stop","sendMessage","next"],"sourceRoot":""}
|
|
@@ -64,6 +64,7 @@ export declare class DeepgramFluxSTT implements SpeechRecognitionInterface {
|
|
|
64
64
|
private isUserCurrentlySpeaking;
|
|
65
65
|
private isUiShowingSpeaking;
|
|
66
66
|
private eagerPromptSentForTurn;
|
|
67
|
+
private cancelledEagerRequestId;
|
|
67
68
|
private turnStartedAt;
|
|
68
69
|
private safetyNetTimeoutId;
|
|
69
70
|
private audioChunksSent;
|
|
@@ -105,10 +106,13 @@ export declare class DeepgramFluxSTT implements SpeechRecognitionInterface {
|
|
|
105
106
|
* the reply. Subject to the backchannel filter and the duration gate.
|
|
106
107
|
*/
|
|
107
108
|
private handleEagerEndOfTurn;
|
|
109
|
+
/** Read and clear the cancelled-eager requestId in one shot. */
|
|
110
|
+
private takeCancelledEagerRequestId;
|
|
108
111
|
/**
|
|
109
112
|
* TurnResumed: the user kept talking after an Eager. Cancel the
|
|
110
113
|
* in-flight prompt and drop the pending PromptRequest so it never
|
|
111
|
-
* surfaces to the host as a committed turn.
|
|
114
|
+
* surfaces to the host as a committed turn. Stash the cancelled
|
|
115
|
+
* eager's requestId for reuse by the next ChatPrompt in this turn.
|
|
112
116
|
*/
|
|
113
117
|
private handleTurnResumed;
|
|
114
118
|
/**
|
|
@@ -154,6 +158,8 @@ export declare class DeepgramFluxSTT implements SpeechRecognitionInterface {
|
|
|
154
158
|
* the host. The chat_prompt is sent with shouldEmitPromptRequest=false
|
|
155
159
|
* so signaling doesn't fire PromptRequest immediately; the STT
|
|
156
160
|
* surfaces it later via emitPendingPromptRequest at the commit point.
|
|
161
|
+
* Pass `reuseRequestId` to bind this ChatPrompt to a previous one;
|
|
162
|
+
* otherwise a fresh uuid is generated.
|
|
157
163
|
*/
|
|
158
164
|
private sendChatPromptRaw;
|
|
159
165
|
/** Surface the pending PromptRequest to the host and clear the slot. */
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import{a as E}from"./chunk-YEJDFRW6.js";import{D as r,F as C,U as h,V as p,f as t,g as m,h as f,j as S,l as d,n as T,o as k,r as M,v as u,w as v}from"./chunk-WJYCZSEJ.js";var n="[Deepgram Flux STT]",R=1e4,l=16e3,b=1280,y=`
|
|
2
|
+
class PcmCaptureProcessor extends AudioWorkletProcessor {
|
|
3
|
+
constructor() {
|
|
4
|
+
super()
|
|
5
|
+
this._buffer = new Float32Array(${b})
|
|
6
|
+
this._offset = 0
|
|
7
|
+
}
|
|
8
|
+
process(inputs, outputs, parameters) {
|
|
9
|
+
const input = inputs[0]?.[0]
|
|
10
|
+
if (!input) return true
|
|
11
|
+
for (let i = 0; i < input.length; i++) {
|
|
12
|
+
this._buffer[this._offset++] = input[i]
|
|
13
|
+
if (this._offset >= this._buffer.length) {
|
|
14
|
+
const int16 = new Int16Array(this._buffer.length)
|
|
15
|
+
for (let j = 0; j < this._buffer.length; j++) {
|
|
16
|
+
const s = Math.max(-1, Math.min(1, this._buffer[j]))
|
|
17
|
+
int16[j] = s < 0 ? s * 0x8000 : s * 0x7FFF
|
|
18
|
+
}
|
|
19
|
+
this.port.postMessage(int16.buffer, [int16.buffer])
|
|
20
|
+
this._offset = 0
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
registerProcessor('pcm-capture-processor', PcmCaptureProcessor)
|
|
27
|
+
`,P=2e3,w=1e3,_=3e4,I=2,a=5,g=3;var $=class{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??P,this.options.echoCancellation=this.options.echoCancellation??!0,this.options.noiseSuppression=this.options.noiseSuppression??!0,this.options.autoGainControl=this.options.autoGainControl??!0;let s=this.options.eagerEotThreshold===0?"disabled":`${this.options.eagerEotThreshold}`,i=this.options.eagerMaxTurnDurationMs===0?"disabled":`${this.options.eagerMaxTurnDurationMs}ms`;t.debug(`${n} init \u2014 features: pure-flux-event-flow, eot_threshold=${this.options.eotThreshold}, eager_eot_threshold=${s}, eot_timeout_ms=${this.options.eotTimeoutMs}, eager_max_turn_duration_ms=${i}`),this.handleAppMessages()}options;connection=null;state="Idle";shouldReconnect=!0;stream=null;audioContext=null;workletNode=null;reconnectAttempts=0;reconnectDelay=w;reconnectTimeoutId=null;digitalHumanSpeaking=!1;pendingPromptRequest=null;isUserCurrentlySpeaking=!1;isUiShowingSpeaking=!1;eagerPromptSentForTurn=!1;cancelledEagerRequestId=void 0;turnStartedAt=null;safetyNetTimeoutId=null;audioChunksSent=0;async startRecognition(){t.info(`${n} Starting speech recognition`),this.shouldReconnect=!0,this.resetReconnectionState(),await this.connect()}async stopRecognition(){t.info(`${n} Stopping speech recognition`),this.shouldReconnect=!1,this.clearReconnectTimeout(),await this.disconnect()}async pause(){return t.info(`${n} Pausing speech recognition`),this.state="Paused",this.clearSafetyNet(),this.resetSpeakingStates(),this.eagerPromptSentForTurn=!1,this.turnStartedAt=null,this.pendingPromptRequest=null,this.cancelledEagerRequestId=void 0,this.stream&&(this.stream.getTracks().forEach(e=>{e.enabled=!1}),t.debug(`${n} Audio tracks disabled`)),!0}async resume(){if(t.info(`${n} Resuming speech recognition`),this.state==="Paused"){if(this.stream)return this.state="Connected",this.stream.getTracks().forEach(e=>{e.enabled=!0}),t.debug(`${n} Audio tracks re-enabled`),!0;if(this.connection)return this.state="Connected",await this.startMicrophone(),!0;this.state="Disconnected"}return t.debug(`${n} Initiating connection`),await this.connect(),!0}setChatMetadata(e){this.options.promptMetadata=e}async connect(){if(this.state==="Connected"){t.warn(`${n} Already connected`);return}if(this.state==="Connecting"){t.warn(`${n} Connection already in progress`);return}this.state="Connecting";try{let e=await this.getToken();t.info(`${n} Connecting to Deepgram Flux v2 \u2014 api_url="${e.api_url}", sdk_version="${e.sdk_version}", token_length=${e.token?.length??0}`);let s=new E({accessToken:e.token,baseUrl:e.api_url}),i={model:this.options.model,encoding:"linear16",sample_rate:String(l),mip_opt_out:"true",...this.options.eotThreshold!==void 0&&{eot_threshold:this.options.eotThreshold},...this.options.eagerEotThreshold!==void 0&&this.options.eagerEotThreshold>0&&{eager_eot_threshold:this.options.eagerEotThreshold},...this.options.eotTimeoutMs!==void 0&&{eot_timeout_ms:this.options.eotTimeoutMs},...this.options.keyterms&&this.options.keyterms.length>0&&{keyterm:this.options.keyterms}};if(this.connection=await s.listen.v2.connect(i),this.connection.connect(),await Promise.race([this.connection.waitForOpen(),new Promise((o,c)=>setTimeout(()=>c(new Error("Connection timeout")),R))]),this.state!=="Paused"&&(this.state="Connected"),t.info(`${n} Connection opened`),this.setupEventHandlers(),this.state==="Paused"){t.info(`${n} Pause requested during connection \u2014 staying paused`),this.resetReconnectionState();return}await this.startMicrophone(),t.info(`${n} Connected successfully`),this.resetReconnectionState()}catch(e){this.state="Disconnected",t.error(`${n} Connection error`,t.serialiseError(e)),this.shouldReconnect&&(this.emitTransientError(e),this.scheduleReconnect())}}async disconnect(){if(!(this.state==="Idle"||this.state==="Disconnected"&&!this.connection)){t.info(`${n} Disconnecting`);try{if(this.stopMicrophone(),this.connection){try{this.connection.sendCloseStream({type:"CloseStream"})}catch{}this.connection.close(),this.connection=null}}catch(e){t.error(`${n} Disconnect error`,t.serialiseError(e))}this.clearSafetyNet(),this.resetSpeakingStates(),this.eagerPromptSentForTurn=!1,this.turnStartedAt=null,this.pendingPromptRequest=null,this.cancelledEagerRequestId=void 0,this.state="Disconnected",this.clientMsgSend(new r(!1))}}scheduleReconnect(){if(this.reconnectAttempts>=a){t.error(`${n} Max reconnection attempts (${a}) reached`),this.clientMsgSend(new u(`Unable to connect to speech recognition service after ${a} attempts`));return}this.reconnectAttempts++,t.info(`${n} Scheduling reconnection attempt ${this.reconnectAttempts}/${a} in ${this.reconnectDelay}ms`),this.reconnectTimeoutId=setTimeout(()=>{this.connect()},this.reconnectDelay),this.reconnectDelay=Math.min(this.reconnectDelay*I,_)}resetReconnectionState(){this.reconnectAttempts=0,this.reconnectDelay=w,this.clearReconnectTimeout()}clearReconnectTimeout(){this.reconnectTimeoutId&&(clearTimeout(this.reconnectTimeoutId),this.reconnectTimeoutId=null)}async getToken(){let e=this.options.model||"flux-general-en",s=`${this.options.connectionUrl}/speech-recognition-service/deepgram/token?model=${encodeURIComponent(e)}`,i=await fetch(s,{method:"GET",headers:{Authorization:`Bearer ${this.options.jwtToken}`,"Content-Type":"application/json"}});if(!i.ok)throw new Error(`Token fetch failed: ${i.status} ${i.statusText}`);return await i.json()}async startMicrophone(){try{if(t.info(`${n} 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==="Paused"){t.info(`${n} Paused during getUserMedia \u2014 keeping stream but disabling tracks`),this.stream.getTracks().forEach(o=>{o.enabled=!1});return}this.audioContext=new AudioContext({sampleRate:l});let e=this.audioContext.createMediaStreamSource(this.stream),s=new Blob([y],{type:"application/javascript"}),i=URL.createObjectURL(s);await this.audioContext.audioWorklet.addModule(i),URL.revokeObjectURL(i),this.workletNode=new AudioWorkletNode(this.audioContext,"pcm-capture-processor"),this.audioChunksSent=0,this.workletNode.port.onmessage=o=>{!this.connection||this.state!=="Connected"||(this.connection.sendMedia(o.data),this.audioChunksSent++,this.audioChunksSent%50===1&&t.debug(`${n} Audio chunks sent: ${this.audioChunksSent}, size: ${o.data.byteLength} bytes`))},e.connect(this.workletNode),this.workletNode.connect(this.audioContext.destination),t.info(`${n} Microphone started (linear16 PCM @ ${l}Hz)`),this.clientMsgSend(new r(!0))}catch(e){t.error(`${n} Microphone error`,t.serialiseError(e)),this.clientMsgSend(new S(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),t.info(`${n} Microphone stopped`)}setupEventHandlers(){this.connection&&(this.connection.on("open",()=>{this.handleConnectionOpen()}),this.connection.on("message",e=>{if(e!==null&&typeof e=="object"&&"type"in e){let s=e;s.type==="TurnInfo"?this.handleTurnInfo(e):s.type==="Connected"?t.info(`${n} v2 connection confirmed`):s.type==="Error"?this.handleFatalError(e):t.debug(`${n} Unhandled v2 message type: ${s.type}`)}}),this.connection.on("close",e=>{this.handleConnectionClose(e)}),this.connection.on("error",e=>{let s={};e instanceof Event&&(s.type=e.type,s.target=e.target?.url??e.target?.readyState??"unknown"),t.error(`${n} WebSocket error event`,e,s),this.emitTransientError(e)}))}handleTurnInfo(e){try{switch(t.debug(`${n} 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:t.debug(`${n} Unknown TurnInfo event: ${e.event}`)}}catch(s){t.error(`${n} Error processing TurnInfo`,t.serialiseError(s))}finally{this.resetSafetyNet()}}handleStartOfTurn(e){t.debug(`${n} StartOfTurn: turn_index=${e.turn_index}`),this.eagerPromptSentForTurn=!1,this.turnStartedAt=null,this.cancelledEagerRequestId=void 0}handleUpdate(e){let s=e.transcript||"";if(s==="")return;if(this.isUiShowingSpeaking||(this.isUiShowingSpeaking=!0,this.turnStartedAt=Date.now(),this.clientMsgSend(new T)),this.isUserCurrentlySpeaking||(this.isUserCurrentlySpeaking=!0,this.dataChannelMsgSend(new h("start"))),this.digitalHumanSpeaking){let o=this.countWords(s);o>=g?(t.info(`${n} User speech detected during avatar speaking (${o} words) \u2014 interrupting`),this.dataChannelMsgSend(new p),this.clientMsgSend(new M),this.digitalHumanSpeaking=!1):t.debug(`${n} User speech during avatar speaking is only ${o} word(s) \u2014 holding off barge-in (potential backchannel)`)}let i={transcript:s,final:!1,confidence:this.calculateWordConfidence(e.words),language_code:this.options.language||""};this.clientMsgSend(new d(i))}handleEagerEndOfTurn(e){let s=e.transcript||"";if(t.debug(`${n} EagerEndOfTurn: confidence=${e.end_of_turn_confidence}, transcript_length=${s.length}`),s.trim()==="")return;if(this.digitalHumanSpeaking&&this.countWords(s)<g){t.info(`${n} EagerEndOfTurn: dropping ${this.countWords(s)}-word backchannel while avatar speaking`);return}let i=this.options.eagerMaxTurnDurationMs??0;if(i>0&&this.turnStartedAt!==null){let o=Date.now()-this.turnStartedAt;if(o>i){t.info(`${n} EagerEndOfTurn: suppressed \u2014 turn duration ${o}ms exceeds eagerMaxTurnDurationMs=${i}; deferring to EndOfTurn`);return}}t.info(`${n} EagerEndOfTurn: sending prompt early (${this.countWords(s)} words, ${s.length} chars)`),this.eagerPromptSentForTurn=!0,this.sendChatPromptRaw(s,this.takeCancelledEagerRequestId())}takeCancelledEagerRequestId(){let e=this.cancelledEagerRequestId;return this.cancelledEagerRequestId=void 0,e}handleTurnResumed(e){t.debug(`${n} TurnResumed: turn_index=${e.turn_index}`),this.pendingPromptRequest!==null&&(this.cancelledEagerRequestId=this.pendingPromptRequest.requestId),this.eagerPromptSentForTurn&&(t.info(`${n} TurnResumed: cancelling in-flight eager prompt via StopSpeaking`),this.dataChannelMsgSend(new p)),this.clearPendingPromptRequest(),this.eagerPromptSentForTurn=!1}handleEndOfTurn(e){let s=e.transcript||"";if(t.info(`${n} EndOfTurn: transcript_length=${s.length}, confidence=${e.end_of_turn_confidence}`),s.trim()!==""){let i={transcript:s,final:!0,confidence:this.calculateWordConfidence(e.words),language_code:this.options.language||""};this.clientMsgSend(new d(i)),this.digitalHumanSpeaking&&this.countWords(s)<g?(t.info(`${n} EndOfTurn: dropping ${this.countWords(s)}-word backchannel while avatar speaking`),this.clearPendingPromptRequest()):this.eagerPromptSentForTurn?(t.debug(`${n} EndOfTurn: skipping ChatPrompt \u2014 eager already fired for this turn`),this.emitPendingPromptRequest()):(this.sendChatPromptRaw(s,this.takeCancelledEagerRequestId()),this.emitPendingPromptRequest())}this.eagerPromptSentForTurn=!1,this.turnStartedAt=null,this.cancelledEagerRequestId=void 0,this.resetSpeakingStates()}handleFatalError(e){t.error(`${n} Fatal error from Deepgram: ${e.code} \u2014 ${e.description}`),this.clientMsgSend(new u(`Deepgram error: ${e.code} \u2014 ${e.description}`))}calculateWordConfidence(e){return!e||e.length===0?1:e.reduce((i,o)=>i+o.confidence,0)/e.length}countWords(e){return e.trim().split(/\s+/).filter(Boolean).length}handleAppMessages(){this.options.messages.subscribe(e=>{switch(e.uneeqMessageType){case"AvatarStartedSpeaking":this.digitalHumanSpeaking=!0;break;case"PromptResult":{e.promptResult.success||this.handleSpeakingEnd();break}case"AvatarAnswer":{e.answerSpeech.replace(/<[^>]*>/g,"")===""&&this.handleSpeakingEnd();break}case"AvatarStoppedSpeaking":{this.handleSpeakingEnd();break}case"SessionEnded":{this.shouldReconnect=!1,this.stopRecognition();break}case"SessionReconnecting":{this.handleSpeakingEnd(),this.shouldReconnect=!1,this.stopRecognition();break}case"CustomMetadataUpdated":{this.options.promptMetadata=e.chatMetadata;break}case"SessionBackendError":{this.handleSpeakingEnd();break}default:}})}handleConnectionOpen(){this.state!=="Paused"&&(this.state="Connected")}handleConnectionClose(e){let s=e?.code??"unknown",i=e?.reason??"";if(t.info(`${n} Connection closed \u2014 code=${s}, reason="${i}"`),this.state==="Paused"){t.info(`${n} Connection closed while paused \u2014 will reconnect on resume`),this.connection=null,this.stopMicrophone(),this.clearSafetyNet(),this.resetSpeakingStates();return}this.state="Disconnected",this.clearSafetyNet(),this.resetSpeakingStates(),this.eagerPromptSentForTurn=!1,this.turnStartedAt=null,this.pendingPromptRequest=null,this.cancelledEagerRequestId=void 0,this.clientMsgSend(new r(!1)),this.shouldReconnect&&(t.info(`${n} Unexpected disconnect, attempting reconnection...`),this.scheduleReconnect())}emitTransientError(e){let s=e instanceof Error?e.message:String(e);this.clientMsgSend(new v(s))}sendChatPromptRaw(e,s){if(!e||e.trim()==="")return;this.options.language&&(this.options.promptMetadata.userSpokenLocale=this.options.language);let i=s??m();s!==void 0&&t.info(`${n} sendChatPromptRaw: reusing requestId=${i} from cancelled eager prompt`);let o={...this.options.promptMetadata},c=this.turnStartedAt!==null?Date.now()-this.turnStartedAt:0;this.pendingPromptRequest={prompt:e,requestId:i,metadata:o,speakingDurationMs:c},this.dataChannelMsgSend(new f(e,this.options.promptMetadata,i,!1))}emitPendingPromptRequest(){let e=this.pendingPromptRequest;e!==null&&(this.clientMsgSend(new C({prompt:e.prompt,requestId:e.requestId,metadata:e.metadata,speakingDurationMs:e.speakingDurationMs})),t.debug(`${n} PromptRequest emitted \u2014 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(()=>{t.warn(`${n} Safety net: no TurnInfo events for ${this.options.safetyNetTimeoutMs}ms while speaking \u2014 resetting`),this.resetSpeakingStates()},this.options.safetyNetTimeoutMs))}clearSafetyNet(){this.safetyNetTimeoutId&&(clearTimeout(this.safetyNetTimeoutId),this.safetyNetTimeoutId=null)}resetSpeakingStates(){this.isUserCurrentlySpeaking&&(this.isUserCurrentlySpeaking=!1,this.dataChannelMsgSend(new h("stop"))),this.isUiShowingSpeaking&&(this.isUiShowingSpeaking=!1,this.clientMsgSend(new k))}dataChannelMsgSend(e){this.options.sendMessage(e)}clientMsgSend(e){this.options.messages.next(e)}};export{$ as DeepgramFluxSTT};
|
package/dist/esm/index.js
CHANGED
|
@@ -84,4 +84,4 @@ m=`).map((l,c)=>(c>0?"m="+l:l).trim()+`\r
|
|
|
84
84
|
Packets lost: ${i},
|
|
85
85
|
Bytes received: ${r},
|
|
86
86
|
Jitter: ${o},
|
|
87
|
-
Audio level (0-1): ${a??"N/A"}`),n===0&&p.warn("WebRTC Connection - Audio - No audio packets received \u2014 remote is probably not sending audio."),i>0&&p.warn(`WebRTC Connection - Audio - packet loss detected: ${i} packets lost.`),a===void 0?p.warn("WebRTC Connection - Audio - Audio level not available in stats."):a===0?p.warn("WebRTC Connection - Audio - Audio level is zero \u2014 digital human might not be talking at this exact moment as audio is silent or muted."):a<.02?p.warn("WebRTC Connection - Audio - Audio level is very low \u2014 audio might be too quiet."):p.info("WebRTC Connection - Audio - Audio level looks normal, digital human is likely speaking.")}logElementStatus(e){p.debug(`Video element muted: ${this.videoElement?.muted}, (should be true as part of avoiding auto play restrictions)`),p.debug(`Audio element muted: ${this.audioElement?.muted}, (should be false to be able to hear the digital human)`),p.debug(`Audio element volume: ${this.audioElement?.volume}, (should be greater than zero to be able to hear the digital human)`);let t=e?.mimeType;t&&this.audioElement&&(p.debug(`Audio element can play webrtc audio codec (${t}) : ${this.audioElement.canPlayType(t)}, (should be 'probably' or 'maybe)'`),t==="audio/opus"&&p.debug(`Audio codec opus is often known as ogg, so checking can play ogg codec: ${this.audioElement.canPlayType("audio/ogg")}, (should be 'probably' or 'maybe')`))}};var Pe={good:"#22c55e",fair:"#eab308",poor:"#ef4444",inactive:"#6b7280"},xn=class{constructor(e,t){this.videoContainerElement=e;this.visibility=t.visibility??"when-degraded",this.position=t.position??"top-right",this.popupTrigger=t.popupTrigger??"click",this.onClickBound=this.onClick.bind(this),this.onMouseEventBound=n=>n.stopPropagation(),this.onDocumentClickBound=this.onDocumentClick.bind(this),this.onMouseEnterBound=this.onMouseEnter.bind(this),this.onMouseLeaveBound=this.onMouseLeave.bind(this),this.visibility!=="hidden"&&this.createElements()}videoContainerElement;container;panel;bars=[];panelVisible=!1;lastMetrics;visibility;position;popupTrigger;onClickBound;onMouseEventBound;onDocumentClickBound;onMouseEnterBound;onMouseLeaveBound;hoverHideTimeout;originalParentPosition;createElements(){getComputedStyle(this.videoContainerElement).position==="static"&&(this.originalParentPosition=this.videoContainerElement.style.position,this.videoContainerElement.style.position="relative"),this.container=document.createElement("div"),this.container.setAttribute("data-testid","network-indicator"),this.container.setAttribute("role","status"),this.container.setAttribute("aria-live","polite"),this.container.setAttribute("aria-label","Network quality: unknown"),Object.assign(this.container.style,{position:"absolute",zIndex:"1000",cursor:"pointer",padding:"6px 7px",borderRadius:"4px",background:"rgba(0, 0, 0, 0.5)",display:"none",alignItems:"flex-end",gap:"2px",pointerEvents:"auto",...this.getPositionStyles()});let t=[6,12,18];for(let n of t){let i=document.createElement("div");i.setAttribute("data-testid","network-indicator-bar"),Object.assign(i.style,{width:"4px",height:`${n}px`,borderRadius:"1px",background:Pe.inactive,transition:"background 0.3s ease"}),this.bars.push(i),this.container.appendChild(i)}this.container.addEventListener("mousedown",this.onMouseEventBound),this.container.addEventListener("mouseup",this.onMouseEventBound),this.popupTrigger==="click"?this.container.addEventListener("click",this.onClickBound):this.popupTrigger==="hover"&&(this.container.addEventListener("mouseenter",this.onMouseEnterBound),this.container.addEventListener("mouseleave",this.onMouseLeaveBound)),this.popupTrigger==="none"&&(this.container.style.cursor="default"),this.videoContainerElement.appendChild(this.container)}getPositionStyles(){switch(this.position){case"top-left":return{top:"8px",left:"8px"};case"top-right":return{top:"8px",right:"8px"};case"bottom-left":return{bottom:"8px",left:"8px"};case"bottom-right":return{bottom:"8px",right:"8px"};default:return{top:"8px",right:"8px"}}}onClick(e){e.stopPropagation(),this.panelVisible?this.hidePanel():this.showPanel()}onDocumentClick(e){this.panelVisible&&this.container&&!this.container.contains(e.target)&&this.hidePanel()}onMouseEnter(){clearTimeout(this.hoverHideTimeout),this.showPanel()}onMouseLeave(){this.scheduleHoverHide()}scheduleHoverHide(){clearTimeout(this.hoverHideTimeout),this.hoverHideTimeout=setTimeout(()=>this.hidePanel(),50)}showPanel(){!this.container||!this.lastMetrics||(this.panel||(this.panel=document.createElement("div"),this.panel.setAttribute("data-testid","network-indicator-panel"),Object.assign(this.panel.style,{position:"fixed",padding:"10px",borderRadius:"6px",background:"rgba(0, 0, 0, 0.85)",color:"#ffffff",fontSize:"11px",fontFamily:"system-ui, -apple-system, sans-serif",whiteSpace:"nowrap",lineHeight:"1.6",minWidth:"180px",zIndex:"10001",pointerEvents:"auto"}),this.popupTrigger==="hover"&&(this.panel.addEventListener("mouseenter",this.onMouseEnterBound),this.panel.addEventListener("mouseleave",this.onMouseLeaveBound)),document.body.appendChild(this.panel)),this.updatePanel(),this.positionPanel(),this.panel.style.display="block",this.panelVisible=!0,document.addEventListener("click",this.onDocumentClickBound))}hidePanel(){this.panel&&(this.panel.style.display="none"),this.panelVisible=!1,document.removeEventListener("click",this.onDocumentClickBound)}positionPanel(){if(!this.panel||!this.container)return;let e=4,t=4,n=window.innerWidth,i=window.innerHeight,r=this.container.getBoundingClientRect(),o={top:Math.max(0,Math.min(r.top,i)),bottom:Math.max(0,Math.min(r.bottom,i)),left:Math.max(0,Math.min(r.left,n)),right:Math.max(0,Math.min(r.right,n))};this.panel.style.top="",this.panel.style.bottom="",this.panel.style.left="",this.panel.style.right="",this.panel.style.visibility="hidden",this.panel.style.display="block";let{width:a,height:l}=this.panel.getBoundingClientRect();this.panel.style.visibility="";let c=o.top-e,u=i-o.bottom-e,w=o.left-e,v=n-o.right-e,C,x;c>=l||u>=l?(C=u>=l&&(c<l||u>=c)?o.bottom+e:o.top-e-l,x=this.position==="top-left"||this.position==="bottom-left"?o.left:o.right-a):(x=v>=a&&(w<a||v>=w)?o.right+e:o.left-e-a,C=o.bottom-l),x=Math.max(t,Math.min(x,n-a-t)),C=Math.max(0,Math.min(C,i-l-t)),this.panel.style.top=`${C}px`,this.panel.style.left=`${x}px`}updatePanel(){if(!this.panel||!this.lastMetrics)return;let e=this.lastMetrics,t=this.getColor(e.qualityLevel);this.panel.innerHTML=[`<div style="font-weight:600;margin-bottom:4px;color:${t}">${e.qualityLevel} Quality</div>`,this.panelSectionHeader("Quality Score (MOS)"),this.panelRow("Audio",e.audioMos.toFixed(1)+" / 5.0"),this.panelRow("Video",e.videoMos.toFixed(1)+" / 5.0"),this.panelSectionHeader("Connection"),this.panelRow("Round Trip",`${e.roundTripTimeMs} ms`),this.panelRow("Packet Loss",`${e.audioPacketLossPercent}% audio / ${e.videoPacketLossPercent}% video`),this.panelRow("Jitter",`${e.audioJitterMs} ms audio / ${e.videoJitterMs} ms video`),this.panelSectionHeader("Throughput"),this.panelRow("Bitrate",`${e.audioBitrateKbps} audio / ${e.videoBitrateKbps} video kbps`),this.panelRow("Frame Rate",`${e.frameRate} fps`)].join("")}panelSectionHeader(e){return`<div style="color:#6b7280;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;margin-top:6px;margin-bottom:2px">${e}</div>`}panelRow(e,t){return`<div style="display:flex;justify-content:space-between;gap:12px"><span style="color:#9ca3af">${e}</span><span>${t}</span></div>`}getColor(e){switch(e){case"Good":return Pe.good;case"Fair":return Pe.fair;case"Poor":return Pe.poor}}update(e){if(this.lastMetrics=e,!this.container)return;this.container.setAttribute("aria-label",`Network quality: ${e.qualityLevel}`);let t=this.getColor(e.qualityLevel),n=e.qualityLevel==="Good"?3:e.qualityLevel==="Fair"?2:1;this.bars.forEach((i,r)=>{i.style.background=r<n?t:Pe.inactive}),this.visibility==="when-degraded"?this.container.style.display=e.qualityLevel==="Good"?"none":"flex":this.container.style.display="flex",this.panelVisible&&(this.updatePanel(),this.positionPanel())}destroy(){this.container&&(this.container.removeEventListener("click",this.onClickBound),this.container.removeEventListener("mousedown",this.onMouseEventBound),this.container.removeEventListener("mouseup",this.onMouseEventBound),this.container.removeEventListener("mouseenter",this.onMouseEnterBound),this.container.removeEventListener("mouseleave",this.onMouseLeaveBound),this.container.remove(),this.container=void 0),this.originalParentPosition!==void 0&&(this.videoContainerElement.style.position=this.originalParentPosition,this.originalParentPosition=void 0),clearTimeout(this.hoverHideTimeout),this.panel&&(this.panel.removeEventListener("mouseenter",this.onMouseEnterBound),this.panel.removeEventListener("mouseleave",this.onMouseLeaveBound),this.panel.remove(),this.panel=void 0),document.removeEventListener("click",this.onDocumentClickBound),this.bars=[],this.panelVisible=!1,this.lastMetrics=void 0}};var kn=class{constructor(e,t,n){this.session=e;this.connectionUrl=t;this.videoContainerElement=n;this.config=e.config,this.clientMessages=e.messages,this._audioUpstreamMode=G(this.config.audioUpstreamMode)}session;connectionUrl;videoContainerElement;stream;messages=new eo;videoElement;audioElement;videoInitialized=!1;dataChannelOpen=!1;sceneReady=!1;maxReconnectAttempts=4;reconnectionAttempts=0;allowReconnection=!0;reconnectTimeoutId;baseReconnectDelayMs=500;maxReconnectDelayMs=1e4;audioStatsInternal;freezeCount=0;lastDecodedFrames=0;lastCheckTime=Date.now();audioStatsAnalyzer;networkQualityInterval;networkIndicator;config;clientMessages;_audioUpstreamMode;get audioUpstreamMode(){return this._audioUpstreamMode}init(){p.info("Connection config",{connectionUrl:this.connectionUrl,forceTURN:this.config.forceTURN}),this.videoContainerElement||(this.videoContainerElement=document.createElement("div"),document.body.appendChild(this.videoContainerElement)),this.clientMessages?.next(new ii);let e=re(this.audioUpstreamMode);this.audioUpstreamMode!=="speech-recognition-service"&&p.info(`audioUpstreamMode='${this.audioUpstreamMode}' is beta; behaviour may change.`);let t=new zt({initialSettings:{AutoConnect:!1,AutoPlayVideo:!1,ForceTURN:this.config.forceTURN,HoveringMouse:!1,KeyboardInput:!1,MaxReconnectAttempts:0,MouseInput:!1,ss:this.connectionUrl,StartVideoMuted:!0,TouchInput:!1,UseMic:e,WaitForStreamer:!0}});this.stream=new ke(t,{videoElementParent:this.videoContainerElement}),d.InitLogging(K.Warning,!1);let n=function(){let i=t.getTextSettingValue(z.SignallingServerUrl);return t.isFlagEnabled(h.BrowserSendOffer)&&(i+=(i.includes("?")?"&":"?")+h.BrowserSendOffer+"=true"),i};this.stream.setSignallingUrlBuilder(n),this.addStreamHandlers(),this.stream.connect(),this.addWebSocketHandlers()}addStreamHandlers(){this.stream?.addEventListener("videoInitialized",this.handleVideoInitialized.bind(this)),this.stream?.addEventListener("dataChannelOpen",this.handleDataChannelOpen.bind(this)),this.stream?.addEventListener("webRtcFailed",this.handleWebRtcFailed.bind(this)),this.stream?.addEventListener("webRtcDisconnected",this.handleWebRtcDisconnected.bind(this)),this.stream?.addEventListener("statsReceived",this.handleWebRtcStats.bind(this)),this.stream?.addResponseEventListener("response-listener",this.handleDataChannelMessage.bind(this)),this.stream?.addEventListener("videoEncoderAvgQP",({data:{avgQP:e}})=>{p.trace("WebRTC: VideoEncoderAvgQP - lower is better, ranges from 0 to 51:",e)}),this.stream?.addEventListener("webRtcAutoConnect",()=>{p.debug("WebRTC: AutoConnect triggered")}),this.stream?.addEventListener("webRtcConnected",this.handleWebRtcConnectedMessage.bind(this)),this.stream?.addEventListener("statsReceived",({data:{aggregatedStats:e}})=>{p.trace("WebRTC: StatsReceived",e)}),this.stream?.addEventListener("webRtcTCPRelayDetected",()=>{p.warn("WebRTC: Stream quailty likely degraded due to network enviroment, stream is relayed over TCP.")})}addWebSocketHandlers(){this.stream?.signallingProtocol?.transport?.webSocket?.addEventListener("error",this.handleWebSocketError.bind(this)),this.stream?.signallingProtocol.on("error",t=>{p.error("Signaling: Received error message from server",t),this.clientMessages?.next(new q(`Signaling server error: ${t}`))}),this.stream?.signallingProtocol.on("index_in_queue",this.handleIndexInQueueMessage.bind(this)),this.stream?.signallingProtocol.on("renderer_taken",this.handleRendererTakenMessage.bind(this)),this.stream?.signallingProtocol.on("renderer_gone",this.handleRendererGoneEvent.bind(this)),this.stream?.signallingProtocol.on("switch_renderer_request",this.handleSwitchRendererRequestEvent.bind(this)),this.stream?.signallingProtocol.on("customer_concurrency_limit_reached",this.handleCustomerConcurrencyLimitReachedMessage.bind(this)),this.stream?.signallingProtocol.on("renderer_error",this.handleRendererErrorMessage.bind(this)),this.stream?.signallingProtocol.on("config",this.checkForStubbedTurnConfig.bind(this))}checkForStubbedTurnConfig(e){let t=e?.peerConnectionOptions?.iceServers??[];p.info("Signaling: Received ice config message with urls:",t.map(i=>({urls:i.urls}))),t.some(i=>i.username?.includes("disabled")&&i.urls?.some(r=>r.includes("0.0.0.0")))&&p.warn("Signaling: TURN server configuration is stubbed (turn:0.0.0.0 with disabled credentials). This should be intentional, is intended for locally run renderers e.g. kiosk, miniprem etc.")}handleWebSocketError(e){p.error("Signaling: WebSocket error",p.serialiseError(e)),this.reconnectIfSafe(e)}handleVideoInitialized(){if(p.info("Video initialized."),this.videoElement=this.videoContainerElement?.getElementsByTagName("video")[0],this.audioElement=this.stream?.webRtcController?.streamController?.audioElement,this.audioStatsAnalyzer=new pe(this.stream,this.videoElement,this.audioElement),this.audioElement&&this.config.speakerDeviceId&&this.setSpeakerDevice(this.audioElement,this.config.speakerDeviceId),this.videoElement)this.videoElement.muted=!0,this.videoElement.play().then(()=>{this.playAudioElement()}).catch(e=>{if(e instanceof DOMException&&e.name==="NotAllowedError")p.warn("Video autoplay blocked by browser. Requesting user gesture to resume playback."),this.clientMessages?.next(new ai);else{let t="Fatal Error: Digital Human video cannot be played.";p.error(t,p.serialiseError(e)),this.clientMessages?.next(new q(t)),this.endSession(Is,!0)}}),this.videoInitialized=!0,this.messages.next(new Mn),this.reconnectionAttempts=0,this.clearReconnectTimer();else{let e="Fatal Error: Video element not found.";p.error(e),this.clientMessages?.next(new q(e)),this.endSession(Is,!0)}setTimeout(()=>{this.clientMessages?.next(new ei)},Ls)}playAudioElement(){this.audioElement?this.audioElement.play().then(()=>{p.info("Audio played successfully."),this.clientMessages?.next(new qn)}).catch(e=>{p.warn("Audio cannot be played.",e),this.clientMessages?.next(new Oe),this.clientMessages?.next(new $n)}):this.handleMissingAudioElement()}resumeVideoPlayback(){if(!this.videoElement){p.warn("resumeVideoPlayback called but no video element available");return}this.videoElement.play().then(()=>{this.playAudioElement()}).catch(e=>{p.error("Video playback failed after user gesture",p.serialiseError(e)),this.clientMessages?.next(new q("Video playback failed after user gesture")),this.endSession(hr,!0)})}pauseAudioElement(){this.audioElement?(this.audioElement.pause(),this.clientMessages?.next(new Oe)):this.handleMissingAudioElement()}muteUpstreamMic(){if(!re(this.audioUpstreamMode)){p.warn(`muteUpstreamMic called in '${this.audioUpstreamMode}' mode \u2014 no PS upstream audio track to mute; ignoring.`);return}if(!this.stream){p.warn("muteUpstreamMic called before PixelStreaming stream is initialised; ignoring.");return}this.stream.muteMicrophone()}unmuteUpstreamMic(){if(!re(this.audioUpstreamMode)){p.warn(`unmuteUpstreamMic called in '${this.audioUpstreamMode}' mode \u2014 no PS upstream audio track to unmute; ignoring.`);return}if(!this.stream){p.warn("unmuteUpstreamMic called before PixelStreaming stream is initialised; ignoring.");return}this.stream.unmuteMicrophone(!1)}setSpeakerDevice(e,t){let n=e;n.setSinkId?n.setSinkId(t).then(()=>{p.info(`Speaker device set to: ${t}`)}).catch(i=>{p.error(`Failed to set speaker device: ${t}`,p.serialiseError(i))}):p.error("setSinkId not supported in this browser, cannot set speaker device")}handleMissingAudioElement(){let e="Error: Digital Human audio element not found, cannot play audio.";p.error(e),this.clientMessages?.next(new q(e))}startAudioStatsMonitor(e=5e3){this.stopAudioStatsMonitor(),p.info("Starting audio stats monitor"),this.audioStatsAnalyzer||(this.audioStatsAnalyzer=new pe(this.stream,this.videoElement,this.audioElement)),this.audioStatsInternal=setInterval(()=>{this.audioStatsAnalyzer?.analyzeAudioStats().catch(t=>{p.error("Error analyzing audio stats",p.serialiseError(t))})},e)}stopAudioStatsMonitor(){this.audioStatsInternal&&clearInterval(this.audioStatsInternal)}startNetworkQualityMonitor(e){this.stopNetworkQualityMonitor(),p.info("Starting network quality monitor"),this.audioStatsAnalyzer||(this.audioStatsAnalyzer=new pe(this.stream,this.videoElement,this.audioElement)),this.videoContainerElement&&e.visibility!=="hidden"&&(this.networkIndicator=new xn(this.videoContainerElement,e));let t=e.collectionIntervalMs??3e3;this.networkQualityInterval=setInterval(()=>{this.audioStatsAnalyzer?.collectNetworkQuality(e).then(n=>{n&&(this.networkIndicator?.update(n),this.clientMessages?.next(new oi(n)))}).catch(n=>{p.error("Error collecting network quality",p.serialiseError(n))})},t)}stopNetworkQualityMonitor(){this.networkQualityInterval&&(clearInterval(this.networkQualityInterval),this.networkQualityInterval=void 0),this.networkIndicator&&(this.networkIndicator.destroy(),this.networkIndicator=void 0)}handleDataChannelOpen(){p.info("DataChannelOpen"),this.dataChannelOpen=!0,this.messages.next(new Tn)}handleWebRtcFailed(e){p.error("WebRTC failed",e),this.reconnectIfSafe(e)}handleWebRtcDisconnected(e){p.warn(`WebRTC disconnected, message: ${e?.data?.eventString}, allowClickToReconnect: ${e?.data?.allowClickToReconnect}`),this.reconnectIfSafe(e)}handleWebRtcStats(e){let t=new Rn(e.data.aggregatedStats);this.messages.next(t);let n=e.data.aggregatedStats.inboundVideoStats?.framesDecoded??0;this.checkForFreeze(n)}checkForFreeze(e){let t=Date.now();if(this.lastDecodedFrames===0&&e===0){p.debug("[WebRTC] Waiting for first video frame...");return}if(e===this.lastDecodedFrames){this.freezeCount++;let n=((t-this.lastCheckTime)/1e3).toFixed(1);p.warn(`[WebRTC] Video FROZEN for ${n}s (count=${this.freezeCount})`),this.freezeCount>=3&&(p.error("[WebRTC] Video frozen 3 times in a row, transferring session..."),this.session.sessionTransferManager.transferSession(),this.freezeCount=0)}else this.freezeCount=0,this.lastDecodedFrames=e,this.lastCheckTime=t}clearReconnectTimer(){this.reconnectTimeoutId&&(clearTimeout(this.reconnectTimeoutId),this.reconnectTimeoutId=void 0)}scheduleReconnect(){let e=Math.min(this.baseReconnectDelayMs*Math.pow(2,this.reconnectionAttempts),this.maxReconnectDelayMs);p.info(`Scheduling reconnection attempt ${this.reconnectionAttempts} in ${e}ms`),this.reconnectTimeoutId=setTimeout(()=>{p.info(`Executing reconnection attempt ${this.reconnectionAttempts}`),this.reconnectTimeoutId=void 0,this.videoInitialized=!1,this.dataChannelOpen=!1,this.sceneReady=!1,this.audioStatsAnalyzer?.resetStats(),this.stream?.reconnect()},e)}reconnectIfSafe(e){try{this.allowReconnection&&!this.reconnectTimeoutId?(p.debug("Checking if should attempt reconnection after event",e),this.reconnectionAttempts<this.maxReconnectAttempts?(this.reconnectionAttempts++,this.clientMessages?.next(new Zn),this.scheduleReconnect()):(p.info("Max reconnection attempts reached. Ending session."),this.clientMessages?.next(new Yn),this.endSession(ks,!1))):p.warn("Reconnection already scheduled or reconnecting is disabled")}catch(t){p.error("Error during reconnection attempt",p.serialiseError(t)),this.clientMessages?.next(new q("Error during reconnection attempt")),this.endSession(ks,!1)}}handleDataChannelMessage(e){p.trace("Signaling: Received data channel message",e);try{let t=JSON.parse(e);if(t.type!==void 0)this.messages.next(new En(t));else throw new Error("Signaling: Engine response missing type")}catch(t){p.error("Signaling: Failed to parse engine response",p.serialiseError(t))}}handleIndexInQueueMessage(e){try{let n=Number(e.message);this.clientMessages?.next(new Vn(n))}catch(t){p.error("Signaling: Failed to parse position in queue message",p.serialiseError(t))}}handleRendererTakenMessage(e){try{let t=e;this.clientMessages?.next(new Jn(t.message)),p.info("Signaling: Renderer taken event",t.message)}catch(t){p.error("Signaling: Failed to parse renderer taken message",p.serialiseError(t))}}handleWebRtcConnectedMessage(){this.clientMessages?.next(new si),p.info("Signaling: WebRTC connected")}handleRendererGoneEvent(e){p.info("Signaling: Renderer gone event",e),this.reconnectIfSafe(e)}handleSwitchRendererRequestEvent(e){p.info("Signaling: Renderer switch renderer request event",e),this.session.sessionTransferManager.transferSession()}handleCustomerConcurrencyLimitReachedMessage(e){p.warn("Signaling: Customer concurrency limit reached message received",e),this.clientMessages?.next(new ri)}handleRendererErrorMessage(e){try{let t=e,n=t.code??-1,i=t.message??"An unknown renderer error occurred";p.error(`Signaling: Renderer error received - code: ${n}, message: ${i}`),this.clientMessages?.next(new Fe(n,i)),this.endSession(Ps,!0)}catch(t){p.error("Signaling: Failed to parse renderer error message",p.serialiseError(t)),this.clientMessages?.next(new Fe(-1,"An unknown renderer error occurred")),this.endSession(Ps,!0)}}socketSend(e){p.trace("Signaling: Sending socket message",e),this.stream?.signallingProtocol?.transport?.webSocket?.send(JSON.stringify(e))}dataSend(e){let t=e.toJSON();if(t.action==="chat_prompt"){let n=t.data?.requestId;p.info(`Signaling: Sending data channel message \u2014 action=chat_prompt, requestId=${n??"unknown"}`),(!(e instanceof se)||e.shouldEmitPromptRequest)&&this.clientMessages?.next(new Qn(t.data))}else p.info("Signaling: Sending data channel message",JSON.stringify(t));this.stream?.emitUIInteraction(e)}dataSendBinary(e){p.info("Signaling: Sending data channel binary message");let t=this.uint8ToBase64(e);this.stream?.emitUIInteraction(t)}uint8ToBase64(e){let t="",n=e.byteLength;for(let i=0;i<n;i++)t+=String.fromCharCode(e[i]);return btoa(t)}endSession(e=dr,t=!0){p.info("Signaling: Ending session"),this.clearReconnectTimer(),this.stopNetworkQualityMonitor(),this.socketSend({message:e,type:"client_gone",sessionId:this.config.sessionId}),this.streamDisconnect(),p.info("Session has ended, reason: "+e),t&&this.clientMessages?.next(new zn(e))}streamDisconnect(){p.info("Signaling: Disconnecting stream"),this.stopNetworkQualityMonitor(),this.stopAudioStatsMonitor(),this.allowReconnection=!1,this.clearReconnectTimer();let e=this.stream?.webRtcController?.statsTimerHandle;e&&window.clearInterval(e);let t=this.stream?.webRtcController?.peerConnectionController;if(t?.peerConnection){t.peerConnection.getReceivers=()=>[];let n=t.peerConnection;Object.defineProperty(t,"peerConnection",{get:()=>n,set:()=>{},configurable:!0})}this.stream?.disconnect(),this.dataChannelOpen=!1,this.videoInitialized=!1,this.sceneReady=!1}startSoftSwitch(){p.info("Signaling: Start soft switching"),this.clientMessages?.next(new ti)}stopSoftSwitch(){p.info("Signaling: Stop soft switching"),this.clientMessages?.next(new ni)}};import{Subject as to}from"rxjs";var Sr=(o=>(o.CloseUp="close_up",o.LooseCloseUp="loose_close_up",o.TightMediumShot="tight_medium_shot",o.MediumShot="medium_shot",o.MediumFullShot="medium_full_shot",o.FullShot="full_shot",o))(Sr||{}),br=(i=>(i.Left="left",i.Right="right",i.Center="center",i.Centre="centre",i))(br||{}),Le=class{constructor(e,t=2e3,n=$()){this.position=e;this.duration=t;this.requestId=n}position;duration;requestId;toJSON(){return{action:"camera_to_anchor",data:{requestId:this.requestId,position:this.position,transition_duration_ms:this.duration}}}};var In=class{constructor(e,t=$()){this.avatarId=e;this.requestId=t}avatarId;requestId;toJSON(){return{action:"change_avatar",data:{id:this.avatarId,requestId:this.requestId}}}};var Pn=class{constructor(e,t=$()){this.url=e;this.requestId=t}url;requestId;toJSON(){return{action:"load_background",data:{url:this.url,requestId:this.requestId}}}};var Ln=class{constructor(e,t=$()){this.actionName=e;this.requestId=t}actionName;requestId;toJSON(){return{action:"trigger_action",data:{action_name:this.actionName,requestId:this.requestId}}}};var Dn=class{constructor(e){this.session=e;this.addTransferSessionListener()}session;isTransferSessionInProgress=!1;addTransferSessionListener(){this.isTransferSessionInProgress=!1,document.addEventListener("keydown",e=>{e.ctrlKey&&e.shiftKey&&e.key.toLowerCase()==="y"&&(p.info("Session transfer via keyboard shortcut triggered"),this.transferSession())})}transferSession(){if(this.isTransferSessionInProgress){p.warn("Transfer already in progress, skipping");return}this.isTransferSessionInProgress=!0,p.info("Beginning session transfer"),this.session.config.videoContainerElement&&(this.session.config.videoContainerElement.querySelectorAll("#freezeFrame,#streamingVideo").forEach(n=>{n.remove()}),this.session.signaling&&this.session.signaling.streamDisconnect()),this.session.createSignaling(this.session.config.videoContainerElement??document.createElement("div")).init()}switchLiveVideo(e,t){p.info("New signaling video is ready for transfer"),this.session.signaling&&e&&t.videoElement&&(t.videoElement.classList.add("incoming-video"),this.session.signaling.videoElement&&t.videoElement&&e?.appendChild(t.videoElement),this.session.signaling.startSoftSwitch(),setTimeout(()=>{this.session.signaling?.streamDisconnect(),this.session.signaling=t,this.isTransferSessionInProgress=!1,this.session.signaling?.stopSoftSwitch(),p.info("Session transfer complete")},1e3))}};var vr=[1103,1200,1402];var Ls=750,On=class{constructor(e){this.config=e;this.sessionTransferManager=new Dn(this),this._audioUpstreamMode=G(this.config.audioUpstreamMode)}config;id;signaling;messages=new to;speechRec;promptMetadata={};audioStreamId;sessionTransferManager;jwt;welcomeRequested=!1;resolutionLogged=!1;_audioUpstreamMode;get audioUpstreamMode(){return this._audioUpstreamMode}updatePromptMetadata(e){try{JSON.parse(JSON.stringify(e)),this.promptMetadata.custom=e,this.messages.next(new jn(this.promptMetadata))}catch(t){p.error("Error parsing custom metadata",e,p.serialiseError(t))}}setEnableMicrophone(e){let t=this.audioUpstreamMode;this.config.enableMicrophone=e;let n=!1;X(t)&&(this.speechRec?(e?this.speechRec.startRecognition():this.speechRec.stopRecognition(),n=!0):p.warn("setEnableMicrophone: STT leg is configured but speech recognition is not initialised yet")),re(t)&&(this.signaling?(e?this.signaling.unmuteUpstreamMic():this.signaling.muteUpstreamMic(),n=!0,X(t)||this.messages.next(new Kn(e))):p.warn("setEnableMicrophone: PS leg is configured but signaling is not initialised yet")),n||p.warn(`setEnableMicrophone(${e}) had nothing to do in mode '${t}' \u2014 session not ready?`)}setCameraAnchorDistance(e,t=0,n=this.signaling){this.config.cameraAnchorDistance=e,n?.dataSend(new Le(e,t))}setCameraAnchorHorizontal(e,t=0,n=this.signaling){this.config.cameraAnchorHorizontal=e,n?.dataSend(new Le(e,t))}changeAvatar(e,t=this.signaling){t?.dataSend(new In(e))}loadBackground(e,t=this.signaling){t?.dataSend(new Pn(e))}userStartSpeaking(e=this.signaling){e?.dataSend(new li("start"))}userStopSpeaking(e=this.signaling){e?.dataSend(new li("stop"))}triggerAction(e,t=this.signaling){t?.dataSend(new Ln(e))}resumeVideoPlayback(){this.signaling?.resumeVideoPlayback()}createSession(){this.messages.subscribe(e=>{if(this.config.messageHandler&&this.config.messageHandler(e),e.uneeqMessageType==="SoftSwitchFinished"||e.uneeqMessageType==="SessionReconnectingFinished"){if(!X(this.audioUpstreamMode))return;if(this.config.enableMicrophone)e.uneeqMessageType==="SessionReconnectingFinished"?(p.info("Session, resuming speech recognition after reconnect"),this.speechRec?.resume()):(p.info("Session, restarting speech recognition after soft switch"),this.speechRec?.startRecognition());else{let t=e.uneeqMessageType==="SoftSwitchFinished"?"soft switch":"reconnect";p.info(`Session, skipping speech recognition restart after ${t} (microphone disabled)`)}}}),this.id=this.config.sessionId,this.jwt=this.config.sessionToken,p.info("Session created with id: "+this.id),this.updatePromptMetadata(this.config.customMetadata||{}),this.promptMetadata=this.initMetadata(),this.listenForTabClosure(),this.signaling=this.createSignaling(this.config.videoContainerElement),this.signaling.init()}createSignaling(e=document.createElement("div")){let t=new URLSearchParams({token:this.jwt});this.config.backgroundUrl&&t.append("backgroundUrl",this.config.backgroundUrl);let n=(this.config.connectionUrl+`/session-service/v1/ws/session?${t.toString()}`).replace("http://","ws://").replace("https://","wss://");p.debug("Socket URL:",n);let i=new kn(this,n,e);return i.messages.subscribe(r=>{this.signalingEventHandler(r,i)}),i}signalingEventHandler(e,t){switch(e.type){case 0:{this.handleSessionReady(t);break}case 1:{this.handleSessionReady(t);break}case 2:{this.handleDataChannelMessage(e,t);break}case 5:{let n=e.stats;!this.resolutionLogged&&n.inboundVideoStats?.frameWidth&&n.inboundVideoStats?.frameHeight&&(p.info("Video resolution:",`${n.inboundVideoStats.frameWidth}x${n.inboundVideoStats.frameHeight}`),this.resolutionLogged=!0),this.config.webRtcStatsEmitMessages&&this.messages.next(new _n(n)),this.config.webRtcStatsLogMessages&&p.info("WebRTC stats",n);break}}}handleSessionReady(e){p.debug(`Session ready check - videoInitialized: ${e.videoInitialized}, dataChannelOpen: ${e.dataChannelOpen}, sceneReady: ${e.sceneReady}`),e.videoInitialized&&e.dataChannelOpen&&e.sceneReady&&(e.reconnectionAttempts=0,this.setCameraAnchorDistance(this.config.cameraAnchorDistance,0,e),this.setCameraAnchorHorizontal(this.config.cameraAnchorHorizontal,0,e),setTimeout(()=>{this.sessionTransferManager.isTransferSessionInProgress&&this.config.videoContainerElement?this.sessionTransferManager.switchLiveVideo(this.config.videoContainerElement,e):(this.initSpeechRecognition(this.jwt),this.sendWelcomePrompt(),document.body.classList.add("uneeq-streaming-live")),this.messages.next(new Bn(this.id)),this.config.networkIndicator&&e.startNetworkQualityMonitor(this.config.networkIndicator)},Ls))}async initSpeechRecognition(e){if(!X(this.audioUpstreamMode)){p.info(`[STT] audioUpstreamMode='${this.audioUpstreamMode}' \u2014 skipping STT provider initialisation (no STT leg in this mode).`);return}if(this.speechRec){p.warn("Speech recognition already initialized");return}let t=this.config.speechRecognitionProvider||"google";p.info(`[STT] Config speechRecognitionProvider: "${this.config.speechRecognitionProvider}", resolved provider: "${t}"`),t!=="google"&&t!=="deepgram"&&(p.warn(`Invalid speechRecognitionProvider value: "${String(t)}". Supported values are "google" or "deepgram". Defaulting to "google".`),t="google",this.config.speechRecognitionProvider="google"),p.info(`[STT] Using provider: ${t}`);try{this.speechRec=await(t==="deepgram"?this.createDeepgramSTT(e):this.createGoogleSTT(e))}catch(n){p.error("[STT] Failed to initialise speech recognition provider",p.serialiseError(n));return}this.config.enableMicrophone&&this.speechRec&&this.speechRec.startRecognition()}async createGoogleSTT(e){let t=this.getSpeechOptions(e);p.debug("Initializing Google STT (speech-recognition-service)");let{GoogleSTT:n}=await import("./chunks/google-stt-ICGJZD6H.js");return new n(t)}async createDeepgramSTT(e){let t=this.config.deepgramConfig||{};if(this.isFluxModel(t.model))return this.createDeepgramFluxSTT(e);p.warn("Deepgram STT is currently in beta, is subject to change, and is not suitable for production use"),p.debug("Initializing Deepgram STT");let n={connectionUrl:this.config.connectionUrl,jwtToken:e,sessionId:this.id,model:t.model,language:t.language,smartFormat:t.smartFormat,interimResults:t.interimResults,utteranceEndMs:t.utteranceEndMs,vadEvents:t.vadEvents,encoding:t.encoding,sampleRate:t.sampleRate,channels:t.channels,fillerWords:t.fillerWords,endpointing:t.endpointing,interruptionWordThreshold:t.interruptionWordThreshold,echoCancellation:t.microphone?.echoCancellation,noiseSuppression:t.microphone?.noiseSuppression,autoGainControl:t.microphone?.autoGainControl,microphoneDeviceId:this.config.microphoneDeviceId,promptMetadata:this.promptMetadata,messages:this.messages,sendMessage:r=>{this.signaling?.dataSend(r)},enableInterrupt:this.config.enableInterruptBySpeech,safetyNetTimeoutMs:t.safetyNetTimeoutMs,keyterms:t.keyterms,noDelay:t.noDelay},{DeepgramSTT:i}=await import("./chunks/deepgram-stt-LHOR4WN6.js");return new i(n)}isFluxModel(e){return e!==void 0&&e.toLowerCase().startsWith("flux")}async createDeepgramFluxSTT(e){p.debug("Initializing Deepgram Flux STT (v2 API)");let t=this.config.deepgramConfig||{},n={connectionUrl:this.config.connectionUrl,jwtToken:e,sessionId:this.id,model:t.model,language:t.language,eotThreshold:t.eotThreshold,eagerEotThreshold:t.eagerEotThreshold,eotTimeoutMs:t.eotTimeoutMs,eagerMaxTurnDurationMs:t.eagerMaxTurnDurationMs,keyterms:t.keyterms,safetyNetTimeoutMs:t.safetyNetTimeoutMs,echoCancellation:t.microphone?.echoCancellation,noiseSuppression:t.microphone?.noiseSuppression,autoGainControl:t.microphone?.autoGainControl,microphoneDeviceId:this.config.microphoneDeviceId,promptMetadata:this.promptMetadata,messages:this.messages,sendMessage:r=>{this.signaling?.dataSend(r)}},{DeepgramFluxSTT:i}=await import("./chunks/deepgram-flux-stt-6DSS2LPY.js");return new i(n)}getSpeechOptions(e){return{apiUrl:this.config.connectionUrl,assetBasePath:this.config.assetBasePath,enableInterrupt:this.config.enableInterruptBySpeech,enableVad:this.config.enableVad,jwtToken:e,hintPhrases:this.config.speechRecognitionHintPhrases,hintPhrasesBoost:this.config.speechRecognitionHintPhrasesBoost,locales:this.config.speechRecognitionLocales,messages:this.messages,promptMetadata:this.promptMetadata,sessionId:this.id,sendMessage:t=>{this.signaling?.dataSend(t)},microphoneDeviceId:this.config.microphoneDeviceId}}initMetadata(){return{userSpokenLocale:"",browserDetectedLocales:this.config.speechRecognitionLocales,userTimezone:Intl.DateTimeFormat().resolvedOptions().timeZone,userScreenWidth:window.screen.availWidth,userScreenHeight:window.screen.availHeight,userAgent:navigator.userAgent,personaId:this.config.personaId,custom:this.config.customMetadata}}handleDataChannelMessage(e,t){switch(p.debug("Processing data channel message",e),e.msg.type){case"nlp_prompt_result":{this.messages.next(new Gn(e.msg));break}case"avatar_started_speaking":{this.messages.next(new Nn);break}case"avatar_stopped_speaking":{this.messages.next(new Un);break}case"error":{p.error(`an error has occurred - ${JSON.stringify(e.msg)}`),this.handleChannelMessageError(e.msg);break}case"scene_ready":{p.debug("Scene is ready"),t.sceneReady=!0,this.handleSessionReady(t),this.messages.next(new Xn);break}case"speech_event":{this.messages.next(new Hn(e.msg));break}case"session_ended":{this.signaling?.endSession(e.msg.reason),this.signaling=void 0;break}case"speech_stream_opened":{this.audioStreamId=e.msg.streamId;break}case"speech_stream_closed":{e.msg.streamId===this.audioStreamId?this.audioStreamId=void 0:p.warn("Received SpeechStreamClosed message for unexpected streamId",e.msg.streamId);break}}}handleChannelMessageError(e){let t=e?.message?.trim()||"An unknown error occurred";e&&vr.includes(e.code)?this.messages.next(new Wn(e)):this.messages.next(new q(t))}sendWelcomePrompt(){p.debug("Checking if the welcome prompt should be sent. Welcome requested already = "+this.welcomeRequested+". Prompt = ",this.config.welcomePrompt),this.config.welcomePrompt&&this.config.welcomePrompt.length>0&&!this.welcomeRequested&&(this.welcomeRequested=!0,this.signaling?.dataSend(new se(this.config.welcomePrompt,this.promptMetadata)))}listenForTabClosure(){window.addEventListener("beforeunload",()=>{p.info("Window unload detected, ending session."),this.signaling?.endSession(),this.signaling=void 0})}};var Fn=class{constructor(e,t=$()){this.prompt=e;this.requestId=t}prompt;requestId;toJSON(){return{action:"speak",data:{requestId:this.requestId,prompt:this.prompt}}}};var me=class{constructor(e,t,n={},i=$()){this.action=e;this.streamId=t;this.metadata=n;this.requestId=i}action;streamId;metadata;requestId;toJSON(){return{action:"speech_stream",data:{requestId:this.requestId,action:this.action,streamId:this.streamId,metadata:this.metadata}}}};var An=class{constructor(e,t,n,i){this.command=e;this.enabled=t;this.category=n;this.verbosity=i}command;enabled;category;verbosity;toJSON(){let e={command:this.command,enabled:this.enabled};return this.category!==void 0&&(e.category=this.category),this.verbosity!==void 0&&(e.verbosity=this.verbosity),{action:"execute_command",data:e}}};async function wr(s,e){try{if(s?.audioStreamId===void 0){p.warn("Audio stream is not open | Call openAudioStream() first");return}let t;if(typeof e=="string"){let o=typeof atob<"u"?atob(e):Buffer.from(e,"base64").toString("binary");t=new Uint8Array(o.length);for(let a=0;a<o.length;a++)t[a]=o.charCodeAt(a)}else if(e instanceof Uint8Array)t=e;else if(typeof Blob<"u"&&e instanceof Blob){let o=await e.arrayBuffer();t=new Uint8Array(o)}else{p.error("speakAudio: Unsupported audio type",e);return}let n=Uint8Array.from([1]),i=Uint8Array.from([s.audioStreamId]),r=new Uint8Array(2+t.length);r.set(n,0),r.set(i,1),r.set(t,2),s.signaling?.dataSendBinary(r)}catch(t){p.error("speakAudioSend: Error sending audio",t)}}var Ds=class{constructor(e){this.config=e;p.setLevel(e.logLevel??"info")}config;session;messageSubscription;init(){if(this.session){let e="Cannot initialize: session already active. Call endSession() first.";throw p.warn(e),new Error(e)}if(this.config.audioUpstreamMode!==void 0&&!Bs(this.config.audioUpstreamMode)){let e=`Invalid audioUpstreamMode: '${String(this.config.audioUpstreamMode)}'. Allowed: 'speech-recognition-service' | 'pixel-streaming' | 'both'.`;throw p.error(e),new Error(e)}p.info(`Initializing Uneeq with config:', ${JSON.stringify(this.config)}, for UserAgent: ${navigator.userAgent}`),this.session=new On(this.config),this.session.createSession(),this.messageSubscription=this.session.messages.subscribe(e=>{e.uneeqMessageType==="SessionEnded"&&(p.info("Session ended, resetting session state"),this.resetSessionState())})}isSessionActive(){return this.session!==void 0}ensureSessionExists(){return this.session?.id?!0:(p.warn("Cannot perform action, session has not started."),!1)}chatPrompt(e){e&&e.trim()!==""&&this.ensureSessionExists()&&this.session?.signaling?.dataSend(new se(e,this.session?.promptMetadata))}speak(e){e&&e.trim()!==""&&this.ensureSessionExists()&&this.session?.signaling?.dataSend(new Fn(e))}openAudioStream(){let e=this.session;this.ensureSessionExists()&&e?.signaling?.dataSend(new me("open",e?.audioStreamId))}closeAudioStream(){let e=this.session;this.ensureSessionExists()&&e?.audioStreamId!==void 0&&e.signaling?.dataSend(new me("close",e.audioStreamId))}interruptAudioStream(){let e=this.session;this.ensureSessionExists()&&e?.audioStreamId!==void 0&&e.signaling?.dataSend(new me("interrupt",e.audioStreamId))}async speakAudio(e){this.ensureSessionExists()&&await wr(this.session,e)}cameraAnchorDistance(e,t){this.ensureSessionExists()&&this.session?.setCameraAnchorDistance(e,t)}cameraAnchorHorizontal(e,t){this.ensureSessionExists()&&this.session?.setCameraAnchorHorizontal(e,t)}changeAvatar(e){e&&e.trim()!==""&&this.ensureSessionExists()&&this.session?.changeAvatar(e)}loadBackground(e){e&&e.trim()!==""&&this.ensureSessionExists()&&this.session?.loadBackground(e)}userStartSpeaking(){this.ensureSessionExists()&&this.session?.userStartSpeaking()}userStopSpeaking(){this.ensureSessionExists()&&this.session?.userStopSpeaking()}triggerAction(e){e&&e.trim()!==""&&this.ensureSessionExists()&&this.session?.triggerAction(e)}resumeVideoPlayback(){this.session?.resumeVideoPlayback()}executeCommand(e,t,n,i){this.ensureSessionExists()&&this.session?.signaling?.dataSend(new An(e,t,n,i))}endSession(){this.ensureSessionExists()&&(this.session?.signaling?.endSession(),this.resetSessionState())}resetSessionState(){this.messageSubscription?.unsubscribe(),this.messageSubscription=void 0,this.session=void 0}unmuteDigitalHuman(){this.ensureSessionExists()&&(this.session?.signaling?.playAudioElement(),p.debug("Unmuted digital human."))}muteDigitalHuman(){this.ensureSessionExists()&&(this.session?.signaling?.pauseAudioElement(),p.debug("Muted digital human."))}stopSpeaking(){this.ensureSessionExists()&&this.session?.signaling?.dataSend(new Us)}setCustomPromptMetadata(e){this.ensureSessionExists()&&this.session?.updatePromptMetadata(e)}enableMicrophone(e=!0){this.ensureSessionExists()&&this.session?.setEnableMicrophone(e)}pauseSpeechRecognition(){if(!this.ensureSessionExists())return!1;let e=G(this.config.audioUpstreamMode);if(!X(e))return p.warn(`pauseSpeechRecognition called in '${e}' mode \u2014 no STT leg to pause; ignoring. Use muteUpstreamMic() to silence the PS mic track.`),!1;let t=this.session?.speechRec?.pause()??!1;return t instanceof Promise?!0:t}resumeSpeechRecognition(){if(!this.ensureSessionExists())return!1;let e=G(this.config.audioUpstreamMode);if(!X(e))return p.warn(`resumeSpeechRecognition called in '${e}' mode \u2014 no STT leg to resume; ignoring. Use unmuteUpstreamMic() to re-enable the PS mic track.`),!1;let t=this.session?.speechRec?.resume()??!1;return t instanceof Promise?!0:t}muteUpstreamMic(){this.ensureSessionExists()&&this.session?.signaling?.muteUpstreamMic()}unmuteUpstreamMic(){this.ensureSessionExists()&&this.session?.signaling?.unmuteUpstreamMic()}setWebRtcStatsEnabled(e,t){this.session?.config&&(this.session.config.webRtcStatsEmitMessages=e,this.session.config.webRtcStatsLogMessages=t,t?this.session.signaling?.startAudioStatsMonitor():this.session.signaling?.stopAudioStatsMonitor())}setNetworkIndicatorEnabled(e){this.ensureSessionExists()&&(e?this.session?.signaling?.startNetworkQualityMonitor(e):this.session?.signaling?.stopNetworkQualityMonitor())}};export{Ir as AvatarAnswerMessage,Fr as AvatarInterruptedMessage,Nn as AvatarStartedSpeakingMessage,Un as AvatarStoppedSpeakingMessage,Sr as CameraAnchorDistance,br as CameraAnchorHorizontal,jn as CustomMetadataUpdated,ri as CustomerConcurrencyLimitReachedMessage,kr as DeviceErrorMessage,Oe as DigitalHumanMuted,$n as DigitalHumanPlayedInMutedModeSuccess,qn as DigitalHumanUnmuted,Kn as EnableMicrophoneUpdatedMessage,xr as LogLevel,fr as NetworkIndicatorPopupTrigger,mr as NetworkIndicatorPosition,pr as NetworkIndicatorVisibility,ur as NetworkQualityLevel,oi as NetworkQualityMessage,Qn as PromptRequestMessage,Gn as PromptResultMessage,Fe as RendererErrorMessage,Xn as SceneReadyMessage,Wn as SessionBackendErrorMessage,Jn as SessionConnectingMessage,Yn as SessionDisconnectedMessage,zn as SessionEndedMessage,q as SessionErrorMessage,Bn as SessionLiveMessage,ei as SessionReconnectingFinishedMessage,Zn as SessionReconnectingMessage,ti as SoftSwitchStartingMessage,ni as SoftSwitchStoppingMessage,Hn as SpeechEventMessage,ci as SpeechRecognitionProvider,Ar as SpeechRecognitionTransientErrorMessage,Pr as SpeechTranscriptionMessage,Ds as Uneeq,Ns as UneeqMessageType,Dr as UserStartedSpeakingMessage,Or as UserStoppedSpeakingMessage,Lr as VadInterruptionAllowedMessage,ai as VideoAutoplayBlockedMessage,ii as VideoLayoutConfiguringMessage,Vn as WaitingInQueueMessage,si as WebRtcConnectedMessage,_n as WebRtcStatsMessage,G as resolveAudioUpstreamMode};
|
|
87
|
+
Audio level (0-1): ${a??"N/A"}`),n===0&&p.warn("WebRTC Connection - Audio - No audio packets received \u2014 remote is probably not sending audio."),i>0&&p.warn(`WebRTC Connection - Audio - packet loss detected: ${i} packets lost.`),a===void 0?p.warn("WebRTC Connection - Audio - Audio level not available in stats."):a===0?p.warn("WebRTC Connection - Audio - Audio level is zero \u2014 digital human might not be talking at this exact moment as audio is silent or muted."):a<.02?p.warn("WebRTC Connection - Audio - Audio level is very low \u2014 audio might be too quiet."):p.info("WebRTC Connection - Audio - Audio level looks normal, digital human is likely speaking.")}logElementStatus(e){p.debug(`Video element muted: ${this.videoElement?.muted}, (should be true as part of avoiding auto play restrictions)`),p.debug(`Audio element muted: ${this.audioElement?.muted}, (should be false to be able to hear the digital human)`),p.debug(`Audio element volume: ${this.audioElement?.volume}, (should be greater than zero to be able to hear the digital human)`);let t=e?.mimeType;t&&this.audioElement&&(p.debug(`Audio element can play webrtc audio codec (${t}) : ${this.audioElement.canPlayType(t)}, (should be 'probably' or 'maybe)'`),t==="audio/opus"&&p.debug(`Audio codec opus is often known as ogg, so checking can play ogg codec: ${this.audioElement.canPlayType("audio/ogg")}, (should be 'probably' or 'maybe')`))}};var Pe={good:"#22c55e",fair:"#eab308",poor:"#ef4444",inactive:"#6b7280"},xn=class{constructor(e,t){this.videoContainerElement=e;this.visibility=t.visibility??"when-degraded",this.position=t.position??"top-right",this.popupTrigger=t.popupTrigger??"click",this.onClickBound=this.onClick.bind(this),this.onMouseEventBound=n=>n.stopPropagation(),this.onDocumentClickBound=this.onDocumentClick.bind(this),this.onMouseEnterBound=this.onMouseEnter.bind(this),this.onMouseLeaveBound=this.onMouseLeave.bind(this),this.visibility!=="hidden"&&this.createElements()}videoContainerElement;container;panel;bars=[];panelVisible=!1;lastMetrics;visibility;position;popupTrigger;onClickBound;onMouseEventBound;onDocumentClickBound;onMouseEnterBound;onMouseLeaveBound;hoverHideTimeout;originalParentPosition;createElements(){getComputedStyle(this.videoContainerElement).position==="static"&&(this.originalParentPosition=this.videoContainerElement.style.position,this.videoContainerElement.style.position="relative"),this.container=document.createElement("div"),this.container.setAttribute("data-testid","network-indicator"),this.container.setAttribute("role","status"),this.container.setAttribute("aria-live","polite"),this.container.setAttribute("aria-label","Network quality: unknown"),Object.assign(this.container.style,{position:"absolute",zIndex:"1000",cursor:"pointer",padding:"6px 7px",borderRadius:"4px",background:"rgba(0, 0, 0, 0.5)",display:"none",alignItems:"flex-end",gap:"2px",pointerEvents:"auto",...this.getPositionStyles()});let t=[6,12,18];for(let n of t){let i=document.createElement("div");i.setAttribute("data-testid","network-indicator-bar"),Object.assign(i.style,{width:"4px",height:`${n}px`,borderRadius:"1px",background:Pe.inactive,transition:"background 0.3s ease"}),this.bars.push(i),this.container.appendChild(i)}this.container.addEventListener("mousedown",this.onMouseEventBound),this.container.addEventListener("mouseup",this.onMouseEventBound),this.popupTrigger==="click"?this.container.addEventListener("click",this.onClickBound):this.popupTrigger==="hover"&&(this.container.addEventListener("mouseenter",this.onMouseEnterBound),this.container.addEventListener("mouseleave",this.onMouseLeaveBound)),this.popupTrigger==="none"&&(this.container.style.cursor="default"),this.videoContainerElement.appendChild(this.container)}getPositionStyles(){switch(this.position){case"top-left":return{top:"8px",left:"8px"};case"top-right":return{top:"8px",right:"8px"};case"bottom-left":return{bottom:"8px",left:"8px"};case"bottom-right":return{bottom:"8px",right:"8px"};default:return{top:"8px",right:"8px"}}}onClick(e){e.stopPropagation(),this.panelVisible?this.hidePanel():this.showPanel()}onDocumentClick(e){this.panelVisible&&this.container&&!this.container.contains(e.target)&&this.hidePanel()}onMouseEnter(){clearTimeout(this.hoverHideTimeout),this.showPanel()}onMouseLeave(){this.scheduleHoverHide()}scheduleHoverHide(){clearTimeout(this.hoverHideTimeout),this.hoverHideTimeout=setTimeout(()=>this.hidePanel(),50)}showPanel(){!this.container||!this.lastMetrics||(this.panel||(this.panel=document.createElement("div"),this.panel.setAttribute("data-testid","network-indicator-panel"),Object.assign(this.panel.style,{position:"fixed",padding:"10px",borderRadius:"6px",background:"rgba(0, 0, 0, 0.85)",color:"#ffffff",fontSize:"11px",fontFamily:"system-ui, -apple-system, sans-serif",whiteSpace:"nowrap",lineHeight:"1.6",minWidth:"180px",zIndex:"10001",pointerEvents:"auto"}),this.popupTrigger==="hover"&&(this.panel.addEventListener("mouseenter",this.onMouseEnterBound),this.panel.addEventListener("mouseleave",this.onMouseLeaveBound)),document.body.appendChild(this.panel)),this.updatePanel(),this.positionPanel(),this.panel.style.display="block",this.panelVisible=!0,document.addEventListener("click",this.onDocumentClickBound))}hidePanel(){this.panel&&(this.panel.style.display="none"),this.panelVisible=!1,document.removeEventListener("click",this.onDocumentClickBound)}positionPanel(){if(!this.panel||!this.container)return;let e=4,t=4,n=window.innerWidth,i=window.innerHeight,r=this.container.getBoundingClientRect(),o={top:Math.max(0,Math.min(r.top,i)),bottom:Math.max(0,Math.min(r.bottom,i)),left:Math.max(0,Math.min(r.left,n)),right:Math.max(0,Math.min(r.right,n))};this.panel.style.top="",this.panel.style.bottom="",this.panel.style.left="",this.panel.style.right="",this.panel.style.visibility="hidden",this.panel.style.display="block";let{width:a,height:l}=this.panel.getBoundingClientRect();this.panel.style.visibility="";let c=o.top-e,u=i-o.bottom-e,w=o.left-e,v=n-o.right-e,C,x;c>=l||u>=l?(C=u>=l&&(c<l||u>=c)?o.bottom+e:o.top-e-l,x=this.position==="top-left"||this.position==="bottom-left"?o.left:o.right-a):(x=v>=a&&(w<a||v>=w)?o.right+e:o.left-e-a,C=o.bottom-l),x=Math.max(t,Math.min(x,n-a-t)),C=Math.max(0,Math.min(C,i-l-t)),this.panel.style.top=`${C}px`,this.panel.style.left=`${x}px`}updatePanel(){if(!this.panel||!this.lastMetrics)return;let e=this.lastMetrics,t=this.getColor(e.qualityLevel);this.panel.innerHTML=[`<div style="font-weight:600;margin-bottom:4px;color:${t}">${e.qualityLevel} Quality</div>`,this.panelSectionHeader("Quality Score (MOS)"),this.panelRow("Audio",e.audioMos.toFixed(1)+" / 5.0"),this.panelRow("Video",e.videoMos.toFixed(1)+" / 5.0"),this.panelSectionHeader("Connection"),this.panelRow("Round Trip",`${e.roundTripTimeMs} ms`),this.panelRow("Packet Loss",`${e.audioPacketLossPercent}% audio / ${e.videoPacketLossPercent}% video`),this.panelRow("Jitter",`${e.audioJitterMs} ms audio / ${e.videoJitterMs} ms video`),this.panelSectionHeader("Throughput"),this.panelRow("Bitrate",`${e.audioBitrateKbps} audio / ${e.videoBitrateKbps} video kbps`),this.panelRow("Frame Rate",`${e.frameRate} fps`)].join("")}panelSectionHeader(e){return`<div style="color:#6b7280;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;margin-top:6px;margin-bottom:2px">${e}</div>`}panelRow(e,t){return`<div style="display:flex;justify-content:space-between;gap:12px"><span style="color:#9ca3af">${e}</span><span>${t}</span></div>`}getColor(e){switch(e){case"Good":return Pe.good;case"Fair":return Pe.fair;case"Poor":return Pe.poor}}update(e){if(this.lastMetrics=e,!this.container)return;this.container.setAttribute("aria-label",`Network quality: ${e.qualityLevel}`);let t=this.getColor(e.qualityLevel),n=e.qualityLevel==="Good"?3:e.qualityLevel==="Fair"?2:1;this.bars.forEach((i,r)=>{i.style.background=r<n?t:Pe.inactive}),this.visibility==="when-degraded"?this.container.style.display=e.qualityLevel==="Good"?"none":"flex":this.container.style.display="flex",this.panelVisible&&(this.updatePanel(),this.positionPanel())}destroy(){this.container&&(this.container.removeEventListener("click",this.onClickBound),this.container.removeEventListener("mousedown",this.onMouseEventBound),this.container.removeEventListener("mouseup",this.onMouseEventBound),this.container.removeEventListener("mouseenter",this.onMouseEnterBound),this.container.removeEventListener("mouseleave",this.onMouseLeaveBound),this.container.remove(),this.container=void 0),this.originalParentPosition!==void 0&&(this.videoContainerElement.style.position=this.originalParentPosition,this.originalParentPosition=void 0),clearTimeout(this.hoverHideTimeout),this.panel&&(this.panel.removeEventListener("mouseenter",this.onMouseEnterBound),this.panel.removeEventListener("mouseleave",this.onMouseLeaveBound),this.panel.remove(),this.panel=void 0),document.removeEventListener("click",this.onDocumentClickBound),this.bars=[],this.panelVisible=!1,this.lastMetrics=void 0}};var kn=class{constructor(e,t,n){this.session=e;this.connectionUrl=t;this.videoContainerElement=n;this.config=e.config,this.clientMessages=e.messages,this._audioUpstreamMode=G(this.config.audioUpstreamMode)}session;connectionUrl;videoContainerElement;stream;messages=new eo;videoElement;audioElement;videoInitialized=!1;dataChannelOpen=!1;sceneReady=!1;maxReconnectAttempts=4;reconnectionAttempts=0;allowReconnection=!0;reconnectTimeoutId;baseReconnectDelayMs=500;maxReconnectDelayMs=1e4;audioStatsInternal;freezeCount=0;lastDecodedFrames=0;lastCheckTime=Date.now();audioStatsAnalyzer;networkQualityInterval;networkIndicator;config;clientMessages;_audioUpstreamMode;get audioUpstreamMode(){return this._audioUpstreamMode}init(){p.info("Connection config",{connectionUrl:this.connectionUrl,forceTURN:this.config.forceTURN}),this.videoContainerElement||(this.videoContainerElement=document.createElement("div"),document.body.appendChild(this.videoContainerElement)),this.clientMessages?.next(new ii);let e=re(this.audioUpstreamMode);this.audioUpstreamMode!=="speech-recognition-service"&&p.info(`audioUpstreamMode='${this.audioUpstreamMode}' is beta; behaviour may change.`);let t=new zt({initialSettings:{AutoConnect:!1,AutoPlayVideo:!1,ForceTURN:this.config.forceTURN,HoveringMouse:!1,KeyboardInput:!1,MaxReconnectAttempts:0,MouseInput:!1,ss:this.connectionUrl,StartVideoMuted:!0,TouchInput:!1,UseMic:e,WaitForStreamer:!0}});this.stream=new ke(t,{videoElementParent:this.videoContainerElement}),d.InitLogging(K.Warning,!1);let n=function(){let i=t.getTextSettingValue(z.SignallingServerUrl);return t.isFlagEnabled(h.BrowserSendOffer)&&(i+=(i.includes("?")?"&":"?")+h.BrowserSendOffer+"=true"),i};this.stream.setSignallingUrlBuilder(n),this.addStreamHandlers(),this.stream.connect(),this.addWebSocketHandlers()}addStreamHandlers(){this.stream?.addEventListener("videoInitialized",this.handleVideoInitialized.bind(this)),this.stream?.addEventListener("dataChannelOpen",this.handleDataChannelOpen.bind(this)),this.stream?.addEventListener("webRtcFailed",this.handleWebRtcFailed.bind(this)),this.stream?.addEventListener("webRtcDisconnected",this.handleWebRtcDisconnected.bind(this)),this.stream?.addEventListener("statsReceived",this.handleWebRtcStats.bind(this)),this.stream?.addResponseEventListener("response-listener",this.handleDataChannelMessage.bind(this)),this.stream?.addEventListener("videoEncoderAvgQP",({data:{avgQP:e}})=>{p.trace("WebRTC: VideoEncoderAvgQP - lower is better, ranges from 0 to 51:",e)}),this.stream?.addEventListener("webRtcAutoConnect",()=>{p.debug("WebRTC: AutoConnect triggered")}),this.stream?.addEventListener("webRtcConnected",this.handleWebRtcConnectedMessage.bind(this)),this.stream?.addEventListener("statsReceived",({data:{aggregatedStats:e}})=>{p.trace("WebRTC: StatsReceived",e)}),this.stream?.addEventListener("webRtcTCPRelayDetected",()=>{p.warn("WebRTC: Stream quailty likely degraded due to network enviroment, stream is relayed over TCP.")})}addWebSocketHandlers(){this.stream?.signallingProtocol?.transport?.webSocket?.addEventListener("error",this.handleWebSocketError.bind(this)),this.stream?.signallingProtocol.on("error",t=>{p.error("Signaling: Received error message from server",t),this.clientMessages?.next(new q(`Signaling server error: ${t}`))}),this.stream?.signallingProtocol.on("index_in_queue",this.handleIndexInQueueMessage.bind(this)),this.stream?.signallingProtocol.on("renderer_taken",this.handleRendererTakenMessage.bind(this)),this.stream?.signallingProtocol.on("renderer_gone",this.handleRendererGoneEvent.bind(this)),this.stream?.signallingProtocol.on("switch_renderer_request",this.handleSwitchRendererRequestEvent.bind(this)),this.stream?.signallingProtocol.on("customer_concurrency_limit_reached",this.handleCustomerConcurrencyLimitReachedMessage.bind(this)),this.stream?.signallingProtocol.on("renderer_error",this.handleRendererErrorMessage.bind(this)),this.stream?.signallingProtocol.on("config",this.checkForStubbedTurnConfig.bind(this))}checkForStubbedTurnConfig(e){let t=e?.peerConnectionOptions?.iceServers??[];p.info("Signaling: Received ice config message with urls:",t.map(i=>({urls:i.urls}))),t.some(i=>i.username?.includes("disabled")&&i.urls?.some(r=>r.includes("0.0.0.0")))&&p.warn("Signaling: TURN server configuration is stubbed (turn:0.0.0.0 with disabled credentials). This should be intentional, is intended for locally run renderers e.g. kiosk, miniprem etc.")}handleWebSocketError(e){p.error("Signaling: WebSocket error",p.serialiseError(e)),this.reconnectIfSafe(e)}handleVideoInitialized(){if(p.info("Video initialized."),this.videoElement=this.videoContainerElement?.getElementsByTagName("video")[0],this.audioElement=this.stream?.webRtcController?.streamController?.audioElement,this.audioStatsAnalyzer=new pe(this.stream,this.videoElement,this.audioElement),this.audioElement&&this.config.speakerDeviceId&&this.setSpeakerDevice(this.audioElement,this.config.speakerDeviceId),this.videoElement)this.videoElement.muted=!0,this.videoElement.play().then(()=>{this.playAudioElement()}).catch(e=>{if(e instanceof DOMException&&e.name==="NotAllowedError")p.warn("Video autoplay blocked by browser. Requesting user gesture to resume playback."),this.clientMessages?.next(new ai);else{let t="Fatal Error: Digital Human video cannot be played.";p.error(t,p.serialiseError(e)),this.clientMessages?.next(new q(t)),this.endSession(Is,!0)}}),this.videoInitialized=!0,this.messages.next(new Mn),this.reconnectionAttempts=0,this.clearReconnectTimer();else{let e="Fatal Error: Video element not found.";p.error(e),this.clientMessages?.next(new q(e)),this.endSession(Is,!0)}setTimeout(()=>{this.clientMessages?.next(new ei)},Ls)}playAudioElement(){this.audioElement?this.audioElement.play().then(()=>{p.info("Audio played successfully."),this.clientMessages?.next(new qn)}).catch(e=>{p.warn("Audio cannot be played.",e),this.clientMessages?.next(new Oe),this.clientMessages?.next(new $n)}):this.handleMissingAudioElement()}resumeVideoPlayback(){if(!this.videoElement){p.warn("resumeVideoPlayback called but no video element available");return}this.videoElement.play().then(()=>{this.playAudioElement()}).catch(e=>{p.error("Video playback failed after user gesture",p.serialiseError(e)),this.clientMessages?.next(new q("Video playback failed after user gesture")),this.endSession(hr,!0)})}pauseAudioElement(){this.audioElement?(this.audioElement.pause(),this.clientMessages?.next(new Oe)):this.handleMissingAudioElement()}muteUpstreamMic(){if(!re(this.audioUpstreamMode)){p.warn(`muteUpstreamMic called in '${this.audioUpstreamMode}' mode \u2014 no PS upstream audio track to mute; ignoring.`);return}if(!this.stream){p.warn("muteUpstreamMic called before PixelStreaming stream is initialised; ignoring.");return}this.stream.muteMicrophone()}unmuteUpstreamMic(){if(!re(this.audioUpstreamMode)){p.warn(`unmuteUpstreamMic called in '${this.audioUpstreamMode}' mode \u2014 no PS upstream audio track to unmute; ignoring.`);return}if(!this.stream){p.warn("unmuteUpstreamMic called before PixelStreaming stream is initialised; ignoring.");return}this.stream.unmuteMicrophone(!1)}setSpeakerDevice(e,t){let n=e;n.setSinkId?n.setSinkId(t).then(()=>{p.info(`Speaker device set to: ${t}`)}).catch(i=>{p.error(`Failed to set speaker device: ${t}`,p.serialiseError(i))}):p.error("setSinkId not supported in this browser, cannot set speaker device")}handleMissingAudioElement(){let e="Error: Digital Human audio element not found, cannot play audio.";p.error(e),this.clientMessages?.next(new q(e))}startAudioStatsMonitor(e=5e3){this.stopAudioStatsMonitor(),p.info("Starting audio stats monitor"),this.audioStatsAnalyzer||(this.audioStatsAnalyzer=new pe(this.stream,this.videoElement,this.audioElement)),this.audioStatsInternal=setInterval(()=>{this.audioStatsAnalyzer?.analyzeAudioStats().catch(t=>{p.error("Error analyzing audio stats",p.serialiseError(t))})},e)}stopAudioStatsMonitor(){this.audioStatsInternal&&clearInterval(this.audioStatsInternal)}startNetworkQualityMonitor(e){this.stopNetworkQualityMonitor(),p.info("Starting network quality monitor"),this.audioStatsAnalyzer||(this.audioStatsAnalyzer=new pe(this.stream,this.videoElement,this.audioElement)),this.videoContainerElement&&e.visibility!=="hidden"&&(this.networkIndicator=new xn(this.videoContainerElement,e));let t=e.collectionIntervalMs??3e3;this.networkQualityInterval=setInterval(()=>{this.audioStatsAnalyzer?.collectNetworkQuality(e).then(n=>{n&&(this.networkIndicator?.update(n),this.clientMessages?.next(new oi(n)))}).catch(n=>{p.error("Error collecting network quality",p.serialiseError(n))})},t)}stopNetworkQualityMonitor(){this.networkQualityInterval&&(clearInterval(this.networkQualityInterval),this.networkQualityInterval=void 0),this.networkIndicator&&(this.networkIndicator.destroy(),this.networkIndicator=void 0)}handleDataChannelOpen(){p.info("DataChannelOpen"),this.dataChannelOpen=!0,this.messages.next(new Tn)}handleWebRtcFailed(e){p.error("WebRTC failed",e),this.reconnectIfSafe(e)}handleWebRtcDisconnected(e){p.warn(`WebRTC disconnected, message: ${e?.data?.eventString}, allowClickToReconnect: ${e?.data?.allowClickToReconnect}`),this.reconnectIfSafe(e)}handleWebRtcStats(e){let t=new Rn(e.data.aggregatedStats);this.messages.next(t);let n=e.data.aggregatedStats.inboundVideoStats?.framesDecoded??0;this.checkForFreeze(n)}checkForFreeze(e){let t=Date.now();if(this.lastDecodedFrames===0&&e===0){p.debug("[WebRTC] Waiting for first video frame...");return}if(e===this.lastDecodedFrames){this.freezeCount++;let n=((t-this.lastCheckTime)/1e3).toFixed(1);p.warn(`[WebRTC] Video FROZEN for ${n}s (count=${this.freezeCount})`),this.freezeCount>=3&&(p.error("[WebRTC] Video frozen 3 times in a row, transferring session..."),this.session.sessionTransferManager.transferSession(),this.freezeCount=0)}else this.freezeCount=0,this.lastDecodedFrames=e,this.lastCheckTime=t}clearReconnectTimer(){this.reconnectTimeoutId&&(clearTimeout(this.reconnectTimeoutId),this.reconnectTimeoutId=void 0)}scheduleReconnect(){let e=Math.min(this.baseReconnectDelayMs*Math.pow(2,this.reconnectionAttempts),this.maxReconnectDelayMs);p.info(`Scheduling reconnection attempt ${this.reconnectionAttempts} in ${e}ms`),this.reconnectTimeoutId=setTimeout(()=>{p.info(`Executing reconnection attempt ${this.reconnectionAttempts}`),this.reconnectTimeoutId=void 0,this.videoInitialized=!1,this.dataChannelOpen=!1,this.sceneReady=!1,this.audioStatsAnalyzer?.resetStats(),this.stream?.reconnect()},e)}reconnectIfSafe(e){try{this.allowReconnection&&!this.reconnectTimeoutId?(p.debug("Checking if should attempt reconnection after event",e),this.reconnectionAttempts<this.maxReconnectAttempts?(this.reconnectionAttempts++,this.clientMessages?.next(new Zn),this.scheduleReconnect()):(p.info("Max reconnection attempts reached. Ending session."),this.clientMessages?.next(new Yn),this.endSession(ks,!1))):p.warn("Reconnection already scheduled or reconnecting is disabled")}catch(t){p.error("Error during reconnection attempt",p.serialiseError(t)),this.clientMessages?.next(new q("Error during reconnection attempt")),this.endSession(ks,!1)}}handleDataChannelMessage(e){p.trace("Signaling: Received data channel message",e);try{let t=JSON.parse(e);if(t.type!==void 0)this.messages.next(new En(t));else throw new Error("Signaling: Engine response missing type")}catch(t){p.error("Signaling: Failed to parse engine response",p.serialiseError(t))}}handleIndexInQueueMessage(e){try{let n=Number(e.message);this.clientMessages?.next(new Vn(n))}catch(t){p.error("Signaling: Failed to parse position in queue message",p.serialiseError(t))}}handleRendererTakenMessage(e){try{let t=e;this.clientMessages?.next(new Jn(t.message)),p.info("Signaling: Renderer taken event",t.message)}catch(t){p.error("Signaling: Failed to parse renderer taken message",p.serialiseError(t))}}handleWebRtcConnectedMessage(){this.clientMessages?.next(new si),p.info("Signaling: WebRTC connected")}handleRendererGoneEvent(e){p.info("Signaling: Renderer gone event",e),this.reconnectIfSafe(e)}handleSwitchRendererRequestEvent(e){p.info("Signaling: Renderer switch renderer request event",e),this.session.sessionTransferManager.transferSession()}handleCustomerConcurrencyLimitReachedMessage(e){p.warn("Signaling: Customer concurrency limit reached message received",e),this.clientMessages?.next(new ri)}handleRendererErrorMessage(e){try{let t=e,n=t.code??-1,i=t.message??"An unknown renderer error occurred";p.error(`Signaling: Renderer error received - code: ${n}, message: ${i}`),this.clientMessages?.next(new Fe(n,i)),this.endSession(Ps,!0)}catch(t){p.error("Signaling: Failed to parse renderer error message",p.serialiseError(t)),this.clientMessages?.next(new Fe(-1,"An unknown renderer error occurred")),this.endSession(Ps,!0)}}socketSend(e){p.trace("Signaling: Sending socket message",e),this.stream?.signallingProtocol?.transport?.webSocket?.send(JSON.stringify(e))}dataSend(e){let t=e.toJSON();if(t.action==="chat_prompt"){let n=t.data?.requestId;p.info(`Signaling: Sending data channel message \u2014 action=chat_prompt, requestId=${n??"unknown"}`),(!(e instanceof se)||e.shouldEmitPromptRequest)&&this.clientMessages?.next(new Qn(t.data))}else p.info("Signaling: Sending data channel message",JSON.stringify(t));this.stream?.emitUIInteraction(e)}dataSendBinary(e){p.info("Signaling: Sending data channel binary message");let t=this.uint8ToBase64(e);this.stream?.emitUIInteraction(t)}uint8ToBase64(e){let t="",n=e.byteLength;for(let i=0;i<n;i++)t+=String.fromCharCode(e[i]);return btoa(t)}endSession(e=dr,t=!0){p.info("Signaling: Ending session"),this.clearReconnectTimer(),this.stopNetworkQualityMonitor(),this.socketSend({message:e,type:"client_gone",sessionId:this.config.sessionId}),this.streamDisconnect(),p.info("Session has ended, reason: "+e),t&&this.clientMessages?.next(new zn(e))}streamDisconnect(){p.info("Signaling: Disconnecting stream"),this.stopNetworkQualityMonitor(),this.stopAudioStatsMonitor(),this.allowReconnection=!1,this.clearReconnectTimer();let e=this.stream?.webRtcController?.statsTimerHandle;e&&window.clearInterval(e);let t=this.stream?.webRtcController?.peerConnectionController;if(t?.peerConnection){t.peerConnection.getReceivers=()=>[];let n=t.peerConnection;Object.defineProperty(t,"peerConnection",{get:()=>n,set:()=>{},configurable:!0})}this.stream?.disconnect(),this.dataChannelOpen=!1,this.videoInitialized=!1,this.sceneReady=!1}startSoftSwitch(){p.info("Signaling: Start soft switching"),this.clientMessages?.next(new ti)}stopSoftSwitch(){p.info("Signaling: Stop soft switching"),this.clientMessages?.next(new ni)}};import{Subject as to}from"rxjs";var Sr=(o=>(o.CloseUp="close_up",o.LooseCloseUp="loose_close_up",o.TightMediumShot="tight_medium_shot",o.MediumShot="medium_shot",o.MediumFullShot="medium_full_shot",o.FullShot="full_shot",o))(Sr||{}),br=(i=>(i.Left="left",i.Right="right",i.Center="center",i.Centre="centre",i))(br||{}),Le=class{constructor(e,t=2e3,n=$()){this.position=e;this.duration=t;this.requestId=n}position;duration;requestId;toJSON(){return{action:"camera_to_anchor",data:{requestId:this.requestId,position:this.position,transition_duration_ms:this.duration}}}};var In=class{constructor(e,t=$()){this.avatarId=e;this.requestId=t}avatarId;requestId;toJSON(){return{action:"change_avatar",data:{id:this.avatarId,requestId:this.requestId}}}};var Pn=class{constructor(e,t=$()){this.url=e;this.requestId=t}url;requestId;toJSON(){return{action:"load_background",data:{url:this.url,requestId:this.requestId}}}};var Ln=class{constructor(e,t=$()){this.actionName=e;this.requestId=t}actionName;requestId;toJSON(){return{action:"trigger_action",data:{action_name:this.actionName,requestId:this.requestId}}}};var Dn=class{constructor(e){this.session=e;this.addTransferSessionListener()}session;isTransferSessionInProgress=!1;addTransferSessionListener(){this.isTransferSessionInProgress=!1,document.addEventListener("keydown",e=>{e.ctrlKey&&e.shiftKey&&e.key.toLowerCase()==="y"&&(p.info("Session transfer via keyboard shortcut triggered"),this.transferSession())})}transferSession(){if(this.isTransferSessionInProgress){p.warn("Transfer already in progress, skipping");return}this.isTransferSessionInProgress=!0,p.info("Beginning session transfer"),this.session.config.videoContainerElement&&(this.session.config.videoContainerElement.querySelectorAll("#freezeFrame,#streamingVideo").forEach(n=>{n.remove()}),this.session.signaling&&this.session.signaling.streamDisconnect()),this.session.createSignaling(this.session.config.videoContainerElement??document.createElement("div")).init()}switchLiveVideo(e,t){p.info("New signaling video is ready for transfer"),this.session.signaling&&e&&t.videoElement&&(t.videoElement.classList.add("incoming-video"),this.session.signaling.videoElement&&t.videoElement&&e?.appendChild(t.videoElement),this.session.signaling.startSoftSwitch(),setTimeout(()=>{this.session.signaling?.streamDisconnect(),this.session.signaling=t,this.isTransferSessionInProgress=!1,this.session.signaling?.stopSoftSwitch(),p.info("Session transfer complete")},1e3))}};var vr=[1103,1200,1402];var Ls=750,On=class{constructor(e){this.config=e;this.sessionTransferManager=new Dn(this),this._audioUpstreamMode=G(this.config.audioUpstreamMode)}config;id;signaling;messages=new to;speechRec;promptMetadata={};audioStreamId;sessionTransferManager;jwt;welcomeRequested=!1;resolutionLogged=!1;_audioUpstreamMode;get audioUpstreamMode(){return this._audioUpstreamMode}updatePromptMetadata(e){try{JSON.parse(JSON.stringify(e)),this.promptMetadata.custom=e,this.messages.next(new jn(this.promptMetadata))}catch(t){p.error("Error parsing custom metadata",e,p.serialiseError(t))}}setEnableMicrophone(e){let t=this.audioUpstreamMode;this.config.enableMicrophone=e;let n=!1;X(t)&&(this.speechRec?(e?this.speechRec.startRecognition():this.speechRec.stopRecognition(),n=!0):p.warn("setEnableMicrophone: STT leg is configured but speech recognition is not initialised yet")),re(t)&&(this.signaling?(e?this.signaling.unmuteUpstreamMic():this.signaling.muteUpstreamMic(),n=!0,X(t)||this.messages.next(new Kn(e))):p.warn("setEnableMicrophone: PS leg is configured but signaling is not initialised yet")),n||p.warn(`setEnableMicrophone(${e}) had nothing to do in mode '${t}' \u2014 session not ready?`)}setCameraAnchorDistance(e,t=0,n=this.signaling){this.config.cameraAnchorDistance=e,n?.dataSend(new Le(e,t))}setCameraAnchorHorizontal(e,t=0,n=this.signaling){this.config.cameraAnchorHorizontal=e,n?.dataSend(new Le(e,t))}changeAvatar(e,t=this.signaling){t?.dataSend(new In(e))}loadBackground(e,t=this.signaling){t?.dataSend(new Pn(e))}userStartSpeaking(e=this.signaling){e?.dataSend(new li("start"))}userStopSpeaking(e=this.signaling){e?.dataSend(new li("stop"))}triggerAction(e,t=this.signaling){t?.dataSend(new Ln(e))}resumeVideoPlayback(){this.signaling?.resumeVideoPlayback()}createSession(){this.messages.subscribe(e=>{if(this.config.messageHandler&&this.config.messageHandler(e),e.uneeqMessageType==="SoftSwitchFinished"||e.uneeqMessageType==="SessionReconnectingFinished"){if(!X(this.audioUpstreamMode))return;if(this.config.enableMicrophone)e.uneeqMessageType==="SessionReconnectingFinished"?(p.info("Session, resuming speech recognition after reconnect"),this.speechRec?.resume()):(p.info("Session, restarting speech recognition after soft switch"),this.speechRec?.startRecognition());else{let t=e.uneeqMessageType==="SoftSwitchFinished"?"soft switch":"reconnect";p.info(`Session, skipping speech recognition restart after ${t} (microphone disabled)`)}}}),this.id=this.config.sessionId,this.jwt=this.config.sessionToken,p.info("Session created with id: "+this.id),this.updatePromptMetadata(this.config.customMetadata||{}),this.promptMetadata=this.initMetadata(),this.listenForTabClosure(),this.signaling=this.createSignaling(this.config.videoContainerElement),this.signaling.init()}createSignaling(e=document.createElement("div")){let t=new URLSearchParams({token:this.jwt});this.config.backgroundUrl&&t.append("backgroundUrl",this.config.backgroundUrl);let n=(this.config.connectionUrl+`/session-service/v1/ws/session?${t.toString()}`).replace("http://","ws://").replace("https://","wss://");p.debug("Socket URL:",n);let i=new kn(this,n,e);return i.messages.subscribe(r=>{this.signalingEventHandler(r,i)}),i}signalingEventHandler(e,t){switch(e.type){case 0:{this.handleSessionReady(t);break}case 1:{this.handleSessionReady(t);break}case 2:{this.handleDataChannelMessage(e,t);break}case 5:{let n=e.stats;!this.resolutionLogged&&n.inboundVideoStats?.frameWidth&&n.inboundVideoStats?.frameHeight&&(p.info("Video resolution:",`${n.inboundVideoStats.frameWidth}x${n.inboundVideoStats.frameHeight}`),this.resolutionLogged=!0),this.config.webRtcStatsEmitMessages&&this.messages.next(new _n(n)),this.config.webRtcStatsLogMessages&&p.info("WebRTC stats",n);break}}}handleSessionReady(e){p.debug(`Session ready check - videoInitialized: ${e.videoInitialized}, dataChannelOpen: ${e.dataChannelOpen}, sceneReady: ${e.sceneReady}`),e.videoInitialized&&e.dataChannelOpen&&e.sceneReady&&(e.reconnectionAttempts=0,this.setCameraAnchorDistance(this.config.cameraAnchorDistance,0,e),this.setCameraAnchorHorizontal(this.config.cameraAnchorHorizontal,0,e),setTimeout(()=>{this.sessionTransferManager.isTransferSessionInProgress&&this.config.videoContainerElement?this.sessionTransferManager.switchLiveVideo(this.config.videoContainerElement,e):(this.initSpeechRecognition(this.jwt),this.sendWelcomePrompt(),document.body.classList.add("uneeq-streaming-live")),this.messages.next(new Bn(this.id)),this.config.networkIndicator&&e.startNetworkQualityMonitor(this.config.networkIndicator)},Ls))}async initSpeechRecognition(e){if(!X(this.audioUpstreamMode)){p.info(`[STT] audioUpstreamMode='${this.audioUpstreamMode}' \u2014 skipping STT provider initialisation (no STT leg in this mode).`);return}if(this.speechRec){p.warn("Speech recognition already initialized");return}let t=this.config.speechRecognitionProvider||"google";p.info(`[STT] Config speechRecognitionProvider: "${this.config.speechRecognitionProvider}", resolved provider: "${t}"`),t!=="google"&&t!=="deepgram"&&(p.warn(`Invalid speechRecognitionProvider value: "${String(t)}". Supported values are "google" or "deepgram". Defaulting to "google".`),t="google",this.config.speechRecognitionProvider="google"),p.info(`[STT] Using provider: ${t}`);try{this.speechRec=await(t==="deepgram"?this.createDeepgramSTT(e):this.createGoogleSTT(e))}catch(n){p.error("[STT] Failed to initialise speech recognition provider",p.serialiseError(n));return}this.config.enableMicrophone&&this.speechRec&&this.speechRec.startRecognition()}async createGoogleSTT(e){let t=this.getSpeechOptions(e);p.debug("Initializing Google STT (speech-recognition-service)");let{GoogleSTT:n}=await import("./chunks/google-stt-ICGJZD6H.js");return new n(t)}async createDeepgramSTT(e){let t=this.config.deepgramConfig||{};if(this.isFluxModel(t.model))return this.createDeepgramFluxSTT(e);p.warn("Deepgram STT is currently in beta, is subject to change, and is not suitable for production use"),p.debug("Initializing Deepgram STT");let n={connectionUrl:this.config.connectionUrl,jwtToken:e,sessionId:this.id,model:t.model,language:t.language,smartFormat:t.smartFormat,interimResults:t.interimResults,utteranceEndMs:t.utteranceEndMs,vadEvents:t.vadEvents,encoding:t.encoding,sampleRate:t.sampleRate,channels:t.channels,fillerWords:t.fillerWords,endpointing:t.endpointing,interruptionWordThreshold:t.interruptionWordThreshold,echoCancellation:t.microphone?.echoCancellation,noiseSuppression:t.microphone?.noiseSuppression,autoGainControl:t.microphone?.autoGainControl,microphoneDeviceId:this.config.microphoneDeviceId,promptMetadata:this.promptMetadata,messages:this.messages,sendMessage:r=>{this.signaling?.dataSend(r)},enableInterrupt:this.config.enableInterruptBySpeech,safetyNetTimeoutMs:t.safetyNetTimeoutMs,keyterms:t.keyterms,noDelay:t.noDelay},{DeepgramSTT:i}=await import("./chunks/deepgram-stt-LHOR4WN6.js");return new i(n)}isFluxModel(e){return e!==void 0&&e.toLowerCase().startsWith("flux")}async createDeepgramFluxSTT(e){p.debug("Initializing Deepgram Flux STT (v2 API)");let t=this.config.deepgramConfig||{},n={connectionUrl:this.config.connectionUrl,jwtToken:e,sessionId:this.id,model:t.model,language:t.language,eotThreshold:t.eotThreshold,eagerEotThreshold:t.eagerEotThreshold,eotTimeoutMs:t.eotTimeoutMs,eagerMaxTurnDurationMs:t.eagerMaxTurnDurationMs,keyterms:t.keyterms,safetyNetTimeoutMs:t.safetyNetTimeoutMs,echoCancellation:t.microphone?.echoCancellation,noiseSuppression:t.microphone?.noiseSuppression,autoGainControl:t.microphone?.autoGainControl,microphoneDeviceId:this.config.microphoneDeviceId,promptMetadata:this.promptMetadata,messages:this.messages,sendMessage:r=>{this.signaling?.dataSend(r)}},{DeepgramFluxSTT:i}=await import("./chunks/deepgram-flux-stt-RQL6OWQ2.js");return new i(n)}getSpeechOptions(e){return{apiUrl:this.config.connectionUrl,assetBasePath:this.config.assetBasePath,enableInterrupt:this.config.enableInterruptBySpeech,enableVad:this.config.enableVad,jwtToken:e,hintPhrases:this.config.speechRecognitionHintPhrases,hintPhrasesBoost:this.config.speechRecognitionHintPhrasesBoost,locales:this.config.speechRecognitionLocales,messages:this.messages,promptMetadata:this.promptMetadata,sessionId:this.id,sendMessage:t=>{this.signaling?.dataSend(t)},microphoneDeviceId:this.config.microphoneDeviceId}}initMetadata(){return{userSpokenLocale:"",browserDetectedLocales:this.config.speechRecognitionLocales,userTimezone:Intl.DateTimeFormat().resolvedOptions().timeZone,userScreenWidth:window.screen.availWidth,userScreenHeight:window.screen.availHeight,userAgent:navigator.userAgent,personaId:this.config.personaId,custom:this.config.customMetadata}}handleDataChannelMessage(e,t){switch(p.debug("Processing data channel message",e),e.msg.type){case"nlp_prompt_result":{this.messages.next(new Gn(e.msg));break}case"avatar_started_speaking":{this.messages.next(new Nn);break}case"avatar_stopped_speaking":{this.messages.next(new Un);break}case"error":{p.error(`an error has occurred - ${JSON.stringify(e.msg)}`),this.handleChannelMessageError(e.msg);break}case"scene_ready":{p.debug("Scene is ready"),t.sceneReady=!0,this.handleSessionReady(t),this.messages.next(new Xn);break}case"speech_event":{this.messages.next(new Hn(e.msg));break}case"session_ended":{this.signaling?.endSession(e.msg.reason),this.signaling=void 0;break}case"speech_stream_opened":{this.audioStreamId=e.msg.streamId;break}case"speech_stream_closed":{e.msg.streamId===this.audioStreamId?this.audioStreamId=void 0:p.warn("Received SpeechStreamClosed message for unexpected streamId",e.msg.streamId);break}}}handleChannelMessageError(e){let t=e?.message?.trim()||"An unknown error occurred";e&&vr.includes(e.code)?this.messages.next(new Wn(e)):this.messages.next(new q(t))}sendWelcomePrompt(){p.debug("Checking if the welcome prompt should be sent. Welcome requested already = "+this.welcomeRequested+". Prompt = ",this.config.welcomePrompt),this.config.welcomePrompt&&this.config.welcomePrompt.length>0&&!this.welcomeRequested&&(this.welcomeRequested=!0,this.signaling?.dataSend(new se(this.config.welcomePrompt,this.promptMetadata)))}listenForTabClosure(){window.addEventListener("beforeunload",()=>{p.info("Window unload detected, ending session."),this.signaling?.endSession(),this.signaling=void 0})}};var Fn=class{constructor(e,t=$()){this.prompt=e;this.requestId=t}prompt;requestId;toJSON(){return{action:"speak",data:{requestId:this.requestId,prompt:this.prompt}}}};var me=class{constructor(e,t,n={},i=$()){this.action=e;this.streamId=t;this.metadata=n;this.requestId=i}action;streamId;metadata;requestId;toJSON(){return{action:"speech_stream",data:{requestId:this.requestId,action:this.action,streamId:this.streamId,metadata:this.metadata}}}};var An=class{constructor(e,t,n,i){this.command=e;this.enabled=t;this.category=n;this.verbosity=i}command;enabled;category;verbosity;toJSON(){let e={command:this.command,enabled:this.enabled};return this.category!==void 0&&(e.category=this.category),this.verbosity!==void 0&&(e.verbosity=this.verbosity),{action:"execute_command",data:e}}};async function wr(s,e){try{if(s?.audioStreamId===void 0){p.warn("Audio stream is not open | Call openAudioStream() first");return}let t;if(typeof e=="string"){let o=typeof atob<"u"?atob(e):Buffer.from(e,"base64").toString("binary");t=new Uint8Array(o.length);for(let a=0;a<o.length;a++)t[a]=o.charCodeAt(a)}else if(e instanceof Uint8Array)t=e;else if(typeof Blob<"u"&&e instanceof Blob){let o=await e.arrayBuffer();t=new Uint8Array(o)}else{p.error("speakAudio: Unsupported audio type",e);return}let n=Uint8Array.from([1]),i=Uint8Array.from([s.audioStreamId]),r=new Uint8Array(2+t.length);r.set(n,0),r.set(i,1),r.set(t,2),s.signaling?.dataSendBinary(r)}catch(t){p.error("speakAudioSend: Error sending audio",t)}}var Ds=class{constructor(e){this.config=e;p.setLevel(e.logLevel??"info")}config;session;messageSubscription;init(){if(this.session){let e="Cannot initialize: session already active. Call endSession() first.";throw p.warn(e),new Error(e)}if(this.config.audioUpstreamMode!==void 0&&!Bs(this.config.audioUpstreamMode)){let e=`Invalid audioUpstreamMode: '${String(this.config.audioUpstreamMode)}'. Allowed: 'speech-recognition-service' | 'pixel-streaming' | 'both'.`;throw p.error(e),new Error(e)}p.info(`Initializing Uneeq with config:', ${JSON.stringify(this.config)}, for UserAgent: ${navigator.userAgent}`),this.session=new On(this.config),this.session.createSession(),this.messageSubscription=this.session.messages.subscribe(e=>{e.uneeqMessageType==="SessionEnded"&&(p.info("Session ended, resetting session state"),this.resetSessionState())})}isSessionActive(){return this.session!==void 0}ensureSessionExists(){return this.session?.id?!0:(p.warn("Cannot perform action, session has not started."),!1)}chatPrompt(e){e&&e.trim()!==""&&this.ensureSessionExists()&&this.session?.signaling?.dataSend(new se(e,this.session?.promptMetadata))}speak(e){e&&e.trim()!==""&&this.ensureSessionExists()&&this.session?.signaling?.dataSend(new Fn(e))}openAudioStream(){let e=this.session;this.ensureSessionExists()&&e?.signaling?.dataSend(new me("open",e?.audioStreamId))}closeAudioStream(){let e=this.session;this.ensureSessionExists()&&e?.audioStreamId!==void 0&&e.signaling?.dataSend(new me("close",e.audioStreamId))}interruptAudioStream(){let e=this.session;this.ensureSessionExists()&&e?.audioStreamId!==void 0&&e.signaling?.dataSend(new me("interrupt",e.audioStreamId))}async speakAudio(e){this.ensureSessionExists()&&await wr(this.session,e)}cameraAnchorDistance(e,t){this.ensureSessionExists()&&this.session?.setCameraAnchorDistance(e,t)}cameraAnchorHorizontal(e,t){this.ensureSessionExists()&&this.session?.setCameraAnchorHorizontal(e,t)}changeAvatar(e){e&&e.trim()!==""&&this.ensureSessionExists()&&this.session?.changeAvatar(e)}loadBackground(e){e&&e.trim()!==""&&this.ensureSessionExists()&&this.session?.loadBackground(e)}userStartSpeaking(){this.ensureSessionExists()&&this.session?.userStartSpeaking()}userStopSpeaking(){this.ensureSessionExists()&&this.session?.userStopSpeaking()}triggerAction(e){e&&e.trim()!==""&&this.ensureSessionExists()&&this.session?.triggerAction(e)}resumeVideoPlayback(){this.session?.resumeVideoPlayback()}executeCommand(e,t,n,i){this.ensureSessionExists()&&this.session?.signaling?.dataSend(new An(e,t,n,i))}endSession(){this.ensureSessionExists()&&(this.session?.signaling?.endSession(),this.resetSessionState())}resetSessionState(){this.messageSubscription?.unsubscribe(),this.messageSubscription=void 0,this.session=void 0}unmuteDigitalHuman(){this.ensureSessionExists()&&(this.session?.signaling?.playAudioElement(),p.debug("Unmuted digital human."))}muteDigitalHuman(){this.ensureSessionExists()&&(this.session?.signaling?.pauseAudioElement(),p.debug("Muted digital human."))}stopSpeaking(){this.ensureSessionExists()&&this.session?.signaling?.dataSend(new Us)}setCustomPromptMetadata(e){this.ensureSessionExists()&&this.session?.updatePromptMetadata(e)}enableMicrophone(e=!0){this.ensureSessionExists()&&this.session?.setEnableMicrophone(e)}pauseSpeechRecognition(){if(!this.ensureSessionExists())return!1;let e=G(this.config.audioUpstreamMode);if(!X(e))return p.warn(`pauseSpeechRecognition called in '${e}' mode \u2014 no STT leg to pause; ignoring. Use muteUpstreamMic() to silence the PS mic track.`),!1;let t=this.session?.speechRec?.pause()??!1;return t instanceof Promise?!0:t}resumeSpeechRecognition(){if(!this.ensureSessionExists())return!1;let e=G(this.config.audioUpstreamMode);if(!X(e))return p.warn(`resumeSpeechRecognition called in '${e}' mode \u2014 no STT leg to resume; ignoring. Use unmuteUpstreamMic() to re-enable the PS mic track.`),!1;let t=this.session?.speechRec?.resume()??!1;return t instanceof Promise?!0:t}muteUpstreamMic(){this.ensureSessionExists()&&this.session?.signaling?.muteUpstreamMic()}unmuteUpstreamMic(){this.ensureSessionExists()&&this.session?.signaling?.unmuteUpstreamMic()}setWebRtcStatsEnabled(e,t){this.session?.config&&(this.session.config.webRtcStatsEmitMessages=e,this.session.config.webRtcStatsLogMessages=t,t?this.session.signaling?.startAudioStatsMonitor():this.session.signaling?.stopAudioStatsMonitor())}setNetworkIndicatorEnabled(e){this.ensureSessionExists()&&(e?this.session?.signaling?.startNetworkQualityMonitor(e):this.session?.signaling?.stopNetworkQualityMonitor())}};export{Ir as AvatarAnswerMessage,Fr as AvatarInterruptedMessage,Nn as AvatarStartedSpeakingMessage,Un as AvatarStoppedSpeakingMessage,Sr as CameraAnchorDistance,br as CameraAnchorHorizontal,jn as CustomMetadataUpdated,ri as CustomerConcurrencyLimitReachedMessage,kr as DeviceErrorMessage,Oe as DigitalHumanMuted,$n as DigitalHumanPlayedInMutedModeSuccess,qn as DigitalHumanUnmuted,Kn as EnableMicrophoneUpdatedMessage,xr as LogLevel,fr as NetworkIndicatorPopupTrigger,mr as NetworkIndicatorPosition,pr as NetworkIndicatorVisibility,ur as NetworkQualityLevel,oi as NetworkQualityMessage,Qn as PromptRequestMessage,Gn as PromptResultMessage,Fe as RendererErrorMessage,Xn as SceneReadyMessage,Wn as SessionBackendErrorMessage,Jn as SessionConnectingMessage,Yn as SessionDisconnectedMessage,zn as SessionEndedMessage,q as SessionErrorMessage,Bn as SessionLiveMessage,ei as SessionReconnectingFinishedMessage,Zn as SessionReconnectingMessage,ti as SoftSwitchStartingMessage,ni as SoftSwitchStoppingMessage,Hn as SpeechEventMessage,ci as SpeechRecognitionProvider,Ar as SpeechRecognitionTransientErrorMessage,Pr as SpeechTranscriptionMessage,Ds as Uneeq,Ns as UneeqMessageType,Dr as UserStartedSpeakingMessage,Or as UserStoppedSpeakingMessage,Lr as VadInterruptionAllowedMessage,ai as VideoAutoplayBlockedMessage,ii as VideoLayoutConfiguringMessage,Vn as WaitingInQueueMessage,si as WebRtcConnectedMessage,_n as WebRtcStatsMessage,G as resolveAudioUpstreamMode};
|