@1interface/voice-core 0.2.1 → 0.3.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/index.d.ts +8 -17
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -21,19 +21,8 @@ export declare interface AudioDeviceInfo {
|
|
|
21
21
|
label: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export declare interface AudioDevices {
|
|
25
|
-
enumerate(): Promise<AudioDevicesSnapshot>;
|
|
26
|
-
onChange(callback: () => void): () => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export declare interface AudioDevicesSnapshot {
|
|
30
|
-
microphones: AudioDeviceInfo[];
|
|
31
|
-
speakers: AudioDeviceInfo[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
24
|
export declare interface AudioPlatform {
|
|
35
25
|
attachBotAudioTrack: BotAudioTrackAttacher;
|
|
36
|
-
readonly devices: AudioDevices;
|
|
37
26
|
requestMicPermission(): Promise<MicPermissionResult>;
|
|
38
27
|
}
|
|
39
28
|
|
|
@@ -53,11 +42,6 @@ export declare interface BotTurnState {
|
|
|
53
42
|
segments: BotTurnSegment[];
|
|
54
43
|
}
|
|
55
44
|
|
|
56
|
-
export declare class BrowserAudioDevices implements AudioDevices {
|
|
57
|
-
enumerate(): Promise<AudioDevicesSnapshot>;
|
|
58
|
-
onChange(callback: () => void): () => void;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
45
|
export declare const browserAudioPlatform: AudioPlatform;
|
|
62
46
|
|
|
63
47
|
export declare const clearBotTurn: ActionCreatorWithoutPayload<"voice/clearBotTurn">;
|
|
@@ -116,7 +100,8 @@ export declare const useAudioPlatform: () => AudioPlatform;
|
|
|
116
100
|
export declare function useMediaDevices(): {
|
|
117
101
|
microphones: AudioDeviceInfo[];
|
|
118
102
|
speakers: AudioDeviceInfo[];
|
|
119
|
-
|
|
103
|
+
selectedMicId: string;
|
|
104
|
+
selectedSpeakerId: string;
|
|
120
105
|
};
|
|
121
106
|
|
|
122
107
|
export declare const useVoice: () => VoiceContextValue;
|
|
@@ -189,6 +174,9 @@ export declare interface VoiceContextValue {
|
|
|
189
174
|
sendText: (text: string) => void;
|
|
190
175
|
muted: boolean;
|
|
191
176
|
serverMuted: boolean;
|
|
177
|
+
sessionEndedSignal: number;
|
|
178
|
+
setInputDevice: (deviceId: string) => void;
|
|
179
|
+
setOutputDevice: (deviceId: string) => void;
|
|
192
180
|
}
|
|
193
181
|
|
|
194
182
|
export declare type VoiceDisplayState = (typeof VOICE_DISPLAY_STATES)[keyof typeof VOICE_DISPLAY_STATES];
|
|
@@ -232,8 +220,11 @@ export declare interface VoiceState {
|
|
|
232
220
|
responseStartTime: number | null;
|
|
233
221
|
interimTranscript: string | null;
|
|
234
222
|
botTurn: BotTurnState;
|
|
223
|
+
sessionEndedSignal: number;
|
|
235
224
|
selectedMicId: string;
|
|
236
225
|
selectedSpeakerId: string;
|
|
226
|
+
microphones: AudioDeviceInfo[];
|
|
227
|
+
speakers: AudioDeviceInfo[];
|
|
237
228
|
}
|
|
238
229
|
|
|
239
230
|
export declare type VoiceTransportFactory = () => Transport;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react"),t=require("react-redux"),r=require("@reduxjs/toolkit"),n=require("@1interface/shared-core"),s={DISCONNECTED:"disconnected",CONNECTING:"connecting",CONNECTED:"connected"},a={IDLE:"idle",LISTENING:"listening",PROCESSING:"processing",SPEAKING:"speaking"},o={CASCADE:"cascade",REALTIME:"realtime"},c={OFFLINE:"offline",CONNECTING:"connecting",READY:"ready",IDLE:"idle",LISTENING:"listening",MUTED:"muted",PROCESSING:"processing",SPEAKING:"speaking"},i=[/rejected/i,/unauthori[sz]ed/i,/forbidden/i,/\b40[13]\b/],u=[/\b402\b/,/\b429\b/,/rate[\s-]?limit/i],l={SELECTED_MIC_ID:"voice_selectedMicId",SELECTED_SPEAKER_ID:"voice_selectedSpeakerId"};function d(){return{connectionStatus:s.DISCONNECTED,pipelineStatus:a.IDLE,listeningEnabled:!1,muted:!1,serverMuted:!1,statusText:null,isAgentWorking:!1,responseStartTime:null,interimTranscript:null,botTurn:{active:!1,chatMsgId:null,segments:[]},selectedMicId:"",selectedSpeakerId:""}}const p=d(),g=r.createSlice({name:"voice",initialState:p,reducers:{setConnectionStatus(e,t){e.connectionStatus=t.payload},setPipelineStatus(e,t){e.pipelineStatus=t.payload},setListeningEnabled(e,t){e.listeningEnabled=t.payload},setStatusText(e,t){e.statusText=t.payload},setIsAgentWorking(e,t){e.isAgentWorking=t.payload},setResponseStartTime(e,t){e.responseStartTime=t.payload},setInterimTranscript(e,t){e.interimTranscript=t.payload},openBotTurn(e,t){e.botTurn={active:!0,chatMsgId:t.payload?.chatMsgId??null,segments:[]}},upsertBotSegment(e,t){const{id:r,text:n,spokenChars:s}=t.payload;e.botTurn.active=!0;const a=e.botTurn.segments.find(e=>e.id===r);a?(a.text=n,a.spokenChars=Math.max(a.spokenChars,s)):(e.botTurn.segments.push({id:r,text:n,spokenChars:s}),e.botTurn.segments.sort((e,t)=>e.id-t.id))},clearBotTurn(e){e.botTurn={active:!1,chatMsgId:null,segments:[]}},setMuted(e,t){e.muted=t.payload},setServerMuted(e,t){e.serverMuted=t.payload},setSelectedMicId(e,t){e.selectedMicId=t.payload},setSelectedSpeakerId(e,t){e.selectedSpeakerId=t.payload},resetVoiceState:()=>d(),resetIndicatorState(e){e.statusText=null,e.isAgentWorking=!1,e.responseStartTime=null,e.interimTranscript=null,e.serverMuted=!1,e.botTurn={active:!1,chatMsgId:null,segments:[]}}}}),{setConnectionStatus:E,setPipelineStatus:S,setListeningEnabled:m,setMuted:I,setServerMuted:T,setStatusText:C,setIsAgentWorking:f,setResponseStartTime:v,setInterimTranscript:k,openBotTurn:h,upsertBotSegment:_,clearBotTurn:x,setSelectedMicId:R,setSelectedSpeakerId:N,resetVoiceState:b,resetIndicatorState:y}=g.actions,M=g.reducer,A=e.createContext(null),D=()=>{const t=e.useContext(A);if(!t)throw new Error("useAudioPlatform must be used inside <AudioPlatformProvider>. <VoiceProvider> mounts one by default; if you replaced VoiceProvider, you must wire your own.");return t};function w(){const e={};for(const[t,r]of Object.entries(n.getCustomHeaders()))r&&(e[t]=r);const t=n.readAccessToken();t&&(e[n.AUTH_HEADERS.ACCESS_TOKEN]=t);const r=n.readDeviceToken();return r&&(e[n.DEVICE_TOKEN_HEADER]=r),e}async function O(e){try{const t=await fetch(n.API_ENDPOINTS.WEBRTC_ICE(e),{headers:w()});return t.ok?(await t.json()).iceServers??[]:(n.logger.warn({message:"Voice ICE fetch failed — continuing without TURN",api_name:"voice-webrtc-ice",status:t.status}),[])}catch(e){return n.logger.warn({message:"Voice ICE fetch error — continuing without TURN",api_name:"voice-webrtc-ice",error_message:e instanceof Error?e.message:String(e)}),[]}}const P="voice-user-",L=(e,t=`${P}${n.generateUniqueId()}`)=>({id:t,sender:"user",message:e,timestamp:(new Date).toISOString()}),V=(e,t)=>{const r=`${e}${n.generateUniqueId()}`;return{id:r,msg_id:r,sender:"agent",message:"",timestamp:(new Date).toISOString(),...t}};function G(){const e=t.useSelector(e=>e.voice.connectionStatus),r=t.useSelector(e=>e.voice.pipelineStatus),n=t.useSelector(e=>e.voice.listeningEnabled),o=t.useSelector(e=>e.voice.muted);return e===s.CONNECTING?c.CONNECTING:e!==s.CONNECTED?c.OFFLINE:n?o?c.MUTED:r===a.PROCESSING?c.PROCESSING:r===a.SPEAKING?c.SPEAKING:r===a.LISTENING?c.LISTENING:c.IDLE:c.READY}const B=e.createContext(null),U=e.createContext({strategyActive:!1,voiceSlot:null,onEnterVoice:null,onFloatingOrbClick:null,voiceInputHidden:!1,headerCenterSlot:null,floatOrbPositionRef:null}),q=r.createListenerMiddleware();q.startListening({actionCreator:R,effect:async e=>{const t=n.getStorageRef();t&&await t.setItem(l.SELECTED_MIC_ID,e.payload)}}),q.startListening({actionCreator:N,effect:async e=>{const t=n.getStorageRef();t&&await t.setItem(l.SELECTED_SPEAKER_ID,e.payload)}}),q.startListening({matcher:r.isAnyOf(n.clearSession,n.sessionExpired),effect:async(e,t)=>{const r=n.getStorageRef();r&&await Promise.all([r.removeItem(l.SELECTED_MIC_ID),r.removeItem(l.SELECTED_SPEAKER_ID)]),t.dispatch(b())}});class F{async enumerate(){const e={microphones:[],speakers:[]};if(!navigator.mediaDevices?.enumerateDevices)return e;try{const e=await navigator.mediaDevices.enumerateDevices();let t=0,r=0;return{microphones:e.filter(e=>"audioinput"===e.kind).map(e=>({deviceId:e.deviceId,label:e.label||"Microphone "+ ++t})),speakers:e.filter(e=>"audiooutput"===e.kind).map(e=>({deviceId:e.deviceId,label:e.label||"Speaker "+ ++r}))}}catch{return e}}onChange(e){const t=navigator.mediaDevices;return t?.addEventListener?(t.addEventListener("devicechange",e),()=>t.removeEventListener("devicechange",e)):()=>{}}}const K=e=>{const t=document.createElement("audio");return t.autoplay=!0,t.style.display="none",t.srcObject=new MediaStream([e]),document.body.appendChild(t),()=>{try{t.pause(),t.srcObject=null,t.remove()}catch{}}},W={attachBotAudioTrack:K,devices:new F,requestMicPermission:async function(){try{return(await navigator.mediaDevices.getUserMedia({audio:!0})).getTracks().forEach(e=>e.stop()),{granted:!0}}catch(e){return{granted:!1,error:e}}}};exports.AudioPlatformContext=A,exports.BrowserAudioDevices=F,exports.ORB_STATE_CONFIG={offline:{opacity:.15,playbackRate:.3,scale:1},connecting:{opacity:.4,playbackRate:.5,scale:1},ready:{opacity:.4,playbackRate:.4,scale:1},idle:{opacity:.6,playbackRate:.6,scale:1},listening:{opacity:.7,playbackRate:.7,scale:1},muted:{opacity:.7,playbackRate:.5,scale:1},processing:{opacity:1,playbackRate:1,scale:1},speaking:{opacity:1,playbackRate:1,scale:1.3}},exports.VOICE_CONNECTION_STATUSES=s,exports.VOICE_CONNECT_TIMEOUT_MS=15e3,exports.VOICE_DISPLAY_STATES=c,exports.VOICE_PIPELINE_MODES=o,exports.VOICE_PIPELINE_STATUSES=a,exports.VOICE_STORAGE_KEYS=l,exports.VoiceContext=B,exports.VoiceUIContext=U,exports.attachBrowserBotAudioTrack=K,exports.browserAudioPlatform=W,exports.clearBotTurn=x,exports.hydrateVoice=e=>async t=>{const[r,n]=await Promise.all([e.getItem(l.SELECTED_MIC_ID),e.getItem(l.SELECTED_SPEAKER_ID)]);r&&t(g.actions.setSelectedMicId(r)),n&&t(g.actions.setSelectedSpeakerId(n))},exports.openBotTurn=h,exports.resetIndicatorState=y,exports.setConnectionStatus=E,exports.setInterimTranscript=k,exports.setIsAgentWorking=f,exports.setListeningEnabled=m,exports.setMuted=I,exports.setPipelineStatus=S,exports.setResponseStartTime=v,exports.setSelectedMicId=R,exports.setSelectedSpeakerId=N,exports.setServerMuted=T,exports.setStatusText=C,exports.upsertBotSegment=_,exports.useAudioPlatform=D,exports.useMediaDevices=function(){const r=D(),[n,s]=e.useState([]),[a,o]=e.useState([]),c=t.useSelector(e=>e.voice.listeningEnabled),i=e.useCallback(async()=>{const e=await r.devices.enumerate();s(e.microphones),o(e.speakers)},[r]);return e.useEffect(()=>(i(),r.devices.onChange(i)),[r,i]),e.useEffect(()=>{c&&i()},[c,i]),{microphones:n,speakers:a,refresh:i}},exports.useVoice=()=>{const t=e.useContext(B);if(!t)throw new Error("useVoice must be used within VoiceProvider");return t},exports.useVoiceDisplayState=G,exports.useVoiceProviderValue=({conversationId:r,voiceBridge:c,transportFactory:l,assistantStartsConversation:d})=>{const p=t.useDispatch(),g=D(),R=t.useSelector(e=>e.voice.muted),N=t.useSelector(e=>e.voice.serverMuted),b=t.useSelector(e=>e.voice.pipelineStatus),y=t.useSelector(e=>e.voice.isAgentWorking),{connect:M,disconnect:A,sendText:B,setMicEnabled:U}=function(r,c,l,d){const p=t.useDispatch(),g=D(),I=t.useSelector(e=>e.auth.accessToken),R=e.useRef(c);e.useEffect(()=>{R.current=c});const N=e.useRef(null),b=e.useRef(new Map),y=e.useRef(""),M=e.useRef(!1),A=e.useRef(!1),G=e.useRef(null),B=e.useRef(""),U=e.useRef(null),q=e.useRef(null),F=e.useRef(null),K=e.useRef(0),W=e.useRef(0),j=e.useRef(!1),H=e.useRef(0),$=e.useRef(null),Y=e.useRef(null),Z=e.useRef(0),z=e.useRef(0),J=e.useRef(null),Q=e.useCallback(()=>{if(null!=$.current)return;const e=Date.now();$.current=e,p(v(e)),p(n.clearLastResponseTime())},[p]),X=e.useCallback(()=>{const e=$.current;$.current=null,p(v(null)),null!=e&&p(n.setLastResponseTime(Date.now()-e))},[p]),ee=e.useCallback(()=>{$.current=null,p(v(null))},[p]),te=e.useCallback((e=n.INDICATOR_TIMER_ACTIONS.KEEP)=>{p(C(null)),p(f(!1)),e===n.INDICATOR_TIMER_ACTIONS.FINALIZE?X():e===n.INDICATOR_TIMER_ACTIONS.CANCEL&&ee()},[p,X,ee]),[re]=e.useState(()=>({setStatusText:e=>p(C(e||null)),setIsAgentWorking:e=>p(f(e)),appendStreamText:()=>{},pushWidget:e=>{const t=V("voice-widget-",{widget:[e],message_type:["widget"]});R.current.setMessages(e=>[...e,t])},onPayment:e=>R.current.onPaymentInit?.(e),onConversationTitle:e=>R.current.onConversationTitle?.(e),onError:e=>{n.logger.error({message:"Voice RTVI server error",api_name:"voice-rtvi",error_message:e});const t=V("voice-error-",{message:e,message_type:["error"]});R.current.setMessages(e=>[...e,t])},onStatusActive:()=>{Q()}})),ne=e.useCallback(()=>{F.current&&(clearTimeout(F.current),F.current=null)},[]),se=e.useCallback(()=>[...b.current.entries()].sort((e,t)=>e[0]-t[0]).map(([,e])=>e.text).join(" ").trim(),[]),ae=e.useCallback(()=>{N.current=null,b.current=new Map,y.current="",p(x())},[p]),oe=e.useCallback(()=>{const e=N.current;if(e){const t=se();t&&R.current.updateStreamingMessage(e,t),R.current.commitStreamingMessages()}ae()},[se,ae]),ce=e.useCallback(e=>{ne(),q.current?.(),q.current=null,U.current=null,j.current=!1,p(m(!1)),p(S(a.IDLE)),p(E(s.DISCONNECTED)),p(k(null)),p(T(!1)),G.current=null,B.current="",M.current=!1,A.current=!1,e?oe():(R.current.discardStreamingMessages(),ae()),te(n.INDICATOR_TIMER_ACTIONS.CANCEL)},[p,te,ne,oe,ae]),ie=e.useCallback(()=>{if(N.current)return;const e=`voice-agent-${n.generateUniqueId()}`;N.current=e,b.current=new Map,y.current="",p(h({chatMsgId:e}))},[p]),ue=e.useCallback(e=>{const t=e.spoken_progress?(e.spoken_progress?.accumulated_text??"")+(e.spoken_progress?.remaining_text??""):e.text??"";if(!t)return;ie();const r=M.current?function(e,t){const r=(e.get(0)?.text??"")+t;return e.set(0,{text:r,completed:!1}),{id:0,text:r,spokenChars:r.length}}(b.current,t):function(e,t,r,n){const s=t.segment_id??0,a="completed"===t.spoken_status;if(a&&!e.has(s)){const t=[...e.entries()].find(([,e])=>e.text===r&&!e.completed);if(t)return t[1].completed=!0,{id:t[0],text:r,spokenChars:r.length}}const o=e.get(s);return e.set(s,{text:r,completed:a||!0===o?.completed}),{id:s,text:r,spokenChars:n&&t.spoken_progress?(t.spoken_progress.accumulated_text??"").length:r.length}}(b.current,e,t,A.current);p(_(r));const n=se();n&&N.current&&n!==y.current&&(y.current=n,R.current.updateStreamingMessage(N.current,n))},[p,ie,se]),le=e.useCallback(e=>{const t=e,r=t?.type,s=t?.data;switch(r){case"status_update":n.handleSharedEvent({kind:"status_update",statusText:s?.text??""},re);break;case"started-thinking":p(f(!0)),Q();break;case"stopped-thinking":te(n.INDICATOR_TIMER_ACTIONS.FINALIZE);break;case"clear_status":break;case"widget":s&&n.handleSharedEvent({kind:"widget",widget:s},re);break;case"payment":s&&n.handleSharedEvent({kind:"payment",payment:s},re);break;case"conversation_title":{const e="string"==typeof s?.text?s.text.trim():"";e&&n.handleSharedEvent({kind:"conversation_title",title:e},re);break}default:n.logger.debug({message:"Voice RTVI unhandled server-message",api_name:"voice-rtvi",event_type:r??"unknown"})}},[re,p,Q,te]),de=e.useRef(()=>{}),pe=e.useRef(async()=>{}),ge=e.useCallback(e=>{const t=()=>e===K.current;return{onConnected:()=>{t()&&(Z.current=0,z.current=0,H.current=0,p(E(s.CONNECTED)),p(m(!0)),p(S(a.IDLE)))},onBotReady:e=>{if(!t())return;const r=e?.about;M.current=r?.pipeline_mode===o.REALTIME,A.current=!0===r?.tts_word_timestamps,ne(),d&&(p(f(!0)),Q())},onDisconnected:()=>{if(!t())return;ce(!0);const e=W.current,s=J.current;if(J.current=null,"terminal"!==s){if("auth"===s){if(z.current>=1||null==r)return;return z.current+=1,void(async()=>{let t=!1;try{t=!!await(n.getTokenRefreshHandler()?.())}catch{}e===W.current&&t&&pe.current()})()}Z.current>=3||null==r?Z.current>=3&&n.showErrorToast("Voice connection lost. Please try again."):(Z.current+=1,Y.current&&clearTimeout(Y.current),Y.current=setTimeout(()=>{Y.current=null,pe.current()},1e3))}else n.logger.warn({message:"Voice RTVI terminal error — no retry",api_name:"voice-rtvi"})},onError:e=>{if(!t())return;const r=e?.data,s=r?.error??r?.message??"Voice error";n.handleSharedEvent({kind:"error",error:s},re),p(S(a.IDLE)),te(n.INDICATOR_TIMER_ACTIONS.CANCEL),r?.fatal&&(i.some(e=>e.test(s))?J.current="auth":u.some(e=>e.test(s))&&(J.current="terminal"),U.current?.disconnect().catch(()=>{}))},onTrackStarted:(e,r)=>{t()&&(r?.local||"audio"!==e.kind||(q.current?.(),q.current=g.attachBotAudioTrack(e)))},onUserMuteStarted:()=>{t()&&p(T(!0))},onUserMuteStopped:()=>{t()&&p(T(!1))},onUserStartedSpeaking:()=>{t()&&(G.current=null,B.current="",p(k(null)),p(S(a.LISTENING)))},onUserStoppedSpeaking:()=>{if(!t())return;p(S(a.PROCESSING)),p(f(!0)),p(C(null)),Q();const e=B.current.trim();if(e){const t=`${P}${n.generateUniqueId()}`;G.current=t,p(k(null)),R.current.setMessages(r=>[...r,L(e,t)])}},onUserTranscript:e=>{if(!t())return;if(!e.final)return void(e.text&&(B.current=e.text,M.current||p(k(e.text))));p(k(null));const r=e.text&&e.text.trim()?e.text:"",n=G.current;G.current=null,n?r&&R.current.setMessages(e=>e.map(e=>e.id===n?{...e,message:r}:e)):r&&R.current.setMessages(e=>[...e,L(r)])},onBotStartedSpeaking:()=>{t()&&(ie(),p(S(a.SPEAKING)),te(n.INDICATOR_TIMER_ACTIONS.FINALIZE))},onBotOutput:e=>{t()&&ue(e)},onBotStoppedSpeaking:()=>{t()&&(oe(),p(S(a.IDLE)),te(n.INDICATOR_TIMER_ACTIONS.FINALIZE))},onServerMessage:e=>{t()&&le(e)}}},[p,g,ne,ce,te,Q,ue,ie,oe,le,re,d,r]),Ee=e.useCallback(async()=>{if(U.current||j.current)return;if(!r)return;if(j.current=!0,n.isTokenAboutToExpire(n.readTokenExpiresAt())&&H.current<1){H.current+=1;const e=W.current;try{await Promise.resolve(n.getTokenRefreshHandler()?.())}catch{}if(e!==W.current)return}const e=++K.current,t=W.current;p(E(s.CONNECTING));const a=()=>t!==W.current||e!==K.current;try{const[{PipecatClient:t,LogLevel:s},o]=await Promise.all([Promise.resolve().then(()=>require("./index.module-DCaqEXL5.cjs")),O(r)]);if(a())return;const c=new t({transport:l(),enableMic:!0,enableCam:!1,callbacks:ge(e)});if(c.setLogLevel(s.WARN),a())return void c.disconnect().catch(()=>{});U.current=c,ne(),F.current=setTimeout(()=>{n.logger.warn({message:"Voice RTVI connect timed out (no bot-ready)",api_name:"voice-rtvi"}),n.showErrorToast("Voice connection timed out. Please try again."),de.current()},15e3),await c.connect({webrtcRequestParams:{endpoint:n.API_ENDPOINTS.WEBRTC_OFFER(r),headers:new Headers(w())},iceConfig:{iceServers:o}})}catch(t){n.logger.error({message:"Voice RTVI connect failed",api_name:"voice-rtvi",error_message:t instanceof Error?t.message:String(t)}),e===K.current&&(n.showErrorToast("Could not connect voice. Please try again."),ce(!1))}},[r,p,l,ge,ne,ce]),Se=e.useCallback((e=!0)=>{W.current+=1,K.current+=1,H.current=0,Y.current&&(clearTimeout(Y.current),Y.current=null),Z.current=0,z.current=0,J.current=null;const t=U.current;t?.disconnect().catch(e=>{n.logger.warn({message:"Voice RTVI disconnect threw",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}),ce(e)},[ce]),me=e.useCallback(e=>{U.current?.enableMic(e)},[]),Ie=e.useCallback(e=>{const t=U.current;if(t){try{t.sendText(e,{run_immediately:!0,audio_response:!0})}catch(e){return void n.logger.error({message:"Voice RTVI send-text failed",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}N.current&&oe(),R.current.setMessages(t=>[...t,L(e)]),p(f(!0)),p(C(null)),Q(),p(S(a.PROCESSING))}},[p,Q,oe]);e.useEffect(()=>{de.current=Se},[Se]),e.useEffect(()=>{pe.current=Ee},[Ee]);const Te=e.useRef(r),Ce=e.useRef(!1);e.useEffect(()=>{const e=Te.current;if(Te.current=r,e===r)return;const t=j.current;t&&(Ce.current=!0),t&&Se(!1),Ce.current&&null!=r&&(Ce.current=!1,Ee())},[r,Se,Ee]);const fe=e.useRef(I);return e.useEffect(()=>{const e=fe.current;fe.current=I,e&&!I&&j.current&&Se(!1)},[I,Se]),e.useEffect(()=>()=>{de.current()},[]),{connect:Ee,disconnect:Se,sendText:Ie,setMicEnabled:me}}(r,c,l,d);e.useEffect(()=>{U(!R&&!N)},[R,N,U]);const q=G();e.useEffect(()=>(c.setVoiceSendText(B),()=>c.setVoiceSendText(null)),[c,B]);const F=e.useRef(!1);return{displayState:q,pipelineStatus:b,isAgentWorking:y,enterVoiceMode:e.useCallback(async()=>{if(F.current)return n.showErrorToast("Microphone permission changed — please reload the page to apply the new setting."),!1;const e=await g.requestMicPermission();if(!e.granted){const t=e.error;return n.logger.error({message:"Mic permission denied",api_name:"voice-mic",error_message:t instanceof Error?t.message:t?String(t):void 0}),F.current=!0,n.showErrorToast("Microphone access is required for voice mode. Please allow it in your browser's site settings and reload the page."),!1}return M(),!0},[g,M]),exitVoiceMode:e.useCallback(()=>{A()},[A]),toggleMute:e.useCallback(()=>{p(I(!R))},[p,R]),sendText:B,muted:R,serverMuted:N}},exports.useVoiceUI=()=>e.useContext(U),exports.voiceListenerMiddleware=q,exports.voiceReducer=M;
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react"),t=require("react-redux"),r=require("@reduxjs/toolkit"),n=require("@1interface/shared-core"),s={DISCONNECTED:"disconnected",CONNECTING:"connecting",CONNECTED:"connected"},o={IDLE:"idle",LISTENING:"listening",PROCESSING:"processing",SPEAKING:"speaking"},i={CASCADE:"cascade",REALTIME:"realtime"},a={OFFLINE:"offline",CONNECTING:"connecting",READY:"ready",IDLE:"idle",LISTENING:"listening",MUTED:"muted",PROCESSING:"processing",SPEAKING:"speaking"},c=[/rejected/i,/unauthori[sz]ed/i,/forbidden/i,/\b40[13]\b/],u=[/\b402\b/,/\b429\b/,/rate[\s-]?limit/i],l={SELECTED_MIC_ID:"voice_selectedMicId",SELECTED_SPEAKER_ID:"voice_selectedSpeakerId"};function d(){return{connectionStatus:s.DISCONNECTED,pipelineStatus:o.IDLE,listeningEnabled:!1,muted:!1,serverMuted:!1,statusText:null,isAgentWorking:!1,responseStartTime:null,interimTranscript:null,botTurn:{active:!1,chatMsgId:null,segments:[]},sessionEndedSignal:0,selectedMicId:"",selectedSpeakerId:"",microphones:[],speakers:[]}}const g=d(),p=r.createSlice({name:"voice",initialState:g,reducers:{setConnectionStatus(e,t){e.connectionStatus=t.payload},setPipelineStatus(e,t){e.pipelineStatus=t.payload},setListeningEnabled(e,t){e.listeningEnabled=t.payload},setStatusText(e,t){e.statusText=t.payload},setIsAgentWorking(e,t){e.isAgentWorking=t.payload},setResponseStartTime(e,t){e.responseStartTime=t.payload},setInterimTranscript(e,t){e.interimTranscript=t.payload},openBotTurn(e,t){e.botTurn={active:!0,chatMsgId:t.payload?.chatMsgId??null,segments:[]}},upsertBotSegment(e,t){const{id:r,text:n,spokenChars:s}=t.payload;e.botTurn.active=!0;const o=e.botTurn.segments.find(e=>e.id===r);o?(o.text=n,o.spokenChars=Math.max(o.spokenChars,s)):(e.botTurn.segments.push({id:r,text:n,spokenChars:s}),e.botTurn.segments.sort((e,t)=>e.id-t.id))},clearBotTurn(e){e.botTurn={active:!1,chatMsgId:null,segments:[]}},setMuted(e,t){e.muted=t.payload},setServerMuted(e,t){e.serverMuted=t.payload},setSelectedMicId(e,t){e.selectedMicId=t.payload},setSelectedSpeakerId(e,t){e.selectedSpeakerId=t.payload},setMicrophones(e,t){e.microphones=t.payload},setSpeakers(e,t){e.speakers=t.payload},signalSessionEnded(e){e.sessionEndedSignal+=1},resetVoiceState:()=>d(),resetIndicatorState(e){e.statusText=null,e.isAgentWorking=!1,e.responseStartTime=null,e.interimTranscript=null,e.serverMuted=!1,e.botTurn={active:!1,chatMsgId:null,segments:[]}}}}),{setConnectionStatus:m,setPipelineStatus:v,setListeningEnabled:I,setMuted:S,setServerMuted:E,setStatusText:_,setIsAgentWorking:T,setResponseStartTime:f,setInterimTranscript:C,openBotTurn:b,upsertBotSegment:h,clearBotTurn:k,setSelectedMicId:R,setSelectedSpeakerId:V,setMicrophones:x,setSpeakers:y,signalSessionEnded:M,resetVoiceState:N,resetIndicatorState:w}=p.actions,A=p.reducer,D=e.createContext(null),O=()=>{const t=e.useContext(D);if(!t)throw new Error("useAudioPlatform must be used inside <AudioPlatformProvider>. <VoiceProvider> mounts one by default; if you replaced VoiceProvider, you must wire your own.");return t};function P(){const e={};for(const[t,r]of Object.entries(n.getCustomHeaders()))r&&(e[t]=r);const t=n.readAccessToken();t&&(e[n.AUTH_HEADERS.ACCESS_TOKEN]=t);const r=n.readDeviceToken();return r&&(e[n.DEVICE_TOKEN_HEADER]=r),e}async function L(e){try{const t=await fetch(n.API_ENDPOINTS.WEBRTC_ICE(e),{headers:P()});return t.ok?(await t.json()).iceServers??[]:(n.logger.warn({message:"Voice ICE fetch failed — continuing without TURN",api_name:"voice-webrtc-ice",status:t.status}),[])}catch(e){return n.logger.warn({message:"Voice ICE fetch error — continuing without TURN",api_name:"voice-webrtc-ice",error_message:e instanceof Error?e.message:String(e)}),[]}}const G="voice-user-",U=(e,t=`${G}${n.generateUniqueId()}`)=>({id:t,sender:"user",message:e,timestamp:(new Date).toISOString()}),B=(e,t)=>{const r=`${e}${n.generateUniqueId()}`;return{id:r,msg_id:r,sender:"agent",message:"",timestamp:(new Date).toISOString(),...t}};function q(){const e=t.useSelector(e=>e.voice.connectionStatus),r=t.useSelector(e=>e.voice.pipelineStatus),n=t.useSelector(e=>e.voice.listeningEnabled),i=t.useSelector(e=>e.voice.muted);return e===s.CONNECTING?a.CONNECTING:e!==s.CONNECTED?a.OFFLINE:n?i?a.MUTED:r===o.PROCESSING?a.PROCESSING:r===o.SPEAKING?a.SPEAKING:r===o.LISTENING?a.LISTENING:a.IDLE:a.READY}const F=e.createContext(null),K=e.createContext({strategyActive:!1,voiceSlot:null,onEnterVoice:null,onFloatingOrbClick:null,voiceInputHidden:!1,headerCenterSlot:null,floatOrbPositionRef:null}),W=r.createListenerMiddleware();W.startListening({actionCreator:R,effect:async e=>{const t=n.getStorageRef();t&&await t.setItem(l.SELECTED_MIC_ID,e.payload)}}),W.startListening({actionCreator:V,effect:async e=>{const t=n.getStorageRef();t&&await t.setItem(l.SELECTED_SPEAKER_ID,e.payload)}}),W.startListening({matcher:r.isAnyOf(n.clearSession,n.sessionExpired),effect:async(e,t)=>{const r=n.getStorageRef();r&&await Promise.all([r.removeItem(l.SELECTED_MIC_ID),r.removeItem(l.SELECTED_SPEAKER_ID)]),t.dispatch(N())}});const j=e=>{const t=document.createElement("audio");return t.autoplay=!0,t.style.display="none",t.srcObject=new MediaStream([e]),document.body.appendChild(t),()=>{try{t.pause(),t.srcObject=null,t.remove()}catch{}}},H={attachBotAudioTrack:j,requestMicPermission:async function(){try{const e=await(navigator.permissions?.query({name:"microphone"}).catch(()=>null));return n.logger.debug({message:"Voice mic permission probe",api_name:"voice-mic",permission_state:e?.state??"unavailable"}),"granted"===e?.state||(await navigator.mediaDevices.getUserMedia({audio:!0})).getTracks().forEach(e=>e.stop()),{granted:!0}}catch(e){return n.logger.warn({message:"Voice mic permission denied",api_name:"voice-mic",error_message:e instanceof Error?e.message:String(e)}),{granted:!1,error:e}}}};exports.AudioPlatformContext=D,exports.ORB_STATE_CONFIG={offline:{opacity:.15,playbackRate:.3,scale:1},connecting:{opacity:.4,playbackRate:.5,scale:1},ready:{opacity:.4,playbackRate:.4,scale:1},idle:{opacity:.6,playbackRate:.6,scale:1},listening:{opacity:.7,playbackRate:.7,scale:1},muted:{opacity:.7,playbackRate:.5,scale:1},processing:{opacity:1,playbackRate:1,scale:1},speaking:{opacity:1,playbackRate:1,scale:1.3}},exports.VOICE_CONNECTION_STATUSES=s,exports.VOICE_CONNECT_TIMEOUT_MS=15e3,exports.VOICE_DISPLAY_STATES=a,exports.VOICE_PIPELINE_MODES=i,exports.VOICE_PIPELINE_STATUSES=o,exports.VOICE_STORAGE_KEYS=l,exports.VoiceContext=F,exports.VoiceUIContext=K,exports.attachBrowserBotAudioTrack=j,exports.browserAudioPlatform=H,exports.clearBotTurn=k,exports.hydrateVoice=e=>async t=>{const[r,n]=await Promise.all([e.getItem(l.SELECTED_MIC_ID),e.getItem(l.SELECTED_SPEAKER_ID)]);r&&t(p.actions.setSelectedMicId(r)),n&&t(p.actions.setSelectedSpeakerId(n))},exports.openBotTurn=b,exports.resetIndicatorState=w,exports.setConnectionStatus=m,exports.setInterimTranscript=C,exports.setIsAgentWorking=T,exports.setListeningEnabled=I,exports.setMuted=S,exports.setPipelineStatus=v,exports.setResponseStartTime=f,exports.setSelectedMicId=R,exports.setSelectedSpeakerId=V,exports.setServerMuted=E,exports.setStatusText=_,exports.upsertBotSegment=h,exports.useAudioPlatform=O,exports.useMediaDevices=function(){return{microphones:t.useSelector(e=>e.voice.microphones),speakers:t.useSelector(e=>e.voice.speakers),selectedMicId:t.useSelector(e=>e.voice.selectedMicId),selectedSpeakerId:t.useSelector(e=>e.voice.selectedSpeakerId)}},exports.useVoice=()=>{const t=e.useContext(F);if(!t)throw new Error("useVoice must be used within VoiceProvider");return t},exports.useVoiceDisplayState=q,exports.useVoiceProviderValue=({conversationId:r,voiceBridge:a,transportFactory:l,assistantStartsConversation:d})=>{const g=t.useDispatch(),p=O(),N=t.useSelector(e=>e.voice.muted),w=t.useSelector(e=>e.voice.serverMuted),A=t.useSelector(e=>e.voice.pipelineStatus),D=t.useSelector(e=>e.voice.isAgentWorking),F=t.useSelector(e=>e.voice.sessionEndedSignal),{connect:K,disconnect:W,sendText:j,setMicEnabled:H,setInputDevice:$,setOutputDevice:Y}=function(r,a,l,d){const g=t.useDispatch(),p=t.useStore(),N=O(),w=t.useSelector(e=>e.auth.accessToken),A=e.useRef(a);e.useEffect(()=>{A.current=a});const D=e.useRef(null),q=e.useRef(new Map),F=e.useRef(""),K=e.useRef(!1),W=e.useRef(!1),j=e.useRef(null),H=e.useRef(""),$=e.useRef(null),Y=e.useRef(null),Z=e.useRef(null),z=e.useRef(!0),J=e.useRef(null),Q=e.useRef(!1),X=e.useRef(""),ee=e.useRef(0),te=e.useRef(0),re=e.useRef(!1),ne=e.useRef(0),se=e.useRef(null),oe=e.useRef(null),ie=e.useRef(0),ae=e.useRef(0),ce=e.useRef(null),ue=e.useRef(null),le=e.useCallback(()=>{if(null!=se.current)return;const e=Date.now();se.current=e,g(f(e)),g(n.clearLastResponseTime())},[g]),de=e.useCallback(()=>{const e=se.current;se.current=null,g(f(null)),null!=e&&g(n.setLastResponseTime(Date.now()-e))},[g]),ge=e.useCallback(()=>{se.current=null,g(f(null))},[g]),pe=e.useCallback((e=n.INDICATOR_TIMER_ACTIONS.KEEP)=>{g(_(null)),g(T(!1)),e===n.INDICATOR_TIMER_ACTIONS.FINALIZE?de():e===n.INDICATOR_TIMER_ACTIONS.CANCEL&&ge()},[g,de,ge]),[me]=e.useState(()=>({setStatusText:e=>g(_(e||null)),setIsAgentWorking:e=>g(T(e)),appendStreamText:()=>{},pushWidget:e=>{const t=B("voice-widget-",{widget:[e],message_type:["widget"]});A.current.setMessages(e=>[...e,t])},onPayment:e=>A.current.onPaymentInit?.(e),onConversationTitle:e=>A.current.onConversationTitle?.(e),onError:e=>{n.logger.error({message:"Voice RTVI server error",api_name:"voice-rtvi",error_message:e});const t=B("voice-error-",{message:"Something went wrong. Please try again.",message_type:["error"]});A.current.setMessages(e=>[...e,t])},onStatusActive:()=>{le()}})),ve=e.useCallback(()=>{Z.current&&(clearTimeout(Z.current),Z.current=null)},[]),Ie=e.useCallback(()=>[...q.current.entries()].sort((e,t)=>e[0]-t[0]).map(([,e])=>e.text).join(" ").trim(),[]),Se=e.useCallback(()=>{D.current=null,q.current=new Map,F.current="",g(k())},[g]),Ee=e.useCallback(()=>{const e=D.current;if(e){const t=Ie();t&&A.current.updateStreamingMessage(e,t),A.current.commitStreamingMessages()}Se()},[Ie,Se]),_e=e.useCallback(e=>{ve(),J.current&&(clearTimeout(J.current),J.current=null),Q.current=!1,X.current="",g(x([])),g(y([])),Y.current?.(),Y.current=null,$.current=null,re.current=!1,g(I(!1)),g(v(o.IDLE)),g(m(s.DISCONNECTED)),g(C(null)),g(E(!1)),j.current=null,H.current="",K.current=!1,W.current=!1,e?Ee():(A.current.discardStreamingMessages(),Se()),pe(n.INDICATOR_TIMER_ACTIONS.CANCEL)},[g,pe,ve,Ee,Se]),Te=e.useCallback(()=>{if(D.current)return;const e=`voice-agent-${n.generateUniqueId()}`;D.current=e,q.current=new Map,F.current="",g(b({chatMsgId:e}))},[g]),fe=e.useCallback(e=>{const t=e.spoken_progress?(e.spoken_progress?.accumulated_text??"")+(e.spoken_progress?.remaining_text??""):e.text??"";if(!t)return;Te();const r=K.current?function(e,t){const r=(e.get(0)?.text??"")+t;return e.set(0,{text:r,completed:!1}),{id:0,text:r,spokenChars:r.length}}(q.current,t):function(e,t,r,n){const s=t.segment_id??0,o="completed"===t.spoken_status;if(o&&!e.has(s)){const t=[...e.entries()].find(([,e])=>e.text===r&&!e.completed);if(t)return t[1].completed=!0,{id:t[0],text:r,spokenChars:r.length}}const i=e.get(s);return e.set(s,{text:r,completed:o||!0===i?.completed}),{id:s,text:r,spokenChars:n&&t.spoken_progress?(t.spoken_progress.accumulated_text??"").length:r.length}}(q.current,e,t,W.current);g(h(r));const n=Ie();n&&D.current&&n!==F.current&&(F.current=n,A.current.updateStreamingMessage(D.current,n))},[g,Te,Ie]),Ce=e.useCallback(e=>{const t=e,s=t?.type,o=t?.data;switch(n.logger.debug({message:"Voice RTVI server-message received",api_name:"voice-rtvi",conversation_id:r??void 0,event_type:s??"unknown"}),s){case"status_update":n.handleSharedEvent({kind:"status_update",statusText:o?.text??""},me);break;case"started-thinking":g(T(!0)),le();break;case"stopped-thinking":pe(n.INDICATOR_TIMER_ACTIONS.FINALIZE);break;case"clear_status":break;case"widget":o&&n.handleSharedEvent({kind:"widget",widget:o},me);break;case"payment":o&&n.handleSharedEvent({kind:"payment",payment:o},me);break;case"conversation_title":{const e="string"==typeof o?.text?o.text.trim():"";e&&n.handleSharedEvent({kind:"conversation_title",title:e},me);break}case"session-ended":ue.current={reason:"string"==typeof o?.reason?o.reason:"ended"};break;default:n.logger.debug({message:"Voice RTVI unhandled server-message",api_name:"voice-rtvi",event_type:s??"unknown"})}},[me,g,le,pe,r]),be=e.useRef(()=>{}),he=e.useRef(async()=>{}),ke=e.useCallback(e=>{const t=()=>e===ee.current;return{onConnected:()=>{if(!t())return;n.logger.info({message:"Voice RTVI connected",api_name:"voice-rtvi",conversation_id:r??void 0}),g(m(s.CONNECTED)),g(I(!0)),g(v(o.IDLE)),$.current&&$.current.isMicEnabled!==z.current&&(n.logger.debug({message:"Voice re-asserting mic state on reconnect",api_name:"voice-rtvi",conversation_id:r??void 0,enabled:z.current}),$.current.enableMic(z.current));const{selectedMicId:e,selectedSpeakerId:i}=p.getState().voice;e&&$.current?.updateMic(e),i&&$.current?.updateSpeaker(i);const a=e=>t=>n.logger.warn({message:"Voice device enumeration failed",api_name:"voice-mic",conversation_id:r??void 0,device_kind:e,error_message:t instanceof Error?t.message:String(t)});$.current?.getAllMics().then(e=>{t()&&g(x(e.map(e=>({deviceId:e.deviceId,label:e.label}))))}).catch(a("mics")),$.current?.getAllSpeakers().then(e=>{t()&&g(y(e.map(e=>({deviceId:e.deviceId,label:e.label}))))}).catch(a("speakers")),Q.current=!0},onBotReady:e=>{if(!t())return;const s=e?.about;K.current=s?.pipeline_mode===i.REALTIME,W.current=!0===s?.tts_word_timestamps,ie.current=0,ae.current=0,ne.current=0,n.logger.info({message:"Voice RTVI bot-ready",api_name:"voice-rtvi",conversation_id:r??void 0,pipeline_mode:s?.pipeline_mode??"cascade",tts_word_timestamps:W.current,assistant_starts:d}),ve(),d&&(g(T(!0)),le())},onDisconnected:()=>{if(!t())return;n.logger.info({message:"Voice RTVI disconnected",api_name:"voice-rtvi",conversation_id:r??void 0,drop_outcome:ce.current??"transient",reconnect_attempts:ie.current}),_e(!0);const e=te.current,s=ce.current;ce.current=null;const o=ue.current;return ue.current=null,o?(n.logger.info({message:"Voice session ended by server — no reconnect",api_name:"voice-rtvi",conversation_id:r??void 0,reason:o.reason}),"idle_timeout"===o.reason&&n.showToast({title:"Voice ended",description:"It got quiet for a while, so we wrapped up. Start again anytime."}),void g(M())):"terminal"===s?(n.logger.warn({message:"Voice RTVI terminal error — no retry",api_name:"voice-rtvi"}),void g(M())):"auth"===s?ae.current>=1||null==r?(n.logger.warn({message:"Voice RTVI auth refresh budget exhausted — giving up",api_name:"voice-rtvi",conversation_id:r??void 0}),void g(M())):(ae.current+=1,n.logger.info({message:"Voice RTVI auth drop — refreshing token then reconnecting",api_name:"voice-rtvi",conversation_id:r??void 0,auth_refresh_count:ae.current}),void(async()=>{let t=!1;try{t=!!await(n.getTokenRefreshHandler()?.())}catch{}e===te.current&&(t?he.current():(n.logger.warn({message:"Voice RTVI token refresh failed — no reconnect",api_name:"voice-rtvi",conversation_id:r??void 0}),g(M())))})()):ie.current>=3||null==r?(ie.current>=3&&(n.logger.warn({message:"Voice RTVI auto-reconnect cap reached — giving up",api_name:"voice-rtvi",conversation_id:r??void 0,reconnect_attempts:ie.current}),n.showErrorToast("Voice connection lost. Please try again.")),void g(M())):(ie.current+=1,n.logger.info({message:"Voice RTVI scheduling auto-reconnect",api_name:"voice-rtvi",conversation_id:r??void 0,reconnect_attempts:ie.current,delay_ms:1e3}),oe.current&&clearTimeout(oe.current),void(oe.current=setTimeout(()=>{oe.current=null,he.current()},1e3)))},onError:e=>{if(!t())return;const s=e?.data,i=s?.error??s?.message??"Voice error";n.handleSharedEvent({kind:"error",error:i},me),g(v(o.IDLE)),pe(n.INDICATOR_TIMER_ACTIONS.CANCEL),s?.fatal&&(c.some(e=>e.test(i))?ce.current="auth":u.some(e=>e.test(i))&&(ce.current="terminal"),n.logger.warn({message:"Voice RTVI fatal error — closing transport",api_name:"voice-rtvi",conversation_id:r??void 0,drop_outcome:ce.current??"generic"}),$.current?.disconnect().catch(()=>{}))},onTrackStarted:(e,s)=>{t()&&(s?.local||"audio"!==e.kind||(n.logger.debug({message:"Voice RTVI bot audio track started",api_name:"voice-rtvi",conversation_id:r??void 0}),Y.current?.(),Y.current=N.attachBotAudioTrack(e)))},onAvailableMicsUpdated:e=>{t()&&(g(x(e.map(e=>({deviceId:e.deviceId,label:e.label})))),Q.current&&(J.current&&clearTimeout(J.current),J.current=setTimeout(()=>{if(J.current=null,!$.current)return;const{selectedMicId:e,microphones:t}=p.getState().voice;n.logger.info({message:"Voice audio devices changed",api_name:"voice-mic",microphones:t.map(e=>e.label).join(", ")}),n.logger.debug({message:"Voice re-acquiring mic (device change)",api_name:"voice-rtvi",enabled:z.current,device:e||"default"}),$.current.updateMic(e)},300)))},onAvailableSpeakersUpdated:e=>{if(!t())return;if(g(y(e.map(e=>({deviceId:e.deviceId,label:e.label})))),!Q.current)return;const r=e.map(e=>e.label).join(", ");r!==X.current&&(X.current=r,n.logger.info({message:"Voice audio output devices changed",api_name:"voice-mic",speakers:r}))},onMicUpdated:e=>{if(!t())return;const s=$.current;s&&s.isMicEnabled!==z.current&&(n.logger.debug({message:"Voice re-asserting mic state after track swap",api_name:"voice-rtvi",conversation_id:r??void 0,enabled:z.current,device:e?.label}),s.enableMic(z.current))},onUserMuteStarted:()=>{t()&&n.logger.debug({message:"Voice RTVI user-mute-started (ignored)",api_name:"voice-rtvi",conversation_id:r??void 0})},onUserMuteStopped:()=>{t()&&(n.logger.debug({message:"Voice RTVI user-mute-stopped",api_name:"voice-rtvi",conversation_id:r??void 0,honored:d}),d&&g(E(!1)))},onUserStartedSpeaking:()=>{t()&&(n.logger.debug({message:"Voice RTVI user started speaking",api_name:"voice-rtvi",conversation_id:r??void 0}),j.current=null,H.current="",g(C(null)),g(v(o.LISTENING)))},onUserStoppedSpeaking:()=>{if(!t())return;n.logger.debug({message:"Voice RTVI user stopped speaking",api_name:"voice-rtvi",conversation_id:r??void 0,has_interim:!!H.current.trim()}),g(v(o.PROCESSING)),g(T(!0)),g(_(null)),le();const e=H.current.trim();if(e){const t=`${G}${n.generateUniqueId()}`;j.current=t,g(C(null)),A.current.setMessages(r=>[...r,U(e,t)])}},onUserTranscript:e=>{if(!t())return;if(!e.final)return void(e.text&&(H.current=e.text,K.current||g(C(e.text))));g(C(null));const s=e.text&&e.text.trim()?e.text:"";n.logger.debug({message:"Voice RTVI user transcript (final)",api_name:"voice-rtvi",conversation_id:r??void 0,has_text:!!s});const o=j.current;j.current=null,o?s&&A.current.setMessages(e=>e.map(e=>e.id===o?{...e,message:s}:e)):s&&A.current.setMessages(e=>[...e,U(s)])},onBotStartedSpeaking:()=>{t()&&(n.logger.debug({message:"Voice RTVI bot started speaking",api_name:"voice-rtvi",conversation_id:r??void 0}),Te(),g(v(o.SPEAKING)),pe(n.INDICATOR_TIMER_ACTIONS.FINALIZE))},onBotOutput:e=>{t()&&fe(e)},onBotStoppedSpeaking:()=>{t()&&(n.logger.debug({message:"Voice RTVI bot stopped speaking",api_name:"voice-rtvi",conversation_id:r??void 0}),Ee(),g(v(o.IDLE)),pe(n.INDICATOR_TIMER_ACTIONS.FINALIZE),d&&p.getState().voice.serverMuted&&(n.logger.warn({message:"Voice greeting mute-hold released on bot-turn end (server unmute missing)",api_name:"voice-rtvi",conversation_id:r??void 0}),g(E(!1))))},onServerMessage:e=>{t()&&Ce(e)}}},[g,p,N,ve,_e,pe,le,fe,Te,Ee,Ce,me,d,r]),Re=e.useCallback(async()=>{if($.current||re.current)return;if(!r)return;if(re.current=!0,n.logger.info({message:"Voice RTVI connecting",api_name:"voice-rtvi",conversation_id:r,assistant_starts:d}),d&&(z.current=!1,g(E(!0)),n.logger.debug({message:"Voice greeting mute-hold armed",api_name:"voice-rtvi",conversation_id:r})),n.isTokenAboutToExpire(n.readTokenExpiresAt())&&ne.current<1){ne.current+=1,n.logger.debug({message:"Voice pre-connect token refresh (freshness gate)",api_name:"voice-rtvi",conversation_id:r,freshness_refresh_count:ne.current});const e=te.current;try{await Promise.resolve(n.getTokenRefreshHandler()?.())}catch{}if(e!==te.current)return}const e=++ee.current,t=te.current;ue.current=null,ce.current=null,g(m(s.CONNECTING));const o=()=>t!==te.current||e!==ee.current;try{const[{PipecatClient:t,LogLevel:s},i]=await Promise.all([Promise.resolve().then(()=>require("./index.module-DCaqEXL5.cjs")),L(r)]);if(o())return;const a=new t({transport:l(),enableMic:!0,enableCam:!1,callbacks:ke(e)});if(a.setLogLevel(s.WARN),o())return void a.disconnect().catch(()=>{});$.current=a,n.logger.debug({message:"Voice RTVI client constructed",api_name:"voice-rtvi",conversation_id:r,enable_mic:z.current}),ve(),Z.current=setTimeout(()=>{n.logger.warn({message:"Voice RTVI connect timed out (no bot-ready)",api_name:"voice-rtvi"}),n.showErrorToast("Voice connection timed out. Please try again."),g(M()),be.current()},15e3),await a.connect({webrtcRequestParams:{endpoint:n.API_ENDPOINTS.WEBRTC_OFFER(r),headers:new Headers(P())},iceConfig:{iceServers:i}})}catch(t){n.logger.error({message:"Voice RTVI connect failed",api_name:"voice-rtvi",error_message:t instanceof Error?t.message:String(t)}),e===ee.current&&(n.showErrorToast("Could not connect voice. Please try again."),_e(!1))}},[r,g,l,ke,ve,_e,d]),Ve=e.useCallback((e=!0)=>{n.logger.info({message:"Voice RTVI disconnect (intentional teardown)",api_name:"voice-rtvi",conversation_id:r??void 0,commit_turn:e}),te.current+=1,ee.current+=1,ne.current=0,oe.current&&(clearTimeout(oe.current),oe.current=null),ie.current=0,ae.current=0,ce.current=null,z.current=!0,g(S(!1));const t=$.current;t?.disconnect().catch(e=>{n.logger.warn({message:"Voice RTVI disconnect threw",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}),_e(e)},[_e,g,r]),xe=e.useCallback(e=>{z.current=e,$.current&&(n.logger.debug({message:"Voice mic enable toggled",api_name:"voice-rtvi",enabled:e}),$.current.enableMic(e))},[]),ye=e.useCallback(e=>{g(R(e)),$.current?.updateMic(e)},[g]),Me=e.useCallback(e=>{g(V(e)),$.current?.updateSpeaker(e)},[g]),Ne=e.useCallback(e=>{const t=$.current;if(t){n.logger.info({message:"Voice RTVI send-text",api_name:"voice-rtvi",conversation_id:r??void 0,run_immediately:!0});try{t.sendText(e,{run_immediately:!0,audio_response:!0})}catch(e){return void n.logger.error({message:"Voice RTVI send-text failed",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}D.current&&Ee(),A.current.setMessages(t=>[...t,U(e)]),g(T(!0)),g(_(null)),le(),g(v(o.PROCESSING))}},[g,le,Ee,r]);e.useEffect(()=>{be.current=Ve},[Ve]),e.useEffect(()=>{he.current=Re},[Re]);const we=e.useRef(r),Ae=e.useRef(!1);e.useEffect(()=>{const e=we.current;if(we.current=r,e===r)return;const t=re.current;t&&(Ae.current=!0),t&&Ve(!1),Ae.current&&null!=r&&(Ae.current=!1,Re())},[r,Ve,Re]);const De=e.useRef(w);return e.useEffect(()=>{const e=De.current;De.current=w,e&&!w&&re.current&&Ve(!1)},[w,Ve]),e.useEffect(()=>()=>{be.current()},[]),{connect:Re,disconnect:Ve,sendText:Ne,setMicEnabled:xe,setInputDevice:ye,setOutputDevice:Me}}(r,a,l,d);e.useEffect(()=>{H(!N&&!w)},[N,w,H]);const Z=q();e.useEffect(()=>(a.setVoiceSendText(j),()=>a.setVoiceSendText(null)),[a,j]);const z=e.useRef(!1);return{displayState:Z,pipelineStatus:A,isAgentWorking:D,enterVoiceMode:e.useCallback(async()=>{if(z.current)return n.showErrorToast("Microphone permission changed — please reload the page to apply the new setting."),!1;const e=await p.requestMicPermission();if(!e.granted){const t=e.error;return n.logger.error({message:"Mic permission denied",api_name:"voice-mic",error_message:t instanceof Error?t.message:t?String(t):void 0}),z.current=!0,n.showErrorToast("Microphone access is required for voice mode. Please allow it in your browser's site settings and reload the page."),!1}return K(),!0},[p,K]),exitVoiceMode:e.useCallback(()=>{W()},[W]),toggleMute:e.useCallback(()=>{g(S(!N))},[g,N]),sendText:j,muted:N,serverMuted:w,sessionEndedSignal:F,setInputDevice:$,setOutputDevice:Y}},exports.useVoiceUI=()=>e.useContext(K),exports.voiceListenerMiddleware=W,exports.voiceReducer=A;
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createContext as e,useContext as t,useRef as r,useEffect as n,useCallback as s,useState as c}from"react";import{useDispatch as a,useSelector as i}from"react-redux";import{createSlice as o,createListenerMiddleware as u,isAnyOf as l}from"@reduxjs/toolkit";import{API_ENDPOINTS as d,logger as p,getCustomHeaders as m,readAccessToken as g,AUTH_HEADERS as E,readDeviceToken as S,DEVICE_TOKEN_HEADER as I,clearLastResponseTime as v,setLastResponseTime as T,INDICATOR_TIMER_ACTIONS as f,generateUniqueId as y,handleSharedEvent as C,getTokenRefreshHandler as h,showErrorToast as k,isTokenAboutToExpire as b,readTokenExpiresAt as M,getStorageRef as N,clearSession as _,sessionExpired as w}from"@1interface/shared-core";const D={DISCONNECTED:"disconnected",CONNECTING:"connecting",CONNECTED:"connected"},x={IDLE:"idle",LISTENING:"listening",PROCESSING:"processing",SPEAKING:"speaking"},L={CASCADE:"cascade",REALTIME:"realtime"},R={OFFLINE:"offline",CONNECTING:"connecting",READY:"ready",IDLE:"idle",LISTENING:"listening",MUTED:"muted",PROCESSING:"processing",SPEAKING:"speaking"},P=15e3,A=[/rejected/i,/unauthori[sz]ed/i,/forbidden/i,/\b40[13]\b/],V=[/\b402\b/,/\b429\b/,/rate[\s-]?limit/i],O={SELECTED_MIC_ID:"voice_selectedMicId",SELECTED_SPEAKER_ID:"voice_selectedSpeakerId"},G={offline:{opacity:.15,playbackRate:.3,scale:1},connecting:{opacity:.4,playbackRate:.5,scale:1},ready:{opacity:.4,playbackRate:.4,scale:1},idle:{opacity:.6,playbackRate:.6,scale:1},listening:{opacity:.7,playbackRate:.7,scale:1},muted:{opacity:.7,playbackRate:.5,scale:1},processing:{opacity:1,playbackRate:1,scale:1},speaking:{opacity:1,playbackRate:1,scale:1.3}};function B(){return{connectionStatus:D.DISCONNECTED,pipelineStatus:x.IDLE,listeningEnabled:!1,muted:!1,serverMuted:!1,statusText:null,isAgentWorking:!1,responseStartTime:null,interimTranscript:null,botTurn:{active:!1,chatMsgId:null,segments:[]},selectedMicId:"",selectedSpeakerId:""}}const F=e=>async t=>{const[r,n]=await Promise.all([e.getItem(O.SELECTED_MIC_ID),e.getItem(O.SELECTED_SPEAKER_ID)]);r&&t(W.actions.setSelectedMicId(r)),n&&t(W.actions.setSelectedSpeakerId(n))},W=o({name:"voice",initialState:B(),reducers:{setConnectionStatus(e,t){e.connectionStatus=t.payload},setPipelineStatus(e,t){e.pipelineStatus=t.payload},setListeningEnabled(e,t){e.listeningEnabled=t.payload},setStatusText(e,t){e.statusText=t.payload},setIsAgentWorking(e,t){e.isAgentWorking=t.payload},setResponseStartTime(e,t){e.responseStartTime=t.payload},setInterimTranscript(e,t){e.interimTranscript=t.payload},openBotTurn(e,t){e.botTurn={active:!0,chatMsgId:t.payload?.chatMsgId??null,segments:[]}},upsertBotSegment(e,t){const{id:r,text:n,spokenChars:s}=t.payload;e.botTurn.active=!0;const c=e.botTurn.segments.find(e=>e.id===r);c?(c.text=n,c.spokenChars=Math.max(c.spokenChars,s)):(e.botTurn.segments.push({id:r,text:n,spokenChars:s}),e.botTurn.segments.sort((e,t)=>e.id-t.id))},clearBotTurn(e){e.botTurn={active:!1,chatMsgId:null,segments:[]}},setMuted(e,t){e.muted=t.payload},setServerMuted(e,t){e.serverMuted=t.payload},setSelectedMicId(e,t){e.selectedMicId=t.payload},setSelectedSpeakerId(e,t){e.selectedSpeakerId=t.payload},resetVoiceState:()=>B(),resetIndicatorState(e){e.statusText=null,e.isAgentWorking=!1,e.responseStartTime=null,e.interimTranscript=null,e.serverMuted=!1,e.botTurn={active:!1,chatMsgId:null,segments:[]}}}}),{setConnectionStatus:K,setPipelineStatus:U,setListeningEnabled:j,setMuted:$,setServerMuted:q,setStatusText:Z,setIsAgentWorking:H,setResponseStartTime:Y,setInterimTranscript:z,openBotTurn:J,upsertBotSegment:Q,clearBotTurn:X,setSelectedMicId:ee,setSelectedSpeakerId:te,resetVoiceState:re,resetIndicatorState:ne}=W.actions,se=W.reducer,ce=e(null),ae=()=>{const e=t(ce);if(!e)throw new Error("useAudioPlatform must be used inside <AudioPlatformProvider>. <VoiceProvider> mounts one by default; if you replaced VoiceProvider, you must wire your own.");return e};function ie(){const e={};for(const[t,r]of Object.entries(m()))r&&(e[t]=r);const t=g();t&&(e[E.ACCESS_TOKEN]=t);const r=S();return r&&(e[I]=r),e}async function oe(e){try{const t=await fetch(d.WEBRTC_ICE(e),{headers:ie()});return t.ok?(await t.json()).iceServers??[]:(p.warn({message:"Voice ICE fetch failed — continuing without TURN",api_name:"voice-webrtc-ice",status:t.status}),[])}catch(e){return p.warn({message:"Voice ICE fetch error — continuing without TURN",api_name:"voice-webrtc-ice",error_message:e instanceof Error?e.message:String(e)}),[]}}const ue="voice-user-",le=(e,t=`${ue}${y()}`)=>({id:t,sender:"user",message:e,timestamp:(new Date).toISOString()}),de=(e,t)=>{const r=`${e}${y()}`;return{id:r,msg_id:r,sender:"agent",message:"",timestamp:(new Date).toISOString(),...t}};function pe(){const e=i(e=>e.voice.connectionStatus),t=i(e=>e.voice.pipelineStatus),r=i(e=>e.voice.listeningEnabled),n=i(e=>e.voice.muted);return e===D.CONNECTING?R.CONNECTING:e!==D.CONNECTED?R.OFFLINE:r?n?R.MUTED:t===x.PROCESSING?R.PROCESSING:t===x.SPEAKING?R.SPEAKING:t===x.LISTENING?R.LISTENING:R.IDLE:R.READY}const me=e(null),ge=({conversationId:e,voiceBridge:t,transportFactory:o,assistantStartsConversation:u})=>{const l=a(),m=ae(),g=i(e=>e.voice.muted),E=i(e=>e.voice.serverMuted),S=i(e=>e.voice.pipelineStatus),I=i(e=>e.voice.isAgentWorking),{connect:N,disconnect:_,sendText:w,setMicEnabled:R}=function(e,t,o,u){const l=a(),m=ae(),g=i(e=>e.auth.accessToken),E=r(t);n(()=>{E.current=t});const S=r(null),I=r(new Map),N=r(""),_=r(!1),w=r(!1),R=r(null),P=r(""),O=r(null),G=r(null),B=r(null),F=r(0),W=r(0),$=r(!1),ee=r(0),te=r(null),re=r(null),ne=r(0),se=r(0),ce=r(null),pe=s(()=>{if(null!=te.current)return;const e=Date.now();te.current=e,l(Y(e)),l(v())},[l]),me=s(()=>{const e=te.current;te.current=null,l(Y(null)),null!=e&&l(T(Date.now()-e))},[l]),ge=s(()=>{te.current=null,l(Y(null))},[l]),Ee=s((e=f.KEEP)=>{l(Z(null)),l(H(!1)),e===f.FINALIZE?me():e===f.CANCEL&&ge()},[l,me,ge]),[Se]=c(()=>({setStatusText:e=>l(Z(e||null)),setIsAgentWorking:e=>l(H(e)),appendStreamText:()=>{},pushWidget:e=>{const t=de("voice-widget-",{widget:[e],message_type:["widget"]});E.current.setMessages(e=>[...e,t])},onPayment:e=>E.current.onPaymentInit?.(e),onConversationTitle:e=>E.current.onConversationTitle?.(e),onError:e=>{p.error({message:"Voice RTVI server error",api_name:"voice-rtvi",error_message:e});const t=de("voice-error-",{message:e,message_type:["error"]});E.current.setMessages(e=>[...e,t])},onStatusActive:()=>{pe()}})),Ie=s(()=>{B.current&&(clearTimeout(B.current),B.current=null)},[]),ve=s(()=>[...I.current.entries()].sort((e,t)=>e[0]-t[0]).map(([,e])=>e.text).join(" ").trim(),[]),Te=s(()=>{S.current=null,I.current=new Map,N.current="",l(X())},[l]),fe=s(()=>{const e=S.current;if(e){const t=ve();t&&E.current.updateStreamingMessage(e,t),E.current.commitStreamingMessages()}Te()},[ve,Te]),ye=s(e=>{Ie(),G.current?.(),G.current=null,O.current=null,$.current=!1,l(j(!1)),l(U(x.IDLE)),l(K(D.DISCONNECTED)),l(z(null)),l(q(!1)),R.current=null,P.current="",_.current=!1,w.current=!1,e?fe():(E.current.discardStreamingMessages(),Te()),Ee(f.CANCEL)},[l,Ee,Ie,fe,Te]),Ce=s(()=>{if(S.current)return;const e=`voice-agent-${y()}`;S.current=e,I.current=new Map,N.current="",l(J({chatMsgId:e}))},[l]),he=s(e=>{const t=e.spoken_progress?(e.spoken_progress?.accumulated_text??"")+(e.spoken_progress?.remaining_text??""):e.text??"";if(!t)return;Ce();const r=_.current?function(e,t){const r=(e.get(0)?.text??"")+t;return e.set(0,{text:r,completed:!1}),{id:0,text:r,spokenChars:r.length}}(I.current,t):function(e,t,r,n){const s=t.segment_id??0,c="completed"===t.spoken_status;if(c&&!e.has(s)){const t=[...e.entries()].find(([,e])=>e.text===r&&!e.completed);if(t)return t[1].completed=!0,{id:t[0],text:r,spokenChars:r.length}}const a=e.get(s);return e.set(s,{text:r,completed:c||!0===a?.completed}),{id:s,text:r,spokenChars:n&&t.spoken_progress?(t.spoken_progress.accumulated_text??"").length:r.length}}(I.current,e,t,w.current);l(Q(r));const n=ve();n&&S.current&&n!==N.current&&(N.current=n,E.current.updateStreamingMessage(S.current,n))},[l,Ce,ve]),ke=s(e=>{const t=e,r=t?.type,n=t?.data;switch(r){case"status_update":C({kind:"status_update",statusText:n?.text??""},Se);break;case"started-thinking":l(H(!0)),pe();break;case"stopped-thinking":Ee(f.FINALIZE);break;case"clear_status":break;case"widget":n&&C({kind:"widget",widget:n},Se);break;case"payment":n&&C({kind:"payment",payment:n},Se);break;case"conversation_title":{const e="string"==typeof n?.text?n.text.trim():"";e&&C({kind:"conversation_title",title:e},Se);break}default:p.debug({message:"Voice RTVI unhandled server-message",api_name:"voice-rtvi",event_type:r??"unknown"})}},[Se,l,pe,Ee]),be=r(()=>{}),Me=r(async()=>{}),Ne=s(t=>{const r=()=>t===F.current;return{onConnected:()=>{r()&&(ne.current=0,se.current=0,ee.current=0,l(K(D.CONNECTED)),l(j(!0)),l(U(x.IDLE)))},onBotReady:e=>{if(!r())return;const t=e?.about;_.current=t?.pipeline_mode===L.REALTIME,w.current=!0===t?.tts_word_timestamps,Ie(),u&&(l(H(!0)),pe())},onDisconnected:()=>{if(!r())return;ye(!0);const t=W.current,n=ce.current;if(ce.current=null,"terminal"!==n){if("auth"===n){if(se.current>=1||null==e)return;return se.current+=1,void(async()=>{let e=!1;try{e=!!await(h()?.())}catch{}t===W.current&&e&&Me.current()})()}ne.current>=3||null==e?ne.current>=3&&k("Voice connection lost. Please try again."):(ne.current+=1,re.current&&clearTimeout(re.current),re.current=setTimeout(()=>{re.current=null,Me.current()},1e3))}else p.warn({message:"Voice RTVI terminal error — no retry",api_name:"voice-rtvi"})},onError:e=>{if(!r())return;const t=e?.data,n=t?.error??t?.message??"Voice error";C({kind:"error",error:n},Se),l(U(x.IDLE)),Ee(f.CANCEL),t?.fatal&&(A.some(e=>e.test(n))?ce.current="auth":V.some(e=>e.test(n))&&(ce.current="terminal"),O.current?.disconnect().catch(()=>{}))},onTrackStarted:(e,t)=>{r()&&(t?.local||"audio"!==e.kind||(G.current?.(),G.current=m.attachBotAudioTrack(e)))},onUserMuteStarted:()=>{r()&&l(q(!0))},onUserMuteStopped:()=>{r()&&l(q(!1))},onUserStartedSpeaking:()=>{r()&&(R.current=null,P.current="",l(z(null)),l(U(x.LISTENING)))},onUserStoppedSpeaking:()=>{if(!r())return;l(U(x.PROCESSING)),l(H(!0)),l(Z(null)),pe();const e=P.current.trim();if(e){const t=`${ue}${y()}`;R.current=t,l(z(null)),E.current.setMessages(r=>[...r,le(e,t)])}},onUserTranscript:e=>{if(!r())return;if(!e.final)return void(e.text&&(P.current=e.text,_.current||l(z(e.text))));l(z(null));const t=e.text&&e.text.trim()?e.text:"",n=R.current;R.current=null,n?t&&E.current.setMessages(e=>e.map(e=>e.id===n?{...e,message:t}:e)):t&&E.current.setMessages(e=>[...e,le(t)])},onBotStartedSpeaking:()=>{r()&&(Ce(),l(U(x.SPEAKING)),Ee(f.FINALIZE))},onBotOutput:e=>{r()&&he(e)},onBotStoppedSpeaking:()=>{r()&&(fe(),l(U(x.IDLE)),Ee(f.FINALIZE))},onServerMessage:e=>{r()&&ke(e)}}},[l,m,Ie,ye,Ee,pe,he,Ce,fe,ke,Se,u,e]),_e=s(async()=>{if(O.current||$.current)return;if(!e)return;if($.current=!0,b(M())&&ee.current<1){ee.current+=1;const e=W.current;try{await Promise.resolve(h()?.())}catch{}if(e!==W.current)return}const t=++F.current,r=W.current;l(K(D.CONNECTING));const n=()=>r!==W.current||t!==F.current;try{const[{PipecatClient:r,LogLevel:s},c]=await Promise.all([import("./index.module-S2alA3-r.js"),oe(e)]);if(n())return;const a=new r({transport:o(),enableMic:!0,enableCam:!1,callbacks:Ne(t)});if(a.setLogLevel(s.WARN),n())return void a.disconnect().catch(()=>{});O.current=a,Ie(),B.current=setTimeout(()=>{p.warn({message:"Voice RTVI connect timed out (no bot-ready)",api_name:"voice-rtvi"}),k("Voice connection timed out. Please try again."),be.current()},15e3),await a.connect({webrtcRequestParams:{endpoint:d.WEBRTC_OFFER(e),headers:new Headers(ie())},iceConfig:{iceServers:c}})}catch(e){p.error({message:"Voice RTVI connect failed",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)}),t===F.current&&(k("Could not connect voice. Please try again."),ye(!1))}},[e,l,o,Ne,Ie,ye]),we=s((e=!0)=>{W.current+=1,F.current+=1,ee.current=0,re.current&&(clearTimeout(re.current),re.current=null),ne.current=0,se.current=0,ce.current=null;const t=O.current;t?.disconnect().catch(e=>{p.warn({message:"Voice RTVI disconnect threw",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}),ye(e)},[ye]),De=s(e=>{O.current?.enableMic(e)},[]),xe=s(e=>{const t=O.current;if(t){try{t.sendText(e,{run_immediately:!0,audio_response:!0})}catch(e){return void p.error({message:"Voice RTVI send-text failed",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}S.current&&fe(),E.current.setMessages(t=>[...t,le(e)]),l(H(!0)),l(Z(null)),pe(),l(U(x.PROCESSING))}},[l,pe,fe]);n(()=>{be.current=we},[we]),n(()=>{Me.current=_e},[_e]);const Le=r(e),Re=r(!1);n(()=>{const t=Le.current;if(Le.current=e,t===e)return;const r=$.current;r&&(Re.current=!0),r&&we(!1),Re.current&&null!=e&&(Re.current=!1,_e())},[e,we,_e]);const Pe=r(g);return n(()=>{const e=Pe.current;Pe.current=g,e&&!g&&$.current&&we(!1)},[g,we]),n(()=>()=>{be.current()},[]),{connect:_e,disconnect:we,sendText:xe,setMicEnabled:De}}(e,t,o,u);n(()=>{R(!g&&!E)},[g,E,R]);const P=pe();n(()=>(t.setVoiceSendText(w),()=>t.setVoiceSendText(null)),[t,w]);const O=r(!1);return{displayState:P,pipelineStatus:S,isAgentWorking:I,enterVoiceMode:s(async()=>{if(O.current)return k("Microphone permission changed — please reload the page to apply the new setting."),!1;const e=await m.requestMicPermission();if(!e.granted){const t=e.error;return p.error({message:"Mic permission denied",api_name:"voice-mic",error_message:t instanceof Error?t.message:t?String(t):void 0}),O.current=!0,k("Microphone access is required for voice mode. Please allow it in your browser's site settings and reload the page."),!1}return N(),!0},[m,N]),exitVoiceMode:s(()=>{_()},[_]),toggleMute:s(()=>{l($(!g))},[l,g]),sendText:w,muted:g,serverMuted:E}},Ee=()=>{const e=t(me);if(!e)throw new Error("useVoice must be used within VoiceProvider");return e},Se=e({strategyActive:!1,voiceSlot:null,onEnterVoice:null,onFloatingOrbClick:null,voiceInputHidden:!1,headerCenterSlot:null,floatOrbPositionRef:null}),Ie=()=>t(Se);function ve(){const e=ae(),[t,r]=c([]),[a,o]=c([]),u=i(e=>e.voice.listeningEnabled),l=s(async()=>{const t=await e.devices.enumerate();r(t.microphones),o(t.speakers)},[e]);return n(()=>(l(),e.devices.onChange(l)),[e,l]),n(()=>{u&&l()},[u,l]),{microphones:t,speakers:a,refresh:l}}const Te=u();Te.startListening({actionCreator:ee,effect:async e=>{const t=N();t&&await t.setItem(O.SELECTED_MIC_ID,e.payload)}}),Te.startListening({actionCreator:te,effect:async e=>{const t=N();t&&await t.setItem(O.SELECTED_SPEAKER_ID,e.payload)}}),Te.startListening({matcher:l(_,w),effect:async(e,t)=>{const r=N();r&&await Promise.all([r.removeItem(O.SELECTED_MIC_ID),r.removeItem(O.SELECTED_SPEAKER_ID)]),t.dispatch(re())}});class fe{async enumerate(){const e={microphones:[],speakers:[]};if(!navigator.mediaDevices?.enumerateDevices)return e;try{const e=await navigator.mediaDevices.enumerateDevices();let t=0,r=0;return{microphones:e.filter(e=>"audioinput"===e.kind).map(e=>({deviceId:e.deviceId,label:e.label||"Microphone "+ ++t})),speakers:e.filter(e=>"audiooutput"===e.kind).map(e=>({deviceId:e.deviceId,label:e.label||"Speaker "+ ++r}))}}catch{return e}}onChange(e){const t=navigator.mediaDevices;return t?.addEventListener?(t.addEventListener("devicechange",e),()=>t.removeEventListener("devicechange",e)):()=>{}}}const ye=e=>{const t=document.createElement("audio");return t.autoplay=!0,t.style.display="none",t.srcObject=new MediaStream([e]),document.body.appendChild(t),()=>{try{t.pause(),t.srcObject=null,t.remove()}catch{}}},Ce={attachBotAudioTrack:ye,devices:new fe,requestMicPermission:async function(){try{return(await navigator.mediaDevices.getUserMedia({audio:!0})).getTracks().forEach(e=>e.stop()),{granted:!0}}catch(e){return{granted:!1,error:e}}}};export{ce as AudioPlatformContext,fe as BrowserAudioDevices,G as ORB_STATE_CONFIG,D as VOICE_CONNECTION_STATUSES,P as VOICE_CONNECT_TIMEOUT_MS,R as VOICE_DISPLAY_STATES,L as VOICE_PIPELINE_MODES,x as VOICE_PIPELINE_STATUSES,O as VOICE_STORAGE_KEYS,me as VoiceContext,Se as VoiceUIContext,ye as attachBrowserBotAudioTrack,Ce as browserAudioPlatform,X as clearBotTurn,F as hydrateVoice,J as openBotTurn,ne as resetIndicatorState,K as setConnectionStatus,z as setInterimTranscript,H as setIsAgentWorking,j as setListeningEnabled,$ as setMuted,U as setPipelineStatus,Y as setResponseStartTime,ee as setSelectedMicId,te as setSelectedSpeakerId,q as setServerMuted,Z as setStatusText,Q as upsertBotSegment,ae as useAudioPlatform,ve as useMediaDevices,Ee as useVoice,pe as useVoiceDisplayState,ge as useVoiceProviderValue,Ie as useVoiceUI,Te as voiceListenerMiddleware,se as voiceReducer};
|
|
1
|
+
import{createContext as e,useContext as t,useRef as n,useEffect as r,useCallback as i,useState as s}from"react";import{useDispatch as c,useStore as a,useSelector as o}from"react-redux";import{createSlice as u,createListenerMiddleware as d,isAnyOf as l}from"@reduxjs/toolkit";import{API_ENDPOINTS as m,logger as p,getCustomHeaders as g,readAccessToken as v,AUTH_HEADERS as _,readDeviceToken as I,DEVICE_TOKEN_HEADER as S,clearLastResponseTime as E,setLastResponseTime as T,INDICATOR_TIMER_ACTIONS as f,generateUniqueId as b,handleSharedEvent as h,showToast as k,getTokenRefreshHandler as y,showErrorToast as V,isTokenAboutToExpire as C,readTokenExpiresAt as M,getStorageRef as w,clearSession as R,sessionExpired as N}from"@1interface/shared-core";const D={DISCONNECTED:"disconnected",CONNECTING:"connecting",CONNECTED:"connected"},x={IDLE:"idle",LISTENING:"listening",PROCESSING:"processing",SPEAKING:"speaking"},A={CASCADE:"cascade",REALTIME:"realtime"},L={OFFLINE:"offline",CONNECTING:"connecting",READY:"ready",IDLE:"idle",LISTENING:"listening",MUTED:"muted",PROCESSING:"processing",SPEAKING:"speaking"},P=15e3,O=[/rejected/i,/unauthori[sz]ed/i,/forbidden/i,/\b40[13]\b/],G=[/\b402\b/,/\b429\b/,/rate[\s-]?limit/i],B={SELECTED_MIC_ID:"voice_selectedMicId",SELECTED_SPEAKER_ID:"voice_selectedSpeakerId"},U={offline:{opacity:.15,playbackRate:.3,scale:1},connecting:{opacity:.4,playbackRate:.5,scale:1},ready:{opacity:.4,playbackRate:.4,scale:1},idle:{opacity:.6,playbackRate:.6,scale:1},listening:{opacity:.7,playbackRate:.7,scale:1},muted:{opacity:.7,playbackRate:.5,scale:1},processing:{opacity:1,playbackRate:1,scale:1},speaking:{opacity:1,playbackRate:1,scale:1.3}};function F(){return{connectionStatus:D.DISCONNECTED,pipelineStatus:x.IDLE,listeningEnabled:!1,muted:!1,serverMuted:!1,statusText:null,isAgentWorking:!1,responseStartTime:null,interimTranscript:null,botTurn:{active:!1,chatMsgId:null,segments:[]},sessionEndedSignal:0,selectedMicId:"",selectedSpeakerId:"",microphones:[],speakers:[]}}const W=e=>async t=>{const[n,r]=await Promise.all([e.getItem(B.SELECTED_MIC_ID),e.getItem(B.SELECTED_SPEAKER_ID)]);n&&t(K.actions.setSelectedMicId(n)),r&&t(K.actions.setSelectedSpeakerId(r))},K=u({name:"voice",initialState:F(),reducers:{setConnectionStatus(e,t){e.connectionStatus=t.payload},setPipelineStatus(e,t){e.pipelineStatus=t.payload},setListeningEnabled(e,t){e.listeningEnabled=t.payload},setStatusText(e,t){e.statusText=t.payload},setIsAgentWorking(e,t){e.isAgentWorking=t.payload},setResponseStartTime(e,t){e.responseStartTime=t.payload},setInterimTranscript(e,t){e.interimTranscript=t.payload},openBotTurn(e,t){e.botTurn={active:!0,chatMsgId:t.payload?.chatMsgId??null,segments:[]}},upsertBotSegment(e,t){const{id:n,text:r,spokenChars:i}=t.payload;e.botTurn.active=!0;const s=e.botTurn.segments.find(e=>e.id===n);s?(s.text=r,s.spokenChars=Math.max(s.spokenChars,i)):(e.botTurn.segments.push({id:n,text:r,spokenChars:i}),e.botTurn.segments.sort((e,t)=>e.id-t.id))},clearBotTurn(e){e.botTurn={active:!1,chatMsgId:null,segments:[]}},setMuted(e,t){e.muted=t.payload},setServerMuted(e,t){e.serverMuted=t.payload},setSelectedMicId(e,t){e.selectedMicId=t.payload},setSelectedSpeakerId(e,t){e.selectedSpeakerId=t.payload},setMicrophones(e,t){e.microphones=t.payload},setSpeakers(e,t){e.speakers=t.payload},signalSessionEnded(e){e.sessionEndedSignal+=1},resetVoiceState:()=>F(),resetIndicatorState(e){e.statusText=null,e.isAgentWorking=!1,e.responseStartTime=null,e.interimTranscript=null,e.serverMuted=!1,e.botTurn={active:!1,chatMsgId:null,segments:[]}}}}),{setConnectionStatus:j,setPipelineStatus:q,setListeningEnabled:$,setMuted:Z,setServerMuted:H,setStatusText:Y,setIsAgentWorking:z,setResponseStartTime:J,setInterimTranscript:Q,openBotTurn:X,upsertBotSegment:ee,clearBotTurn:te,setSelectedMicId:ne,setSelectedSpeakerId:re,setMicrophones:ie,setSpeakers:se,signalSessionEnded:ce,resetVoiceState:ae,resetIndicatorState:oe}=K.actions,ue=K.reducer,de=e(null),le=()=>{const e=t(de);if(!e)throw new Error("useAudioPlatform must be used inside <AudioPlatformProvider>. <VoiceProvider> mounts one by default; if you replaced VoiceProvider, you must wire your own.");return e};function me(){const e={};for(const[t,n]of Object.entries(g()))n&&(e[t]=n);const t=v();t&&(e[_.ACCESS_TOKEN]=t);const n=I();return n&&(e[S]=n),e}async function pe(e){try{const t=await fetch(m.WEBRTC_ICE(e),{headers:me()});return t.ok?(await t.json()).iceServers??[]:(p.warn({message:"Voice ICE fetch failed — continuing without TURN",api_name:"voice-webrtc-ice",status:t.status}),[])}catch(e){return p.warn({message:"Voice ICE fetch error — continuing without TURN",api_name:"voice-webrtc-ice",error_message:e instanceof Error?e.message:String(e)}),[]}}const ge="voice-user-",ve=(e,t=`${ge}${b()}`)=>({id:t,sender:"user",message:e,timestamp:(new Date).toISOString()}),_e=(e,t)=>{const n=`${e}${b()}`;return{id:n,msg_id:n,sender:"agent",message:"",timestamp:(new Date).toISOString(),...t}};function Ie(){const e=o(e=>e.voice.connectionStatus),t=o(e=>e.voice.pipelineStatus),n=o(e=>e.voice.listeningEnabled),r=o(e=>e.voice.muted);return e===D.CONNECTING?L.CONNECTING:e!==D.CONNECTED?L.OFFLINE:n?r?L.MUTED:t===x.PROCESSING?L.PROCESSING:t===x.SPEAKING?L.SPEAKING:t===x.LISTENING?L.LISTENING:L.IDLE:L.READY}const Se=e(null),Ee=({conversationId:e,voiceBridge:t,transportFactory:u,assistantStartsConversation:d})=>{const l=c(),g=le(),v=o(e=>e.voice.muted),_=o(e=>e.voice.serverMuted),I=o(e=>e.voice.pipelineStatus),S=o(e=>e.voice.isAgentWorking),w=o(e=>e.voice.sessionEndedSignal),{connect:R,disconnect:N,sendText:L,setMicEnabled:P,setInputDevice:B,setOutputDevice:U}=function(e,t,u,d){const l=c(),g=a(),v=le(),_=o(e=>e.auth.accessToken),I=n(t);r(()=>{I.current=t});const S=n(null),w=n(new Map),R=n(""),N=n(!1),L=n(!1),P=n(null),B=n(""),U=n(null),F=n(null),W=n(null),K=n(!0),ae=n(null),oe=n(!1),ue=n(""),de=n(0),Ie=n(0),Se=n(!1),Ee=n(0),Te=n(null),fe=n(null),be=n(0),he=n(0),ke=n(null),ye=n(null),Ve=i(()=>{if(null!=Te.current)return;const e=Date.now();Te.current=e,l(J(e)),l(E())},[l]),Ce=i(()=>{const e=Te.current;Te.current=null,l(J(null)),null!=e&&l(T(Date.now()-e))},[l]),Me=i(()=>{Te.current=null,l(J(null))},[l]),we=i((e=f.KEEP)=>{l(Y(null)),l(z(!1)),e===f.FINALIZE?Ce():e===f.CANCEL&&Me()},[l,Ce,Me]),[Re]=s(()=>({setStatusText:e=>l(Y(e||null)),setIsAgentWorking:e=>l(z(e)),appendStreamText:()=>{},pushWidget:e=>{const t=_e("voice-widget-",{widget:[e],message_type:["widget"]});I.current.setMessages(e=>[...e,t])},onPayment:e=>I.current.onPaymentInit?.(e),onConversationTitle:e=>I.current.onConversationTitle?.(e),onError:e=>{p.error({message:"Voice RTVI server error",api_name:"voice-rtvi",error_message:e});const t=_e("voice-error-",{message:"Something went wrong. Please try again.",message_type:["error"]});I.current.setMessages(e=>[...e,t])},onStatusActive:()=>{Ve()}})),Ne=i(()=>{W.current&&(clearTimeout(W.current),W.current=null)},[]),De=i(()=>[...w.current.entries()].sort((e,t)=>e[0]-t[0]).map(([,e])=>e.text).join(" ").trim(),[]),xe=i(()=>{S.current=null,w.current=new Map,R.current="",l(te())},[l]),Ae=i(()=>{const e=S.current;if(e){const t=De();t&&I.current.updateStreamingMessage(e,t),I.current.commitStreamingMessages()}xe()},[De,xe]),Le=i(e=>{Ne(),ae.current&&(clearTimeout(ae.current),ae.current=null),oe.current=!1,ue.current="",l(ie([])),l(se([])),F.current?.(),F.current=null,U.current=null,Se.current=!1,l($(!1)),l(q(x.IDLE)),l(j(D.DISCONNECTED)),l(Q(null)),l(H(!1)),P.current=null,B.current="",N.current=!1,L.current=!1,e?Ae():(I.current.discardStreamingMessages(),xe()),we(f.CANCEL)},[l,we,Ne,Ae,xe]),Pe=i(()=>{if(S.current)return;const e=`voice-agent-${b()}`;S.current=e,w.current=new Map,R.current="",l(X({chatMsgId:e}))},[l]),Oe=i(e=>{const t=e.spoken_progress?(e.spoken_progress?.accumulated_text??"")+(e.spoken_progress?.remaining_text??""):e.text??"";if(!t)return;Pe();const n=N.current?function(e,t){const n=(e.get(0)?.text??"")+t;return e.set(0,{text:n,completed:!1}),{id:0,text:n,spokenChars:n.length}}(w.current,t):function(e,t,n,r){const i=t.segment_id??0,s="completed"===t.spoken_status;if(s&&!e.has(i)){const t=[...e.entries()].find(([,e])=>e.text===n&&!e.completed);if(t)return t[1].completed=!0,{id:t[0],text:n,spokenChars:n.length}}const c=e.get(i);return e.set(i,{text:n,completed:s||!0===c?.completed}),{id:i,text:n,spokenChars:r&&t.spoken_progress?(t.spoken_progress.accumulated_text??"").length:n.length}}(w.current,e,t,L.current);l(ee(n));const r=De();r&&S.current&&r!==R.current&&(R.current=r,I.current.updateStreamingMessage(S.current,r))},[l,Pe,De]),Ge=i(t=>{const n=t,r=n?.type,i=n?.data;switch(p.debug({message:"Voice RTVI server-message received",api_name:"voice-rtvi",conversation_id:e??void 0,event_type:r??"unknown"}),r){case"status_update":h({kind:"status_update",statusText:i?.text??""},Re);break;case"started-thinking":l(z(!0)),Ve();break;case"stopped-thinking":we(f.FINALIZE);break;case"clear_status":break;case"widget":i&&h({kind:"widget",widget:i},Re);break;case"payment":i&&h({kind:"payment",payment:i},Re);break;case"conversation_title":{const e="string"==typeof i?.text?i.text.trim():"";e&&h({kind:"conversation_title",title:e},Re);break}case"session-ended":ye.current={reason:"string"==typeof i?.reason?i.reason:"ended"};break;default:p.debug({message:"Voice RTVI unhandled server-message",api_name:"voice-rtvi",event_type:r??"unknown"})}},[Re,l,Ve,we,e]),Be=n(()=>{}),Ue=n(async()=>{}),Fe=i(t=>{const n=()=>t===de.current;return{onConnected:()=>{if(!n())return;p.info({message:"Voice RTVI connected",api_name:"voice-rtvi",conversation_id:e??void 0}),l(j(D.CONNECTED)),l($(!0)),l(q(x.IDLE)),U.current&&U.current.isMicEnabled!==K.current&&(p.debug({message:"Voice re-asserting mic state on reconnect",api_name:"voice-rtvi",conversation_id:e??void 0,enabled:K.current}),U.current.enableMic(K.current));const{selectedMicId:t,selectedSpeakerId:r}=g.getState().voice;t&&U.current?.updateMic(t),r&&U.current?.updateSpeaker(r);const i=t=>n=>p.warn({message:"Voice device enumeration failed",api_name:"voice-mic",conversation_id:e??void 0,device_kind:t,error_message:n instanceof Error?n.message:String(n)});U.current?.getAllMics().then(e=>{n()&&l(ie(e.map(e=>({deviceId:e.deviceId,label:e.label}))))}).catch(i("mics")),U.current?.getAllSpeakers().then(e=>{n()&&l(se(e.map(e=>({deviceId:e.deviceId,label:e.label}))))}).catch(i("speakers")),oe.current=!0},onBotReady:t=>{if(!n())return;const r=t?.about;N.current=r?.pipeline_mode===A.REALTIME,L.current=!0===r?.tts_word_timestamps,be.current=0,he.current=0,Ee.current=0,p.info({message:"Voice RTVI bot-ready",api_name:"voice-rtvi",conversation_id:e??void 0,pipeline_mode:r?.pipeline_mode??"cascade",tts_word_timestamps:L.current,assistant_starts:d}),Ne(),d&&(l(z(!0)),Ve())},onDisconnected:()=>{if(!n())return;p.info({message:"Voice RTVI disconnected",api_name:"voice-rtvi",conversation_id:e??void 0,drop_outcome:ke.current??"transient",reconnect_attempts:be.current}),Le(!0);const t=Ie.current,r=ke.current;ke.current=null;const i=ye.current;return ye.current=null,i?(p.info({message:"Voice session ended by server — no reconnect",api_name:"voice-rtvi",conversation_id:e??void 0,reason:i.reason}),"idle_timeout"===i.reason&&k({title:"Voice ended",description:"It got quiet for a while, so we wrapped up. Start again anytime."}),void l(ce())):"terminal"===r?(p.warn({message:"Voice RTVI terminal error — no retry",api_name:"voice-rtvi"}),void l(ce())):"auth"===r?he.current>=1||null==e?(p.warn({message:"Voice RTVI auth refresh budget exhausted — giving up",api_name:"voice-rtvi",conversation_id:e??void 0}),void l(ce())):(he.current+=1,p.info({message:"Voice RTVI auth drop — refreshing token then reconnecting",api_name:"voice-rtvi",conversation_id:e??void 0,auth_refresh_count:he.current}),void(async()=>{let n=!1;try{n=!!await(y()?.())}catch{}t===Ie.current&&(n?Ue.current():(p.warn({message:"Voice RTVI token refresh failed — no reconnect",api_name:"voice-rtvi",conversation_id:e??void 0}),l(ce())))})()):be.current>=3||null==e?(be.current>=3&&(p.warn({message:"Voice RTVI auto-reconnect cap reached — giving up",api_name:"voice-rtvi",conversation_id:e??void 0,reconnect_attempts:be.current}),V("Voice connection lost. Please try again.")),void l(ce())):(be.current+=1,p.info({message:"Voice RTVI scheduling auto-reconnect",api_name:"voice-rtvi",conversation_id:e??void 0,reconnect_attempts:be.current,delay_ms:1e3}),fe.current&&clearTimeout(fe.current),void(fe.current=setTimeout(()=>{fe.current=null,Ue.current()},1e3)))},onError:t=>{if(!n())return;const r=t?.data,i=r?.error??r?.message??"Voice error";h({kind:"error",error:i},Re),l(q(x.IDLE)),we(f.CANCEL),r?.fatal&&(O.some(e=>e.test(i))?ke.current="auth":G.some(e=>e.test(i))&&(ke.current="terminal"),p.warn({message:"Voice RTVI fatal error — closing transport",api_name:"voice-rtvi",conversation_id:e??void 0,drop_outcome:ke.current??"generic"}),U.current?.disconnect().catch(()=>{}))},onTrackStarted:(t,r)=>{n()&&(r?.local||"audio"!==t.kind||(p.debug({message:"Voice RTVI bot audio track started",api_name:"voice-rtvi",conversation_id:e??void 0}),F.current?.(),F.current=v.attachBotAudioTrack(t)))},onAvailableMicsUpdated:e=>{n()&&(l(ie(e.map(e=>({deviceId:e.deviceId,label:e.label})))),oe.current&&(ae.current&&clearTimeout(ae.current),ae.current=setTimeout(()=>{if(ae.current=null,!U.current)return;const{selectedMicId:e,microphones:t}=g.getState().voice;p.info({message:"Voice audio devices changed",api_name:"voice-mic",microphones:t.map(e=>e.label).join(", ")}),p.debug({message:"Voice re-acquiring mic (device change)",api_name:"voice-rtvi",enabled:K.current,device:e||"default"}),U.current.updateMic(e)},300)))},onAvailableSpeakersUpdated:e=>{if(!n())return;if(l(se(e.map(e=>({deviceId:e.deviceId,label:e.label})))),!oe.current)return;const t=e.map(e=>e.label).join(", ");t!==ue.current&&(ue.current=t,p.info({message:"Voice audio output devices changed",api_name:"voice-mic",speakers:t}))},onMicUpdated:t=>{if(!n())return;const r=U.current;r&&r.isMicEnabled!==K.current&&(p.debug({message:"Voice re-asserting mic state after track swap",api_name:"voice-rtvi",conversation_id:e??void 0,enabled:K.current,device:t?.label}),r.enableMic(K.current))},onUserMuteStarted:()=>{n()&&p.debug({message:"Voice RTVI user-mute-started (ignored)",api_name:"voice-rtvi",conversation_id:e??void 0})},onUserMuteStopped:()=>{n()&&(p.debug({message:"Voice RTVI user-mute-stopped",api_name:"voice-rtvi",conversation_id:e??void 0,honored:d}),d&&l(H(!1)))},onUserStartedSpeaking:()=>{n()&&(p.debug({message:"Voice RTVI user started speaking",api_name:"voice-rtvi",conversation_id:e??void 0}),P.current=null,B.current="",l(Q(null)),l(q(x.LISTENING)))},onUserStoppedSpeaking:()=>{if(!n())return;p.debug({message:"Voice RTVI user stopped speaking",api_name:"voice-rtvi",conversation_id:e??void 0,has_interim:!!B.current.trim()}),l(q(x.PROCESSING)),l(z(!0)),l(Y(null)),Ve();const t=B.current.trim();if(t){const e=`${ge}${b()}`;P.current=e,l(Q(null)),I.current.setMessages(n=>[...n,ve(t,e)])}},onUserTranscript:t=>{if(!n())return;if(!t.final)return void(t.text&&(B.current=t.text,N.current||l(Q(t.text))));l(Q(null));const r=t.text&&t.text.trim()?t.text:"";p.debug({message:"Voice RTVI user transcript (final)",api_name:"voice-rtvi",conversation_id:e??void 0,has_text:!!r});const i=P.current;P.current=null,i?r&&I.current.setMessages(e=>e.map(e=>e.id===i?{...e,message:r}:e)):r&&I.current.setMessages(e=>[...e,ve(r)])},onBotStartedSpeaking:()=>{n()&&(p.debug({message:"Voice RTVI bot started speaking",api_name:"voice-rtvi",conversation_id:e??void 0}),Pe(),l(q(x.SPEAKING)),we(f.FINALIZE))},onBotOutput:e=>{n()&&Oe(e)},onBotStoppedSpeaking:()=>{n()&&(p.debug({message:"Voice RTVI bot stopped speaking",api_name:"voice-rtvi",conversation_id:e??void 0}),Ae(),l(q(x.IDLE)),we(f.FINALIZE),d&&g.getState().voice.serverMuted&&(p.warn({message:"Voice greeting mute-hold released on bot-turn end (server unmute missing)",api_name:"voice-rtvi",conversation_id:e??void 0}),l(H(!1))))},onServerMessage:e=>{n()&&Ge(e)}}},[l,g,v,Ne,Le,we,Ve,Oe,Pe,Ae,Ge,Re,d,e]),We=i(async()=>{if(U.current||Se.current)return;if(!e)return;if(Se.current=!0,p.info({message:"Voice RTVI connecting",api_name:"voice-rtvi",conversation_id:e,assistant_starts:d}),d&&(K.current=!1,l(H(!0)),p.debug({message:"Voice greeting mute-hold armed",api_name:"voice-rtvi",conversation_id:e})),C(M())&&Ee.current<1){Ee.current+=1,p.debug({message:"Voice pre-connect token refresh (freshness gate)",api_name:"voice-rtvi",conversation_id:e,freshness_refresh_count:Ee.current});const t=Ie.current;try{await Promise.resolve(y()?.())}catch{}if(t!==Ie.current)return}const t=++de.current,n=Ie.current;ye.current=null,ke.current=null,l(j(D.CONNECTING));const r=()=>n!==Ie.current||t!==de.current;try{const[{PipecatClient:n,LogLevel:i},s]=await Promise.all([import("./index.module-S2alA3-r.js"),pe(e)]);if(r())return;const c=new n({transport:u(),enableMic:!0,enableCam:!1,callbacks:Fe(t)});if(c.setLogLevel(i.WARN),r())return void c.disconnect().catch(()=>{});U.current=c,p.debug({message:"Voice RTVI client constructed",api_name:"voice-rtvi",conversation_id:e,enable_mic:K.current}),Ne(),W.current=setTimeout(()=>{p.warn({message:"Voice RTVI connect timed out (no bot-ready)",api_name:"voice-rtvi"}),V("Voice connection timed out. Please try again."),l(ce()),Be.current()},15e3),await c.connect({webrtcRequestParams:{endpoint:m.WEBRTC_OFFER(e),headers:new Headers(me())},iceConfig:{iceServers:s}})}catch(e){p.error({message:"Voice RTVI connect failed",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)}),t===de.current&&(V("Could not connect voice. Please try again."),Le(!1))}},[e,l,u,Fe,Ne,Le,d]),Ke=i((t=!0)=>{p.info({message:"Voice RTVI disconnect (intentional teardown)",api_name:"voice-rtvi",conversation_id:e??void 0,commit_turn:t}),Ie.current+=1,de.current+=1,Ee.current=0,fe.current&&(clearTimeout(fe.current),fe.current=null),be.current=0,he.current=0,ke.current=null,K.current=!0,l(Z(!1));const n=U.current;n?.disconnect().catch(e=>{p.warn({message:"Voice RTVI disconnect threw",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}),Le(t)},[Le,l,e]),je=i(e=>{K.current=e,U.current&&(p.debug({message:"Voice mic enable toggled",api_name:"voice-rtvi",enabled:e}),U.current.enableMic(e))},[]),qe=i(e=>{l(ne(e)),U.current?.updateMic(e)},[l]),$e=i(e=>{l(re(e)),U.current?.updateSpeaker(e)},[l]),Ze=i(t=>{const n=U.current;if(n){p.info({message:"Voice RTVI send-text",api_name:"voice-rtvi",conversation_id:e??void 0,run_immediately:!0});try{n.sendText(t,{run_immediately:!0,audio_response:!0})}catch(e){return void p.error({message:"Voice RTVI send-text failed",api_name:"voice-rtvi",error_message:e instanceof Error?e.message:String(e)})}S.current&&Ae(),I.current.setMessages(e=>[...e,ve(t)]),l(z(!0)),l(Y(null)),Ve(),l(q(x.PROCESSING))}},[l,Ve,Ae,e]);r(()=>{Be.current=Ke},[Ke]),r(()=>{Ue.current=We},[We]);const He=n(e),Ye=n(!1);r(()=>{const t=He.current;if(He.current=e,t===e)return;const n=Se.current;n&&(Ye.current=!0),n&&Ke(!1),Ye.current&&null!=e&&(Ye.current=!1,We())},[e,Ke,We]);const ze=n(_);return r(()=>{const e=ze.current;ze.current=_,e&&!_&&Se.current&&Ke(!1)},[_,Ke]),r(()=>()=>{Be.current()},[]),{connect:We,disconnect:Ke,sendText:Ze,setMicEnabled:je,setInputDevice:qe,setOutputDevice:$e}}(e,t,u,d);r(()=>{P(!v&&!_)},[v,_,P]);const F=Ie();r(()=>(t.setVoiceSendText(L),()=>t.setVoiceSendText(null)),[t,L]);const W=n(!1);return{displayState:F,pipelineStatus:I,isAgentWorking:S,enterVoiceMode:i(async()=>{if(W.current)return V("Microphone permission changed — please reload the page to apply the new setting."),!1;const e=await g.requestMicPermission();if(!e.granted){const t=e.error;return p.error({message:"Mic permission denied",api_name:"voice-mic",error_message:t instanceof Error?t.message:t?String(t):void 0}),W.current=!0,V("Microphone access is required for voice mode. Please allow it in your browser's site settings and reload the page."),!1}return R(),!0},[g,R]),exitVoiceMode:i(()=>{N()},[N]),toggleMute:i(()=>{l(Z(!v))},[l,v]),sendText:L,muted:v,serverMuted:_,sessionEndedSignal:w,setInputDevice:B,setOutputDevice:U}},Te=()=>{const e=t(Se);if(!e)throw new Error("useVoice must be used within VoiceProvider");return e},fe=e({strategyActive:!1,voiceSlot:null,onEnterVoice:null,onFloatingOrbClick:null,voiceInputHidden:!1,headerCenterSlot:null,floatOrbPositionRef:null}),be=()=>t(fe);function he(){return{microphones:o(e=>e.voice.microphones),speakers:o(e=>e.voice.speakers),selectedMicId:o(e=>e.voice.selectedMicId),selectedSpeakerId:o(e=>e.voice.selectedSpeakerId)}}const ke=d();ke.startListening({actionCreator:ne,effect:async e=>{const t=w();t&&await t.setItem(B.SELECTED_MIC_ID,e.payload)}}),ke.startListening({actionCreator:re,effect:async e=>{const t=w();t&&await t.setItem(B.SELECTED_SPEAKER_ID,e.payload)}}),ke.startListening({matcher:l(R,N),effect:async(e,t)=>{const n=w();n&&await Promise.all([n.removeItem(B.SELECTED_MIC_ID),n.removeItem(B.SELECTED_SPEAKER_ID)]),t.dispatch(ae())}});const ye=e=>{const t=document.createElement("audio");return t.autoplay=!0,t.style.display="none",t.srcObject=new MediaStream([e]),document.body.appendChild(t),()=>{try{t.pause(),t.srcObject=null,t.remove()}catch{}}},Ve={attachBotAudioTrack:ye,requestMicPermission:async function(){try{const e=await(navigator.permissions?.query({name:"microphone"}).catch(()=>null));return p.debug({message:"Voice mic permission probe",api_name:"voice-mic",permission_state:e?.state??"unavailable"}),"granted"===e?.state||(await navigator.mediaDevices.getUserMedia({audio:!0})).getTracks().forEach(e=>e.stop()),{granted:!0}}catch(e){return p.warn({message:"Voice mic permission denied",api_name:"voice-mic",error_message:e instanceof Error?e.message:String(e)}),{granted:!1,error:e}}}};export{de as AudioPlatformContext,U as ORB_STATE_CONFIG,D as VOICE_CONNECTION_STATUSES,P as VOICE_CONNECT_TIMEOUT_MS,L as VOICE_DISPLAY_STATES,A as VOICE_PIPELINE_MODES,x as VOICE_PIPELINE_STATUSES,B as VOICE_STORAGE_KEYS,Se as VoiceContext,fe as VoiceUIContext,ye as attachBrowserBotAudioTrack,Ve as browserAudioPlatform,te as clearBotTurn,W as hydrateVoice,X as openBotTurn,oe as resetIndicatorState,j as setConnectionStatus,Q as setInterimTranscript,z as setIsAgentWorking,$ as setListeningEnabled,Z as setMuted,q as setPipelineStatus,J as setResponseStartTime,ne as setSelectedMicId,re as setSelectedSpeakerId,H as setServerMuted,Y as setStatusText,ee as upsertBotSegment,le as useAudioPlatform,he as useMediaDevices,Te as useVoice,Ie as useVoiceDisplayState,Ee as useVoiceProviderValue,be as useVoiceUI,ke as voiceListenerMiddleware,ue as voiceReducer};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1interface/voice-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@pipecat-ai/client-js": "^1.11.0",
|
|
30
|
-
"@1interface/shared-core": "^0.2.
|
|
30
|
+
"@1interface/shared-core": "^0.2.2"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@reduxjs/toolkit": "^2.0.0",
|