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

3 months ago
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
}