UNPKG

@millisai/web-sdk

Version:

Millis AI's web sdk to build voice agents on web and browser extension.

89 lines (78 loc) 12.6 kB
var w=Object.defineProperty;var S=Object.getOwnPropertySymbols;var D=Object.prototype.hasOwnProperty,E=Object.prototype.propertyIsEnumerable;var A=(n,a,t)=>a in n?w(n,a,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[a]=t,h=(n,a)=>{for(var t in a||(a={}))D.call(a,t)&&A(n,t,a[t]);if(S)for(var t of S(a))E.call(a,t)&&A(n,t,a[t]);return n};var k=(n,a)=>{for(var t in a)w(n,t,{get:a[t],enumerable:!0})};var d=(n,a,t)=>new Promise((e,s)=>{var i=r=>{try{u(t.next(r))}catch(c){s(c)}},o=r=>{try{u(t.throw(r))}catch(c){s(c)}},u=r=>r.done?e(r.value):Promise.resolve(r.value).then(i,o);u((t=t.apply(n,a)).next())});var v={};k(v,{createClient:()=>M,createMicrophoneAudioTrack:()=>y});import{EventEmitter as U}from"eventemitter3";import I from"eventemitter3";var C=` class captureAndPlaybackProcessor extends AudioWorkletProcessor { audioData = []; index = 0; pause = false; constructor() { super(); //set listener to receive audio data, data is float32 array. this.port.onmessage = (e) => { if (e.data === "clear") { // Clear all buffer. this.audioData = []; this.index = 0; } else if (e.data === "pause") { this.pause = true; } else if (e.data === "unpause") { this.pause = false; } else if (e.data.length > 0) { this.audioData.push(this.convertUint8ToFloat32(e.data)); } }; } convertUint8ToFloat32(array) { const targetArray = new Float32Array(array.byteLength / 2); // A DataView is used to read our 16-bit little-endian samples out of the Uint8Array buffer const sourceDataView = new DataView(array.buffer); // Loop through, get values, and divide by 32,768 for (let i = 0; i < targetArray.length; i++) { targetArray[i] = sourceDataView.getInt16(i * 2, true) / Math.pow(2, 16 - 1); } return targetArray; } convertFloat32ToUint8(array) { const buffer = new ArrayBuffer(array.length * 2); const view = new DataView(buffer); for (let i = 0; i < array.length; i++) { const value = array[i] * 32768; view.setInt16(i * 2, value, true); // true for little-endian } return new Uint8Array(buffer); } process(inputs, outputs, parameters) { // Capture const input = inputs[0]; const inputChannel1 = input[0]; const inputChannel2 = input[1]; this.port.postMessage(this.convertFloat32ToUint8(inputChannel1)); // Playback const output = outputs[0]; const outputChannel1 = output[0]; const outputChannel2 = output[1]; // start playback. for (let i = 0; i < outputChannel1.length; ++i) { if (this.audioData.length > 0 && !this.pause) { outputChannel1[i] = this.audioData[0][this.index]; outputChannel2[i] = this.audioData[0][this.index]; this.index++; if (this.index == this.audioData[0].length) { this.audioData.shift(); this.index = 0; if (this.audioData.length == 0) { this.port.postMessage("playback_finished"); } } } else { outputChannel1[i] = 0; outputChannel2[i] = 0; } } return true; } } registerProcessor( "capture-and-playback-processor", captureAndPlaybackProcessor, ); `;import{create as _,ConverterType as R}from"@alexanderolsen/libsamplerate-js";var N=16e3,p=class extends I{constructor(t){super();this.audioContext=null;this.stream=null;this.audioNode=null;this.captureNode=null;this.audioData=[];this.audioDataIndex=0;this.pause=!1;this.needResample=!1;this.resamplerCreated=!1;this.config=t}print(t){this.config.debug&&console.log("[millis audio service]",t)}start(){return d(this,null,function*(){let t=N;this.isFirefox()?(this.audioContext=new AudioContext({latencyHint:"interactive"}),this.needResample=!0):(this.needResample=!1,this.audioContext=new AudioContext({latencyHint:"interactive",sampleRate:t})),this.print("starting audio service, firefox: "+this.isFirefox()+", need resample: "+this.needResample);try{this.print("requesting microphone permission"),this.stream=yield y(t,this.config),this.print("microphone permission granted")}catch(e){throw this.print("microphone permission denied"),new Error("User didn't give microphone permission")}if(this.print("starting audio capture and playback processor, worklet: "+this.isAudioWorkletSupported()),this.isAudioWorkletSupported()){this.print("Starting audio worklet"),this.audioContext.resume();let e=new Blob([C],{type:"application/javascript"}),s=URL.createObjectURL(e);yield this.audioContext.audioWorklet.addModule(s),this.print("Audio worklet loaded"),this.audioNode=new AudioWorkletNode(this.audioContext,"capture-and-playback-processor"),this.print("Audio worklet setup"),this.audioNode.port.onmessage=r=>{r.data instanceof Uint8Array?this.emit("data",r.data):r.data==="playback_finished"&&this.onPlaybackFinished()};let i=this.audioContext.createMediaStreamSource(this.stream);i.connect(this.audioNode),this.audioNode.connect(this.audioContext.destination);let o=this.audioContext.createAnalyser();i.connect(o),this.emit("useraudioready",{analyser:o,stream:this.stream});let u=this.audioContext.createAnalyser();this.audioNode.connect(u),u.connect(this.audioContext.destination),this.emit("analyzer",u)}else{this.print("Starting audio capture node");let e=this.audioContext.createMediaStreamSource(this.stream);this.captureNode=this.audioContext.createScriptProcessor(2048,1,1),this.captureNode.onaudioprocess=o=>{if(this.captureNode&&this.audioContext){if(this.needResample&&!this.resamplerCreated){let l=R.SRC_SINC_FASTEST,b=1;this.resamplerCreated=!0,_(b,this.audioContext.sampleRate,t,{converterType:l}).then(m=>{this.inputResampler=m}),_(b,t,this.audioContext.sampleRate,{converterType:l}).then(m=>{this.outputResampler=m})}var u=o.inputBuffer.getChannelData(0),r=null;this.inputResampler!=null?r=this.inputResampler.full(u):r=u;let c=T(r);this.emit("data",c);let g=o.outputBuffer.getChannelData(0);for(let l=0;l<g.length;++l)this.audioData.length>0&&!this.pause?(g[l]=this.audioData[0][this.audioDataIndex++],this.audioDataIndex===this.audioData[0].length&&(this.audioData.shift(),this.audioDataIndex=0,this.audioData.length==0&&this.onPlaybackFinished())):g[l]=0}},e.connect(this.captureNode),this.captureNode.connect(this.audioContext.destination),this.print("Audio capture node setup");let s=this.audioContext.createAnalyser();e.connect(s),this.emit("useraudioready",{analyser:s,stream:this.stream});let i=this.audioContext.createAnalyser();this.captureNode.connect(i),i.connect(this.audioContext.destination),this.emit("analyzer",i)}})}stop(){return d(this,null,function*(){var t,e,s,i;(t=this.audioContext)==null||t.suspend(),(e=this.audioContext)==null||e.close(),this.isAudioWorkletSupported()?((s=this.audioNode)==null||s.disconnect(),this.audioNode=null):this.captureNode&&(this.captureNode.disconnect(),this.captureNode.onaudioprocess=null,this.captureNode=null,this.audioData=[],this.audioDataIndex=0),(i=this.stream)==null||i.getTracks().forEach(o=>o.stop()),this.audioContext=null,this.stream=null})}play(t){var s;if(this.isAudioWorkletSupported())(s=this.audioNode)==null||s.port.postMessage(t);else{let i=P(t);var e=null;this.outputResampler!=null?e=this.outputResampler.full(i):e=i,this.audioData.push(e)}}setpause(t){var e;this.isAudioWorkletSupported()?(e=this.audioNode)==null||e.port.postMessage(t?"pause":"unpause"):this.pause=t}reset(){var t;this.isAudioWorkletSupported()?(t=this.audioNode)==null||t.port.postMessage("clear"):(this.audioData=[],this.audioDataIndex=0)}onPlaybackFinished(){this.emit("playback_finished")}isAudioWorkletSupported(){return/Chrome/.test(navigator.userAgent)&&/Google Inc/.test(navigator.vendor)}isFirefox(){return/Firefox/.test(navigator.userAgent)}};function P(n){let a=new Float32Array(n.byteLength/2),t=new DataView(n.buffer);for(let e=0;e<a.length;e++)a[e]=t.getInt16(e*2,!0)/Math.pow(2,15);return a}function T(n){let a=new ArrayBuffer(n.length*2),t=new DataView(a);for(let e=0;e<n.length;e++){let s=n[e]*32768;t.setInt16(e*2,s,!0)}return new Uint8Array(a)}var x=(s=>(s.IDLE="idle",s.PREPARE_ANSWER="prepare_answer",s.ANSWER="answer",s.PAUSE="pause",s))(x||{});var F="wss://api-west.millis.ai:8080/millis";var f=class extends U{constructor(t){super();this.startAnswering=0;this.count=0;this.agentState="idle";this.state=0;this.muted=!1;this.readyPayload=void 0;this.ws=null,this.audioService=null,this.latencyEstimator=null;let e={debug:!1};this.config=h(h({},e),t),this.print("init")}reset(){this.state=0,this.agentState="idle",this.count=0,this.startAnswering=0,this.muted=!1}print(t){this.config.debug&&console.log("[millis]",t)}connect(t){this.print("starting websocket"),this.ws=new WebSocket(this.config.endPoint||F),this.ws.binaryType="arraybuffer",this.ws.onopen=()=>{this.print("websocket connected, sending initiate message"),this.emit("onopen"),this.send(JSON.stringify({method:"initiate",data:{public_key:this.config.publicKey,agent:t.agent,metadata:t.metadata,include_metadata_in_prompt:t.include_metadata_in_prompt,session_continuation:t.session_continuation}}))},this.ws.onmessage=e=>{var s;if(e.data instanceof ArrayBuffer){if(this.print("audio data received"),this.startAnswering>0&&(this.emit("onlatency",Date.now()-this.startAnswering),this.print("latency: "+(Date.now()-this.startAnswering)),this.startAnswering=0,this.switchState("answer")),!this.config.isBenchmark){let i=new Uint8Array(e.data);(s=this.audioService)==null||s.play(i),this.emit("onaudio",i)}}else{let i=JSON.parse(e.data);if(i.method==="onready"){this.onready(i.payload);return}this.handle(i)||this.emit(i.method,i.data,i.payload),this.print(`received ${i.method}`)}},this.ws.onclose=e=>{this.stop(),this.emit("onclose",e),this.print("websocket disconnected"),this.switchState("idle")},this.ws.onerror=e=>{this.stop(),this.emit("onerror",e),this.print("websocket error: "+e)}}send(t){var e;((e=this.ws)==null?void 0:e.readyState)===1&&this.ws.send(t)}handle(t){var e,s,i;switch(t.method){case"start_answering":return this.startAnswering=Date.now(),this.switchState("prepare_answer"),!0;case"clear":return(e=this.audioService)==null||e.reset(),this.switchState("idle"),!0;case"pause":return(s=this.audioService)==null||s.setpause(!0),this.switchState("pause"),!0;case"unpause":return(i=this.audioService)==null||i.setpause(!1),this.switchState("answer"),!0;case"pong":return this.print("Ping rtt "+(Date.now()-Number(t.data))),!0}return!1}onready(t){this.readyPayload=t,this.switchState("idle"),this.switchConnectionState(2)}start(t,e,s){return d(this,null,function*(){this.print("starting conversation"),this.audioService=new p(this.config),this.audioService.on("data",o=>{this.muted?o.fill(0):this.print("sending audio data"),this.send(o),this.count++,this.count%1e3==0&&(this.print("sending ping"),this.send(JSON.stringify({method:"ping",data:Date.now().toString()}))),this.switchConnectionState(3)}),this.audioService.on("analyzer",o=>{this.emit("analyzer",o)}),this.audioService.on("useraudioready",o=>{this.print("user audio ready"),this.emit("useraudioready",o)}),this.audioService.on("playback_finished",()=>{this.agentState==="answer"&&this.switchState("idle")}),this.print("starting audio service"),yield this.audioService.start(),this.print("audio service started");let i;typeof t=="object"&&("agent"in t||"call_id"in t)?i=t:(i={agent:typeof t=="string"?{agent_id:t}:{agent_config:t}},e&&(i.metadata=e),s&&(i.include_metadata_in_prompt=s)),this.connect(i)})}stop(){return d(this,null,function*(){var t;this.audioService&&(this.print("stopping audio service"),this.audioService.stop(),this.audioService=null,this.print("audio service stopped")),(t=this.ws)==null||t.close(),this.reset()})}mute(){this.muted=!0}unmute(){this.muted=!1}switchState(t){this.agentState=t,this.emit("onagentstate",t)}switchConnectionState(t){this.state===0?this.state=t:(this.state===3&&t===2||this.state===2&&t===3)&&(this.state=1,this.emit("onready",this.readyPayload))}};function M(n){return new f(n)}function y(n,a){return d(this,null,function*(){var e,s;let t=h({sampleRate:n,echoCancellation:!0,noiseSuppression:!0,channelCount:1,autoGainControl:!0,latency:0},((e=a==null?void 0:a.audioSource)==null?void 0:e.constraints)||{});return(s=a==null?void 0:a.audioSource)!=null&&s.deviceId&&(t.deviceId=a.audioSource.deviceId),navigator.mediaDevices.getUserMedia({audio:t})})}var st=v;export{x as AgentState,st as default}; //# sourceMappingURL=index.mjs.map