在移动应用开发中,语音交互功能能够极大提升用户体验,让操作更加便捷自然。讯飞语音听写技术凭借其高准确率和稳定性,成为众多开发者的选择。本文将详细介绍如何在 Uniapp 项目中,实现安卓端的讯飞语音听写功能,帮助你快速为应用增添实用的语音交互能力。
但是,由于uniapp录音管理器 uni.getRecorderManager()的实时监听音频大小帧的功能onFrameRecorded不支持app,app端只能在录音结束后获取临时录音文件,因此需要把录音文件转成base64,在切片传输给讯飞的接口。
一、准备工作
1. 注册讯飞开放平台账号
首先,你需要前往讯飞开放平台注册账号,完成实名认证。认证通过后,你将获得使用讯飞相关服务的权限。
2. 创建应用并获取 AppID 和密钥
在讯飞开放平台控制台中,创建一个新的应用。创建成功后,你会得到该应用的 AppID、AppKey 和 AppSecret,这些信息在后续集成过程中至关重要,用于验证应用身份。
二、实现代码
以下是已经实现的uniapp在app端的一个简单的利用讯飞语音听写api完成的语音识别的demo,复制代码到你的项目中,把你申请的appid、apiSecret、apiKey替换到代码中,即可运行识别。
<template>
<div class="asr">
<button class="btn" shape="circle" type="info" @touchstart="openMedia" @touchend="stopMedia">按住说话</button>
<view v-if="show" class="iating">
<text text="正在说话中...." color="#49ABFE" size="27rpx" line-height="60rpx" align="center"></text>
</view>
</div>
</template>
<script>
import CryptoJS from "crypto-js";
export default {
data() {
return {
config: {
hostUrl: "wss://iat-api.xfyun.cn/v2/iat",
host: "iat-api.xfyun.cn",
appid: "申请的讯飞appid",
apiSecret: "申请的讯飞apiSecret",
apiKey: "申请的apiKey",
uri: "/v2/iat",
highWaterMark: 1280,
},
uniSocketTask: null,
show: false,
resultText: "",
resultTextTemp: "",
renderText: ""
};
},
methods: {
// 鉴权签名
getAuthStr(date) {
let signatureOrigin = `host: ${this.config.host}\ndate: ${date}\nGET ${this.config.uri} HTTP/1.1`;
let signatureSha = CryptoJS.HmacSHA256(signatureOrigin, this.config.apiSecret);
let signature = CryptoJS.enc.Base64.stringify(signatureSha);
let authorizationOrigin =
`api_key="${this.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;
},
getUrl() {
let date = new Date().toUTCString();
let wssUrl =
this.config.hostUrl +
"?authorization=" +
this.getAuthStr(date) +
"&date=" +
encodeURIComponent(date) +
"&host=" +
this.config.host;
console.log("websocke科大讯飞的地址为", wssUrl);
return wssUrl;
},
// 打开麦克风
openMedia() {
this.connectSocket();
},
// 创建连接并返回数据
connectSocket() {
if (this.uniSocketTask === null) {
this.uniSocketTask = uni.connectSocket({
url: this.getUrl(),
success() {},
});
this.uniSocketTask.onOpen(() => {
console.log("监听到开启连接成功");
this.startRecord();
});
this.uniSocketTask.onClose(() => {
console.log("监听到关闭连接成功");
this.uniSocketTask = null;
});
this.uniSocketTask.onError(() => {
console.log("监听到连接发生错误");
});
this.uniSocketTask.onMessage((res) => {
const message = JSON.parse(res.data);
if (res.data) {
console.log("收到服务器消息,并开始渲染", message);
this.renderResult(message);
if (message.code === 0 && message.data.status === 2) {
console.log('最后一条', this.renderText);
this.closeSocket();
// 注意:这里需要根据实际情况处理事件触发
// this.$emit('renderText', this.renderText);
}
if (message.code !== 0) {
this.closeSocket();
console.error(message);
}
} else {
console.log("未监听到消息:原因:", JSON.stringify(res));
}
});
} else {
console.log("socketTask实例已存在");
}
},
// 发送给科大讯飞的第一帧的模板数据格式
getInitialFrame() {
return {
common: {
app_id: this.config.appid,
},
business: {
language: "zh_cn",
domain: "iat",
accent: "mandarin",
dwa: "wpgs", // 可选参数,动态修正
vad_eos: 5000,
},
data: {
status: 0,
format: "audio/L16;rate=16000",
encoding: "lame"
},
};
},
// 发送消息
sendMessage(sendData) {
console.log('发送', JSON.stringify(sendData));
this.uniSocketTask.send({
data: JSON.stringify(sendData),
success() {},
fail() {
console.log("发送失败");
},
});
},
// 关闭连接
closeSocket() {
console.log("开始尝试关闭连接");
this.uniSocketTask.close();
},
// 开启录音
startRecord() {
const recordOption = {
sampleRate: 16000,
format: "mp3",
};
const recordManager = uni.getRecorderManager();
recordManager.onStart(() => {
console.log("开始录音");
this.show = true;
});
recordManager.onStop((res) => {
console.log("录音停止,文件路径为:", res.tempFilePath);
this.sendMessage(this.getInitialFrame());
this.pathToBase64(res.tempFilePath).then(base64 => {
let buff = base64.split(",")[1];
const arrayBuffer = uni.base64ToArrayBuffer(buff);
const audioString = this.toString(arrayBuffer);
console.log("文件读取成功", audioString.length);
let offset = 0;
while (offset < audioString.length) {
const subString = audioString.substring(offset, offset + 1280);
offset += 1280;
const isEnd = offset >= audioString.length;
let params = {
data: {
status: isEnd ? 2 : 1,
format: "audio/L16;rate=16000",
encoding: "lame",
audio: btoa(subString)
}
};
this.sendMessage(params);
}
}).catch(error => {
console.error(error);
});
});
recordManager.onError((err) => {
console.log("录音出现错误", err);
});
recordManager.start(recordOption);
},
// 关闭录音
stopMedia() {
const recordManager = uni.getRecorderManager();
recordManager.stop();
this.show = false;
},
// 工具方法
toString(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return binary;
},
// 录音文件路径转base64
pathToBase64(path) {
return new Promise((resolve, reject) => {
if (typeof plus === 'object') {
plus.io.resolveLocalFileSystemURL(path, function(entry) {
entry.file(function(file) {
var fileReader = new plus.io.FileReader();
fileReader.onload = function(evt) {
resolve(evt.target.result);
};
fileReader.onerror = function(error) {
reject(error);
};
fileReader.readAsDataURL(file);
}, function(error) {
reject(error);
});
}, function(error) {
reject(error);
});
return;
}
reject(new Error('not support'));
});
},
// 讯飞回复字段拼接
renderResult(jsonData) {
if (jsonData.data && jsonData.data.result) {
let data = jsonData.data.result;
let str = "";
let ws = data.ws;
for (let i = 0; i < ws.length; i++) {
str = str + ws[i].cw[0].w;
}
if (data.pgs) {
if (data.pgs === "apd") {
this.resultText = this.resultTextTemp;
}
this.resultTextTemp = this.resultText + str;
} else {
this.resultText = this.resultText + str;
}
this.renderText = this.resultTextTemp || this.resultText || "";
}
console.log("渲染后的数据为", this.renderText);
}
}
}
</script>
<style scoped>
.asr {
display: flex;
justify-content: center;
padding: 20rpx;
}
.btn {
width: 200rpx;
height: 200rpx;
}
.iating {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
background-color: rgba(0, 0, 0, 0.7);
padding: 30rpx;
border-radius: 20rpx;
z-index: 999;
}
</style>