antist.ai

工程记录 / 桌面音频

Windows 上的实时日语字幕

一个桌面程序,偷听 Windows 正在播的任何声音,约 1.8 秒后在屏幕底部浮出带假名注音的日文,句子一结束就被高精度版本原地覆写,下面再跟一行中文译文。这一页是工程侧的写法:架构、实测数字,以及最花时间的那三类故障。

状态

可用,本人日常在跑,尚未公开分发。这一页没有下载链接,因为目前还没有可以诚实指过去的东西。这里公开的是可迁移的那部分:架构和测量结果。

问题:给没人做过字幕的声音配字幕

生肉日剧、线上会议、面试——需要字幕的场合,恰恰就是没有字幕的场合。云端识别解决了准确度这一半,另外两半解决不了:对话里能被察觉的延迟,以及把会议音频送去别人服务器。所以识别必须在本地跑。下面每一个有意思的决定,都是被这条约束逼出来的。

架构:两条轨,因为快和准是两个问题

一个模型没法既快又对。流式模型必须在句子还没成形时就吐字,所以它在猜;离线模型能看到整句,准得多,但要等说话人停下。只跑一个,等于选择在哪一半上失败。所以两个模型同时跑同一段音频,准的那条原地覆写快的那条。

系统声音(WASAPI 环回)→ 两个识别器并行 → 透明悬浮层

流式轨 —— 流式 Zipformer

持续吐出局部结果,落后说话人约 1.8 秒。这个延迟是模型自带的前瞻窗口,不是能优化掉的开销。对方还在说的时候,你读的就是这一轨。

定稿轨 —— VAD + SenseVoice

语音活动检测按 0.6 秒静音切句,把每个完整句子交给离线模型。它在句子结束后 1.2–1.5 秒落地,覆写流式轨的猜测。两条轨之间差着将近十个准确度百分点。

翻译 —— 完全非阻塞

每条定稿行由独立 worker 发去云端翻译,中位往返约 0.7–1.0 秒,且一点都不许碰音频线程——碰了会怎样,见下面第一条教训。

实测,不是估计

2026-08-01 实测,对照 47 分钟播客的 605 条人工字幕——是人工写的,不是平台自动字幕;拿自动字幕当参照,量到的只是两个识别系统有多像。准确度为假名 CER。

指标实测备注
字幕覆盖率(双轨合并)100.00%605 条参照字幕里,没有一条是两轨同时哑掉的
识别准确度(定稿轨)90.3%假名 CER 9.7%,47 分钟全长
识别准确度(流式轨)80.6%修端点检测前是 72.6%——一个 bug 值 8 个点
音频丢帧0.0%早期两个版本分别丢 48% 和 10.9%,成因都是在采集线程上做同步工作
首字延迟约 1.8 秒流式模型固有的前瞻窗口
定稿延迟句末后 1.2–1.5 秒被 0.6 秒静音阈值结构性支配
算力余量RTF 0.16–0.23双轨同跑,单块音频处理中位 2–3 毫秒

三类值得抄走的故障

这三条都跟字幕没关系。它们是 AI 辅助开发最容易批量生产的那种 bug——因为三条全都不报错。

  1. 1. 采集线程上不许出现任何「耗时随输入规模增长」的操作

    WASAPI 给你的是环形缓冲区:你没来得及取走的,会被下一段音频直接覆盖,而且悄无声息。第一版在采集线程里同步等翻译的 HTTP 请求,丢了 48% 的音频;后来某版把离线解码内联回主循环,丢了 10.9%。两次都没有抛过一个异常。由此得出的规矩:采集线程只许拷贝字节然后交出去,网络请求、模型推理、写文件一律进 worker。
  2. 2. reset() 不等于「回到全新状态」

    句间重置识别器留下的残渣,足以把模型困进一个循环:它什么都不吐,端点检测把空输出读成静音,每 2.4 秒空开一枪,然后又重置。真人连说十八秒,一个字都没出来。修法是分情况的,不是一刀切:正常断句仍然 reset 以保住左上下文,只在结果取回来是空的时候才新建 stream。全部换成新建能治住卡死,但要赔上准确度。
  3. 3. 「已初始化」标记必须是最后一步,绝不能是中间副产品

    模型初始化中途给句柄赋了值,然后抛异常。守卫语句读到那个句柄,认定初始化已完成,于是把一个半死的对象永久缓存了下来。完成标记的含义必须是「全部就绪且线程活着」,而不是「我们走到了能给某个字段赋值的那一步」。

工具栈

每一层的选型只为一个理由:识别不出本机。

语言
Python 3.14
GUI
PyQt6 + Fluent Widgets
悬浮层
PyQt6-WebEngine —— 用 HTML 的 ruby 渲染注音
音频采集
soundcard(WASAPI 环回)
识别
sherpa-onnx —— 流式 Zipformer + SenseVoice
注音
fugashi + unidic
翻译
Google / MiniMax,云端,不占线程

为什么注音要用浏览器引擎来画

整件事的目的是提高阅读速度,而 ruby 文本——印在汉字上方的小假名——是唯一能让陌生日文从「解析」变成「扫读」的排版特性。在原生控件里把 ruby 做对,本身就是一个项目;而 HTML 的 ruby 元素已经存在二十年了。所以字幕层是一个透明、可点穿的浏览器视图。不体面的那个选择,恰好是快的那个。