import pako from 'pako'; export class AsrClient { constructor (url) { this.URL = url; // 柚子 // this.appid = "4200510043"; // this.token = "BWP-R4zL035PE0rtyzdzU1UPxjc-9VTJ"; // 测试 this.appid = "5592672364"; this.token = "wdoyVaxUik1NqR3Ei3L7ACeNmqlkpMEl"; this.sk = "TIlmvF4EeKEEfFs74eyyZ8Wn-kXn-WwJ"; this.cluster = "volcengine_input_common"; this.workflow = "audio_in,resample,partition,vad,fe,decode,itn"; this.uid = "user_id"; this.nbest = 1; this.show_utterances = true; this.result_type = "full"; this.format = "wav"; this.codec = "raw"; this.sample_rate = 16000; this.channels = 1; this.bits = 16; this.authType = "TOKEN"; this.socket = null; this.recv_latch = null; this.recv_timeout = 5; this.recv_suc = true; this.asr_response = new ASRResponse(); } asr_sync_connect ($form='') { console.log('初始化接受参数:',$form) if($form){ uni.setStorageSync('asytip',$form) }else{ $form = uni.getStorageSync('asytip') } const self = this; try { // 构造参数消息 const params_msg = self.construct_param(); // 创建 WebSocket 连接 self.socket = uni.connectSocket({ // url: 'wss://openspeech.bytedance.com/api/v2/asr', url: self.URL + "?api_jwt=" + self.token, header: { 'Authorization': 'Bearer; ' + self.token }, binaryType: 'arraybuffer', success: () => { console.log('WebSocket 连接成功'); }, fail: (err) => { console.error('WebSocket 连接失败', err); } }); console.log("1------------------------", self.socket); console.log("2------------------------", self.socket.readyState); // 监听WebSocket接收到的消息事件 self.socket.onMessage(function (res) { console.log('收到服务器消息', res,' whywhy:',$form); if (res.data instanceof ArrayBuffer) { const bytes = new Uint8Array(res.data); self.parse_response(bytes,$form) } // 处理接收到的消息 }); // 监听WebSocket连接打开事件 self.socket.onOpen(function () { console.log('WebSocket连接已打开!'); console.log("3------------------------", self.socket.readyState); self.socket.send({ data: params_msg.buffer, success: () => { console.log('请求头发送成功'); }, fail: (err) => { console.error('完整的客户端请求发送失败', err); uni.$emit("asrEvent_close", { data: err.errMsg }); } }); // 连接打开后的操作 }); // 监听WebSocket连接关闭事件 self.socket.onClose(function (res) { console.log('WebSocket已关闭!'); // 连接关闭后的操作 }); } catch (error) { console.error("Error in asr_sync_connect:", error); return false; } } async asr_send (audio, is_last) { try { const self = this; const payload = self.construct_audio_payload(audio, is_last); return new Promise((resolve) => { if (!self.socket || self.socket.readyState !== 1) { console.error("WebSocket is not open"); resolve(null); return; } self.socket.send({ data: payload.buffer, success: () => { // console.log('完整的客户端请求发送成功',connected); }, fail: (err) => { console.error('完整的客户端请求发送失败', err); } }); }); } catch (error) { console.error("Error in asr_send:", error); return null; } } asr_close () { console.log('inner asr_close'); uni.$emit("ws_send_ready", { data: false, event: '主动关闭' }); if (this.socket && this.socket.readyState === 1) { console.log('inner asr_closeasr_close'); this.socket.close(); } } gzip_compress (content) { // 使用 pako 库进行 GZIP 压缩 return pako.gzip(content); } gzip_decompress (content) { // 使用 pako 库进行 GZIP 解压缩 return pako.ungzip(content); } construct_param () { try { // 构造请求 ID const reqid = this.generateUUID(); // 构造 ASR 参数对象 const app = new App(this.appid, this.cluster, this.token); const user = new User(this.uid); const request = new Request(reqid, this.workflow, this.nbest, this.show_utterances, this.result_type, 1); const audio = new Audio(this.format, this.sample_rate, this.bits, this.channels); const asr_params = new ASRParams(app, user, request, audio); // 将对象序列化为 JSON 字符串 const json_data = JSON.stringify(asr_params); // 将字符串转换为字节数组 const bytes = []; for (let i = 0; i < json_data.length; i++) { const charCode = json_data.charCodeAt(i); bytes.push(charCode & 0xFF); // 获取字符的字节值(低 8 位) } // 创建 Int8Array const int8Array = new Int8Array(bytes); // 对数据进行 GZIP 压缩 // const compressed = this.gzip_compress(int8Array); const compressed = int8Array; // 构造消息头部 const header = new Int8Array(4); header[0] = (0b0001 << 4) | (4 >> 2); // 协议版本和头部长度 header[1] = (0b0001 << 4) | 0b0000; // 消息类型和序列号标志 // header[2] = (0b0001 << 4) | 0b0001; // 序列化方式和压缩方式 header[2] = (0b0001 << 4) | 0b0000; // 序列化方式和压缩方式 header[3] = 0; // 保留字节 // 构造 payload 长度部分 const payload_len = compressed.length; const pl_byte = new Int8Array(4); new DataView(pl_byte.buffer).setUint32(0, payload_len, false); // 大端模式 // 拼接所有部分 const header_part = new Int8Array(header.buffer); const pl_part = new Int8Array(pl_byte.buffer); const compressed_part = new Int8Array(compressed); return this.concat_byte(header_part, pl_part, compressed); } catch (error) { console.error("Error constructing param:", error); return new Int8Array(); } } parse_response (bytes,$form='') { try { var self = this; // 解析头部 const header_len = (bytes[0] & 0x0f) << 2; const message_type = (bytes[1] & 0xf0) >>> 4; const message_type_flag = bytes[1] & 0x0f; const message_serial = (bytes[2] & 0xf0) >>> 4; const message_compress = bytes[2] & 0x0f; let payload_offset = header_len; let payload_len = 0; let payload = null; if (message_type === 0b1001) { // FULL_SERVER_RESPONSE // 读取 payload 长度 const len_bytes = bytes.subarray(payload_offset, payload_offset + 4); payload_len = new DataView(len_bytes.buffer).getUint32(0, false); // 大端模式 payload_offset += 4; } else if (message_type === 0b1011) { // SERVER_ACK // 读取序列号 const seq_bytes = bytes.subarray(payload_offset, payload_offset + 4); const seq = new DataView(seq_bytes.buffer).getUint32(0, false); // 大端模式 payload_offset += 4; // 如果还有数据,读取 payload 长度 if (bytes.length > payload_offset + 4) { const len_bytes = bytes.subarray(payload_offset, payload_offset + 4); payload_len = new DataView(len_bytes.buffer).getUint32(0, false); // 大端模式 payload_offset += 4; } } else if (message_type === 0b1111) { // ERROR_MESSAGE_FROM_SERVER // 读取错误码 const error_code_bytes = bytes.subarray(payload_offset, payload_offset + 4); const error_code = new DataView(error_code_bytes.buffer).getUint32(0, false); // 大端模式 payload_offset += 4; // 读取 payload 长度 const len_bytes = bytes.subarray(payload_offset, payload_offset + 4); payload_len = new DataView(len_bytes.buffer).getUint32(0, false); // 大端模式 payload_offset += 4; } else { console.error("Unsupported message type:", message_type); self.asr_close(); return -1; } // 提取 payload 数据 payload = bytes.subarray(payload_offset, payload_offset + payload_len); // 解压缩 payload if (message_compress === 0b0001) { // GZIP try { payload = this.gzip_decompress(payload); } catch (error) { console.error("Failed to decompress payload:", error); self.asr_close(); return -1; } } // 反序列化 JSON 数据 if (message_serial === 0b0001) { // JSON try { // const payload_text = new TextDecoder().decode(payload); // 将 Uint8Array 转换为字符串 let jsonString = ''; for (let i = 0; i < payload.length; i++) { jsonString += String.fromCharCode(payload[i]); } jsonString = decodeURIComponent(escape(jsonString)); this.asr_response = JSON.parse(jsonString); // this.asr_response = JSON.parse(payload_text); } catch (error) { console.error("Failed to parse JSON payload:", error); self.asr_close(); return -1; } } // 检查响应码 console.error("ASR response code:", '--code:', this.asr_response.code, '--websokect status:', this.socket.readyState); if (this.asr_response.code == 1000 && this.asr_response.sequence == 1) { uni.$emit("ws_send_ready", { data: true, event: '1000' }); } else if (this.asr_response.code == 400) { uni.$emit("ws_send_ready", { data: false, event: '400' }); } else if (this.asr_response.code !== 1000) { uni.$emit("ws_send_ready", { data: false, event: '!=1000' }); console.error("ASR response error:", this.asr_response, '--code:', this.asr_response.code, '--websokect status:', this.socket.readyState); //连接数太多 let codeGroup = [ { code: 1000, message: '成功' }, { code: 1001, message: '请求参数无效' }, { code: 1002, message: '无访问权限' }, { code: 1003, message: '访问超频' }, { code: 1004, message: '访问超额' }, { code: 1005, message: '服务器繁忙' }, { code: 1008, message: '保留号段' }, { code: 1009, message: '保留号段' }, { code: 1010, message: '音频过长' }, { code: 1011, message: '音频过大' }, { code: 1012, message: '音频格式无效' }, { code: 1013, message: '音频静音' }, { code: 1014, message: '保留号段' }, { code: 1015, message: '保留号段' }, { code: 1016, message: '保留号段' }, { code: 1017, message: '保留号段' }, { code: 1018, message: '保留号段' }, { code: 1019, message: '保留号段' }, { code: 1020, message: '识别等待超时' }, { code: 1021, message: '识别处理超时' }, { code: 1022, message: '识别错误' }, { code: 1023, message: '保留号段' }, { code: 1024, message: '保留号段' }, { code: 1025, message: '保留号段' }, { code: 1026, message: '保留号段' }, { code: 1027, message: '保留号段' }, { code: 1028, message: '保留号段' }, { code: 1029, message: '保留号段' }, { code: 1030, message: '保留号段' }, { code: 1031, message: '保留号段' }, { code: 1032, message: '保留号段' }, { code: 1033, message: '保留号段' }, { code: 1034, message: '保留号段' }, { code: 1035, message: '保留号段' }, { code: 1036, message: '保留号段' }, { code: 1037, message: '保留号段' }, { code: 1038, message: '保留号段' }, { code: 1039, message: '保留号段' }, { code: 1099, message: '未知错误' }, { code: 400, message: '识别中' } ]; uni.$emit("asrEvent_close", { data: codeGroup.filter(item => { return item.code == self.asr_response.code })[0].message, code: self.asr_response.code }); self.asr_close(); return -1; } //最后一条消息,关闭ws if (this.asr_response['sequence'] < 0) { self.asr_close(); console.log("ASR response:", this.asr_response,' asr来源:',$form); uni.$emit('asrEvent_message', { data: JSON.stringify(self.asr_response) }); } return 0; } catch (error) { console.error("Error parsing response:", error); self.asr_close(); //开启webSocket return -1; } } generateHeader (options) { const header = new Int8Array(4); header[0] = (options.version << 4) | options.headerSize; header[1] = (options.messageType << 4) | options.flags; header[2] = (options.serialMethod << 4) | options.compression; header[3] = options.reserved; return header; } construct_audio_payload (audio, is_last) { try { // 构造消息头部 const header = new Int8Array(4); header[0] = (0b0001 << 4) | (4 >> 2); // 协议版本和头部长度 header[1] = is_last ? (0b0010 << 4) | 0b0010 // AUDIO_ONLY_CLIENT_REQUEST 和 NEGATIVE_SEQUENCE_SERVER_ASSGIN : (0b0010 << 4) | 0b0000; // AUDIO_ONLY_CLIENT_REQUEST 和 NO_SEQUENCE_NUMBER header[2] = (0b0001 << 4) | 0b0001; // 序列化方式和压缩方式 header[2] = (0b0001 << 4) | 0b0000; // 序列化方式和压缩方式 header[3] = 0; // 保留字节 // 对音频数据进行 GZIP 压缩 // const compressed_audio = this.gzip_compress(audio); const compressed_audio = audio; console.log(is_last, audio, compressed_audio) // 构造 payload 长度部分 const payload_len = compressed_audio.length; const pl_byte = new ArrayBuffer(4); new DataView(pl_byte).setUint32(0, payload_len, false); // 大端模式 // 拼接所有部分 const header_part = new Int8Array(header.buffer); const pl_part = new Int8Array(pl_byte); const compressed_part = new Int8Array(compressed_audio); return this.concat_byte(header_part, pl_part, compressed_part); } catch (error) { console.error("Error constructing audio payload:", error); return new Int8Array(); } } generateUUID () { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } concat_byte (...arrays) { const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0); const result = new Int8Array(totalLength); let offset = 0; for (const arr of arrays) { result.set(arr, offset); offset += arr.length; } return result; } // 设置各种参数的方法 setAppid (appid) { this.appid = appid; } setToken (token) { this.token = token; } setSk (sk) { this.sk = sk; } setCluster (cluster) { this.cluster = cluster; } setWorkflow (workflow) { this.workflow = workflow; } setUid (uid) { this.uid = uid; } setShow_utterances (show_utterances) { this.show_utterances = show_utterances; } setResult_type (result_type) { this.result_type = result_type; } setFormat (format) { this.format = format; } setSample_rate (sample_rate) { this.sample_rate = sample_rate; } setChannels (channels) { this.channels = channels; } setBits (bits) { this.bits = bits; } setAuthType (authType) { this.authType = authType; } getAsrResponse () { return this.asr_response; } } export class ASRResponse { constructor () { this.reqid = "unknown"; this.code = 0; this.message = ""; this.sequence = 0; this.result = []; this.addition = new Addition(); } } export class Result { constructor () { this.text = ""; this.confidence = 0; this.language = ""; this.utterances = []; this.global_confidence = 0; } } export class Utterances { constructor () { this.text = ""; this.start_time = 0; this.end_time = 0; this.definite = false; this.language = ""; this.words = []; } } export class Words { constructor () { this.text = ""; this.start_time = 0; this.end_time = 0; this.blank_duration = 0; } } export class Addition { constructor () { this.duration = ""; } } export class ASRParams { constructor (app, user, request, audio) { this.app = app; this.user = user; this.request = request; this.audio = audio; } } export class App { constructor (appid, cluster, token) { this.appid = appid; this.cluster = cluster; this.token = token; } } export class User { constructor (uid) { this.uid = uid; } } export class Request { constructor (reqid, workflow, nbest, show_utterances, result_type, sequence) { this.reqid = reqid; this.workflow = workflow; this.nbest = nbest; this.show_utterances = show_utterances; this.result_type = result_type; this.sequence = sequence; } } export class Audio { constructor (format, rate, bits, channels) { this.format = format; this.rate = rate; this.bits = bits; this.channels = channels; } } export default { AsrClient }