企业解决方案十六-录音记录标记与关键信息提炼

一、引言:语音交互的前端技术挑战

在现代Web应用中,语音交互已成为提升用户体验的重要手段。然而,实现稳定、高效的语音识别功能并非易事。本文将通过一个实际项目案例,详细介绍如何基于讯飞语音听写API,构建一个支持实时语音转写、语音合成播报、音频文件上传的完整前端解决方案。

该项目实现了一个物品记录管理系统,用户可以通过语音说出物品名称及数量(如"苹果五个确定"),系统自动识别并记录,同时支持撤销、确认、上传等功能。

二、项目背景与技术选型

2.1 需求分析

项目核心需求包括:

  1. 实时语音转写:用户说话时实时转换为文字

  2. 格式校验:只识别特定格式的内容(如"物品+数量确定")

  3. 语音播报:系统通过语音反馈操作结果

  4. 音频上传:将录音文件上传至服务器保存

  5. 双通道录音:听写和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构建的语音记录系统的完整实现方案。通过双通道录音架构,成功解决了实时听写和音频上传的矛盾需求。

项目的主要技术亮点包括:

  1. 双录音通道设计:实现了听写和录制的完全解耦

  2. 完整的WebSocket集成:正确实现了讯飞API的鉴权和三帧协议

  3. 智能语音指令解析:支持格式校验、内容提取、确认撤销

  4. 语音交互闭环:听写+合成形成完整的语音交互体验

未来可优化的方向:

  • 增加更丰富的语音指令(如修改、插入等)

  • 支持多语言识别

  • 实现云端同步和跨设备共享

  • 添加实时字幕显示效果

希望本文能对正在开发语音相关功能的开发者有所帮助。如有问题或建议,欢迎留言交流。