最近,我一直在探索本地化、高性能的 AI 应用。今天分享我封装的一款极简桌面应用:一个支持中英混合的实时语音转文字(STT)工具。它完全在本地运行,延迟低,且能自动添加标点,非常适合会议、访谈记录或语音写作。
项目开源地址 : github.com/jianchang51...
技术选型:构建应用的基石
要实现这个目标,我需要解决两大核心问题:语音识别引擎 和 图形用户界面(GUI)。
1. 语音识别引擎:Sherpa-Onnx
在众多开源 STT 引擎中,我最终选择了 Sherpa-Onnx。理由如下:
- 高性能与低延迟:它使用了先进的 Paraformer 模型,这是一种非自回归(Non-autoregressive)模型,可以并行解码,极大地降低了识别延迟,是实现"实时"效果的关键。
- 跨平台与易部署:基于 ONNX Runtime,使得模型可以轻松地在 Windows, macOS, Linux 上运行,无需复杂的环境配置。ONNX 也为未来的 GPU 加速提供了可能。
- 强大的流式识别能力:内置了优秀的端点检测(VAD)算法。这意味着它能智能地判断一句话是否说完,从而自动切分语音流,这对于生成自然段落至关重要。
2. GUI 框架:PySide6
对于桌面应用,我需要一个成熟、跨平台的 GUI 框架。PySide6 (Qt for Python) 是我的不二之选:
- 原生体验:Qt 提供了接近原生应用的外观和性能。
- 强大的多线程支持 :通过
QThread和信号槽(Signal/Slot)机制,可以完美地将耗时的语音识别任务与 UI 刷新分离开,避免界面卡顿,这是构建响应式实时应用的核心。 - 成熟的生态:拥有丰富的组件和完善的文档。
架构设计与核心逻辑解析
整个应用的架构可以分为三个层次:音频采集层 、识别处理层 和 界面交互层。
1. 音频采集与处理 (Worker 线程)
所有耗时操作都被封装在 Worker 这个 QThread 子类中,以防止阻塞主 UI 线程。
核心流程:
- 初始化 :
Worker线程启动后,首先加载 Sherpa-Onnx 的识别器(Recognizer)和后处理用的标点符号模型。 - 音频流 : 使用
sounddevice库打开麦克风输入流,以 0.1 秒为单位持续读取音频数据块(chunk)。 - 流式识别 :
- 将音频数据喂给
recognizer.create_stream()创建的流。 - 循环调用
recognizer.decode_stream(stream)来获取最新的识别结果。这个结果是中间结果,即你正在说的话。 - 通过
new_word信号将这个中间结果实时发送给 UI 线程进行展示。
- 将音频数据喂给
python
# Worker.run() 核心逻辑简化
# ...
mic_stream = sd.InputStream(...)
mic_stream.start()
self.running = True
last_result = ""
while self.running:
samples, _ = mic_stream.read(self.samples_per_read)
# 1. 喂数据
stream.accept_waveform(self.sample_rate, samples)
# 2. 解码
while recognizer.is_ready(stream):
recognizer.decode_stream(stream)
# 3. 获取中间结果
result = recognizer.get_result(stream)
if result != last_result:
self.new_word.emit(result) # 发射信号更新UI
last_result = result
# 4. 判断一句话是否结束
if recognizer.is_endpoint(stream):
if result:
# ... 后处理 ...
self.new_segment.emit(punctuated) # 发射完整段落信号
recognizer.reset(stream)
2. 两阶段文本处理
一个优秀的转录工具不仅要转得准,还要读得顺。我设计了一个两阶段文本处理流程:
阶段一:实时中间结果 Sherpa-Onnx 的流式识别结果是无标点的纯文本。我将其直接展示在界面上方的一个小文本框中,让用户能立刻看到反馈。
阶段二:段落整理与自动标点 这是提升体验的关键一步。当 Sherpa-Onnx 的端点检测判断用户停顿下来(一句话说完)时:
- 获取这句完整的、无标点的文本结果。
- 调用一个独立的标点符号恢复模型 (代码中的
OnnxModel类)。这个模型也是基于 ONNX,它接收纯文本,然后预测出其中每个词后面应该跟什么标点(如,、。、?或无标点)。 - 将添加了标点的完整句子通过
new_segment信号发送给 UI,追加到主文本区域。
这种识别"与"润色"分离的设计,既保证了前端的低延迟反馈,又确保了最终文本的高可读性。
python
# OnnxModel 类的作用
class OnnxModel:
def __call__(self, text: str) -> str:
# ... 复杂的文本分词和ID转换 ...
# 调用ONNX模型进行推理
out = self.sess.run(...)
# ... 根据模型输出拼接带标点的文本 ...
return "".join(ans)
# 在 Worker 线程中调用
if is_endpoint:
if result:
punctuated = PUNCT_MODEL(result) # 调用标点模型
self.new_segment.emit(punctuated) # 发送最终结果
3. UI 交互与线程通信 (RealTimeWindow)
UI 主线程 (RealTimeWindow) 的职责非常纯粹:响应用户操作 和接收子线程信号并更新界面。
- 启动/停止 : 点击按钮时,创建或销毁
Worker线程,并更新按钮状态。 - 数据展示 :
worker.new_word.connect(self.update_realtime):当收到new_word信号时,调用update_realtime函数刷新上方的小文本框。worker.new_segment.connect(self.append_segment):当收到new_segment信号时,调用append_segment函数将带标点的完整句子追加到下方的主文本框。
这种清晰的职责划分,是 Qt/PySide 编程的最佳实践,也是保证应用稳定流畅的基石。

思考与未来展望
在开发过程中,我也遇到了一些挑战和思考:
- 模型选择的权衡 :Paraformer 速度快,但在极安静环境下的长停顿可能会被误判为端点。代码中通过
rule_min_trailing_silence等参数进行了调整,这是一个在"灵敏度"和"完整性"之间的权衡。 - 资源占用 :纯 CPU 运行时,模型会占用一定的计算资源。未来可以探索使用支持 GPU 的
onnxruntime-gpu包,并提供选项让用户切换,进一步降低 CPU 负载。 - 功能扩展 :
- 说话人识别 (Speaker Diarization):在会议场景中,区分不同的人声会非常有价值。
- 多语言支持:Sherpa-Onnx 支持多种语言模型,可以很方便地扩展,让用户选择不同的识别语言。
- 关键词优化:允许用户添加自定义词典,提高特定领域(如编程、医学)术语的识别准确率。