5.12开发博客

铁路客运站设备运维管理系统 —— 离线语音输入功能的实现

一、项目背景

铁路客运站设备运维管理涉及大量的数据查询与录入工作:巡检线路、故障字典、保养标准、备件型号、设备厂商……这些信息分散在十几个配置模块中,现场运维人员需要在嘈杂的站台、候车室等环境中快速找到所需信息。

传统的键盘输入方式存在两个痛点:

  • 效率低:在移动场景下,掏出手机打字查询故障信息既慢又不方便。
  • 使用门槛高:部分一线的年长工作人员对键盘输入不熟练,更习惯用说话的方式表达需求。

因此,我在系统中集成了一套完全离线运行的语音输入方案,让运维人员可以对着麦克风说出问题,系统自动识别并给出答案。

二、技术选型:为什么选 Whisper?

市面上的语音识别方案主要有三类:

方案 代表 优点 缺点
云端 API 百度语音、讯飞、Azure Speech 识别率高 需要联网、有费用、数据隐私问题
浏览器内置 Web Speech API 零成本、免安装 仅 Chrome 支持较好、离线不可用
本地模型 OpenAI Whisper 完全离线、隐私安全、免费 需要本地算力

铁路场景的特殊性在于:许多车站的地下区域、设备间信号覆盖差,网络不稳定。同时,故障数据属于生产敏感信息,不宜发送到第三方云端。因此我选择了 faster-whisper(OpenAI Whisper 的 C++ 推理优化版)作为语音识别引擎。

三、核心挑战与解决方案

挑战一:模型加载慢,首次使用等待时间长

Whisper 的 tiny 模型约 75MB,首次下载 + 加载到内存需要 10~30 秒。如果每次启动都重新加载,用户体验会很差。

解决方案:懒加载 + 全局单例 + 异步锁

# 模型作为模块级全局变量,只加载一次
_whisper_model = None
_model_loading = False
_model_load_lock = asyncio.Lock()

async def _get_whisper_model():
    global _whisper_model, _model_loading

    # 已加载,直接返回
    if _whisper_model is not None:
        return _whisper_model

    # 其他请求正在加载中,轮询等待
    if _model_loading:
        for _ in range(30):
            await asyncio.sleep(1)
            if _whisper_model is not None:
                return _whisper_model

    # 加锁,避免并发重复加载
    async with _model_load_lock:
        if _whisper_model is not None:
            return _whisper_model
        _model_loading = True
        # 在线程池中加载,不阻塞事件循环
        await asyncio.to_thread(load_model)
        _model_loading = False
        return _whisper_model

这样模型在服务运行期间只加载一次,后续请求可以直接使用。

挑战二:CPU 推理速度与精度的平衡

faster-whisper 支持多种模型大小,从 tiny(39M 参数)到 large-v3(1.5B 参数)。在 CPU 环境下,大模型推理太慢,无法满足实时交互。

解决方案:tiny 模型 + int8 量化 + VAD 过滤

_whisper_model = WhisperModel(
    "tiny",           # 最小模型,适合 CPU
    device="cpu",     # 无需 GPU
    compute_type="int8",  # 8位整数量化,速度大幅提升
)
# 转写时使用 VAD 过滤静音段,减少无效计算
segments, info = model.transcribe(
    tmp_path,
    language='zh',      # 指定中文,减少语言检测开销
    beam_size=5,        # 适中的束搜索宽度
    vad_filter=True,    # 语音活动检测,跳过静音
)

经实测,tiny 模型在普通笔记本 CPU 上,5 秒语音的识别时间约 1~2 秒,完全满足交互需求。

挑战三:浏览器音频采集

前端需要在浏览器中录音并上传。浏览器的 MediaRecorder API 支持直接录制音频,但需要注意格式兼容性。

解决方案:WebM + Opus 编码

async function startRecording() {
    // 获取麦克风权限
    recordingStream = await navigator.mediaDevices.getUserMedia({ audio: true });

    // 使用 WebM 格式,Opus 编码(体积小、质量好)
    mediaRecorder = new MediaRecorder(recordingStream, {
        mimeType: 'audio/webm;codecs=opus'
    });

    // 录制停止后自动上传识别
    mediaRecorder.onstop = async function() {
        const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
        const formData = new FormData();
        formData.append('file', audioBlob, 'recording.webm');

        const resp = await fetch('/api/v1/ai-agent/voice-to-text', {
            method: 'POST',
            body: formData
        });
        const result = await resp.json();
        // 将识别结果填入输入框
        document.getElementById('chatInput').value = result.data.text;
    };

    mediaRecorder.start();
}

WebM + Opus 的优点是:压缩率高(60 秒录音仅约 100KB)、浏览器原生支持、无需引入额外的编码库。

挑战四:完整交互闭环

语音输入只是"输入"这一环。为了让体验闭环,还需要"语音输出"——让系统把答案也读出来。

解决方案:Web Speech API 实现 TTS

