UNPKG

metro-agent

Version:

Metro Agent

2 lines (1 loc) 8.56 kB
import t from"openai";import e from"recorder-core";class s{constructor(t,e){this.client=t,this.queue=[],this.isProcessing=!1,this.currentAudio=null,this.delay=e||0}async add(t){const e={text:t,status:"pending"};return this.queue.push(e),this.generateAudio(e).catch(console.error),this.isProcessing||this.processQueue(),new Promise(((t,s)=>{const i=()=>{"completed"===e.status?t():"error"===e.status?s("Playback failed"):setTimeout(i,50)};i()}))}async generateAudio(t){try{t.blob=await this.client.tts({text:t.text}),t.status="generated",this.isProcessing||this.queue[0]!==t||this.processQueue()}catch(e){throw this.removeTask(t),e}}async processQueue(){if(!this.isProcessing){for(this.isProcessing=!0;this.queue.length>0;){const t=this.queue[0];if(t.blob&&"pending"!==t.status||await new Promise((e=>{const s=()=>{"pending"!==t.status?e(null):setTimeout(s,50)};s()})),this.queue.includes(t))try{t.status="playing";const e=URL.createObjectURL(t.blob);this.currentAudio=new Audio(e),await this.currentAudio.play(),await new Promise((t=>{this.currentAudio.addEventListener("ended",(()=>{URL.revokeObjectURL(e),t()})),this.currentAudio.addEventListener("error",(()=>{throw URL.revokeObjectURL(e),new Error("Playback failed")}))})),this.delay>0&&await new Promise((t=>setTimeout(t,this.delay))),t.status="completed",this.removeTask(t)}catch(e){t.status="error",this.removeTask(t),console.error("[音频队列播放错误] ",t,e)}finally{this.currentAudio=null}}this.isProcessing=!1}}removeTask(t){const e=this.queue.indexOf(t);e>=0&&this.queue.splice(e,1)}clear(){this.queue=[],this.currentAudio&&(this.currentAudio.pause(),URL.revokeObjectURL(this.currentAudio.src),this.currentAudio=null),this.isProcessing=!1}}class i{constructor(t){this.sampleRate=16e3,this.socketTask=null,this.rec=null,this.readyState=0,this.bitRate=16,this.recStatus=0,this.isNotAllow=!1,this.wsUrl=t.wsUrl,this.sampleRate=t.sampleRate||16e3,this.onError=t?.onError||(()=>{}),this.onMessage=t?.onMessage||(()=>{}),this.onProcess=t?.onProcess||(()=>{}),this.init()}init(){if(console.log("[Recorder] init"),this.recStatus&&this.close(),/^ws/.test(this.wsUrl))this.socketTask=new WebSocket(this.wsUrl);else{const t=new URL(this.wsUrl,window.location.href);t.protocol="http:"===location.protocol?"ws:":"wss:",this.socketTask=new WebSocket(t.toString())}this.bindEvents()}bindEvents(){this.socketTask&&(this.socketTask.addEventListener("open",(async()=>{this.readyState=this.socketTask?.readyState||0,1===this.socketTask?.readyState&&await this.start(),console.log("[Recorder] open readyState-> ",this.readyState)})),this.socketTask.addEventListener("close",(t=>{console.log("[Recorder] close readyState-> ",this.readyState,t),this.readyState=this.socketTask?.readyState||0,1e3!==t?.code&&this.reconnect()})),this.socketTask.addEventListener("error",(()=>{console.log("[Recorder] error readyState-> ",this.readyState),this.readyState=this.socketTask?.readyState||0,this.reconnect()})),this.socketTask.addEventListener("message",(t=>{try{const e=JSON.parse(t.data);this.onMessage(e)}catch(e){this.onError(e),console.log("处理服务端应用数据失败-> ",e,t.data)}})))}async start(){try{return await this.open(),this.recStatus=1,this.rec.start(),{success:!0}}catch(t){throw t}}open(){return new Promise(((t,s)=>{e.TrafficImgUrl=null,this.rec=e({type:"unknown",bitRate:this.bitRate,sampleRate:this.sampleRate,onProcess:this.onProcess}),this.rec.open((()=>{this.isNotAllow=!1,t({success:!0})}),((t,e)=>{this.isNotAllow=!0,console.log((e?"UserNotAllow,":"")+"无法录音:"+t),s({msg:t,isUserNotAllow:e})}))}))}close(){this.recStatus=0,this.socketTask?.close(),this.rec.close(),this.rec=null}reconnect(){this.reconnectTimeout&&clearTimeout(this.reconnectTimeout),this.reconnectTimeout=setTimeout((()=>{this.init()}),5e3)}send(t){this.socketTask?.send(t)}}class o{constructor(t){this.clearBufferIdx=0,this.ttsQueue=t.ttsClient?new s(t.ttsClient,t.ttsDelay):null,this.asrWsUrl=t.asrWsUrl,this.wakeStatus=t.wakeStatus||!1,this.asrConfig=t.asrConfig||null,this.sampleRate=t.sampleRate||16e3,this.wakeupWord=t.wakeupWord||["你好小越","小越你好"],this.dormancyWord=t.dormancyWord||["再见小越","小越再见"],this.welcomeWord=t.welcomeWord||"你好,我是小越,请问有什么需要帮助?",this.goodbyeWord=t.goodbyeWord||"好的,再见。",this.apiKey=t.apiKey||"",this.baseURL=t.baseURL||"https://dashscope.aliyuncs.com/compatible-mode/v1",this.model=t.model||"qwq-32b",this.stream=!1!==t.stream,this.chatLoading=!1,this.maxRetries=t.maxRetries||3,this.timeout=t.timeout||6e4,this.wakeTimeout=t.wakeTimeout||2e4,this.Recorder=e,this.fetch=t.fetch||null,this.onWake=t?.onWake||(()=>{}),this.onDormancy=t?.onDormancy||(()=>{}),this.onMessage=t?.onMessage||(()=>{}),this.onChange=t?.onChange||(()=>{}),this.onStart=t?.onStart||(()=>{}),this.onSend=t?.onSend||null,this.onStream=t?.onStream||(()=>{}),this.onCompleted=t?.onCompleted||(()=>{}),this.onError=t?.onError||(()=>{}),this.onRecProcess=t?.onRecProcess||this.defaultRecProcess,this.onAudioWaveform=t?.onAudioWaveform||(()=>{}),this.start()}start(){let t="";this.recClient=new i({wsUrl:this.asrWsUrl,sampleRate:this.sampleRate,onMessage:e=>{let s=e;1e4===e?.code&&e.data.data&&(e.data.data&&(e.data.last?t+=this.formatContent(e.data.data):t=this.formatContent(e.data.data),s={last:e.data.last,text:t}),e.data.last&&(t="")),s.text&&e.data.last&&(this.changeWakeSilence(s.text),this.onMessage(s,this.wakeStatus,this.ttsQueue?.isProcessing)),s.text&&this.wakeTimeoutFn(),console.log("[Recorder] message-> ",s)},onProcess:(t,e,s,i,o,a)=>{this.onRecProcess({buffers:t,powerLevel:e,bufferDuration:s,bufferSampleRate:i,newBufferIdx:o,asyncEnd:a}),this.onAudioWaveform&&this.onAudioWaveform(e)}}),this.baseURL&&!this.client&&this.chat()}formatContent(t){try{const e="string"==typeof t?JSON.parse(t):t,{ws:s}=e;if(s){const t=[];return s?.forEach((e=>{e.cw.forEach((e=>{t.push(e.w)}))})),t.join("")}return""}catch(t){return console.error("[formatContent] ",t),""}}ArrayBufferToBase64(t){const e=new Uint8Array(t);let s="";for(let t=0;t<e.length;t++)s+=String.fromCharCode(e[t]);return btoa(s)}defaultRecProcess({buffers:t,bufferSampleRate:s,newBufferIdx:i}){for(let e=this.clearBufferIdx;e<i;e++)t[e]=null;this.clearBufferIdx=i;const o=e.SampleData(t.filter((t=>t)),s,this.sampleRate).data,a=this.ArrayBufferToBase64(o.buffer);this.ttsQueue?.isProcessing||this.chatLoading||this.recClientSend(a)}recClientSend(t){this.onSend?this.onSend(this.recClient,t):this.recClient.send(t)}chat(){this.client=new t({fetch:this.fetch,apiKey:this.apiKey,baseURL:this.baseURL,timeout:this.timeout,maxRetries:this.maxRetries,dangerouslyAllowBrowser:!0})}async sendChatMessage(t,e){this.onStart(),this.wakeTimeoutTimer&&clearTimeout(this.wakeTimeoutTimer);try{if(this.chatLoading=!0,this.currentChatController=new AbortController,this.currentChatResponse=await this.client.chat.completions.create(Object.assign({messages:t,model:this.model,stream:this.stream,signal:this.stream?null:this.currentChatController.signal},e||{})),this.stream){for await(const t of this.currentChatResponse)this.onStream(t);this.onCompleted()}else this.onCompleted(this.currentChatResponse);this.chatLoading=!1,this.wakeTimeoutFn()}catch(t){this.onError(t),this.chatLoading=!1,this.wakeTimeoutFn()}}async tts(t){return this.wakeTimeoutTimer&&clearTimeout(this.wakeTimeoutTimer),this.ttsQueue?.add(t).finally((()=>{this.wakeTimeoutFn()}))}ttsClear(){this.ttsQueue?.clear()}get ttsSpeaking(){return this.ttsQueue?.isProcessing}setWakeStatus(t){this.wakeStatus=t,this.onChange("set")}setWakeupWord(t){this.wakeupWord=t}close(){this.destroy(),this.recClient.close()}stop(){this.destroy()}destroy(){this.wakeStatus=!1,this.chatLoading=!1,this.ttsQueue?.clear(),this.stream?this.currentChatResponse.abort():this.currentChatController?.abort()}wakeTimeoutFn(){this.wakeTimeoutTimer&&clearTimeout(this.wakeTimeoutTimer),this.wakeTimeoutTimer=setTimeout((()=>{this.wakeStatus=!1,this.ttsClear(),this.onDormancy(this.goodbyeWord),this.onChange("timeout"),console.log("[WakeTimeout] ",this.wakeStatus)}),this.wakeTimeout)}changeWakeSilence(t){try{const e=String(t).match(/[\u4e00-\u9fa5]/gi)?.join("")||"";e&&(!this.wakeStatus&&this.wakeupWord.some((t=>e.includes(t)))?(this.wakeStatus=!0,this.onWake(this.welcomeWord),this.onChange("wake")):this.wakeStatus&&this.dormancyWord.some((t=>e.includes(t)))&&(this.wakeStatus=!1,this.ttsClear(),this.onDormancy(this.goodbyeWord),this.onChange("dormancy")))}catch(t){console.log("changeWakeSilence err-> ",t)}}}export{o as MetroAgent};