# xf-asr
**Repository Path**: shankun/xf-asr
## Basic Information
- **Project Name**: xf-asr
- **Description**: 基于 Vue3 + Vite5,使用科大讯飞 ASR 实现语音转文字
语音听写流式接口,用于1分钟内的即时语音转文字技术,支持实时返回识别结果,达到一边上传音频一边获得识别文本的效果。
代码拉下来可直接使用,保姆级教学
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2025-05-20
- **Last Updated**: 2025-05-20
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 基于 Vue3 + Vite5,使用科大讯飞 ASR 实现语音转文字
语音听写流式接口,用于1分钟内的即时语音转文字技术,支持实时返回识别结果,达到一边上传音频一边获得识别文本的效果。
## [官方文档](https://www.xfyun.cn/doc/asr/voicedictation/API.html)
## 环境准备
- 开发环境:`Node.js v20.10.0` + `npm v10.2.3`
- 开发工具:`WebStorm 2024.1.7`
- 源代码管理:`Git`
- npm镜像:`https://registry.npmmirror.com/`

## 一、快速开始
使用 `npm` 包管理工具快速创建一个项目
```sh
npm create vue@latest
````
项目名称为 `xf-asr`,其它功能暂时都不需要,可以直接回车。进入项目目录,执行 `npm install` 安装依赖,执行 `npm run dev` 启动项目。
**执行 `git init` 初始化 git 仓库(个人习惯)方便管理,并执行 `git add .` ,在执行 `git commit -m 'init' `**
## 二、下载 SDK
在官方文档中找到[语音听写流式API demo js语言](https://xfyun-doc.xfyun.cn/static/16902656744143549/iat-js-demo.zip)
点击下载,里面包含了我们需要用到的录音管理器,以及语音识别的代码。
解压刚才下载好的 `iat-js-demo.zip` 文件,将里面的 `dist` 文件夹复制到项目的 `public` 目录下,并改名为 `asr-sdk`。
在终端执行 `npm install crypto-js` 安装`crypto-js`包,用于加密。
## 三、引入 SDK
修改 `index.html` 文件,其它位置不用修改
```html
Vite App
```
## 四、封装 useXfAsr 语音识别插件
在项目 `src` 目录下创建一个 `hooks` 目录,在该目录下创建一个 `useXfAsr.js` 文件,并添加以下代码:
```javascript
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` 文件,添加如下代码:
```vue
识别结果:{{ resultText }}
```

## 六、心得体会
在这里,作者用最简洁的代码调用讯飞语音识别,通过封装 `useXfAsr` 函数,可以很方便的项目的任何地方进行调用。
由于讯飞 SDK 的要求,这里的语音时长最大为 60 秒,所以,如果用户在录音过程中,超过 60 秒,则需要重新开始录音。
通过接口密钥基于 `hmac-sha256` 计算签名,向服务器端发送 `Websocket` 协议握手请求。握手成功后,客户端通过 `Websocket` 连接同时上传和接收数据。数据上传完毕,客户端需要上传一次数据结束标识。接收到服务器端的结果全部返回标识后断开 `Websocket` 连接。
具体的细节请参考[官方文档](https://www.xfyun.cn/doc/asr/voicedictation/API.html)及示例。
## 七、项目原码请前往 [xf-asr](https://gitee.com/JiaChenHuang/xf-asr)
各位小伙伴,欢迎大家使用,有什么问题,欢迎留言,一起交流。