倩女幽魂安智客户端
1.87 GB · 2025-11-15
最近,我一直在探索本地化、高性能的 AI 应用。今天分享我封装的一款极简桌面应用:一个支持中英混合的实时语音转文字(STT)工具。它完全在本地运行,延迟低,且能自动添加标点,非常适合会议、访谈记录或语音写作。
项目开源地址: github.com/jianchang51…
要实现这个目标,我需要解决两大核心问题:语音识别引擎 和 图形用户界面(GUI)。
在众多开源 STT 引擎中,我最终选择了 Sherpa-Onnx。理由如下:
对于桌面应用,我需要一个成熟、跨平台的 GUI 框架。PySide6 (Qt for Python) 是我的不二之选:
QThread 和信号槽(Signal/Slot)机制,可以完美地将耗时的语音识别任务与 UI 刷新分离开,避免界面卡顿,这是构建响应式实时应用的核心。整个应用的架构可以分为三个层次:音频采集层、识别处理层 和 界面交互层。
Worker 线程)所有耗时操作都被封装在 Worker 这个 QThread 子类中,以防止阻塞主 UI 线程。
核心流程:
Worker 线程启动后,首先加载 Sherpa-Onnx 的识别器(Recognizer)和后处理用的标点符号模型。sounddevice 库打开麦克风输入流,以 0.1 秒为单位持续读取音频数据块(chunk)。recognizer.create_stream() 创建的流。recognizer.decode_stream(stream) 来获取最新的识别结果。这个结果是中间结果,即你正在说的话。new_word 信号将这个中间结果实时发送给 UI 线程进行展示。# 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)
一个优秀的转录工具不仅要转得准,还要读得顺。我设计了一个两阶段文本处理流程:
阶段一:实时中间结果 Sherpa-Onnx 的流式识别结果是无标点的纯文本。我将其直接展示在界面上方的一个小文本框中,让用户能立刻看到反馈。
阶段二:段落整理与自动标点 这是提升体验的关键一步。当 Sherpa-Onnx 的端点检测判断用户停顿下来(一句话说完)时:
OnnxModel 类)。这个模型也是基于 ONNX,它接收纯文本,然后预测出其中每个词后面应该跟什么标点(如 ,、。、? 或无标点)。new_segment 信号发送给 UI,追加到主文本区域。这种识别”与“润色”分离的设计,既保证了前端的低延迟反馈,又确保了最终文本的高可读性。
# 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) # 发送最终结果
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 编程的最佳实践,也是保证应用稳定流畅的基石。
在开发过程中,我也遇到了一些挑战和思考:
rule_min_trailing_silence 等参数进行了调整,这是一个在“灵敏度”和“完整性”之间的权衡。onnxruntime-gpu 包,并提供选项让用户切换,进一步降低 CPU 负载。