|
|
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
|
|
|
} |