在之前文章《腾讯IM web版本实现迅飞语音听写(流式版)》实现了腾讯IM web版本实现迅飞语音听写,本文将基于uniapp vue2/vue3(cli 脚手架)的Demo项目集成迅飞语音听写(流式版):
主要代码:
// \src\TUIKit\components\TUIChat\message-input\index.vue
<template>
<!-- 录音按钮 -->
<VoiceToText @change="changeVoiceToText"/>
<!-- 输入框 -->
<MessageInputEditor ref="editor"/>
</template>
// voice-to-text/index.vue
<template>
<div v-if="!openShow" class="message-input-asr" @click="openMedia" />
<div v-if="openShow" class="message-input-asr" :class="{active: openShow}" @click="stopMedia" />
</template>
<script setup>
import { onMounted, ref } from "vue";
import ChatApi from '@/api/chat.js'
const emits = defineEmits(['change'])
const openShow = ref(false)
// 发给科大讯飞的每一帧的定义
//参考接口调用流程: https://www.xfyun.cn/doc/asr/voicedictation/API.html#%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B
const FRAME = {
STATUS_FIRST_FRAME: 0, //第一帧音频
STATUS_CONTINUE_FRAME: 1, //中间的音频
STATUS_LAST_FRAME: 2, //最后一帧音频,最后一帧必须要发送
};
let status = FRAME.STATUS_FIRST_FRAME;
//心跳定时器
let heartTimer = null;
// 心跳数据(发给科大讯飞的数据)发送队列
let sendDataStack = [];
//websocket实例化
let uniSocketTask = null;
//打开麦克风
function openMedia() {
status = FRAME.STATUS_FIRST_FRAME; //初始化音频状态(设置当前录音状态为首帧)
sendDataStack = []
stopMedia()
connectSocket(); //创建websocket连接
startRecord(); //打开录音
}
//关闭麦克风
function stopMedia() {
//关闭录音
stopRecord();
//为什么关闭录音的时候不直接关闭websocket连接,这是因为:
//关闭麦克风的时候会回调出录音最后一帧,并且会触发监听录音结束事件(也不是在这个地方结束)。
//发送最后一帧后,得到最后一帧返回的消息后,此时此刻才应该关闭websocket连接
//但是可以关闭心跳定时器
clearInterval(heartTimer);
//初始化已经渲染了的数据
renderText.value = "";
resultText = "";
resultTextTemp = "";
}
/**
* 获取麦克风权限并开始录音(步骤一)
*/
// 录音配置项
const recordOption = {
duration: 60000, //指定录音的时长,单位 ms,微信可设置的最大值为 600000(10 分钟),科大讯飞最高支持时间为 60000 (1分钟)
sampleRate: 16000, // 采样率(pc不支持)
numberOfChannels: 1.25, // 录音通道数
// encodeBitRate: 48000, // 编码码率(默认就是48000)
// frameSize 为指定帧大小,单位 KB。传入 frameSize 后,每录制指定帧大小的内容后,会回调录制的文件内容,不指定则不会回调。
// 暂仅支持 mp3、pcm 格式。科大讯飞建议:每40ms发送一个节数为1280B的为压缩PCM格式
frameSize: 1,
format: "pcm", // 音频格式,默认是 aac
};
let recordManager = null;
//开启录音
const startRecord = () => {
openShow.value = true
recordManager = uni.getRecorderManager();
recordManager.onStart(() => {
console.log("开始录音");
// ...
});
// recordManager.onPause(() => {
// console.log("录音暂停");
// });
recordManager.onStop((res) => {
// tempFilePath String 录音文件的临时路径
console.log("录音停止,文件路径为:", res.tempFilePath);
});
recordManager.onError((err) => {
// errMsg String 错误信息
console.log("录音出现错误", err);
});
//获得录音结果的回调函数
recordManager.onFrameRecorded((res) => {
// console.log("开始产生录音结果:");
// frameBuffer: type:[ArrayBuffer] 录音分片结果数据
// isLastFrame: type:[Boolean] 当前帧是否正常录音结束前的最后一帧
const { frameBuffer, isLastFrame } = res;
// console.log("录音分片结果:", toBase64(frameBuffer));
// console.log("开始判断当前帧为第几帧");
// 判断当前录音数据是否为最后一帧
if (isLastFrame) {
console.log("当前帧为录音最后一帧");
status = FRAME.STATUS_LAST_FRAME;
}
//存入心跳数据栈中,再合适的时候发送给科大讯飞(第四步)
sendDataStack.push(packFrame(frameBuffer)); //入栈
});
recordManager.start(recordOption);
};
//关闭录音
const stopRecord = () => {
recordManager?.stop();
openShow.value = false
};
//音频转码(步骤二)[把音频片段转换成base64]
function toBase64(buffer) {
return uni.arrayBufferToBase64(buffer);
}
//创建连接并返回数据
async function connectSocket() {
const res = await ChatApi.getXFVoiceAuthUrl()
uniSocketTask = uni.connectSocket({
url: res.url,
success() {},
});
//监听连接成功的事件
uniSocketTask.onOpen(() => {
console.log("监听到开启连接成功");
//启动心跳定时器
onHeartBeat();
});
//监听连接关闭的事件
uniSocketTask.onClose(() => {
console.log("监听到关闭连接成功");
uniSocketTask = null;
});
uniSocketTask.onError(() => {
console.log("监听到连接发生错误");
});
//监听科大讯飞消息返回
uniSocketTask.onMessage((res) => {
//收到消息
const message = JSON.parse(res.data);
//判断是否存在数据
if (res.data) {
console.log("收到服务器消息,并开始渲染");
renderResult(message);
if (message.code === 0 && message.data.status === 2) {
//此时此刻我们可以得知当前科大讯飞返回的数据为最后一帧可以关闭连接
//该函数为当前页唯一的关闭连接函数
closeSocket();
}
//收到不正常服务器消息,返回错误到控制台
if (message.code !== 0) {
closeSocket();
console.error(message);
}
} else {
console.log("未监听到消息:原因:", JSON.stringify(res));
}
});
}
//关闭连接
function closeSocket() {
console.log("开始尝试关闭连接");
// 关闭心跳
if (heartTimer) {
clearInterval(heartTimer);
}
// 关闭连接
uniSocketTask.close();
}
//发送给科大讯飞的每一帧的模板数据格式
let frame = {
common: {
app_id: '自己的appid',
},
business: {
language: "zh_cn",
domain: "iat",
accent: "mandarin",
dwa: "wpgs", // 可选参数,动态修正
vad_eos: 5000,
},
data: {
status: 0,
format: "audio/L16;rate=16000",
encoding: "raw",
audio: "", //音频内容
},
};
//封装录音后的每一"帧"数据(步骤三)
function packFrame(audioData) {
//转载音频数据
frame.data.audio = toBase64(audioData);
switch (status) {
//首条空白消息(第一帧)
case FRAME.STATUS_FIRST_FRAME:
// console.log("当前帧为第一帧");
frame.data.status = 0;
status = FRAME.STATUS_CONTINUE_FRAME;
break;
//正文(中间帧)
case FRAME.STATUS_CONTINUE_FRAME:
// console.log("当前帧为中间帧");
frame.data.status = 1;
break;
//传输结束(最后一帧)
case FRAME.STATUS_LAST_FRAME:
// console.log("当前帧为最后一帧");
frame.data.status = 2;
break;
}
return JSON.stringify(frame);
}
//启动心跳连接定时器,每40ms发送一次数据(第五步发送数据)
function onHeartBeat() {
let sendData = null; //发送给科大讯飞的每一帧数据
heartTimer = setInterval(() => {
//发送队列中有数据的时候执行下述逻辑发送数据,否则不执行下述函数
console.log("当前发送队列中数据的长度为", sendDataStack.length);
if (sendDataStack.length !== 0) {
sendData = sendDataStack.shift();
uniSocketTask.send({
data: sendData,
success() {
// console.log("发送成功");
},
fail() {
console.log("发送失败");
},
});
}
}, 40);
}
//根据科大讯飞语音听写识别结果进行渲染(最后一步,根据onMessage()方法返回的数据进行渲染)
let resultText = "";
let resultTextTemp = "";
let renderText = ref("");
function renderResult(jsonData) {
// console.log("开始执行渲染函数", 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;
}
// 开启wpgs会有此字段(前提:在控制台开通动态修正功能)
// 取值为 "apd"时表示该片结果是追加到前面的最终结果;取值为"rpl" 时表示替换前面的部分结果,替换范围为rg字段
if (data.pgs) {
if (data.pgs === "apd") {
// 将resultTextTemp同步给resultText
resultText = resultTextTemp;
}
// 将结果存储在resultTextTemp中
resultTextTemp = resultText + str;
} else {
resultText = resultText + str;
}
renderText.value = resultTextTemp || resultText || "";
}
console.log("渲染后的数据为");
console.log(renderText.value);
emits.$emit('change', {
content: renderText.value,
})
}
</script>