xfyun-sdk
Version:
科大讯飞语音识别 SDK,支持浏览器中实时语音听写功能
3 lines (2 loc) • 37.6 kB
JavaScript
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("crypto-js"),require("react")):"function"==typeof define&&define.amd?define(["exports","crypto-js","react"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).XfyunSDK={},e.CryptoJS,e.React)}(this,function(e,t,o){"use strict";function r(e){var t=Object.create(null);return e&&Object.keys(e).forEach(function(o){if("default"!==o){var r=Object.getOwnPropertyDescriptor(e,o);Object.defineProperty(t,o,r.get?r:{enumerable:!0,get:function(){return e[o]}})}}),t.default=e,Object.freeze(t)}var s,n=r(t);function i(){return"undefined"!=typeof window&&"function"==typeof window.btoa}function a(e,t="utf-8"){if(i()){return"utf-8"===t?window.btoa(window.unescape(encodeURIComponent(e))):window.atob(e)}return Buffer.from(e,t).toString("base64")}function c(e,t,o="iat-api.xfyun.cn",r="/v2/iat"){const s="wss://"+o+r,a=(new Date).toUTCString(),c=`host: ${o}\ndate: ${a}\nGET ${r} HTTP/1.1`,h=n.HmacSHA256(c,t),d=`api_key="${e}", algorithm="hmac-sha256", headers="host date request-line", signature="${n.enc.Base64.stringify(h)}"`,l=i()?window.btoa(window.unescape(encodeURIComponent(d))):Buffer.from(d).toString("base64");return`${s}?authorization=${encodeURIComponent(l)}&date=${encodeURIComponent(a)}&host=${encodeURIComponent(o)}`}function h(){if("undefined"==typeof MediaRecorder)return"audio/webm";if("function"!=typeof MediaRecorder.isTypeSupported)return"audio/webm";const e=["audio/webm","audio/webm;codecs=opus","audio/ogg;codecs=opus"];for(const t of e)if(MediaRecorder.isTypeSupported(t))return t;return"audio/webm"}function d(e){const t=window.webkitAudioContext||window.AudioContext;return e?new t({sampleRate:e}):new t}function l(e){let t=0;for(let o=0;o<e.length;o++)t+=e[o]*e[o];return 100*Math.sqrt(t/e.length)}function u(e){const t=new Uint8Array(e);if(i()){let e="";const o=8192;for(let r=0;r<t.length;r+=o){const s=t.slice(r,Math.min(r+o,t.length));e+=String.fromCharCode.apply(null,Array.from(s))}return window.btoa(e)}return Buffer.from(t).toString("base64")}function p(e,t){if(!e||"object"!=typeof e)return"";const o=e;if(!Array.isArray(o.ws))return"";try{return o.ws.map(e=>Array.isArray(e.cw)?e.cw.map(e=>e.w||"").join(""):"").join("")}catch(o){return t?t.error("解析讯飞结果失败:",o,"原始数据:",e):console.error("[XfyunASR] 解析讯飞结果失败:",o,"原始数据:",e),""}}e.LogLevel=void 0,(s=e.LogLevel||(e.LogLevel={}))[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR";class g{constructor(t="[XfyunASR]"){this.level=e.LogLevel.INFO,this.prefix=t}setLevel(t){switch(t){case"debug":this.level=e.LogLevel.DEBUG;break;case"info":this.level=e.LogLevel.INFO;break;case"warn":this.level=e.LogLevel.WARN;break;case"error":this.level=e.LogLevel.ERROR}}debug(...t){this.level<=e.LogLevel.DEBUG&&console.debug(this.prefix,...t)}info(...t){this.level<=e.LogLevel.INFO&&console.info(this.prefix,...t)}warn(...t){this.level<=e.LogLevel.WARN&&console.warn(this.prefix,...t)}error(...t){this.level<=e.LogLevel.ERROR&&console.error(this.prefix,...t)}}const m={[WebSocket.CONNECTING]:"CONNECTING",[WebSocket.OPEN]:"OPEN",[WebSocket.CLOSING]:"CLOSING",[WebSocket.CLOSED]:"CLOSED"};class f{constructor(e,t={}){if(this.websocket=null,this.websocketCloseTimer=null,this.connectingTimer=null,this.state="idle",this.destroyed=!1,!e.appId||!e.apiKey||!e.apiSecret)throw new Error("缺少必要参数: appId, apiKey, apiSecret 不能为空");this.options=e,this.handlers=t;const o=this.getModulePrefix();this.logger=new g(o),this.logger.setLevel(e.logLevel||"info"),this.logger.info(`${o} 实例创建`,e)}ensureWebSocket(){if(!this.websocket)throw this.logger.error("WebSocket 未初始化"),new Error("WebSocket 未初始化,请先调用 start() 方法");return this.websocket}safeSend(e){try{const t=this.ensureWebSocket();return t.readyState===WebSocket.OPEN?(t.send(e),this.logger.debug("WebSocket 发送数据成功"),!0):(this.logger.warn(`WebSocket 未就绪,当前状态: ${m[t.readyState]}`),!1)}catch(e){return this.logger.error("WebSocket 发送数据失败:",e),!1}}safeCloseWebSocket(){this.websocket&&(this.websocket.readyState!==WebSocket.OPEN&&this.websocket.readyState!==WebSocket.CONNECTING||this.websocket.close(1e3,"正常关闭"),this.websocket=null,this.logger.debug("WebSocket 已安全关闭"))}initWebSocket(){try{this.setState("connecting");const e=this.generateAuthUrl();this.logger.info("正在连接 WebSocket"),this.websocket=new WebSocket(e),this.setupWebSocketHandlers(),this.setupConnectingTimeout()}catch(e){this.logger.error("初始化 WebSocket 失败:",e),this.handleError({code:this.getErrorCodePrefix()+3,message:"初始化 WebSocket 失败",data:e})}}setupWebSocketHandlers(){this.websocket&&(this.websocket.onopen=()=>this.handleWebSocketOpen(),this.websocket.onmessage=e=>this.handleWebSocketMessage(e),this.websocket.onerror=e=>this.handleWebSocketError(e),this.websocket.onclose=e=>this.handleWebSocketClose(e))}handleWebSocketOpen(){this.clearConnectingTimer(),this.logger.info("WebSocket 连接成功"),this.setState("connected"),this.onConnected()}onConnected(){}handleWebSocketMessage(e){try{this.logger.debug("收到 WebSocket 消息"),this.parseMessage(e.data)}catch(e){this.logger.error("解析 WebSocket 消息失败:",e),this.handleError({code:this.getErrorCodePrefix()+5,message:"解析消息失败",data:e})}}handleWebSocketError(e){this.clearConnectingTimer(),this.logger.error("WebSocket 连接错误:",e),this.handleError({code:this.getErrorCodePrefix()+2,message:"WebSocket 连接错误",data:e})}handleWebSocketClose(e){this.logger.info("WebSocket 连接关闭:",e.code,e.reason),this.websocket=null,this.onWebSocketClosed(e)}onWebSocketClosed(e){}setupConnectingTimeout(){"undefined"!=typeof window&&(this.connectingTimer=window.setTimeout(()=>{"connecting"!==this.state||this.destroyed||(this.logger.warn("WebSocket connecting 超时,强制关闭"),this.safeCloseWebSocket(),this.handleError({code:this.getErrorCodePrefix()+5,message:"WebSocket 连接超时"}))},this.constructor.CONNECTING_TIMEOUT_MS))}clearWebSocketCloseTimer(){this.websocketCloseTimer&&"undefined"!=typeof window&&(window.clearTimeout(this.websocketCloseTimer),this.websocketCloseTimer=null)}clearConnectingTimer(){this.connectingTimer&&"undefined"!=typeof window&&(window.clearTimeout(this.connectingTimer),this.connectingTimer=null)}scheduleWebSocketClose(e=1e3){this.clearWebSocketCloseTimer(),"undefined"!=typeof window&&(this.websocketCloseTimer=window.setTimeout(()=>{this.safeCloseWebSocket(),this.websocketCloseTimer=null},e))}getState(){return this.state}setState(e){const t=this.STATE_TRANSITIONS[this.state]||[];t.includes(e)||this.logger.warn(`⚠️ 非法状态转换: ${this.state} -> ${e}`,`合法转换: [${t.join(", ")}]`);const o=this.state;this.state=e,this.handlers.onStateChange&&this.handlers.onStateChange(e),this.logger.debug(`状态变更: ${o} -> ${e}`)}handleError(e){this.clearWebSocketCloseTimer(),this.clearConnectingTimer(),this.setState("error"),this.handlers.onError&&this.handlers.onError(e),this.handlers.onStop&&this.handlers.onStop(),this.logger.error(`${this.getModulePrefix()} 错误:`,e)}destroy(){this.destroyed=!0,this.clearWebSocketCloseTimer(),this.clearConnectingTimer(),this.safeCloseWebSocket(),this.setState("stopped"),this.logger.info(`${this.getModulePrefix()} 实例已销毁`)}isDestroyed(){return this.destroyed}setHandlers(e){if(!e||"object"!=typeof e)throw new TypeError("handlers 必须是有效的对象");this.handlers={...this.handlers,...e}}}f.CONNECTING_TIMEOUT_MS=1e4;const S={language:"zh_cn",domain:"iat",accent:"mandarin",vadEos:3e3,maxAudioSize:1048576,autoStart:!1,audioFormat:"audio/L16;rate=16000",reconnectAttempts:3,reconnectInterval:3e3,enableReconnect:!1,logLevel:"info"};class b extends f{constructor(e,t={}){super({...S,...e},t),this.recorder=null,this.audioContext=null,this.audioSource=null,this.analyser=null,this.audioDataQueue=[],this.totalAudioBytes=0,this.recognitionResult="",this.volumeTimer=null,this.microphoneStream=null,this.cachedBusinessParams=null,this.reconnectCount=0,this.reconnectTimer=null,this.isReconnecting=!1,this.STATE_TRANSITIONS={idle:["connecting"],connecting:["connected","stopped","error"],connected:["recording","stopped","error"],recording:["stopped","error"],stopped:["idle","connecting"],error:["idle","connecting"]},this.options.autoStart}getModulePrefix(){return"[XfyunASR]"}getErrorCodePrefix(){return 1e4}generateAuthUrl(){return c(this.options.apiKey,this.options.apiSecret)}onConnected(){this.startRecording()}onWebSocketClosed(e){"stopped"===this.state||"error"===this.state||this.destroyed||this.handleReconnect()}parseMessage(e){if("string"!=typeof e)return;const t=JSON.parse(e);if(0!==t.code)return this.handleError({code:t.code,message:t.message||"识别错误"}),void this.handleReconnect();this.processRecognitionResult(t)}setHandlers(e){const t=["onStart","onStop","onRecognitionResult","onProcess","onError","onStateChange"];for(const o of t)if(e[o]&&"function"!=typeof e[o])throw new TypeError(`${o} 必须是函数`);super.setHandlers(e)}async start(){if(this.destroyed)this.logger.error("实例已销毁,无法启动");else try{if(!navigator.mediaDevices||!window.WebSocket)return void this.handleError({code:10001,message:"浏览器不支持语音识别功能,请使用现代浏览器"});if("connecting"===this.state||"connected"===this.state||"recording"===this.state)return void this.logger.warn("语音识别已在进行中,忽略此次启动请求");this.setState("connecting"),this.recognitionResult="",this.audioDataQueue=[],this.totalAudioBytes=0,this.reconnectCount=0,this.cachedBusinessParams=null,await this.initMicrophone(),this.initWebSocket(),this.handlers.onStart&&this.handlers.onStart()}catch(e){this.releaseMicrophone(),this.audioContext&&(this.audioContext.close(),this.audioContext=null),this.analyser=null,this.audioSource=null,this.recorder=null,this.handleError({code:10003,message:"启动语音识别失败",data:e})}}stop(){if("idle"!==this.state&&"stopped"!==this.state){this.clearReconnectTimer(),this.isReconnecting=!1;try{this.setState("stopped"),this.recorder&&"inactive"!==this.recorder.state&&this.recorder.stop(),this.stopVolumeDetection(),this.sendEndFrame(),this.scheduleWebSocketClose(1e3),this.releaseMicrophone(),this.audioContext&&(this.audioContext.close(),this.audioContext=null),this.handlers.onStop&&this.handlers.onStop()}catch(e){this.handleError({code:10004,message:"停止语音识别失败",data:e})}}}destroy(){this.destroyed=!0,this.clearReconnectTimer(),this.clearConnectingTimer(),this.clearWebSocketCloseTimer(),this.safeCloseWebSocket(),this.recorder&&"inactive"!==this.recorder.state&&this.recorder.stop(),this.recorder=null,this.stopVolumeDetection(),this.releaseMicrophone(),this.audioContext&&(this.audioContext.close(),this.audioContext=null),this.setState("stopped"),this.logger.info("XfyunASR 实例已销毁")}getResult(){return this.recognitionResult}clearResult(){this.recognitionResult=""}isRecording(){return"recording"===this.state}async initMicrophone(){try{this.microphoneStream=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:!0,noiseSuppression:!0,autoGainControl:!0,sampleRate:16e3},video:!1}),this.logger.info("成功获取麦克风权限"),this.audioContext=d(),this.analyser=this.audioContext.createAnalyser(),this.analyser.fftSize=2048,this.audioSource=this.audioContext.createMediaStreamSource(this.microphoneStream),this.audioSource.connect(this.analyser);const e=h();if(!e)throw new Error("浏览器不支持任何可用的音频编码格式");this.logger.info("使用音频格式:",e),this.recorder=new MediaRecorder(this.microphoneStream,{mimeType:e,audioBitsPerSecond:16e3}),this.recorder.ondataavailable=e=>{if(e.data.size>0){const t=new FileReader;t.onload=()=>{if("recording"===this.state&&!this.destroyed&&t.result instanceof ArrayBuffer)try{const e=u(t.result);this.audioDataQueue.push(e),this.sendAudioData()}catch(e){this.logger.error("处理音频数据失败:",e)}},t.onerror=e=>{this.logger.error("读取音频数据失败:",e)},t.readAsArrayBuffer(e.data)}},this.recorder.onerror=e=>{this.logger.error("录音出错:",e),this.handleError({code:10009,message:"录音出错",data:e})},this.logger.info("麦克风和录音器初始化完成")}catch(e){throw this.releaseMicrophone(),this.audioContext&&(this.audioContext.close(),this.audioContext=null),this.logger.error("初始化麦克风失败:",e),e}}startRecording(){this.recorder&&"recording"!==this.recorder.state&&(this.recorder.start(500),this.startVolumeDetection(),this.setState("recording"),this.logger.info("开始录音"))}releaseMicrophone(){if(this.audioSource){try{this.audioSource.disconnect()}catch{}this.audioSource=null}if(this.analyser){try{this.analyser.disconnect()}catch{}this.analyser=null}this.microphoneStream&&(this.microphoneStream.getTracks().forEach(e=>e.stop()),this.microphoneStream=null)}stopVolumeDetection(){this.volumeTimer&&(window.clearInterval(this.volumeTimer),this.volumeTimer=null)}buildBusinessParams(){if(this.cachedBusinessParams)return this.cachedBusinessParams;const e={language:this.options.language,domain:this.options.domain,accent:this.options.accent,vad_eos:this.options.vadEos,dwa:"wpgs",pd:"speech",ptt:0,rlang:"zh-cn",vinfo:1,nunum:1,speex_size:70,nbest:1,wbest:5};return void 0!==this.options.punctuation&&("boolean"==typeof this.options.punctuation?e.punctuation=this.options.punctuation?"on":"off":e.punctuation=this.options.punctuation),this.options.hotWords&&this.options.hotWords.length>0&&(e.hotwords=this.options.hotWords.join(",")),this.cachedBusinessParams=e,e}processRecognitionResult(e){if(!e.data||!e.data.result)return;const t=p(e.data.result,this.logger),o=e.data.result.ls;this.logger.debug("解析识别结果:",t,"是否最终结果:",o),t&&(this.recognitionResult+=t,this.handlers.onRecognitionResult&&this.handlers.onRecognitionResult(t,o)),o&&(this.reconnectCount=0)}sendStartFrame(){if(this.destroyed)this.logger.warn("实例已销毁,无法发送开始帧");else try{const e={common:{app_id:this.options.appId},business:this.buildBusinessParams(),data:{status:0,format:this.options.audioFormat||"audio/L16;rate=16000",encoding:"raw",audio:""}};if(this.logger.debug("发送开始帧"),!this.safeSend(JSON.stringify(e)))throw new Error("WebSocket 发送失败")}catch(e){this.logger.error("发送开始帧失败:",e),this.handleError({code:10008,message:"发送开始帧失败",data:e})}}sendAudioData(){if("recording"===this.state&&!this.destroyed)for(;this.audioDataQueue.length>0;){const e=this.audioDataQueue.shift();if(!e)continue;const t=this.options.maxAudioSize||1048576;if(this.totalAudioBytes+e.length>t){this.logger.warn("音频数据超过大小限制,停止发送"),this.audioDataQueue=[];break}try{const t={common:{app_id:this.options.appId},data:{status:1,format:this.options.audioFormat||"audio/L16;rate=16000",encoding:"raw",audio:e}};if(!this.safeSend(JSON.stringify(t))){this.audioDataQueue.unshift(e),this.handleError({code:10007,message:"发送音频数据失败: WebSocket 未就绪或发送失败"});break}this.totalAudioBytes+=e.length,this.logger.debug("发送音频数据帧, 大小:",e.length)}catch(t){this.logger.error("发送音频数据失败:",t),this.audioDataQueue.unshift(e),this.handleError({code:10007,message:"发送音频数据失败",data:t})}}}sendEndFrame(){const e={common:{app_id:this.options.appId},business:this.buildBusinessParams(),data:{status:2,format:this.options.audioFormat||"audio/L16;rate=16000",encoding:"raw",audio:""}};this.logger.debug("发送结束帧"),this.safeSend(JSON.stringify(e))||this.logger.warn("发送结束帧失败,WebSocket 未就绪")}startVolumeDetection(){if(!this.analyser)return;const e=this.analyser.frequencyBinCount,t=new Float32Array(e);this.volumeTimer=window.setInterval(()=>{if(this.analyser&&"recording"===this.state&&!this.destroyed){this.analyser.getFloatTimeDomainData(t);const e=l(t);this.handlers.onProcess&&this.handlers.onProcess(e)}},100)}clearReconnectTimer(){this.reconnectTimer&&(window.clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.clearConnectingTimer()}handleReconnect(){if(this.destroyed)return;if(!this.options.enableReconnect)return;if(this.isReconnecting)return;const e=this.options.reconnectAttempts||3,t=this.options.reconnectInterval||3e3;if(this.reconnectCount>=e)return this.logger.warn("已达到最大重连次数,重连停止"),void this.setState("error");this.isReconnecting=!0,this.reconnectCount++;const o=Math.min(t*Math.pow(2,this.reconnectCount-1),3e4);this.logger.info(`正在尝试第 ${this.reconnectCount} 次重连,间隔 ${o}ms...`),this.reconnectTimer=window.setTimeout(()=>{this.isReconnecting=!1,"error"!==this.state&&"idle"!==this.state&&"connecting"!==this.state||this.start()},o)}}const y={voice_name:"xiaoyan",speed:50,pitch:50,volume:50,accent:"accent=mandarin",audioFormat:"mp3",sampleRate:16e3,enableCache:!0,logLevel:"info"},w={mp3:"audio/mpeg",wav:"audio/wav",pcm:"audio/pcm"},C={8e3:"8000",16e3:"16000",24e3:"24000",48e3:"48000"};class k extends f{constructor(e,t={}){super({...y,...e},t),this.audioChunks=[],this.currentText="",this.textIndex=0,this.STATE_TRANSITIONS={idle:["connecting"],connecting:["connected","stopped","error"],connected:["synthesizing","stopped","error"],synthesizing:["stopped","error"],stopped:["idle","connecting"],error:["idle","connecting"]}}getModulePrefix(){return"[XfyunTTS]"}getErrorCodePrefix(){return 2e4}generateAuthUrl(){return c(this.options.apiKey,this.options.apiSecret,"tts-api.xfyun.cn","/v2/tts")}parseMessage(e){if("string"==typeof e)try{const t=JSON.parse(e);if(0!==t.code)return void this.handleError({code:t.code,message:t.message||"合成错误"});if(t.data&&void 0!==t.data.current_index){const e=t.data.current_index,o=this.currentText.length;this.handlers.onProgress&&this.handlers.onProgress(e,o)}}catch(e){this.logger.error("解析 TTS 消息失败:",e)}else"synthesizing"!==this.state&&"connected"!==this.state||(this.audioChunks.push(e),this.handlers.onAudioData&&this.handlers.onAudioData(e))}start(e){this.destroyed?this.logger.error("实例已销毁,无法启动"):e&&0!==e.trim().length?"synthesizing"!==this.state&&"connecting"!==this.state?(this.currentText=e,this.textIndex=0,this.audioChunks=[],this.initWebSocket()):this.logger.warn("合成正在进行中,忽略此次请求"):this.handleError({code:20001,message:"合成文本不能为空"})}stop(){"idle"!==this.state&&"stopped"!==this.state&&(this.clearWebSocketCloseTimer(),this.setState("stopped"),this.safeCloseWebSocket(),this.handlers.onStop&&this.handlers.onStop(),this.logger.info("TTS 合成已停止"))}getAudioData(){if(0===this.audioChunks.length)return null;const e=this.audioChunks.reduce((e,t)=>e+t.byteLength,0),t=new ArrayBuffer(e),o=new Uint8Array(t);let r=0;for(const e of this.audioChunks)o.set(new Uint8Array(e),r),r+=e.byteLength;return t}exportAudio(){const e=this.getMimeType(),t=this.getAudioData();return t?new Blob([t],{type:e}):null}downloadAudio(e="synthesis"){if(!e||"string"!=typeof e)throw new TypeError("filename 必须是字符串");if(0===e.trim().length)throw new Error("filename 不能为空");if(e.includes("/")||e.includes("\\")||e.includes("\0"))throw new Error("filename 包含非法字符");if("undefined"==typeof document||"undefined"==typeof URL)return void this.logger.warn("downloadAudio is only available in browser environment");const t=this.exportAudio();if(!t)return void this.logger.warn("No audio data to download");const o=URL.createObjectURL(t),r=document.createElement("a");r.href=o,r.download=e+this.getFileExtension(),document.body.appendChild(r),r.click(),document.body.removeChild(r),URL.revokeObjectURL(o)}getFileExtension(){return"."+(this.options.audioFormat||"mp3")}getMimeType(){const e=this.options.audioFormat||"mp3";return w[e]||"audio/mpeg"}sendStartFrame(){if(this.destroyed)this.logger.warn("实例已销毁,无法发送开始帧");else try{const e=this.options.audioFormat||"mp3",t=this.options.sampleRate||16e3,o={common:{app_id:this.options.appId},business:{aue:"pcm"===e?"raw":"lame",auf:C[t]||"16000",voice_name:this.options.voice_name||"xiaoyan",speed:this.options.speed??50,pitch:this.options.pitch??50,volume:this.options.volume??50,tte:"UTF8",reg:0,bg:0},data:{status:0,text:a(this.currentText,"utf-8")}};if(this.logger.debug("发送 TTS 开始帧"),!this.safeSend(JSON.stringify(o)))throw new Error("WebSocket 发送失败");this.setState("synthesizing")}catch(e){this.logger.error("发送 TTS 开始帧失败:",e),this.handleError({code:20004,message:"发送开始帧失败",data:e})}}}const E={type:"asr",from:"cn",to:"en",domain:"iner",autoStart:!1,vadEos:5e3,sampleRate:16e3,logLevel:"info"},x={cn:"cn",en:"en",ja:"ja",ko:"ko",fr:"fr",es:"es",it:"it",de:"de",pt:"pt",vi:"vi",id:"id",ms:"ms",ru:"ru",ar:"ar",hi:"hi",th:"th"};class T extends f{constructor(e,t={}){super({...E,...e},t),this.microphoneStream=null,this.audioContext=null,this.recorder=null,this.audioDataQueue=[],this.STATE_TRANSITIONS={idle:["connecting"],connecting:["connected","stopped","error"],connected:["translating","stopped","error"],translating:["stopped","error"],stopped:["idle","connecting"],error:["idle","connecting"]}}getModulePrefix(){return"[XfyunTranslator]"}getErrorCodePrefix(){return 3e4}generateAuthUrl(){const e="text"===this.options.type?"/v2/translate":"/v2/itr";return c(this.options.apiKey,this.options.apiSecret,"itr-api.xfyun.cn",e)}onConnected(){"asr"===this.options.type&&(this.initRecorder(),this.sendStartFrame())}parseMessage(e){if("string"==typeof e)try{const t=JSON.parse(e);if(0!==t.code)return void this.handleError({code:t.code,message:t.message||"翻译错误"});if(t.data){const e="text"===this.options.type,o=t.data.status,r={sourceLanguage:this.options.from||"cn",targetLanguage:this.options.to||"en",sourceText:t.data.result?.source||"",targetText:t.data.result?.target||"",isFinal:e||2===o,confidence:t.data.result?.confidence};this.handlers.onResult&&this.handlers.onResult(r),r.isFinal&&(this.setState("stopped"),this.handlers.onEnd&&this.handlers.onEnd())}}catch(e){this.logger.error("解析翻译消息失败:",e)}}setHandlers(e){const t=["onStart","onEnd","onStop","onResult","onError","onStateChange"];for(const o of t)if(e[o]&&"function"!=typeof e[o])throw new TypeError(`${o} 必须是函数`);super.setHandlers(e)}async start(e){if(this.destroyed)return void this.logger.error("实例已销毁,无法启动");if("translating"===this.state||"connecting"===this.state)return void this.logger.warn("翻译正在进行中,忽略此次请求");if("text"===(this.options.type||"asr")){if(!e||0===e.trim().length)return void this.handleError({code:30001,message:"翻译文本不能为空"});await this.startTextTranslation(e)}else await this.startSpeechTranslation()}stop(){"idle"!==this.state&&"stopped"!==this.state&&(this.clearWebSocketCloseTimer(),this.cleanupRecordingResources(),this.setState("stopped"),this.websocket&&("asr"===this.options.type&&this.sendTranslationEndFrame(),this.scheduleWebSocketClose(500)),this.handlers.onStop&&this.handlers.onStop(),this.logger.info("翻译已停止"))}destroy(){this.destroyed=!0,this.clearWebSocketCloseTimer(),this.clearConnectingTimer(),this.cleanupRecordingResources(),this.safeCloseWebSocket(),this.audioDataQueue=[],this.setState("stopped"),this.logger.info("XfyunTranslator 实例已销毁")}cleanupRecordingResources(){this.recorder&&(this.stopRecorder(),this.recorder=null),this.microphoneStream&&this.releaseMicrophone(),this.audioContext&&(this.audioContext.close(),this.audioContext=null)}async startTextTranslation(e){this.setState("connecting"),this.connectingTimer=window.setTimeout(()=>{"connecting"===this.state&&this.handleError({code:30004,message:"连接超时"})},1e4);try{const t=c(this.options.apiKey,this.options.apiSecret,"itr-api.xfyun.cn","/v2/translate");this.websocket=new WebSocket(t),this.websocket.onopen=()=>{this.clearConnectingTimer(),this.logger.info("文本翻译 WebSocket 连接成功"),this.setState("connected"),this.sendTextFrame(e)},this.websocket.onmessage=e=>{this.parseMessage(e.data)},this.websocket.onerror=e=>{this.clearConnectingTimer(),this.logger.error("文本翻译 WebSocket 错误:",e),this.handleError({code:30002,message:"WebSocket 连接错误",data:e})},this.websocket.onclose=()=>{this.setState("stopped"),this.websocket=null}}catch(e){this.clearConnectingTimer(),this.logger.error("文本翻译失败:",e),this.handleError({code:30003,message:"文本翻译失败",data:e})}}async startSpeechTranslation(){this.setState("connecting");try{this.microphoneStream=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:!0,noiseSuppression:!0,autoGainControl:!0,sampleRate:this.options.sampleRate||16e3},video:!1}),this.audioContext=d(this.options.sampleRate||16e3);const e=c(this.options.apiKey,this.options.apiSecret,"itr-api.xfyun.cn","/v2/itr");this.websocket=new WebSocket(e),this.websocket.onopen=()=>{this.clearConnectingTimer(),this.logger.info("语音翻译 WebSocket 连接成功"),this.setState("connected"),this.initRecorder(),this.sendStartFrame()},this.websocket.onmessage=e=>{this.parseMessage(e.data)},this.websocket.onerror=e=>{this.clearConnectingTimer(),this.logger.error("语音翻译 WebSocket 错误:",e),this.handleError({code:30004,message:"WebSocket 连接错误",data:e})},this.websocket.onclose=()=>{this.stopRecorder(),this.releaseMicrophone(),this.audioContext&&(this.audioContext.close(),this.audioContext=null),this.setState("stopped"),this.websocket=null}}catch(e){this.releaseMicrophone(),this.audioContext&&(this.audioContext.close(),this.audioContext=null),this.logger.error("语音翻译初始化失败:",e),this.handleError({code:30005,message:"初始化失败",data:e})}}initRecorder(){if(!this.microphoneStream)return;const e=h();this.recorder=new MediaRecorder(this.microphoneStream,{mimeType:e,audioBitsPerSecond:16e3}),this.recorder.ondataavailable=e=>{if(e.data.size>0){const t=new FileReader;t.onload=()=>{if("translating"===this.state&&t.result instanceof ArrayBuffer){const e=u(t.result);this.audioDataQueue.push(e),this.sendAudioData()}},t.readAsArrayBuffer(e.data)}},this.recorder.start(500),this.setState("translating")}stopRecorder(){this.recorder&&"inactive"!==this.recorder.state&&this.recorder.stop(),this.recorder=null}releaseMicrophone(){this.microphoneStream&&(this.microphoneStream.getTracks().forEach(e=>e.stop()),this.microphoneStream=null)}sendTextFrame(e){if(!this.websocket||this.websocket.readyState!==WebSocket.OPEN)return;const t=x[this.options.from||"cn"]||"cn",o=x[this.options.to||"en"]||"en",r={common:{app_id:this.options.appId},business:{from:t,to:o,data_type:"text"},data:{text:a(e,"utf-8")}};this.safeSend(JSON.stringify(r))?this.setState("translating"):this.logger.warn("发送文本翻译帧失败")}sendStartFrame(){const e=x[this.options.from||"cn"]||"cn",t=x[this.options.to||"en"]||"en",o={common:{app_id:this.options.appId},business:{from:e,to:t,domain:this.options.domain||"iner",data_type:"audio",vad_eos:this.options.vadEos||5e3,rlang:"zh-cn"},data:{status:0,format:"audio/L16;rate=16000",encoding:"raw",audio:""}};this.safeSend(JSON.stringify(o))||this.logger.warn("发送语音翻译开始帧失败")}sendAudioData(){if("translating"===this.state)for(;this.audioDataQueue.length>0;){const e=this.audioDataQueue.shift();if(!e)continue;const t={common:{app_id:this.options.appId},business:{from:x[this.options.from||"cn"]||"cn",to:x[this.options.to||"en"]||"en",domain:this.options.domain||"iner",data_type:"audio"},data:{status:1,format:"audio/L16;rate=16000",encoding:"raw",audio:e}};if(!this.safeSend(JSON.stringify(t))){this.audioDataQueue.unshift(e);break}}}sendTranslationEndFrame(){const e={common:{app_id:this.options.appId},business:{from:x[this.options.from||"cn"]||"cn",to:x[this.options.to||"en"]||"en",data_type:"audio"},data:{status:2,format:"audio/L16;rate=16000",encoding:"raw",audio:""}};this.safeSend(JSON.stringify(e))||this.logger.warn("发送翻译结束帧失败")}}T.translateText=async function(e,t){return e&&"string"==typeof e&&0!==e.trim().length?new Promise((o,r)=>{const s=new T({...t,type:"text"},{onResult:e=>{s.destroy(),o(e)},onError:e=>{s.destroy(),r(new Error(e.message))}});s.start(e.trim())}):Promise.reject(new Error("翻译文本不能为空"))};const v={container:{display:"flex",flexDirection:"column",gap:"15px",padding:"20px",fontFamily:"Arial, sans-serif"},button:{padding:"10px 20px",fontSize:"16px",border:"none",borderRadius:"4px",backgroundColor:"#2196F3",color:"white",cursor:"pointer",outline:"none",transition:"background-color 0.3s",minWidth:"120px"},buttonActive:{backgroundColor:"#FF9800"},buttonRecording:{backgroundColor:"#F44336"},buttonDisabled:{backgroundColor:"#BDBDBD",cursor:"not-allowed"},status:{fontSize:"14px",color:"#757575",marginTop:"10px"},textArea:{width:"100%",minHeight:"80px",padding:"10px",fontSize:"14px",border:"1px solid #E0E0E0",borderRadius:"4px",resize:"vertical",fontFamily:"monospace"},input:{width:"100%",minHeight:"100px",padding:"10px",fontSize:"16px",border:"1px solid #E0E0E0",borderRadius:"4px",resize:"vertical"},resultContainer:{border:"1px solid #E0E0E0",borderRadius:"4px",padding:"15px",backgroundColor:"#F5F5F5",fontSize:"16px",lineHeight:"1.5",whiteSpace:"pre-wrap",wordBreak:"break-word",maxHeight:"200px",overflowY:"auto"},progressBarContainer:{width:"100%",height:"20px",backgroundColor:"#E0E0E0",borderRadius:"10px",overflow:"hidden"},progressBar:{height:"100%",backgroundColor:"#4CAF50",transition:"width 0.3s"},volumeBarContainer:{width:"100%",height:"10px",backgroundColor:"#EEEEEE",borderRadius:"5px",overflow:"hidden"},volumeBar:{height:"100%",backgroundColor:"#4CAF50",transition:"width 0.1s"},downloadButton:{marginTop:"10px",padding:"8px 16px",fontSize:"14px",border:"1px solid #2196F3",borderRadius:"4px",backgroundColor:"transparent",color:"#2196F3",cursor:"pointer",transition:"all 0.3s"}};function R(e,t,o,r="active"){return o?{...e,...v.buttonDisabled}:t?{...e,..."recording"===r?v.buttonRecording:v.buttonActive}:e}function W(e){return{idle:"空闲",connecting:"连接中...",connected:"已连接",stopped:"已停止",error:"错误",...e}}W();const A=W({recording:"录音中..."}),N=W({synthesizing:"合成中..."}),L=W({translating:"翻译中..."});e.BaseWebSocketClient=f,e.Logger=g,e.SDK_VERSION="1.2.3",e.SpeechRecognizer=({appId:e,apiKey:t,apiSecret:r,language:s="zh_cn",domain:n="iat",accent:i="mandarin",hotWords:a,punctuation:c=!0,autoStart:h=!1,onStart:d,onStop:l,onResult:u,onError:p,className:g="",buttonClassName:m="",buttonStartText:f="开始录音",buttonStopText:S="停止录音",showVolume:y=!0,showStatus:w=!0})=>{const[C,k]=o.useState(""),[E,x]=o.useState("idle"),[T,W]=o.useState(0),N=o.useRef(null),L=o.useRef("idle"),I=o.useRef(!1);o.useEffect(()=>{L.current=E},[E]),o.useEffect(()=>{if(!e||!t||!r)return void console.error("缺少必要参数: appId, apiKey, apiSecret");I.current=!1;const o=new b({appId:e,apiKey:t,apiSecret:r,language:s,domain:n,accent:i,hotWords:a,punctuation:c,autoStart:h},{onStart:()=>{I.current||(x("recording"),d?.())},onStop:()=>{I.current||(x("stopped"),l?.())},onRecognitionResult:(e,t)=>{I.current||(k(t=>t+e),u?.(e,t))},onProcess:e=>{I.current||W(e)},onError:e=>{I.current||(x("error"),p?.(e))},onStateChange:e=>{I.current||x(e)}});return N.current=o,h&&o.start(),()=>{I.current=!0,o.destroy(),N.current=null}},[e,t,r,s,n,i,a,c,h]);const O=o.useCallback(()=>{N.current&&!I.current&&(k(""),N.current.start())},[]),M=o.useCallback(()=>{N.current&&!I.current&&N.current.stop()},[]),F=o.useCallback(()=>{"recording"===L.current?M():O()},[O,M]),D=o.useMemo(()=>R(v.button,"recording"===E,"connecting"===E||"error"===E,"recording"),[E]),B=o.useMemo(()=>`${Math.min(100,T)}%`,[T]),P="recording"===E,z="connecting"===E||"error"===E;return o.createElement("div",{style:v.container,className:g},o.createElement("button",{style:D,className:m,onClick:F,disabled:z},P?S:f),w&&o.createElement("div",{style:v.status},"状态: ",A[E]),y&&P&&o.createElement("div",{style:{width:"100%",margin:"15px 0"}},o.createElement("div",{style:v.volumeBarContainer},o.createElement("div",{style:{...v.volumeBar,width:B}}))),o.createElement("div",{style:v.resultContainer},C))},e.SpeechSynthesizer=({appId:e,apiKey:t,apiSecret:r,voiceName:s="xiaoyan",speed:n=50,pitch:i=50,volume:a=50,audioFormat:c="mp3",sampleRate:h=16e3,onStart:d,onStop:l,onError:u,className:p="",buttonClassName:g="",inputClassName:m="",showProgress:f=!0,showStatus:S=!0})=>{const[b,y]=o.useState("你好,欢迎使用讯飞语音合成"),[w,C]=o.useState("idle"),[E,x]=o.useState({current:0,total:0}),[T,W]=o.useState(null),A=o.useRef(null),L=o.useRef(!1);o.useEffect(()=>{if(!e||!t||!r)return void console.error("缺少必要参数: appId, apiKey, apiSecret");L.current=!1;const o=new k({appId:e,apiKey:t,apiSecret:r,voice_name:s,speed:n,pitch:i,volume:a,audioFormat:c,sampleRate:h},{onStart:()=>{L.current||(C("synthesizing"),d?.())},onStop:()=>{L.current||(C("stopped"),l?.())},onEnd:()=>{L.current||(C("stopped"),W(o.exportAudio()))},onProgress:(e,t)=>{L.current||x({current:e,total:t})},onError:e=>{L.current||(C("error"),u?.(e))},onStateChange:e=>{L.current||C(e)}});return A.current=o,()=>{L.current=!0,o.destroy(),A.current=null}},[e,t,r,s,n,i,a,c,h]);const I=o.useCallback(()=>{if(A.current&&!L.current){if(!b||0===b.trim().length)return void u?.({code:20001,message:"合成文本不能为空"});W(null),A.current.start(b)}},[b,u]),O=o.useCallback(()=>{A.current&&!L.current&&A.current.stop()},[]),M=o.useCallback(e=>{y(e.target.value)},[]),F=o.useCallback(()=>{if(T){const e=URL.createObjectURL(T),t=document.createElement("a");t.href=e,t.download=`synthesis.${c}`,document.body.appendChild(t),t.click(),document.body.removeChild(t),URL.revokeObjectURL(e)}},[T,c]),D=o.useCallback(()=>{"synthesizing"===w?O():I()},[w,I,O]),B=o.useMemo(()=>R(v.button,"synthesizing"===w,"connecting"===w||"error"===w,"active"),[w]),P=o.useMemo(()=>0===E.total?"0%":`${Math.round(E.current/E.total*100)}%`,[E]),z="synthesizing"===w,_="connecting"===w||"error"===w;return o.createElement("div",{style:v.container,className:p},o.createElement("textarea",{value:b,onChange:M,style:v.input,className:m,placeholder:"请输入要合成的文本"}),o.createElement("button",{style:B,className:g,onClick:D,disabled:_},z?"停止合成":"开始合成"),S&&o.createElement("div",{style:v.status},"状态: ",N[w]),f&&z&&E.total>0&&o.createElement("div",{style:{width:"100%",marginTop:"10px"}},o.createElement("div",{style:v.progressBarContainer},o.createElement("div",{style:{...v.progressBar,width:P}})),o.createElement("div",{style:{textAlign:"center",marginTop:"5px"}},Math.round(E.current/E.total*100),"%")),T&&"stopped"===w&&o.createElement("button",{style:v.downloadButton,onClick:F},"下载音频"))},e.Translator=({appId:e,apiKey:t,apiSecret:r,type:s="text",from:n="cn",to:i="en",domain:a="iner",vadEos:c=5e3,className:h="",buttonClassName:d="",inputClassName:l="",resultClassName:u="",showSourceText:p=!0,showTargetText:g=!0,onStart:m,onStop:f,onError:S})=>{const[b,y]=o.useState(""),[w,C]=o.useState(""),[k,E]=o.useState("idle"),[x,W]=o.useState(!1),A=o.useRef(null),N=o.useRef(!1);o.useEffect(()=>{if(!e||!t||!r)return void console.error("缺少必要参数: appId, apiKey, apiSecret");N.current=!1;const o=new T({appId:e,apiKey:t,apiSecret:r,type:s,from:n,to:i,domain:a||"iner"},{onStart:()=>{N.current||(W(!0),E("translating"),m?.())},onStop:()=>{N.current||(W(!1),E("stopped"),f?.())},onEnd:()=>{N.current||(W(!1),E("stopped"))},onResult:e=>{N.current||(C(e.targetText),e.isFinal&&C(e=>e+"\n"))},onError:e=>{N.current||(E("error"),S?.(e))},onStateChange:e=>{N.current||E(e)}});return A.current=o,()=>{N.current=!0,o.destroy(),A.current=null}},[e,t,r,s,n,i,a,c]);const I=o.useCallback(async e=>{if(A.current&&!N.current)if("text"===s){if(!e||0===e.trim().length)return void S?.({code:20001,message:"翻译文本不能为空"});y(e),C(""),await A.current.start(e)}else"asr"===s&&(y(""),C(""),await A.current.start());else S?.({code:20001,message:"Translator 未初始化"})},[s,S]),O=o.useCallback(()=>{A.current?.stop()},[]),M=o.useCallback(e=>{y(e.target.value)},[]),F=o.useCallback(()=>{y(""),C("")},[]),D=o.useCallback(()=>{x?O():"text"===s?I(b):I()},[x,s,b,I,O]),B=o.useMemo(()=>R(v.button,x,"connecting"===k||"error"===k,"active"),[k,x]),P="connecting"===k||"error"===k;return o.createElement("div",{style:v.container,className:h},o.createElement("textarea",{value:b,onChange:M,style:v.textArea,className:l,placeholder:"text"===s?"请输入要翻译的文本":"点击开始语音翻译",disabled:x}),o.createElement("button",{style:B,className:d,onClick:D,disabled:P},x?"停止翻译":"开始翻译"),(p||g)&&w&&o.createElement("div",{style:v.resultContainer,className:u},p&&b&&o.createElement("div",{style:{fontSize:"14px",lineHeight:"1.5",marginBottom:"10px",color:"#333"}},o.createElement("strong",null,"原文:"),o.createElement("br",null),b),g&&o.createElement("div",{style:{fontSize:"14px",lineHeight:"1.5",color:"#2196F3",fontWeight:"bold"}},o.createElement("strong",null,"译文:"),o.createElement("br",null),w)),o.createElement("div",{style:{fontSize:"12px",color:"#757575",marginTop:"5px"}},"状态: ",L[k]),o.createElement("button",{style:{...v.button,backgroundColor:"transparent",color:"#757575"},onClick:F},"清空"))},e.XfyunASR=b,e.XfyunTTS=k,e.XfyunTranslator=T,e.arrayBufferToBase64=u,e.calculateVolume=l,e.generateAuthUrl=c,e.parseXfyunResult=p});
//# sourceMappingURL=index.umd.js.map