UNPKG

@ponderai/web

Version:

Ponder Web SDK – easiest way to build voice AI agents for the web

3 lines (2 loc) 5.65 kB
class t{constructor({assistantId:t,host:s,actions:e=[],instructions:i=""}){this.assistantId=t,this.rawHost=s,this.actions=e,this.instructions=i,this.onSpeechStart=()=>{},this.onSpeechEnd=()=>{},this.onMessage=()=>{},this.onConnected=()=>{},this.onDisconnected=()=>{},this.onFunctionCall=(t,s)=>{},this._audioCtx=null,this._mediaStream=null,this._mediaSource=null,this._workletNode=null,this._ws=null,this._messages=[],this._isUserSpeaking=!1,this._activeAudioSources=[],this._nextTime=null,this._blockAudio=!1,this._onConnectedInternal=()=>{},this._onDisconnectedInternal=()=>{},this._onMessageInternal=()=>{}}async connect(){if(this._ws||this._audioCtx)return;this._audioCtx=new(window.AudioContext||window.webkitAudioContext)({sampleRate:16e3}),await this.#t();const t=this.#s(this.rawHost)+"/ws/talk";this._ws=new WebSocket(t),this._ws.onopen=()=>this.#e(),this._ws.onclose=()=>{this.disconnect().then((()=>{this._onDisconnectedInternal(),this.onDisconnected()}))},this._ws.onerror=()=>console.error("Error connecting Ponder"),this._ws.onmessage=t=>this.#i(JSON.parse(t.data))}async disconnect(){if(this._workletNode&&(this._workletNode.port.onmessage=null,this._workletNode.disconnect(),this._workletNode=null),this._mediaSource&&(this._mediaSource.disconnect(),this._mediaSource=null),this._mediaStream&&(this._mediaStream.getTracks().forEach((t=>t.stop())),this._mediaStream=null),this._activeAudioSources.forEach((t=>t.stop())),this._activeAudioSources=[],this._nextTime=null,this._audioCtx&&"closed"!==this._audioCtx.state)try{await this._audioCtx.close()}catch{}if(this._audioCtx=null,this._ws&&(this._ws.readyState===WebSocket.OPEN||this._ws.readyState===WebSocket.CONNECTING))try{this._ws.close()}catch{}this._ws=null}updateActions(t){this.actions=t,this._ws&&this._ws.readyState===WebSocket.OPEN&&this._ws.send(JSON.stringify({type:"update_tools",tools:this.#o(t)}))}updateInstructions(t){this.instructions=t,this._ws&&this._ws.readyState===WebSocket.OPEN&&this._ws.send(JSON.stringify({type:"update_instructions",instructions:t}))}sendTextMessage(t){if(!t||!t.trim())return;this.#n();const s={role:"user",text:t.trim(),type:"text"};this._messages.push(s),this._ws&&this._ws.readyState===WebSocket.OPEN&&this._ws.send(JSON.stringify({type:"text_input",messages:this._messages}))}#s(t){let s=t.endsWith("/")?t.slice(0,-1):t;return s.startsWith("https://")?"wss://"+s.slice(8):s.startsWith("http://")?"ws://"+s.slice(7):s}#o(t){return t.map((t=>({type:"function",function:{name:t.name,description:t.description,parameters:{type:"object",required:(t.arguments??[]).filter((t=>t.required)).map((t=>t.name)),additionalProperties:!1,properties:Object.fromEntries((t.arguments??[]).map((t=>[t.name,{type:t.type,description:t.description}])))}}})))}async#t(){this._mediaStream=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,echoCancellation:!0,autoGainControl:!0,noiseSuppression:!0}}),this._mediaSource=this._audioCtx.createMediaStreamSource(this._mediaStream);const t=URL.createObjectURL(new Blob(["\n class PonderMicProcessor extends AudioWorkletProcessor {\n process(inputs) {\n if (inputs[0] && inputs[0][0]) this.port.postMessage(inputs[0][0]);\n return true;\n }\n }\n registerProcessor('ponder-mic-processor', PonderMicProcessor);\n "],{type:"application/javascript"}));await this._audioCtx.audioWorklet.addModule(t),URL.revokeObjectURL(t),this._workletNode=new AudioWorkletNode(this._audioCtx,"ponder-mic-processor"),this._workletNode.port.onmessage=({data:t})=>{this._ws?.readyState!==WebSocket.OPEN||this._blockAudio||this._ws.send(JSON.stringify({type:"audio_input",audio:Array.from(t),messages:this._messages}))};const s=this._audioCtx.createGain();s.gain.value=0,this._mediaSource.connect(this._workletNode),this._workletNode.connect(s),s.connect(this._audioCtx.destination)}#e(){this._ws.send(JSON.stringify({type:"init_session",assistant_id:this.assistantId,tools:this.#o(this.actions),instructions:this.instructions}))}#i(t){switch(t.type){case"session_initialized":this._onConnectedInternal(),this.onConnected();break;case"text":this._messages.push(t),this._onMessageInternal(t),this.onMessage(t);break;case"speech_start":this._isUserSpeaking=!0,this.#n(),this.onSpeechStart();break;case"speech_end":this._isUserSpeaking=!1,this.onSpeechEnd();break;case"audio":if(!this._isUserSpeaking&&this._audioCtx){const s=Uint8Array.from(atob(t.audio),(t=>t.charCodeAt(0)));this.#a(s)}break;case"function_call":this._messages.push(t),this.#r(t.function_calls[0])}}#a(t){if(t.length%2)return;const s=new Float32Array(new Int16Array(t.buffer)).map((t=>t/32768)),e=this._audioCtx.createBuffer(1,s.length,this._audioCtx.sampleRate);e.copyToChannel(s,0);const i=this._audioCtx.createBufferSource();i.buffer=e,i.connect(this._audioCtx.destination);const o=null===this._nextTime||this._nextTime<this._audioCtx.currentTime?this._audioCtx.currentTime:this._nextTime;i.start(o),this._nextTime=o+e.duration,this._activeAudioSources.push(i)}#n(){this._activeAudioSources.forEach((t=>t.stop())),this._activeAudioSources=[],this._nextTime=null}async#r(t){let s="function not found";this._blockAudio=!0;try{s=await this.onFunctionCall(t.name,JSON.parse(t.arguments)),void 0===s&&(s="success"),console.log("Function response:",s)}catch(t){s="error calling function",console.error(t)}this._messages.push({role:"tool",type:"function_response",response:s,call_id:t.call_id}),this._ws?.readyState===WebSocket.OPEN&&(this._ws.send(JSON.stringify({type:"function_response",messages:this._messages})),this._blockAudio=!1)}}export{t as default}; //# sourceMappingURL=index.js.map