@easyv/biz-components 0.0.24 → 0.0.26
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.
|
@@ -62,9 +62,10 @@ export declare class XFStreamVoiceManager {
|
|
|
62
62
|
readyToSend: boolean;
|
|
63
63
|
private inStopping;
|
|
64
64
|
private stopResolve;
|
|
65
|
+
private closeTimer;
|
|
65
66
|
constructor(config: XFStreamVoiceManagerConfig);
|
|
66
67
|
renderResult(resultData: string): void;
|
|
67
|
-
setConfig(newConfig: XFStreamVoiceManagerConfig): void;
|
|
68
|
+
setConfig(newConfig: Partial<XFStreamVoiceManagerConfig>): void;
|
|
68
69
|
sendMessage(message: string): void;
|
|
69
70
|
recordConfigSend(): Promise<void>;
|
|
70
71
|
startNewSocket(): void;
|
|
@@ -38,6 +38,7 @@ class XFStreamVoiceManager {
|
|
|
38
38
|
__publicField(this, "readyToSend", false);
|
|
39
39
|
__publicField(this, "inStopping", false);
|
|
40
40
|
__publicField(this, "stopResolve", null);
|
|
41
|
+
__publicField(this, "closeTimer", null);
|
|
41
42
|
this.config = config;
|
|
42
43
|
this.initializeRecorder();
|
|
43
44
|
}
|
|
@@ -86,7 +87,7 @@ class XFStreamVoiceManager {
|
|
|
86
87
|
...this.config,
|
|
87
88
|
...newConfig
|
|
88
89
|
};
|
|
89
|
-
(_a = this.recorder) == null ? void 0 : _a.
|
|
90
|
+
(_a = this.recorder) == null ? void 0 : _a.destroy();
|
|
90
91
|
this.initializeRecorder();
|
|
91
92
|
}
|
|
92
93
|
sendMessage(message) {
|
|
@@ -162,6 +163,7 @@ class XFStreamVoiceManager {
|
|
|
162
163
|
};
|
|
163
164
|
this.wsInstance.onclose = async () => {
|
|
164
165
|
var _a2, _b2;
|
|
166
|
+
clearTimeout(this.closeTimer);
|
|
165
167
|
this.config.onWsClose();
|
|
166
168
|
if (this.continueRecord) {
|
|
167
169
|
this.readyToSend = false;
|
|
@@ -237,8 +239,10 @@ class XFStreamVoiceManager {
|
|
|
237
239
|
const stopPromise = new Promise((resolve) => {
|
|
238
240
|
this.stopResolve = resolve;
|
|
239
241
|
});
|
|
240
|
-
setTimeout(() => {
|
|
242
|
+
this.closeTimer = setTimeout(() => {
|
|
243
|
+
var _a2;
|
|
241
244
|
this.closeWs();
|
|
245
|
+
(_a2 = this.stopResolve) == null ? void 0 : _a2.call(this, this.resultText);
|
|
242
246
|
}, 3e3);
|
|
243
247
|
await stopPromise;
|
|
244
248
|
if (isWsClosed(this.wsInstance)) {
|
|
@@ -265,13 +269,10 @@ class XFStreamVoiceManager {
|
|
|
265
269
|
await this.stopRecord();
|
|
266
270
|
}
|
|
267
271
|
destroy() {
|
|
272
|
+
var _a;
|
|
268
273
|
this.isDestroy = true;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
this.closeWs();
|
|
272
|
-
(_a = this.recorder) == null ? void 0 : _a.stop();
|
|
273
|
-
(_b = this.recorder) == null ? void 0 : _b.destroy();
|
|
274
|
-
}, 100);
|
|
274
|
+
this.closeWs();
|
|
275
|
+
(_a = this.recorder) == null ? void 0 : _a.destroy();
|
|
275
276
|
}
|
|
276
277
|
}
|
|
277
278
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"XFStreamVoiceManager.es.js","sources":["../../../src/utils/xunFeiVoice/XFStreamVoiceManager.ts"],"sourcesContent":["import CryptoJS from 'crypto-js';\nimport { RecorderManager } from './RecorderManager';\nimport type { OnMessageInfo, RecordFrameInfo } from './types';\nimport { isWsClosed, toBase64 } from './utils';\n\ntype errorType = 'socketConnect' | 'message' | 'record';\nlet startCount = 0;\n\nexport interface XFStreamVoiceManagerConfig {\n onError: (errInfo: { type: errorType; message: string }) => void;\n /** ws 收到消息的回调\n * @param info.message 消息内容\n * @param info.isLatest 是否是最后一条消息。 isLatest 为 true 时, message 为完整的语音识别结果。\n */\n onMessage: (info: OnMessageInfo) => void;\n /** webSocket 关闭的回调。\n * 每次录音都会新建一个 webSocket 连接。录音结束后关闭 webSocket\n */\n onWsClose: () => void;\n /**\n * 录音开始的回调\n */\n onRecordStart?: () => void;\n /**\n * 录音开始的回调\n */\n onRecordStop?: () => void;\n /**\n * 录音最后一帧的回调。\n * @param totalData 所有的录音数据, ArrayBuffer[]。\n */\n onLastFrame?: (totalData: ArrayBuffer[]) => void;\n /**\n * 获取链接讯飞语音识别地址的 url\n * @returns websocket url。详情查看讯飞文档里的鉴权方法:https://www.xfyun.cn/doc/asr/voicedictation/API.html#%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B\n */\n getWebSocketUrl?: () => string;\n /**\n * 讯飞语音的 appid\n */\n xunFeiAppId: string;\n}\n\n/** 现在前端mock,后续移到接口下发 */\nconst APPID = '93b73e33';\nconst API_SECRET = 'ZGJhMzQ5ZTJlMDgyYmE1ZTFlZDlmYjg0';\nconst API_KEY = 'fe43de7a071e3a32ec03c6f09fe7bedf';\n/**\n * 获取websocket url\n * 该接口需要后端提供,这里为了方便前端处理\n */\nfunction getWebSocketUrl() {\n // 请求地址根据语种不同变化\n const url = 'wss://iat-api.xfyun.cn/v2/iat';\n const host = 'iat-api.xfyun.cn';\n const apiKey = API_KEY;\n const apiSecret = API_SECRET;\n const date = new Date().toUTCString();\n const algorithm = 'hmac-sha256';\n const headers = 'host date request-line';\n const signatureOrigin = `host: ${host}\\ndate: ${date}\\nGET /v2/iat HTTP/1.1`;\n const signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);\n const signature = CryptoJS.enc.Base64.stringify(signatureSha);\n const authorizationOrigin = `api_key=\"${apiKey}\", algorithm=\"${algorithm}\", headers=\"${headers}\", signature=\"${signature}\"`;\n const authorization = window.btoa(authorizationOrigin);\n return `${url}?authorization=${authorization}&date=${date}&host=${host}`;\n}\n\n/**\n * 讯飞流式语音识别\n * 大致流程:\n * 1. recorder start 后开始接受录音,通过 recorder.onFrameRecorded 把数据透出给上层。数据中 isLastFrame 表示是否最后一次数据, frameBuffer 表示真实数据。\n * 2. 上层拿到录音数据后,通过 ws 发送给讯飞。如果 isLastFrame 为 true 时,给讯飞的数据中 status 为 2。如果 isLastFrame 为 false 时,给讯飞的数据中 status 为 1。\n * 3. ws 返回的数据,通过 renderResult 处理。数据中 status 为 2 表示本次录音数据识别结束了,这时候需要关闭 ws,避免消耗资源。且这两个情况服务端会主动断开连接(整个会话时长最多持续60s,或者超过10s未发送数据)。\n * 讯飞流式语音文档:https://www.xfyun.cn/doc/asr/voicedictation/API.html#%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B\n * 4. 断开链接后会关闭 recorder ,重置录音等数据,方便下次使用。\n * 备注:使用时,建议在收到 onWsClose 事件后,才让前端恢复到可以再次输入录音的状态\n */\nexport class XFStreamVoiceManager {\n wsInstance: WebSocket | null = null;\n recorder: RecorderManager | null = null;\n isDestroy = false;\n resultText = '';\n resultTextTemp = '';\n continueRecord = false;\n startTimeStamp = 0;\n recordStatus: 'start' | 'stop' = 'stop';\n readyToSend = false;\n\n private inStopping = false;\n private stopResolve: ((value: string) => void) | null = null;\n\n constructor(public config: XFStreamVoiceManagerConfig) {\n this.initializeRecorder();\n }\n renderResult(resultData: string) {\n // 识别结束\n const jsonData = JSON.parse(resultData);\n if (jsonData.data && jsonData.data.result) {\n const data = jsonData.data.result;\n let str = '';\n const ws = data.ws;\n for (let i = 0; i < ws.length; i++) {\n str = str + ws[i].cw[0].w;\n }\n // 开启 wpgs 会有此字段(前提:在控制台开通动态修正功能)\n // 取值为 \"apd\"时表示该片结果是追加到前面的最终结果;取值为\"rpl\" 时表示替换前面的部分结果,替换范围为rg字段\n if (data.pgs) {\n if (data.pgs === 'apd') {\n // 将resultTextTemp同步给resultText\n this.resultText = this.resultTextTemp;\n }\n // 将结果存储在resultTextTemp中\n this.resultTextTemp = this.resultText + str;\n } else {\n this.resultText = this.resultText + str;\n }\n }\n /**\n * 识别结果是否结束标识:\n * 0:识别的第一块结果。\n * 1:识别中间结果。\n * 2:识别最后一块结果\n */\n if (jsonData.code === 0 && jsonData.data.status === 2) {\n this.config.onMessage({\n message: this.resultText,\n isLatest: true,\n tempMessage: this.resultTextTemp,\n });\n this.closeWs();\n return;\n }\n if (jsonData.code === 0 && jsonData.data.status === 1) {\n this.config.onMessage({\n message: this.resultText,\n isLatest: false,\n tempMessage: this.resultTextTemp,\n });\n }\n // code 不为0时表示出错\n if (jsonData.code !== 0) {\n this.closeWs();\n this.config.onError({ type: 'message', message: resultData });\n }\n }\n\n setConfig(newConfig: XFStreamVoiceManagerConfig) {\n this.config = {\n ...this.config,\n ...newConfig,\n };\n this.recorder?.close();\n this.initializeRecorder();\n }\n sendMessage(message: string) {\n if (this.wsInstance?.readyState === WebSocket.OPEN && this.readyToSend) {\n this.wsInstance.send(message);\n }\n }\n\n async recordConfigSend() {\n if (this.isDestroy) {\n return;\n }\n if (this.recordStatus === 'stop') {\n this.closeWs();\n return;\n }\n try {\n // 开始录音\n await this.recorder?.start({\n sampleRate: 16000,\n frameSize: 1280,\n });\n const params = {\n common: {\n app_id: this.config.xunFeiAppId || APPID,\n },\n business: {\n language: 'zh_cn',\n domain: 'iat',\n accent: 'mandarin',\n vad_eos: 5000,\n dwa: 'wpgs',\n },\n data: {\n status: 0,\n format: 'audio/L16;rate=16000',\n encoding: 'raw',\n },\n };\n this.readyToSend = true;\n this.sendMessage(JSON.stringify(params));\n } catch (error) {\n console.error('record error:', error);\n this.config.onError({ type: 'record', message: '录音失败, 请检查权限!' });\n }\n }\n\n startNewSocket() {\n console.log(\n '%c ❤️ love ==== start new socket:',\n 'color: red; font-size: 16px;',\n startCount,\n new Date().toLocaleString(),\n );\n startCount++;\n const websocketUrl = this.config.getWebSocketUrl?.() || getWebSocketUrl();\n if ('WebSocket' in window) {\n this.wsInstance = new WebSocket(websocketUrl);\n } else {\n this.config.onError({ type: 'socketConnect', message: '浏览器不支持 WebSocket' });\n return;\n }\n this.wsInstance.onmessage = (e) => {\n // 处理返回数据\n this.renderResult(e.data);\n };\n this.wsInstance.onerror = (e) => {\n console.error('socket error:', e);\n if (this.inStopping) {\n this.inStopping = false;\n this.stopResolve?.(this.resultText);\n }\n };\n this.wsInstance.onclose = async () => {\n this.config.onWsClose();\n if (this.continueRecord) {\n this.readyToSend = false;\n await this.recorder?.close();\n this.startRecord();\n }\n if (this.inStopping) {\n this.inStopping = false;\n this.stopResolve?.(this.resultText);\n }\n };\n this.wsInstance.onopen = () => {\n this.recordConfigSend();\n };\n }\n\n onFrameRecorded({ isLastFrame, frameBuffer }: RecordFrameInfo) {\n if (this.isDestroy) {\n return;\n }\n if (!this.wsInstance) {\n return;\n }\n if (this.wsInstance.readyState === this.wsInstance.OPEN) {\n this.sendMessage(\n JSON.stringify({\n data: {\n status: isLastFrame ? 2 : 1,\n format: 'audio/L16;rate=16000',\n encoding: 'raw',\n audio: toBase64(frameBuffer),\n },\n }),\n );\n }\n if (isLastFrame) {\n this.readyToSend = false;\n }\n }\n\n initializeRecorder() {\n this.recorder = new RecorderManager();\n this.recorder.onFrameRecorded = (data: RecordFrameInfo) => {\n this.onFrameRecorded(data);\n };\n this.recorder.onLastFrame = (data: ArrayBuffer[]) => {\n this.config.onLastFrame?.(data);\n };\n this.recorder.onStop = () => {\n this.config.onRecordStop?.();\n };\n this.recorder.onStart = () => {\n this.config.onRecordStart?.();\n };\n }\n\n startRecord() {\n if (this.isDestroy) {\n return;\n }\n this.recordStatus = 'start';\n this.startTimeStamp = Date.now();\n this.resultText = '';\n this.resultTextTemp = '';\n this.startNewSocket();\n }\n\n async stopRecord() {\n this.recordStatus = 'stop';\n this.inStopping = true;\n console.log('stop record ====', Date.now() - this.startTimeStamp, this.recorder?.status);\n this.recorder?.stop();\n // 如果 recorder 还没有初始化完成就关闭了,那么需要手动关闭 ws,否则在收到最后一帧消息时关闭ws\n // if (this.recorder?.status !== 'initialized') {\n // this.closeWs();\n // }\n // 这里为了防止 ws 没有返回结束的数据,导致 ws 一直没关,所以加个定时器,5s 后手动关闭\n const stopPromise = new Promise((resolve) => {\n this.stopResolve = resolve;\n });\n setTimeout(() => {\n this.closeWs();\n }, 3000);\n await stopPromise;\n if (isWsClosed(this.wsInstance)) {\n this.stopResolve?.(this.resultText);\n }\n this.stopResolve = null;\n }\n\n closeWs() {\n if (this.wsInstance?.readyState !== WebSocket.CLOSED) {\n try {\n this.wsInstance?.close();\n } catch {\n // 有的浏览器在 ws 为 connecting 的时候 close 会报错,这里忽略掉\n }\n }\n }\n /** 开启持续监听 */\n async startContinueRecord() {\n this.continueRecord = true;\n await this.startRecord();\n }\n\n async stopContinueRecord() {\n this.continueRecord = false;\n await this.stopRecord();\n }\n\n destroy() {\n this.isDestroy = true;\n setTimeout(() => {\n this.closeWs();\n this.recorder?.stop();\n this.recorder?.destroy();\n }, 100);\n }\n}\n"],"names":["_a","_b"],"mappings":";;;;;;AAMA,IAAI,aAAa;AAsCjB,MAAM,QAAQ;AACd,MAAM,aAAa;AACnB,MAAM,UAAU;AAKhB,SAAS,kBAAkB;AAEzB,QAAM,MAAM;AACZ,QAAM,OAAO;AACb,QAAM,SAAS;AACf,QAAM,YAAY;AAClB,QAAM,QAAO,oBAAI,KAAK,GAAE,YAAY;AACpC,QAAM,YAAY;AAClB,QAAM,UAAU;AACV,QAAA,kBAAkB,SAAS,IAAI;AAAA,QAAW,IAAI;AAAA;AACpD,QAAM,eAAe,SAAS,WAAW,iBAAiB,SAAS;AACnE,QAAM,YAAY,SAAS,IAAI,OAAO,UAAU,YAAY;AACtD,QAAA,sBAAsB,YAAY,MAAM,iBAAiB,SAAS,eAAe,OAAO,iBAAiB,SAAS;AAClH,QAAA,gBAAgB,OAAO,KAAK,mBAAmB;AACrD,SAAO,GAAG,GAAG,kBAAkB,aAAa,SAAS,IAAI,SAAS,IAAI;AACxE;AAYO,MAAM,qBAAqB;AAAA,EAchC,YAAmB,QAAoC;AAbvD,sCAA+B;AAC/B,oCAAmC;AACnC,qCAAY;AACZ,sCAAa;AACb,0CAAiB;AACjB,0CAAiB;AACjB,0CAAiB;AACjB,wCAAiC;AACjC,uCAAc;AAEN,sCAAa;AACb,uCAAgD;AAErC,SAAA,SAAA;AACjB,SAAK,mBAAmB;AAAA,EAAA;AAAA,EAE1B,aAAa,YAAoB;AAEzB,UAAA,WAAW,KAAK,MAAM,UAAU;AACtC,QAAI,SAAS,QAAQ,SAAS,KAAK,QAAQ;AACnC,YAAA,OAAO,SAAS,KAAK;AAC3B,UAAI,MAAM;AACV,YAAM,KAAK,KAAK;AAChB,eAAS,IAAI,GAAG,IAAI,GAAG,QAAQ,KAAK;AAClC,cAAM,MAAM,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE;AAAA,MAAA;AAI1B,UAAI,KAAK,KAAK;AACR,YAAA,KAAK,QAAQ,OAAO;AAEtB,eAAK,aAAa,KAAK;AAAA,QAAA;AAGpB,aAAA,iBAAiB,KAAK,aAAa;AAAA,MAAA,OACnC;AACA,aAAA,aAAa,KAAK,aAAa;AAAA,MAAA;AAAA,IACtC;AAQF,QAAI,SAAS,SAAS,KAAK,SAAS,KAAK,WAAW,GAAG;AACrD,WAAK,OAAO,UAAU;AAAA,QACpB,SAAS,KAAK;AAAA,QACd,UAAU;AAAA,QACV,aAAa,KAAK;AAAA,MAAA,CACnB;AACD,WAAK,QAAQ;AACb;AAAA,IAAA;AAEF,QAAI,SAAS,SAAS,KAAK,SAAS,KAAK,WAAW,GAAG;AACrD,WAAK,OAAO,UAAU;AAAA,QACpB,SAAS,KAAK;AAAA,QACd,UAAU;AAAA,QACV,aAAa,KAAK;AAAA,MAAA,CACnB;AAAA,IAAA;AAGC,QAAA,SAAS,SAAS,GAAG;AACvB,WAAK,QAAQ;AACb,WAAK,OAAO,QAAQ,EAAE,MAAM,WAAW,SAAS,YAAY;AAAA,IAAA;AAAA,EAC9D;AAAA,EAGF,UAAU,WAAuC;;AAC/C,SAAK,SAAS;AAAA,MACZ,GAAG,KAAK;AAAA,MACR,GAAG;AAAA,IACL;AACA,eAAK,aAAL,mBAAe;AACf,SAAK,mBAAmB;AAAA,EAAA;AAAA,EAE1B,YAAY,SAAiB;;AAC3B,UAAI,UAAK,eAAL,mBAAiB,gBAAe,UAAU,QAAQ,KAAK,aAAa;AACjE,WAAA,WAAW,KAAK,OAAO;AAAA,IAAA;AAAA,EAC9B;AAAA,EAGF,MAAM,mBAAmB;;AACvB,QAAI,KAAK,WAAW;AAClB;AAAA,IAAA;AAEE,QAAA,KAAK,iBAAiB,QAAQ;AAChC,WAAK,QAAQ;AACb;AAAA,IAAA;AAEE,QAAA;AAEI,cAAA,UAAK,aAAL,mBAAe,MAAM;AAAA,QACzB,YAAY;AAAA,QACZ,WAAW;AAAA,MAAA;AAEb,YAAM,SAAS;AAAA,QACb,QAAQ;AAAA,UACN,QAAQ,KAAK,OAAO,eAAe;AAAA,QACrC;AAAA,QACA,UAAU;AAAA,UACR,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,KAAK;AAAA,QACP;AAAA,QACA,MAAM;AAAA,UACJ,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,UAAU;AAAA,QAAA;AAAA,MAEd;AACA,WAAK,cAAc;AACnB,WAAK,YAAY,KAAK,UAAU,MAAM,CAAC;AAAA,aAChC,OAAO;AACN,cAAA,MAAM,iBAAiB,KAAK;AACpC,WAAK,OAAO,QAAQ,EAAE,MAAM,UAAU,SAAS,gBAAgB;AAAA,IAAA;AAAA,EACjE;AAAA,EAGF,iBAAiB;;AACP,YAAA;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,OACA,oBAAI,KAAK,GAAE,eAAe;AAAA,IAC5B;AACA;AACA,UAAM,iBAAe,gBAAK,QAAO,oBAAZ,gCAAmC,gBAAgB;AACxE,QAAI,eAAe,QAAQ;AACpB,WAAA,aAAa,IAAI,UAAU,YAAY;AAAA,IAAA,OACvC;AACL,WAAK,OAAO,QAAQ,EAAE,MAAM,iBAAiB,SAAS,oBAAoB;AAC1E;AAAA,IAAA;AAEG,SAAA,WAAW,YAAY,CAAC,MAAM;AAE5B,WAAA,aAAa,EAAE,IAAI;AAAA,IAC1B;AACK,SAAA,WAAW,UAAU,CAAC,MAAM;;AACvB,cAAA,MAAM,iBAAiB,CAAC;AAChC,UAAI,KAAK,YAAY;AACnB,aAAK,aAAa;AACb,SAAAA,MAAA,KAAA,gBAAA,gBAAAA,IAAA,WAAc,KAAK;AAAA,MAAU;AAAA,IAEtC;AACK,SAAA,WAAW,UAAU,YAAY;;AACpC,WAAK,OAAO,UAAU;AACtB,UAAI,KAAK,gBAAgB;AACvB,aAAK,cAAc;AACb,gBAAAA,MAAA,KAAK,aAAL,gBAAAA,IAAe;AACrB,aAAK,YAAY;AAAA,MAAA;AAEnB,UAAI,KAAK,YAAY;AACnB,aAAK,aAAa;AACb,SAAAC,MAAA,KAAA,gBAAA,gBAAAA,IAAA,WAAc,KAAK;AAAA,MAAU;AAAA,IAEtC;AACK,SAAA,WAAW,SAAS,MAAM;AAC7B,WAAK,iBAAiB;AAAA,IACxB;AAAA,EAAA;AAAA,EAGF,gBAAgB,EAAE,aAAa,eAAgC;AAC7D,QAAI,KAAK,WAAW;AAClB;AAAA,IAAA;AAEE,QAAA,CAAC,KAAK,YAAY;AACpB;AAAA,IAAA;AAEF,QAAI,KAAK,WAAW,eAAe,KAAK,WAAW,MAAM;AAClD,WAAA;AAAA,QACH,KAAK,UAAU;AAAA,UACb,MAAM;AAAA,YACJ,QAAQ,cAAc,IAAI;AAAA,YAC1B,QAAQ;AAAA,YACR,UAAU;AAAA,YACV,OAAO,SAAS,WAAW;AAAA,UAAA;AAAA,QAE9B,CAAA;AAAA,MACH;AAAA,IAAA;AAEF,QAAI,aAAa;AACf,WAAK,cAAc;AAAA,IAAA;AAAA,EACrB;AAAA,EAGF,qBAAqB;AACd,SAAA,WAAW,IAAI,gBAAgB;AAC/B,SAAA,SAAS,kBAAkB,CAAC,SAA0B;AACzD,WAAK,gBAAgB,IAAI;AAAA,IAC3B;AACK,SAAA,SAAS,cAAc,CAAC,SAAwB;;AAC9C,uBAAA,QAAO,gBAAP,4BAAqB;AAAA,IAC5B;AACK,SAAA,SAAS,SAAS,MAAM;;AAC3B,uBAAK,QAAO,iBAAZ;AAAA,IACF;AACK,SAAA,SAAS,UAAU,MAAM;;AAC5B,uBAAK,QAAO,kBAAZ;AAAA,IACF;AAAA,EAAA;AAAA,EAGF,cAAc;AACZ,QAAI,KAAK,WAAW;AAClB;AAAA,IAAA;AAEF,SAAK,eAAe;AACf,SAAA,iBAAiB,KAAK,IAAI;AAC/B,SAAK,aAAa;AAClB,SAAK,iBAAiB;AACtB,SAAK,eAAe;AAAA,EAAA;AAAA,EAGtB,MAAM,aAAa;;AACjB,SAAK,eAAe;AACpB,SAAK,aAAa;AACV,YAAA,IAAI,qBAAqB,KAAK,IAAA,IAAQ,KAAK,iBAAgB,UAAK,aAAL,mBAAe,MAAM;AACxF,eAAK,aAAL,mBAAe;AAMf,UAAM,cAAc,IAAI,QAAQ,CAAC,YAAY;AAC3C,WAAK,cAAc;AAAA,IAAA,CACpB;AACD,eAAW,MAAM;AACf,WAAK,QAAQ;AAAA,OACZ,GAAI;AACD,UAAA;AACF,QAAA,WAAW,KAAK,UAAU,GAAG;AAC1B,iBAAA,gBAAA,8BAAc,KAAK;AAAA,IAAU;AAEpC,SAAK,cAAc;AAAA,EAAA;AAAA,EAGrB,UAAU;;AACR,UAAI,UAAK,eAAL,mBAAiB,gBAAe,UAAU,QAAQ;AAChD,UAAA;AACF,mBAAK,eAAL,mBAAiB;AAAA,MAAM,QACjB;AAAA,MAAA;AAAA,IAER;AAAA,EACF;AAAA;AAAA,EAGF,MAAM,sBAAsB;AAC1B,SAAK,iBAAiB;AACtB,UAAM,KAAK,YAAY;AAAA,EAAA;AAAA,EAGzB,MAAM,qBAAqB;AACzB,SAAK,iBAAiB;AACtB,UAAM,KAAK,WAAW;AAAA,EAAA;AAAA,EAGxB,UAAU;AACR,SAAK,YAAY;AACjB,eAAW,MAAM;;AACf,WAAK,QAAQ;AACb,iBAAK,aAAL,mBAAe;AACf,iBAAK,aAAL,mBAAe;AAAA,OACd,GAAG;AAAA,EAAA;AAEV;"}
|
|
1
|
+
{"version":3,"file":"XFStreamVoiceManager.es.js","sources":["../../../src/utils/xunFeiVoice/XFStreamVoiceManager.ts"],"sourcesContent":["import CryptoJS from 'crypto-js';\nimport { RecorderManager } from './RecorderManager';\nimport type { OnMessageInfo, RecordFrameInfo } from './types';\nimport { isWsClosed, toBase64 } from './utils';\n\ntype errorType = 'socketConnect' | 'message' | 'record';\nlet startCount = 0;\n\nexport interface XFStreamVoiceManagerConfig {\n onError: (errInfo: { type: errorType; message: string }) => void;\n /** ws 收到消息的回调\n * @param info.message 消息内容\n * @param info.isLatest 是否是最后一条消息。 isLatest 为 true 时, message 为完整的语音识别结果。\n */\n onMessage: (info: OnMessageInfo) => void;\n /** webSocket 关闭的回调。\n * 每次录音都会新建一个 webSocket 连接。录音结束后关闭 webSocket\n */\n onWsClose: () => void;\n /**\n * 录音开始的回调\n */\n onRecordStart?: () => void;\n /**\n * 录音开始的回调\n */\n onRecordStop?: () => void;\n /**\n * 录音最后一帧的回调。\n * @param totalData 所有的录音数据, ArrayBuffer[]。\n */\n onLastFrame?: (totalData: ArrayBuffer[]) => void;\n /**\n * 获取链接讯飞语音识别地址的 url\n * @returns websocket url。详情查看讯飞文档里的鉴权方法:https://www.xfyun.cn/doc/asr/voicedictation/API.html#%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B\n */\n getWebSocketUrl?: () => string;\n /**\n * 讯飞语音的 appid\n */\n xunFeiAppId: string;\n}\n\n/** 现在前端mock,后续移到接口下发 */\nconst APPID = '93b73e33';\nconst API_SECRET = 'ZGJhMzQ5ZTJlMDgyYmE1ZTFlZDlmYjg0';\nconst API_KEY = 'fe43de7a071e3a32ec03c6f09fe7bedf';\n/**\n * 获取websocket url\n * 该接口需要后端提供,这里为了方便前端处理\n */\nfunction getWebSocketUrl() {\n // 请求地址根据语种不同变化\n const url = 'wss://iat-api.xfyun.cn/v2/iat';\n const host = 'iat-api.xfyun.cn';\n const apiKey = API_KEY;\n const apiSecret = API_SECRET;\n const date = new Date().toUTCString();\n const algorithm = 'hmac-sha256';\n const headers = 'host date request-line';\n const signatureOrigin = `host: ${host}\\ndate: ${date}\\nGET /v2/iat HTTP/1.1`;\n const signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);\n const signature = CryptoJS.enc.Base64.stringify(signatureSha);\n const authorizationOrigin = `api_key=\"${apiKey}\", algorithm=\"${algorithm}\", headers=\"${headers}\", signature=\"${signature}\"`;\n const authorization = window.btoa(authorizationOrigin);\n return `${url}?authorization=${authorization}&date=${date}&host=${host}`;\n}\n\n/**\n * 讯飞流式语音识别\n * 大致流程:\n * 1. recorder start 后开始接受录音,通过 recorder.onFrameRecorded 把数据透出给上层。数据中 isLastFrame 表示是否最后一次数据, frameBuffer 表示真实数据。\n * 2. 上层拿到录音数据后,通过 ws 发送给讯飞。如果 isLastFrame 为 true 时,给讯飞的数据中 status 为 2。如果 isLastFrame 为 false 时,给讯飞的数据中 status 为 1。\n * 3. ws 返回的数据,通过 renderResult 处理。数据中 status 为 2 表示本次录音数据识别结束了,这时候需要关闭 ws,避免消耗资源。且这两个情况服务端会主动断开连接(整个会话时长最多持续60s,或者超过10s未发送数据)。\n * 讯飞流式语音文档:https://www.xfyun.cn/doc/asr/voicedictation/API.html#%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B\n * 4. 断开链接后会关闭 recorder ,重置录音等数据,方便下次使用。\n * 备注:使用时,建议在收到 onWsClose 事件后,才让前端恢复到可以再次输入录音的状态\n */\nexport class XFStreamVoiceManager {\n wsInstance: WebSocket | null = null;\n recorder: RecorderManager | null = null;\n isDestroy = false;\n resultText = '';\n resultTextTemp = '';\n continueRecord = false;\n startTimeStamp = 0;\n recordStatus: 'start' | 'stop' = 'stop';\n readyToSend = false;\n\n private inStopping = false;\n private stopResolve: ((value: string) => void) | null = null;\n private closeTimer: NodeJS.Timeout | null = null;\n\n constructor(public config: XFStreamVoiceManagerConfig) {\n this.initializeRecorder();\n }\n renderResult(resultData: string) {\n // 识别结束\n const jsonData = JSON.parse(resultData);\n if (jsonData.data && jsonData.data.result) {\n const data = jsonData.data.result;\n let str = '';\n const ws = data.ws;\n for (let i = 0; i < ws.length; i++) {\n str = str + ws[i].cw[0].w;\n }\n // 开启 wpgs 会有此字段(前提:在控制台开通动态修正功能)\n // 取值为 \"apd\"时表示该片结果是追加到前面的最终结果;取值为\"rpl\" 时表示替换前面的部分结果,替换范围为rg字段\n if (data.pgs) {\n if (data.pgs === 'apd') {\n // 将resultTextTemp同步给resultText\n this.resultText = this.resultTextTemp;\n }\n // 将结果存储在resultTextTemp中\n this.resultTextTemp = this.resultText + str;\n } else {\n this.resultText = this.resultText + str;\n }\n }\n /**\n * 识别结果是否结束标识:\n * 0:识别的第一块结果。\n * 1:识别中间结果。\n * 2:识别最后一块结果\n */\n if (jsonData.code === 0 && jsonData.data.status === 2) {\n this.config.onMessage({\n message: this.resultText,\n isLatest: true,\n tempMessage: this.resultTextTemp,\n });\n this.closeWs();\n return;\n }\n if (jsonData.code === 0 && jsonData.data.status === 1) {\n this.config.onMessage({\n message: this.resultText,\n isLatest: false,\n tempMessage: this.resultTextTemp,\n });\n }\n // code 不为0时表示出错\n if (jsonData.code !== 0) {\n this.closeWs();\n this.config.onError({ type: 'message', message: resultData });\n }\n }\n\n setConfig(newConfig: Partial<XFStreamVoiceManagerConfig>) {\n this.config = {\n ...this.config,\n ...newConfig,\n };\n this.recorder?.destroy();\n this.initializeRecorder();\n }\n sendMessage(message: string) {\n if (this.wsInstance?.readyState === WebSocket.OPEN && this.readyToSend) {\n this.wsInstance.send(message);\n }\n }\n\n async recordConfigSend() {\n if (this.isDestroy) {\n return;\n }\n if (this.recordStatus === 'stop') {\n this.closeWs();\n return;\n }\n try {\n // 开始录音\n await this.recorder?.start({\n sampleRate: 16000,\n frameSize: 1280,\n });\n const params = {\n common: {\n app_id: this.config.xunFeiAppId || APPID,\n },\n business: {\n language: 'zh_cn',\n domain: 'iat',\n accent: 'mandarin',\n vad_eos: 5000,\n dwa: 'wpgs',\n },\n data: {\n status: 0,\n format: 'audio/L16;rate=16000',\n encoding: 'raw',\n },\n };\n this.readyToSend = true;\n this.sendMessage(JSON.stringify(params));\n } catch (error) {\n console.error('record error:', error);\n this.config.onError({ type: 'record', message: '录音失败, 请检查权限!' });\n }\n }\n\n startNewSocket() {\n console.log(\n '%c ❤️ love ==== start new socket:',\n 'color: red; font-size: 16px;',\n startCount,\n new Date().toLocaleString(),\n );\n startCount++;\n const websocketUrl = this.config.getWebSocketUrl?.() || getWebSocketUrl();\n if ('WebSocket' in window) {\n this.wsInstance = new WebSocket(websocketUrl);\n } else {\n this.config.onError({ type: 'socketConnect', message: '浏览器不支持 WebSocket' });\n return;\n }\n this.wsInstance.onmessage = (e) => {\n // 处理返回数据\n this.renderResult(e.data);\n };\n this.wsInstance.onerror = (e) => {\n console.error('socket error:', e);\n if (this.inStopping) {\n this.inStopping = false;\n this.stopResolve?.(this.resultText);\n }\n };\n this.wsInstance.onclose = async () => {\n clearTimeout(this.closeTimer!);\n this.config.onWsClose();\n if (this.continueRecord) {\n this.readyToSend = false;\n await this.recorder?.close();\n this.startRecord();\n }\n if (this.inStopping) {\n this.inStopping = false;\n this.stopResolve?.(this.resultText);\n }\n };\n this.wsInstance.onopen = () => {\n this.recordConfigSend();\n };\n }\n\n onFrameRecorded({ isLastFrame, frameBuffer }: RecordFrameInfo) {\n if (this.isDestroy) {\n return;\n }\n if (!this.wsInstance) {\n return;\n }\n if (this.wsInstance.readyState === this.wsInstance.OPEN) {\n this.sendMessage(\n JSON.stringify({\n data: {\n status: isLastFrame ? 2 : 1,\n format: 'audio/L16;rate=16000',\n encoding: 'raw',\n audio: toBase64(frameBuffer),\n },\n }),\n );\n }\n if (isLastFrame) {\n this.readyToSend = false;\n }\n }\n\n initializeRecorder() {\n this.recorder = new RecorderManager();\n this.recorder.onFrameRecorded = (data: RecordFrameInfo) => {\n this.onFrameRecorded(data);\n };\n this.recorder.onLastFrame = (data: ArrayBuffer[]) => {\n this.config.onLastFrame?.(data);\n };\n this.recorder.onStop = () => {\n this.config.onRecordStop?.();\n };\n this.recorder.onStart = () => {\n this.config.onRecordStart?.();\n };\n }\n\n startRecord() {\n if (this.isDestroy) {\n return;\n }\n this.recordStatus = 'start';\n this.startTimeStamp = Date.now();\n this.resultText = '';\n this.resultTextTemp = '';\n this.startNewSocket();\n }\n\n async stopRecord() {\n this.recordStatus = 'stop';\n this.inStopping = true;\n console.log('stop record ====', Date.now() - this.startTimeStamp, this.recorder?.status);\n this.recorder?.stop();\n // 如果 recorder 还没有初始化完成就关闭了,那么需要手动关闭 ws,否则在收到最后一帧消息时关闭ws\n // if (this.recorder?.status !== 'initialized') {\n // this.closeWs();\n // }\n // 这里为了防止 ws 没有返回结束的数据,导致 ws 一直没关,所以加个定时器,5s 后手动关闭\n const stopPromise = new Promise((resolve) => {\n this.stopResolve = resolve;\n });\n this.closeTimer = setTimeout(() => {\n this.closeWs();\n this.stopResolve?.(this.resultText);\n }, 3000);\n await stopPromise;\n if (isWsClosed(this.wsInstance)) {\n this.stopResolve?.(this.resultText);\n }\n this.stopResolve = null;\n }\n\n closeWs() {\n if (this.wsInstance?.readyState !== WebSocket.CLOSED) {\n try {\n this.wsInstance?.close();\n } catch {\n // 有的浏览器在 ws 为 connecting 的时候 close 会报错,这里忽略掉\n }\n }\n }\n /** 开启持续监听 */\n async startContinueRecord() {\n this.continueRecord = true;\n await this.startRecord();\n }\n\n async stopContinueRecord() {\n this.continueRecord = false;\n await this.stopRecord();\n }\n\n destroy() {\n this.isDestroy = true;\n this.closeWs();\n this.recorder?.destroy();\n }\n}\n"],"names":["_a","_b"],"mappings":";;;;;;AAMA,IAAI,aAAa;AAsCjB,MAAM,QAAQ;AACd,MAAM,aAAa;AACnB,MAAM,UAAU;AAKhB,SAAS,kBAAkB;AAEzB,QAAM,MAAM;AACZ,QAAM,OAAO;AACb,QAAM,SAAS;AACf,QAAM,YAAY;AAClB,QAAM,QAAO,oBAAI,KAAK,GAAE,YAAY;AACpC,QAAM,YAAY;AAClB,QAAM,UAAU;AACV,QAAA,kBAAkB,SAAS,IAAI;AAAA,QAAW,IAAI;AAAA;AACpD,QAAM,eAAe,SAAS,WAAW,iBAAiB,SAAS;AACnE,QAAM,YAAY,SAAS,IAAI,OAAO,UAAU,YAAY;AACtD,QAAA,sBAAsB,YAAY,MAAM,iBAAiB,SAAS,eAAe,OAAO,iBAAiB,SAAS;AAClH,QAAA,gBAAgB,OAAO,KAAK,mBAAmB;AACrD,SAAO,GAAG,GAAG,kBAAkB,aAAa,SAAS,IAAI,SAAS,IAAI;AACxE;AAYO,MAAM,qBAAqB;AAAA,EAehC,YAAmB,QAAoC;AAdvD,sCAA+B;AAC/B,oCAAmC;AACnC,qCAAY;AACZ,sCAAa;AACb,0CAAiB;AACjB,0CAAiB;AACjB,0CAAiB;AACjB,wCAAiC;AACjC,uCAAc;AAEN,sCAAa;AACb,uCAAgD;AAChD,sCAAoC;AAEzB,SAAA,SAAA;AACjB,SAAK,mBAAmB;AAAA,EAAA;AAAA,EAE1B,aAAa,YAAoB;AAEzB,UAAA,WAAW,KAAK,MAAM,UAAU;AACtC,QAAI,SAAS,QAAQ,SAAS,KAAK,QAAQ;AACnC,YAAA,OAAO,SAAS,KAAK;AAC3B,UAAI,MAAM;AACV,YAAM,KAAK,KAAK;AAChB,eAAS,IAAI,GAAG,IAAI,GAAG,QAAQ,KAAK;AAClC,cAAM,MAAM,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE;AAAA,MAAA;AAI1B,UAAI,KAAK,KAAK;AACR,YAAA,KAAK,QAAQ,OAAO;AAEtB,eAAK,aAAa,KAAK;AAAA,QAAA;AAGpB,aAAA,iBAAiB,KAAK,aAAa;AAAA,MAAA,OACnC;AACA,aAAA,aAAa,KAAK,aAAa;AAAA,MAAA;AAAA,IACtC;AAQF,QAAI,SAAS,SAAS,KAAK,SAAS,KAAK,WAAW,GAAG;AACrD,WAAK,OAAO,UAAU;AAAA,QACpB,SAAS,KAAK;AAAA,QACd,UAAU;AAAA,QACV,aAAa,KAAK;AAAA,MAAA,CACnB;AACD,WAAK,QAAQ;AACb;AAAA,IAAA;AAEF,QAAI,SAAS,SAAS,KAAK,SAAS,KAAK,WAAW,GAAG;AACrD,WAAK,OAAO,UAAU;AAAA,QACpB,SAAS,KAAK;AAAA,QACd,UAAU;AAAA,QACV,aAAa,KAAK;AAAA,MAAA,CACnB;AAAA,IAAA;AAGC,QAAA,SAAS,SAAS,GAAG;AACvB,WAAK,QAAQ;AACb,WAAK,OAAO,QAAQ,EAAE,MAAM,WAAW,SAAS,YAAY;AAAA,IAAA;AAAA,EAC9D;AAAA,EAGF,UAAU,WAAgD;;AACxD,SAAK,SAAS;AAAA,MACZ,GAAG,KAAK;AAAA,MACR,GAAG;AAAA,IACL;AACA,eAAK,aAAL,mBAAe;AACf,SAAK,mBAAmB;AAAA,EAAA;AAAA,EAE1B,YAAY,SAAiB;;AAC3B,UAAI,UAAK,eAAL,mBAAiB,gBAAe,UAAU,QAAQ,KAAK,aAAa;AACjE,WAAA,WAAW,KAAK,OAAO;AAAA,IAAA;AAAA,EAC9B;AAAA,EAGF,MAAM,mBAAmB;;AACvB,QAAI,KAAK,WAAW;AAClB;AAAA,IAAA;AAEE,QAAA,KAAK,iBAAiB,QAAQ;AAChC,WAAK,QAAQ;AACb;AAAA,IAAA;AAEE,QAAA;AAEI,cAAA,UAAK,aAAL,mBAAe,MAAM;AAAA,QACzB,YAAY;AAAA,QACZ,WAAW;AAAA,MAAA;AAEb,YAAM,SAAS;AAAA,QACb,QAAQ;AAAA,UACN,QAAQ,KAAK,OAAO,eAAe;AAAA,QACrC;AAAA,QACA,UAAU;AAAA,UACR,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,KAAK;AAAA,QACP;AAAA,QACA,MAAM;AAAA,UACJ,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,UAAU;AAAA,QAAA;AAAA,MAEd;AACA,WAAK,cAAc;AACnB,WAAK,YAAY,KAAK,UAAU,MAAM,CAAC;AAAA,aAChC,OAAO;AACN,cAAA,MAAM,iBAAiB,KAAK;AACpC,WAAK,OAAO,QAAQ,EAAE,MAAM,UAAU,SAAS,gBAAgB;AAAA,IAAA;AAAA,EACjE;AAAA,EAGF,iBAAiB;;AACP,YAAA;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,OACA,oBAAI,KAAK,GAAE,eAAe;AAAA,IAC5B;AACA;AACA,UAAM,iBAAe,gBAAK,QAAO,oBAAZ,gCAAmC,gBAAgB;AACxE,QAAI,eAAe,QAAQ;AACpB,WAAA,aAAa,IAAI,UAAU,YAAY;AAAA,IAAA,OACvC;AACL,WAAK,OAAO,QAAQ,EAAE,MAAM,iBAAiB,SAAS,oBAAoB;AAC1E;AAAA,IAAA;AAEG,SAAA,WAAW,YAAY,CAAC,MAAM;AAE5B,WAAA,aAAa,EAAE,IAAI;AAAA,IAC1B;AACK,SAAA,WAAW,UAAU,CAAC,MAAM;;AACvB,cAAA,MAAM,iBAAiB,CAAC;AAChC,UAAI,KAAK,YAAY;AACnB,aAAK,aAAa;AACb,SAAAA,MAAA,KAAA,gBAAA,gBAAAA,IAAA,WAAc,KAAK;AAAA,MAAU;AAAA,IAEtC;AACK,SAAA,WAAW,UAAU,YAAY;;AACpC,mBAAa,KAAK,UAAW;AAC7B,WAAK,OAAO,UAAU;AACtB,UAAI,KAAK,gBAAgB;AACvB,aAAK,cAAc;AACb,gBAAAA,MAAA,KAAK,aAAL,gBAAAA,IAAe;AACrB,aAAK,YAAY;AAAA,MAAA;AAEnB,UAAI,KAAK,YAAY;AACnB,aAAK,aAAa;AACb,SAAAC,MAAA,KAAA,gBAAA,gBAAAA,IAAA,WAAc,KAAK;AAAA,MAAU;AAAA,IAEtC;AACK,SAAA,WAAW,SAAS,MAAM;AAC7B,WAAK,iBAAiB;AAAA,IACxB;AAAA,EAAA;AAAA,EAGF,gBAAgB,EAAE,aAAa,eAAgC;AAC7D,QAAI,KAAK,WAAW;AAClB;AAAA,IAAA;AAEE,QAAA,CAAC,KAAK,YAAY;AACpB;AAAA,IAAA;AAEF,QAAI,KAAK,WAAW,eAAe,KAAK,WAAW,MAAM;AAClD,WAAA;AAAA,QACH,KAAK,UAAU;AAAA,UACb,MAAM;AAAA,YACJ,QAAQ,cAAc,IAAI;AAAA,YAC1B,QAAQ;AAAA,YACR,UAAU;AAAA,YACV,OAAO,SAAS,WAAW;AAAA,UAAA;AAAA,QAE9B,CAAA;AAAA,MACH;AAAA,IAAA;AAEF,QAAI,aAAa;AACf,WAAK,cAAc;AAAA,IAAA;AAAA,EACrB;AAAA,EAGF,qBAAqB;AACd,SAAA,WAAW,IAAI,gBAAgB;AAC/B,SAAA,SAAS,kBAAkB,CAAC,SAA0B;AACzD,WAAK,gBAAgB,IAAI;AAAA,IAC3B;AACK,SAAA,SAAS,cAAc,CAAC,SAAwB;;AAC9C,uBAAA,QAAO,gBAAP,4BAAqB;AAAA,IAC5B;AACK,SAAA,SAAS,SAAS,MAAM;;AAC3B,uBAAK,QAAO,iBAAZ;AAAA,IACF;AACK,SAAA,SAAS,UAAU,MAAM;;AAC5B,uBAAK,QAAO,kBAAZ;AAAA,IACF;AAAA,EAAA;AAAA,EAGF,cAAc;AACZ,QAAI,KAAK,WAAW;AAClB;AAAA,IAAA;AAEF,SAAK,eAAe;AACf,SAAA,iBAAiB,KAAK,IAAI;AAC/B,SAAK,aAAa;AAClB,SAAK,iBAAiB;AACtB,SAAK,eAAe;AAAA,EAAA;AAAA,EAGtB,MAAM,aAAa;;AACjB,SAAK,eAAe;AACpB,SAAK,aAAa;AACV,YAAA,IAAI,qBAAqB,KAAK,IAAA,IAAQ,KAAK,iBAAgB,UAAK,aAAL,mBAAe,MAAM;AACxF,eAAK,aAAL,mBAAe;AAMf,UAAM,cAAc,IAAI,QAAQ,CAAC,YAAY;AAC3C,WAAK,cAAc;AAAA,IAAA,CACpB;AACI,SAAA,aAAa,WAAW,MAAM;;AACjC,WAAK,QAAQ;AACR,OAAAD,MAAA,KAAA,gBAAA,gBAAAA,IAAA,WAAc,KAAK;AAAA,OACvB,GAAI;AACD,UAAA;AACF,QAAA,WAAW,KAAK,UAAU,GAAG;AAC1B,iBAAA,gBAAA,8BAAc,KAAK;AAAA,IAAU;AAEpC,SAAK,cAAc;AAAA,EAAA;AAAA,EAGrB,UAAU;;AACR,UAAI,UAAK,eAAL,mBAAiB,gBAAe,UAAU,QAAQ;AAChD,UAAA;AACF,mBAAK,eAAL,mBAAiB;AAAA,MAAM,QACjB;AAAA,MAAA;AAAA,IAER;AAAA,EACF;AAAA;AAAA,EAGF,MAAM,sBAAsB;AAC1B,SAAK,iBAAiB;AACtB,UAAM,KAAK,YAAY;AAAA,EAAA;AAAA,EAGzB,MAAM,qBAAqB;AACzB,SAAK,iBAAiB;AACtB,UAAM,KAAK,WAAW;AAAA,EAAA;AAAA,EAGxB,UAAU;;AACR,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,eAAK,aAAL,mBAAe;AAAA,EAAQ;AAE3B;"}
|