function speakText(text) {
    if (!('speechSynthesis' in window)) return;

    const cleanText = text.replace(/\*\*/g, '').replace(/`/g, '');
    const utterance = new SpeechSynthesisUtterance(cleanText);
    utterance.lang = 'zh-CN';  // 中文语音
    utterance.rate = 1.0;

    // 优先选中文语音包
    const voices = speechSynthesis.getVoices();
    const zhVoice = voices.find(v => v.lang.startsWith('zh-CN'));
    if (zhVoice) utterance.voice = zhVoice;

    speechSynthesis.speak(utterance);
}

这样用户的操作路径变为:点击麦克风 → 说话 → 松开 → 自动识别 → 自动查询 → 语音朗读结果,全程不需要打字。

四、系统架构总览

┌──────────────────────────────────────────────────┐
│                    浏览器前端                      │
│  ┌──────────┐    ┌──────────┐    ┌────────────┐  │
│  │ 麦克风录音 │ → │ WebM编码  │ → │ HTTP上传   │  │
│  │(MediaRecorder)│ │(Opus)    │    │(FormData)  │  │
│  └──────────┘    └──────────┘    └─────┬──────┘  │
│                                        │         │
│  ┌──────────┐    ┌────────────────────┐│         │
│  │ TTS朗读   │ ← │ 显示回答           ││         │
│  │(WebSpeech)│    │(Markdown渲染)      ││         │
│  └──────────┘    └──────────┬─────────┘│         │
└──────────────────────────────┼──────────┼─────────┘
                               │          │
┌──────────────────────────────┼──────────┼─────────┐
│                    FastAPI 后端                    │
│                               ▼          ▼         │
│  ┌──────────────┐    ┌──────────────────────┐     │
│  │ /voice-to-text│    │  /chat(智能问答)    │     │
│  │ Whiser 识别   │    │  关键词匹配 + 数据库查询│    │
│  └──────┬───────┘    └───────────┬──────────┘     │
│         │                        │                │
│  ┌──────▼────────────────────────▼──────────┐     │
│  │          WhisperModel (全局单例)           │     │
│  │   tiny · CPU · int8 · faster-whisper     │     │
│  └──────────────────────────────────────────┘     │
│                                                    │
│  ┌──────────────────────────────────────────┐     │
│  │         SQLite / PostgreSQL 数据库         │     │
│  │  车站·巡检·保养·故障·备件·厂商·流程         │     │
│  └──────────────────────────────────────────┘     │
└────────────────────────────────────────────────────┘

五、关键交互流程

语音输入一次完整的用户体验流程如下:

  1. 预检:点击麦克风时,前端先调用 /voice-status 检查模型是否就绪。若模型未加载,提示"正在初始化";若已就绪,直接开始录音。
  2. 录音:按钮变红、带脉冲动画,提示"正在录音...",最长 60 秒自动停止。
  3. 上传识别:松开按钮 → 音频 Blob 通过 FormData 上传 → 后端保存为临时文件 → Whisper 转写 → 删除临时文件 → 返回文字。
  4. 自动填入:前端收到文字后填入输入框,用户可直接修改或发送。
  5. 智能问答:文字发往 /chat 接口 → 后端提取关键词、识别意图(故障/巡检/保养/备件/厂商)→ 查询数据库 → 返回格式化答案。
  6. 语音朗读:前端收到答案后,通过 Web Speech API 朗读,同时以 Markdown 格式渲染展示。

六、难点攻克细节

并发安全

后端是异步的,多个请求可能同时触发模型加载。我使用了两层保护:首先是 _model_loading 标志位让后来的请求轮询等待;其次是 asyncio.Lock() 确保只有一个协程真正执行加载。

临时文件清理

Whisper 需要文件路径作为输入,但上传的音频数据在内存中。我使用 tempfile.NamedTemporaryFile 创建临时文件,并在 finally 块中确保删除,即使识别过程抛异常也不会残留垃圾文件。

短音频过滤

如果用户误触麦克风后立即松开,会产生极短的音频(通常 < 1KB),Whisper 无法从中识别出有效语音。我在后端做了最小数据量检查:不足 1000 字节直接返回错误提示。

录音超时保护

为防止用户忘记关闭录音,前端设置了 60 秒超时强制停止,防止浏览器内存占用过大。

七、效果展示

场景 语音输入 系统响应
故障查询 "电梯运行异响怎么解决?" 返回该故障的症状、原因、解决方案
巡检查询 "北京南站有哪些巡检线路?" 列出该站所有巡检线路及项目数量
厂商查询 "日立电梯售后电话是什么?" 返回厂商联系人、电话、地址
备件查询 "北京南站备件库存有哪些?" 返回库位信息和备件型号清单

八、不足与改进方向

  1. 识别精度受限:tiny 模型在噪音环境下的识别率不如 large 模型。后续可考虑使用 small 或 medium 模型,或引入降噪预处理。
  2. 无流式识别:当前是"录完再识别"的模式,无法实时显示中间结果。可探索 WebSocket + 流式 Whisper。
  3. 方言支持:Whisper 对普通话的识别较好,但对各地方言支持有限。可考虑针对铁路行业术语做微调。
  4. 模型首次下载:tiny 模型约 75MB,在网络差的部署环境下首次下载较慢。可考虑预置模型到部署包中。

九、总结

本文介绍了一个基于 faster-whisper 的完全离线语音输入方案,从前端麦克风采集到后端模型推理,再到智能问答和语音朗读,形成了完整的"语音交互闭环"。该方案无需任何云服务、无需 GPU、不需要网络连接,特别适合铁路车站这种对网络依赖低、数据隐私要求高的工业运维场景。

核心代码量不到 300 行(前后端合计),即可实现从语音 → 文字 → 查询 → 语音朗读的完整体验,证明在小模型 + 量化推理的技术路线下,端侧语音交互已经具备了实用化的条件。

posted @ 2026-05-12 23:26  douzishuo  阅读(30)  评论(0)    收藏  举报