由于公司业务需求需要实现在微信小程序中实现语音测评功能,因为之前在H5中已经实现了该功能认为在小程序中问题不大,但是在实际开发中遇到了不少坑。
- 问题一:语音测评流式版在微信小程序中无法正确的传输数据并获取返回值
在H5实现时使用 new WebSocket() 实例进行传输数据 并成功的拿到了测评的数据
ws = new WebSocket(url)
但是在小程序中 wx.connectSocket() 无法将数据流正常的传给科大讯飞的api
wx.connectSocket()
解决方案:使用微信录音功能将录音文件上传服务器,在通过服务端调用科大讯飞api进行语音测评得到结果后返回给前端
小程序代码如下:
// 将数据发送到服务器
async _sendMp3(path) {
this.data.toast.linShow({
icon: "loading",
title: '解析中请稍后',
duration: 11111111,
mask: true
})
wx.uploadFile({
url: `${nodeServer}/xls/postData`, // node 服务接口
filePath: path, // 录音文件临时地址
header: {
"Content-Type": "multipart/form-data" //必须是这个格式
},
formData: {
text: this.data.XLSpeakerTitle // 要对比的文本
},
name: "mp3",
success: async (res) => {
const {
Data,
Code
} = JSON.parse(res.data);
if (Code == 200) {
const score = parseInt(Data.score)
this.setData({
showScore: false,
score,
});
// 开始动画
this.countUp = new WxCountUp('score', score, {}, this);
this.countUp.start();
this._AddSpeakItem(score, Data.path);
// 假如当前得分超过了历史最高分 将历史最高分更新
this._UpDateMax(score)
}
},
fail: () => {
this.data.toast.linShow({
title: '解析失败请,请稍后再试',
})
},
complete: () => {
this.data.toast.linHide()
}
})
},
node js 服务端代码如下:
const router = require("koa-router")();
const { _init } = require("../ws/index");
const { put } = require("../ali-oss/index");
const { removeFile } = require("../fs");
const { SuccessModel, ErrorModel } = require("../model/resModel");
router.prefix("/xls");
router.post("/postData", async (ctx, next) => {
const { text } = ctx.request.body;
// 得到mp3 文件
const path = ctx.request.files.mp3.path;
// 得到对比分数
const { Code, Data, Msg } = await _init(path, text);
// 将上传的mp3 文件上传到 ali-oss
const mp3path = await put(path);
// 将上传的文件删除 防止上传的录音文件越来越多 导致服务器内存压力
removeFile(path);
if (Code == 0) {
ctx.body = new SuccessModel({
score: Data,
path: mp3path.url.split("aliyuncs.com/")[1],
});
} else {
ctx.body = new ErrorModel(Code, Msg ? Msg : "分数解析失败,请稍后再试");
}
});
科大讯飞语音测评
const CryptoJS = require("crypto-js");
const WebSocket = require("ws");
var fs = require("fs");
const APPID = "xxxxxxxxx";
const API_SECRET = "xxxxxxxxxx";
const API_KEY = "xxxxxxxxxx";
// 系统配置
let xmlParser = require("../util/parser");
const config = {
// 请求地址
hostUrl: "wss://ise-api.xfyun.cn/v2/open-ise",
host: "iat-api.xfyun.cn",
//在控制台-我的应用-语音评测(流式版)获取
appid: APPID,
//在控制台-我的应用-语音评测(流式版)获取
apiSecret: API_SECRET,
//在控制台-我的应用-语音评测(流式版)获取
apiKey: API_KEY,
file: "./read_sentence_cn.pcm", //请填写您的音频文件路径
uri: "/v2/open-ise",
highWaterMark: 1280,
};
// 帧定义
const FRAME = {
STATUS_FIRST_FRAME: 0,
STATUS_CONTINUE_FRAME: 1,
STATUS_LAST_FRAME: 2,
};
// 设置当前临时状态为初始化
let status = FRAME.STATUS_FIRST_FRAME;
let ws;
const _init = (path, text) => {
// 一定要初始化状态
status = FRAME.STATUS_FIRST_FRAME;
// 合成websocket接口,一次请求成功,未中断链接情况下,再次发送新请求导致 10140 错误
const url = getWsUrl();
console.log("url-----------", url);
ws = new WebSocket(url);
return new Promise((resolve, reject) => {
// 连接建立完毕,读取数据进行识别
ws.on("open", (event) => {
console.log("websocket connect!");
// config.file
var readerStream = fs.createReadStream(path, {
highWaterMark: config.highWaterMark,
});
readerStream.on("data", function (chunk) {
send(chunk, text);
});
// 最终帧发送结束
readerStream.on("end", function () {
status = FRAME.STATUS_LAST_FRAME;
send("");
});
});
// 得到识别结果后进行处理,仅供参考,具体业务具体对待
ws.on("message", (data, err) => {
if (err) {
ws.close();
return;
}
res = JSON.parse(data);
if (res.code != 0) {
ws.close();
// reject(`error code ${res.code}, reason ${res.message}`);
reject({ Code: res.code, Msg: res.message });
return;
}
if (res.data.status == 2) {
const { data } = res.data;
let b = new Buffer(data, "base64");
let iseResult = `最终识别结果: ${b.toString()}`;
const scroeData = xmlParser.parse(iseResult, {
attributeNamePrefix: "",
ignoreAttributes: false,
});
resolve({
Code: 0,
Data:
scroeData.xml_result.read_sentence.rec_paper.read_sentence
.total_score,
});
}
});
// 资源释放
ws.on("close", () => {
console.log("connect close!-------------------------");
});
// 建连错误
ws.on("error", (err) => {
reject("websocket connect err: " + err);
console.log("websocket connect err: " + err);
});
});
};
// 获取长连接地址
function getWsUrl() {
// 获取当前时间 RFC1123格式
let date = new Date().toUTCString();
const wssUrl =
config.hostUrl +
"?authorization=" +
getAuthStr(date) +
"&date=" +
date +
"&host=" +
config.host;
return wssUrl;
}
// 传输数据
function send(data, text = "今天天气怎么样") {
let frame = "";
switch (status) {
case FRAME.STATUS_FIRST_FRAME:
// 第一次数据发送:
frame = {
common: { app_id: config.appid },
business: {
// 服务类型指定 ise(开放评测)
sub: "ise",
// 中文:cn_vip 英文:en_vip
ent: "cn_vip",
// 题型:句子朗读
category: "read_sentence",
// 待评测文本编码 utf-8
text: `\uFEFF${text}`,
// 待评测文本编码 utf-8 gbk
tte: "utf-8",
// 跳过ttp直接使用ssb中的文本进行评测(使用时结合cmd参数查看),默认值true
ttp_skip: true,
cmd: "ssb",
aue: "lame",
auf: "audio/L16;rate=16000",
},
data: { status: 0 },
};
ws.send(JSON.stringify(frame));
// 后续数据发送
frame = {
common: { app_id: config.appid },
business: { aus: 1, cmd: "auw", aue: "lame" },
data: { status: 1, data: data.toString("base64") },
};
status = FRAME.STATUS_CONTINUE_FRAME;
break;
case FRAME.STATUS_CONTINUE_FRAME:
frame = {
common: { app_id: config.appid },
business: { aus: 2, cmd: "auw", aue: "lame" },
data: { status: 1, data: data.toString("base64") },
};
break;
case FRAME.STATUS_LAST_FRAME:
frame = {
common: { app_id: config.appid },
business: { aus: 4, cmd: "auw", aue: "lame" },
data: { status: 2, data: data.toString("base64") },
};
break;
}
ws.send(JSON.stringify(frame));
}
// 鉴权签名
function getAuthStr(date) {
let signatureOrigin = `host: ${config.host}\ndate: ${date}\nGET ${config.uri} HTTP/1.1`;
let signatureSha = CryptoJS.HmacSHA256(signatureOrigin, config.apiSecret);
let signature = CryptoJS.enc.Base64.stringify(signatureSha);
let authorizationOrigin = `api_key="${config.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
let authStr = CryptoJS.enc.Base64.stringify(
CryptoJS.enc.Utf8.parse(authorizationOrigin)
);
return authStr;
}
module.exports = { _init };
2.问题2:文本朗读功能,刚开始做的时候我们是使用科大讯飞的语音合成功能,由于科大讯飞是收费的 推荐大家使用微信小程序同声传译插件很好用而且是免费,如果日使用量很大的话可以申请大额数量,缺点是朗读的文字有大小限制,我测试了下大概是200 字左右,如果超过两百字需要手动截取,分批合成
WechatSI 使用微信同声传译插件
WechatSI.textToSpeech({
lang: "zh_CN",
tts: true,
content: this.data.XLSpeakerTitle,
success: (res) => {
this.data.WechatSIStatus = "success"
this._palyBGAduio(res.filename, "演示")
},
fail: (err) => {
this.setData({
isPlay: false,
isPlayDemo: false
})
this.data.toast.linShow({
title: err.msg,
})
}
})
3.问题三实现最高分回放功能,这里我的解决方案是在解决方案一的的同时,将上传到服务器的录音文件同时上传到阿里云OSS 返回给前端阿里云的图片地址和当前语音的测评分数,前端判断当前得分是否超过最高分,如果超过最高分将最高分更新
阿里云OSS 上传如下:
const OSS = require("ali-oss");
const client = new OSS({
bucket: "xlxtimg",
// region以杭州为例(oss-cn-hangzhou),其他region按实际情况填写。
region: "oss-cn-beijing",
// 阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录RAM控制台创建RAM账号。
accessKeyId: "xxxxxxxx",
accessKeySecret: "xxxxxxxxxxxxxxxxxx",
});
async function put(file) {
try {
//object-name可以自定义为文件名(例如file.txt)或目录(例如abc/test/file.txt)的形式,实现将文件上传至当前Bucket或Bucket下的指定目录。
let result = await client.put(`xlsLee/${file}`, file);
return result;
} catch (e) {
console.log(e);
}
}
module.exports = { put };
以上就是我实现微信小程序进行语音测评 并将语音文件长传阿里云的实践过程