You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
apply-assistant-v3/root/utils/AsrClient.js

591 lines
18 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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
}