基于 Vue3 + Vite5,使用科大讯飞 ASR 实现语音转文字
语音听写流式接口,用于1分钟内的即时语音转文字技术,支持实时返回识别结果,达到一边上传音频一边获得识别文本的效果。
官方文档
环境准备
- 开发环境:
Node.js v20.10.0
+npm v10.2.3
- 开发工具:
WebStorm 2024.1.7
- 源代码管理:
Git
- npm镜像:
https://registry.npmmirror.com/
一、快速开始
使用 npm
包管理工具快速创建一个项目
npm create vue@latest
项目名称为 xf-asr
,其它功能暂时都不需要,可以直接回车。进入项目目录,执行 npm install
安装依赖,执行 npm run dev
启动项目。
执行 git init
初始化 git 仓库(个人习惯)方便管理,并执行 git add .
,在执行 git commit -m 'init'
二、下载 SDK
在官方文档中找到语音听写流式API demo js语言
点击下载,里面包含了我们需要用到的录音管理器,以及语音识别的代码。
解压刚才下载好的 iat-js-demo.zip
文件,将里面的 dist
文件夹复制到项目的 public
目录下,并改名为 asr-sdk
。
在终端执行 npm install crypto-js
安装crypto-js
包,用于加密。
三、引入 SDK
修改 index.html
文件,其它位置不用修改
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<script src="/asr-sdk/index.umd.js"></script> <!-- 引入 SDK -->
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
四、封装 useXfAsr 语音识别插件
在项目 src
目录下创建一个 hooks
目录,在该目录下创建一个 useXfAsr.js
文件,并添加以下代码:
import {computed, ref} from "vue";
import CryptoJS from "crypto-js";
// TODO 自己去讯飞官网获获取 apiKey、apiSecret、app_id 等信息
const apiKey = "xxx";
const apiSecret = "xxx";
const app_id = "xxx";
/**
* 获取websocket url
* 该接口需要后端提供,这里为了方便前端处理
*/
function getWebSocketUrl() {
const url = "wss://iat-api.xfyun.cn/v2/iat";
const host = "iat-api.xfyun.cn";
const date = new Date().toUTCString();
const signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`;
const signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
const signature = CryptoJS.enc.Base64.stringify(signatureSha);
const authorizationOrigin = `api_key="${apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
const authorization = btoa(authorizationOrigin);
return `${url}?authorization=${authorization}&date=${date}&host=${host}`;
}
/**
* 将音频二进制数据转换为base64编码
* @param buffer
* @returns {string}
*/
function bufferToBase64(buffer) {
let binary = "";
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i ++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
export function useXfAsr() {
const resultText = ref(); // 识别结果
let resultTextTemp = "";
let iatWS = null; // websocket
let countdownInterval = null; // 倒计时
const nextTime = ref(60); // 录音时长(最大60秒)
const recorder = new RecorderManager("/asr-sdk");
const recordStatus = ref("CLOSED"); // CONNECTING | OPEN | CLOSING | CLOSED
const recordText = computed(() => {
if (recordStatus.value === "CONNECTING") {
return "建立连接中";
} else if (recordStatus.value === "OPEN") {
return `录音中(${nextTime.value})`;
} else if (recordStatus.value === "CLOSING") {
return "关闭连接中";
} else if (recordStatus.value === "CLOSED") {
return "开始录音";
}
});
/**
* 录音开始事件
*/
recorder.onStart = () => {
updateStatus("OPEN");
};
/**
* 监听已录制完指定帧大小的文件事件。如果设置了 frameSize,则会回调此事件。
* @param isLastFrame 当前帧是否正常录音结束前的最后一帧
* @param frameBuffer 录音分片数据
*/
recorder.onFrameRecorded = ({ isLastFrame, frameBuffer }) => {
if (iatWS.readyState === iatWS.OPEN) {
const data = {
data: {
// 0 :第一帧音频、1 :中间的音频、2 :最后一帧音频,最后一帧必须要发送
status: isLastFrame ? 2 : 1,
format: "audio/L16;rate=16000",
encoding: "raw",
audio: bufferToBase64(frameBuffer),
},
};
iatWS.send(JSON.stringify(data));
if (isLastFrame) {
updateStatus("CLOSING");
}
}
};
/**
* 录音结束事件
*/
recorder.onStop = () => {
clearInterval(countdownInterval);
};
/**
* 倒计时
*/
function countdown() {
nextTime.value = 60;
countdownInterval = setInterval(() => {
nextTime.value --;
if (nextTime.value <= 0) {
clearInterval(countdownInterval);
recorder.stop();
}
}, 1000);
}
/**
* 更新状态
* @param status CONNECTING | OPEN | CLOSING | CLOSED
*/
function updateStatus(status) {
recordStatus.value = status;
if (status === "OPEN") {
countdown();
} else if (status === "CONNECTING") {
resultText.value = "";
resultTextTemp = "";
}
}
/**
* 渲染识别结果
* @param resultData
*/
function renderResult(resultData) {
let jsonData = JSON.parse(resultData);
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.value = resultTextTemp;
}
// 将结果存储在resultTextTemp中
resultTextTemp = resultText.value + str;
} else {
resultText.value = resultText.value + str;
}
}
if (jsonData.code === 0 && jsonData.data.status === 2) {
iatWS.close();
}
if (jsonData.code !== 0) {
iatWS.close();
console.error(jsonData);
}
}
/**
* 开始录音
*/
function startRecording() {
if (recordStatus.value !== "CLOSED") return;
const url = getWebSocketUrl();
if ("WebSocket" in window) {
iatWS = new WebSocket(url);
} else if ("MozWebSocket" in window) {
iatWS = new MozWebSocket(url);
} else {
console.error(new Error("浏览器不支持WebSocket"));
return;
}
updateStatus("CONNECTING");
iatWS.onopen = (e) => {
recorder.start({ sampleRate: 16000, frameSize: 1280 });
const params = {
common: { app_id },
business: { language: "zh_cn", domain: "iat", accent: "mandarin", vad_eos: 5000, dwa: "wpgs" },
data: { status: 0, format: "audio/L16;rate=16000", encoding: "raw" },
};
iatWS.send(JSON.stringify(params));
};
iatWS.onmessage = (e) => {
renderResult(e.data);
};
iatWS.onerror = (e) => {
recorder.stop();
updateStatus("CLOSED");
};
iatWS.onclose = (e) => {
recorder.stop();
updateStatus("CLOSED");
};
}
/**
* 停止录音
*/
function stopRecording() {
recorder.stop();
}
return {
resultText,
recordText,
startRecording,
stopRecording,
};
}
五、插件使用
修改 src/App.vue
文件,添加如下代码:
<script setup>
import {useXfAsr} from "@/hooks/useXfAsr.js";
const { startRecording, stopRecording, recordText, resultText } = useXfAsr();
</script>
<template>
<div class="app-container">
<h1>识别结果:{{ resultText }}</h1>
<button @click="startRecording">{{ recordText }}</button>
<button @click="stopRecording">停止录音</button>
</div>
</template>
六、心得体会
在这里,作者用最简洁的代码调用讯飞语音识别,通过封装 useXfAsr
函数,可以很方便的项目的任何地方进行调用。
由于讯飞 SDK 的要求,这里的语音时长最大为 60 秒,所以,如果用户在录音过程中,超过 60 秒,则需要重新开始录音。
通过接口密钥基于 hmac-sha256
计算签名,向服务器端发送 Websocket
协议握手请求。握手成功后,客户端通过 Websocket
连接同时上传和接收数据。数据上传完毕,客户端需要上传一次数据结束标识。接收到服务器端的结果全部返回标识后断开 Websocket
连接。
具体的细节请参考官方文档及示例。
七、项目原码请前往 xf-asr
各位小伙伴,欢迎大家使用,有什么问题,欢迎留言,一起交流。