

一、引言:语音交互的前端技术挑战
在现代Web应用中,语音交互已成为提升用户体验的重要手段。然而,实现稳定、高效的语音识别功能并非易事。本文将通过一个实际项目案例,详细介绍如何基于讯飞语音听写API,构建一个支持实时语音转写、语音合成播报、音频文件上传的完整前端解决方案。
该项目实现了一个物品记录管理系统,用户可以通过语音说出物品名称及数量(如"苹果五个确定"),系统自动识别并记录,同时支持撤销、确认、上传等功能。
二、项目背景与技术选型
2.1 需求分析
项目核心需求包括:
-
实时语音转写:用户说话时实时转换为文字
-
格式校验:只识别特定格式的内容(如"物品+数量确定")
-
语音播报:系统通过语音反馈操作结果
-
音频上传:将录音文件上传至服务器保存
-
双通道录音:听写和WAV录制互不干扰
2.2 技术栈
| 技术组件 | 用途 | 版本/来源 |
|---|---|---|
| Vue.js | 前端框架 | 2.x |
| 讯飞语音听写API | 实时语音识别 | WebSocket v2 |
| 讯飞语音合成API | 语音播报 | WebSocket |
| Recorder.js | 音频帧采集 | 自研录音库 |
| js-audio-recorder | WAV文件录制 | npm包 |
| CryptoJS | 加密鉴权 | 3.x |
| Element UI | UI组件库 | 2.x |
2.3 核心难点
开发过程中面临以下技术挑战:
-
单一录音源的矛盾:听写需要实时发送音频帧,上传需要完整的WAV文件,同一录音源无法同时满足
-
并发录音处理:两个录音通道需要同时工作且互不干扰
-
WebSocket连接管理:听写会话需要正确开启和关闭
-
语音指令解析:需要从识别文本中提取结构化数据
三、双通道录音架构设计
3.1 架构概述
为解决单一录音源的矛盾,项目采用双录音通道架构:
text
┌─────────────────────────────────────────────────────┐
│ 用户麦克风 │
└─────────────────┬───────────────────────────────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ 通道1:听写 │ │ 通道2:WAV │
│ Recorder.js │ │js-audio- │
│ │ │recorder │
└───────┬───────┘ └───────┬───────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ 实时音频帧 │ │ 完整WAV文件 │
│ (L16/16kHz) │ │ (采样16kHz) │
└───────┬───────┘ └───────┬───────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ 讯飞听写WebSocket│ │ 服务器上传 │
└───────────────┘ └───────────────┘
3.2 录音通道1:听写专用录音器
听写录音器使用自研的Recorder库,核心特性:
-
实时帧回调 :通过
onFrameRecorded回调持续输出音频帧 -
低延迟配置:启用回声消除、噪声抑制
-
格式适配:输出L16格式,16kHz采样率,单声道
javascript
// 听写录音器配置
recorder.startWithConstraints({
sampleRate: 16000,
frameSize: 1280,
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: false
}
});
3.3 录音通道2:WAV文件录制器
WAV录制器使用js-audio-recorder库,特性:
-
文件生成:可生成标准WAV Blob对象
-
独立生命周期:不受听写会话影响
-
自动重启机制:上传后自动开始新录音
javascript
// WAV录音器配置
const recorder = new Recorder2({
sampleBits: 16,
sampleRate: 16000,
numChannels: 1,
echoCancellation: true,
noiseSuppression: true
});
四、讯飞语音听写API集成
4.1 WebSocket鉴权机制
讯飞听写API使用HMAC-SHA256签名进行鉴权,签名生成流程:
javascript
getWebSocketUrl() {
const host = "iat-api.xfyun.cn";
const date = new Date().toGMTString();
const algorithm = "hmac-sha256";
const headers = "host date request-line";
// 1. 构建签名原文
const signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`;
// 2. 使用API Secret进行HMAC-SHA256加密
const signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
const signature = CryptoJS.enc.Base64.stringify(signatureSha);
// 3. 构建Authorization
const authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
const authorization = base64.encode(authorizationOrigin);
// 4. 拼接完整URL
return `${url}?authorization=${authorization}&date=${encodeURI(date)}&host=${host}`;
}
4.2 三帧数据协议
讯飞听写API采用三帧协议传输音频数据:
javascript
// 第一帧:配置参数(status=0)
const startFrame = {
common: { app_id: appId },
business: {
language: "zh_cn",
domain: "open-ed",
accent: "mandarin",
vad_eos: 1000,
dwa: "wpgs" // 开启动态修正
},
data: {
status: 0,
format: "audio/L16;rate=16000",
encoding: "raw"
}
};
// 第二帧:音频数据(status=1)
const audioFrame = {
data: {
status: 1,
format: "audio/L16;rate=16000",
encoding: "raw",
audio: toBase64(frameBuffer) // Base64编码的音频帧
}
};
// 第三帧:结束标志(status=2)
const endFrame = {
data: {
status: 2,
format: "audio/L16;rate=16000",
encoding: "raw",
audio: ""
}
};
4.3 识别结果解析
听写API返回的结果需要解析ws(字序列)字段:
javascript
wsTask.onmessage = function (message) {
const jsonData = JSON.parse(message.data);
if (jsonData.data && jsonData.data.result) {
let str = "";
const ws = jsonData.data.result.ws;
for (let i = 0; i < ws.length; i++) {
str = str + ws[i].cw[0].w; // 取第一个候选词
}
// pgs字段表示识别状态:apd(识别中)或rpl(最终结果)
if (data.pgs === "apd") {
_this.resultTextTemp = _this.resultText + str;
} else {
_this.resultText = _this.resultText + str;
}
}
// status=2表示本次会话结束
if (jsonData.data && jsonData.data.status === 2) {
// 处理最终结果
_this.processFormattedResult(finalText);
}
}
五、语音合成与播报系统
5.1 TTS WebSocket集成
讯飞语音合成API同样使用WebSocket协议,需要生成带签名的URL:
javascript
getTtsWebSocketUrl(apiKey, apiSecret) {
let url = "wss://cbm01.cn-huabei-1.xf-yun.com/v1/private/mcd9m97e6";
let host = location.host;
let date = new Date().toGMTString();
// 签名算法与听写相同
// ...
return `${url}?authorization=${authorization}&date=${date}&host=${host}`;
}
5.2 音频播放器集成
使用自研的AudioPlayer库播放合成音频:
javascript
// 初始化播放器
audioPlayer = new AudioPlayer("../../player");
audioPlayer.start({
autoPlay: true,
sampleRate: 16000,
resumePlayDuration: 1000
});
// 接收音频数据并播放
ttsWS.onmessage = (e) => {
const jsonData = JSON.parse(e.data);
if (jsonData.payload && jsonData.payload.audio) {
audioPlayer.postMessage({
type: "base64",
data: jsonData.payload.audio.audio,
isLastData: jsonData.header.status === 2
});
}
};
5.3 播报流程管理
为避免播报冲突,使用Promise和回调管理播报流程:
javascript
async speakText(text, onComplete = null) {
return new Promise((resolve, reject) => {
// 确保播放器已初始化
if (!audioPlayer) {
import('../../player/index.umd.js').then(module => {
audioPlayer = new module.default("../../player");
this.doSpeak(text, resolve, reject);
});
} else {
this.doSpeak(text, resolve, reject);
}
});
}
六、语音指令解析与业务逻辑
6.1 格式校验规则
系统只识别符合特定格式的语音内容:
javascript
isValidFormat(text) {
// 支持1-3个"X确定"结构的句子
const pattern1 = /^[^确定]+确定$/; // 单条
const pattern2 = /^[^确定]+确定[^确定]+确定$/; // 两条
const pattern3 = /^[^确定]+确定[^确定]+确定[^确定]+确定$/; // 三条
const cleanText = text.replace(/\s/g, '');
return pattern1.test(cleanText) || pattern2.test(cleanText) || pattern3.test(cleanText);
}
6.2 内容提取与转换
从识别文本中提取物品内容,并支持中文数字转换:
javascript
parseAndExtractContent(text) {
// 匹配数字+确定的模式
const pattern = /(\d+|(?:零|一|二|三|四|五|六|七|八|九|十|百|千|万|亿)+)确定/g;
const matches = [];
let match;
while ((match = pattern.exec(text)) !== null) {
matches.push(match[1].trim());
}
return matches;
}
processItemText(item) {
// 中文数字转阿拉伯数字
const map = {
'零': '0', '一': '1', '二': '2', '三': '3', '四': '4',
'五': '5', '六': '6', '七': '7', '八': '8', '九': '9', '十': '10'
};
return item.replace(/[零一二三四五六七八九十]/g, match => map[match]);
}
6.3 确认与撤销机制
系统实现了完整的确认/撤销逻辑:
javascript
async batchConfirmItems(onComplete = null) {
let combinedItem = this.combineItems(this.pendingTexts);
let confirmText = this.processItemText(combinedItem);
// 语音播报待确认内容
await this.speakText(confirmText);
// 设置超时自动确认(1秒)
this.confirmTimer = setTimeout(async () => {
if (this.isConfirming && this.pendingTexts.length > 0) {
this.accumulatedText += combinedItem + ";";
this.text = this.accumulatedText;
this.pendingTexts = [];
this.isConfirming = false;
}
}, 1000);
}
用户可以通过语音指令进行确认或撤销:
-
说"确定"自动确认当前批次
-
说"撤销"删除待确认内容或最后一条记录
七、音频上传与数据持久化
7.1 WAV文件获取与重置
javascript
async getWAVDataAndReset() {
if (!wavRecorder) return null;
// 获取当前录制的WAV数据
const wavBlob = wavRecorder.getWAVBlob();
// 停止并重新开始WAV录制
wavRecorder.stop();
wavRecorderInitialized = false;
wavRecorder = null;
await this.startWAVRecording();
return wavBlob;
}
7.2 文件上传
javascript
async uploadWAVData() {
const wavBlob = await this.getWAVDataAndReset();
let formData = new FormData();
const fileOfBlob = new File([wavBlob], new Date().getTime() + '.wav');
formData.append('multipartFile', fileOfBlob);
formData.append('userName', this.user.name);
formData.append('originContent', this.accumulatedText.replaceAll(" ", ","));
const response = await fetch(`https://${WHITE_IP}/document/upload_train`, {
method: 'POST',
body: formData
});
if (res.code === "200") {
this.$message.success("记录上传成功!");
this.accumulatedText = ""; // 清空累积内容
}
}
八、键盘快捷键支持
为提升操作效率,添加了A键快捷键支持:
javascript
mounted() {
this.handleKeyPress = (event) => {
if (event.key === 'a' || event.key === 'A') {
const activeElement = document.activeElement;
const isTextArea = activeElement && activeElement.tagName === 'TEXTAREA';
if (!isTextArea) {
event.preventDefault();
if (!this.isStartingRecord && !this.isRecording) {
this.voiceSend(); // 开始录音
} else if (this.isRecording) {
// 结束录音
wsTask.send(JSON.stringify(endParams));
}
}
}
};
window.addEventListener('keydown', this.handleKeyPress);
}
九、常见问题与解决方案
9.1 麦克风权限处理
javascript
async initAllRecorders() {
try {
await Promise.all([
this.startIATRecording(),
this.startWAVRecording()
]);
} catch (error) {
console.error("初始化录音通道失败:", error);
this.$message.error("麦克风初始化失败,请检查权限");
}
}
9.2 回声消除与噪声抑制
在录音配置中启用回声消除和噪声抑制:
javascript
const constraints = {
audio: {
echoCancellation: true, // 回声消除
noiseSuppression: true, // 噪声抑制
autoGainControl: false, // 自动增益控制
sampleRate: 16000,
channelCount: 1
}
};
9.3 WebSocket连接管理
确保每个听写会话正确开启和关闭:
javascript
async voiceSend() {
// 关闭现有连接
if (wsTask && wsTask.readyState === WebSocket.OPEN) {
wsTask.close();
await new Promise(resolve => setTimeout(resolve, 300));
}
// 重置状态
this.resultText = "";
this.resultTextTemp = "";
this.isRecording = true;
// 建立新连接
await this.wsInit();
}
十、性能优化与最佳实践
10.1 录音资源管理
-
页面加载时自动启动双通道录音,避免重复请求权限
-
组件销毁时释放所有录音资源
-
使用单例模式管理录音器实例
10.2 WebSocket重连机制
javascript
wsTask.onclose = function () {
wsFlag = false;
if (_this.isStartingRecord) {
// 异常关闭时恢复按钮状态
_this.isStartingRecord = false;
_this.$message.warning("连接异常,请重试");
}
}
10.3 用户体验优化
-
提供视觉反馈(按钮禁用状态、消息提示)
-
语音播报操作结果,减少视觉依赖
-
支持键盘快捷键,提升操作效率
-
自动超时确认,避免用户等待
十一、总结与展望
本文详细介绍了基于讯飞语音API构建的语音记录系统的完整实现方案。通过双通道录音架构,成功解决了实时听写和音频上传的矛盾需求。
项目的主要技术亮点包括:
-
双录音通道设计:实现了听写和录制的完全解耦
-
完整的WebSocket集成:正确实现了讯飞API的鉴权和三帧协议
-
智能语音指令解析:支持格式校验、内容提取、确认撤销
-
语音交互闭环:听写+合成形成完整的语音交互体验
未来可优化的方向:
-
增加更丰富的语音指令(如修改、插入等)
-
支持多语言识别
-
实现云端同步和跨设备共享
-
添加实时字幕显示效果
希望本文能对正在开发语音相关功能的开发者有所帮助。如有问题或建议,欢迎留言交流。