实时韩语字幕的汉谚混写:Whisper turbo 与后文纠错

把已有的 Hanja 消歧模型接到麦克风和浏览器标签页音频。记录增量上下文、候选重排、字幕回改协议,以及 32 段 FLEURS 音频上没有带来 CER 改善的 N-best 联合打分实验。

现状

上一篇做的是整句转换:纯谚文输入,Kiwi 提取候选区间,XLM-R 对候选汉字打分,最后选择一组互不重叠的区间。这次把输入接到麦克风和浏览器标签页音频,希望说话过程中就能看到汉谚混写字幕,后面几个词到达后,前面选错的汉字还可以更新。

下面的配置和结果来自 2026 年 9 月 13 日的执行记录。

组件本次使用的配置
汉字消歧已有的 XLM-R base / run6,权重保持不变
ASRlarge-v3-turbo,韩语转写
推理faster-whisper 1.2.1、CTranslate2 4.6.0,CUDA FP16
连续音频与界面WhisperLiveKit 0.2.26,固定源码 revision 363e4f6
运行环境单张 V100 16 GB,Python 3.11,Torch 2.7.1 + CUDA 12.6
网页更新配置LocalAgreement,最小 ASR 更新间隔 1 秒,最近两条 ASR 行保留回改窗口

代码已发布到 Evsio0n/hanja-wsd。

演示

19.3 秒录屏,左侧是播放韩语新闻的标签页,右侧是字幕页。录制开始前字幕会话已经运行,原录屏没有音轨。视频包含 YTN 新闻画面,相关权利归原权利人;这段材料用于展示界面与音频输入方式。

下载 H.264 MP4。发布版裁掉了系统菜单、书签栏和 Dock,保留源视频与字幕区域。后文的准确率实验使用单独的测试音频。

ASR 与汉字转换

手上的消歧训练数据是文本:句子、目标区间、金标汉字和候选集合。语音测试材料提供韩语转写。若直接训练语音到混写文本,还要准备音频对应的汉字标注,并处理训练目标中的转换与保留决策。这一轮先保留已有权重,把两个模型串接起来,分别观察 ASR 错字和汉字消歧错误。

麦克风 / 浏览器标签页
        ↓ 16 kHz mono PCM
WhisperLiveKit + faster-whisper turbo
        ↓ 原始谚文行与临时 buffer
Hanja 候选打分、区间选择
        ↓ 混写行与回改状态
字幕页

选择 turbo 的范围是这套本地 V100 流程。它已有可用的 CTranslate2 权重,能直接接到现成的音频处理和 WebSocket 接口。此次没有覆盖各家的在线 ASR 服务,也没有足够的语音测试量来给出通用排名。

网页复用 WhisperLiveKit 的 AudioWorklet、PCM、VAD、LocalAgreement 和连续音频处理。最初的 CLI 实验会对已经到达的音频前缀反复解码,上限是一个 30 秒 utterance;持续听新闻需要音频缓冲裁剪、稳定转写与临时转写的管理,所以网页采用上游连续处理路径。

ASR 和混写结果分别保存。lines、buffer_transcription 等原字段保留原始谚文,转换层增加 mixed_lines 和对应的 mixed buffer。界面上的「原文/汉谚混写」切换读取两套字段,已输出的汉字也不会作为下一轮 ASR 的输入。

短更新间隔加上先前文本 prompt 的初始配置,在测试录音上出现过重复词。选定的配置把最小更新间隔设为 1 秒,并使用 initial_prompt=None、condition_on_previous_text=False,避免把已经误识别的文本继续作为 decoder prompt。音频上下文仍由流式缓冲保留。实测里仍有误听和重复;这里的 1 秒描述调度参数,端到端延迟还包含 ASR、转换和浏览器处理时间。

候选随上下文重排

의사 可以对应 醫師、意思、義士等候选。测试中按空格逐步增加输入,得到下面的输出:

已到达的文本候选第一名显示结果
그는 의사를醫師,0.5536그는 醫師를
그는 의사를 분명히意思,0.9960그는 意思를 분명히
그는 의사를 분명히 밝혔다.意思,0.9980그는 意思를 분명히 밝혔다.

新增的 분명히 提供了右上下文。HanjaReranker.analyze() 用当前文本重新构造目标区间和候选输入,重新计算分数,因此前一版的 醫師 可以被 意思 替换。首次输出时,候选列表仍被保留。

这个适配层的候选预选上限是 64,原来的历史难词评估上限是 12。词典按已有频次顺序提供候选,超出上限时标记 candidate_truncated。后续重排发生在保留集合内,落在词典或预选集合之外的金标仍需要候选生成阶段处理。

上下文长度设为 128 tokens。直接截断整句时,句子后部的目标词可能随着长度增长被截掉。marked_tokens() 先对带目标标记的完整句子分词,再用 offset mapping 找到目标范围;crop_context() 为目标、候选和 special tokens 预留预算,余量分配给左右上下文。分别对左侧、目标、右侧做 SentencePiece 编码会引入人为词界,因此这里使用一次分词的结果切片。

候选第一名和屏幕上的汉字还隔着转换门槛。例如 그는 공사를 的第一名是 公事,概率 0.4346,界面保留 공사;补上 시작했다. 后,工事 得到 0.5414,输出变成 그는 工事를 始作했다.。统计模型选择是否纠正时,需要把这种保留谚文的情况单独记录。

字幕的回改边界

ASR 可以先稳定一个谚文词,汉字选择还需要后面的语义。在网页实现中,HanjaProjector.project() 每次取最近两条非空 ASR 行,加上 diarization buffer 和 transcription buffer,一起进行候选打分。转换后的区间再分配回各自的行,行边界保持不变。

较早的行进入 256 项的缓存。返回值中的 hanja_mutable_from 标记本次可回改部分的起点,hanja_revision 标记投影版本。默认窗口只覆盖近期文本,跨越更远上下文的纠错需要另外的段落级策略。音频结束时等待上游排空,再处理 ready_to_stop 和关闭连接。

CLI 则提供单独的 utterance 级更新协议,便于对接其他 ASR。输入携带 segment_id、递增的 input_revision、文本和 final;输出同时带完整快照以及 append、replace、commit 操作。客户端按 base_revision 回放,迟到的旧版本直接忽略。

CLI 的 provisional 显示还使用 0.12 的 revision margin。若同一区间仍然对齐,而新候选相对旧候选的概率优势小于这个值,暂时保留旧显示;final=True 时去掉这项保持规则,使用最终排序。ASR 改写了区间位置或词形后,旧选择随原有对齐关系失效。这个 margin 控制显示抖动,数值尚未经过独立开发集调优;网页当前使用完整快照投影。

协议里的 offset 单位是 Unicode code point。JavaScript 字符串索引使用 UTF-16 code unit,处理补充平面的字符时要先转换索引,或直接使用完整快照更新界面。推理异常发生在状态提交之前,失败的一轮保留旧版本,防止客户端拿到半次更新。

前缀实验

十个手工编写的句子逐词输入,用目标词的候选第一名记录初始和最终选择。下面列出发生变化的五例和保留错误的一例;完整结果在仓库的前缀记录。

用例初始选择最终选择金标
工程公事工事工事
外交官公事公使公使
火葬化粧火葬火葬
苹果謝過沙果沙果
意向醫師意思意思
发电發展發展發電

十例中有六例初始模型选择错误,其中五例在增加上下文后恢复,最终九例正确。这组句子围绕已知歧义词编写,覆盖范围限于这些目标词。

发电例的完整输入是 우리는 발전을 위해 석탄을 태웠다.。在 우리는 발전을 时,發展 的概率是 0.9826;加入「燃烧煤炭」的上下文后降到 0.6739,發電 升到 0.3248,最终顺序仍然错误。

ASR N-best 联合打分

前面的纠错固定了谚文词形。若 ASR 已经听错词,还要在多个转写候选之间选择。因此另做了一个 late-fusion 实验,使用 CTranslate2.Whisper.generate(),beam_size=5、num_hypotheses=4、return_scores=True,获得 decoder 实际产生的候选及分数。相同文本去重后保留最高分;这条实验路径不生成 word timestamps,避免借用其他候选的对齐结果。

Hanja 打分只比较多个 ASR 候选共有的歧义区间。common_anchors() 要求词形位于可精确对齐的连续文本块,候选汉字集合一致、至少有两个候选,而且没有被截断。否则,某条转写可能因为可转换词更少,或者多出一个概率必为 1 的单候选词,得到不相称的优势。

记转写候选为 ,共同区间集合为 ,区间 上最高的汉字候选概率为 ,使用的分数是:

s_ASR 是 CTranslate2 返回的、经过长度惩罚的 decoder 分数。Hanja 部分是候选集合内的置信度启发式,两者还没有经过联合训练或校准。没有共同区间时保留 ASR 第一名。

本次固定 λ=0.05,只比较 ASR 分差不超过 0.05 的候选,联合分数增益超过 0.001 才替换。设 λ=0 时必须精确返回同一次解码的第一名,用作对照;测试参考文本只进入最后的误差计算。

测试使用 Google FLEURS 的韩语 test rows 0–31。前八段沿用回归样本,后 24 段是新增测试。CER 的分母是归一化后的参考字符总数,分子是 Levenshtein 编辑数;归一化采用 NFC、小写,保留 Unicode 字母和数字,移除空白与标点。

样本参考字符ASR 第一名 CER联合打分 CERN-best oracle CER
前 8 段34614 / 346 = 4.05%4.05%11 / 346 = 3.18%
新增 24 段1,15127 / 1,151 = 2.35%2.35%24 / 1,151 = 2.09%

联合打分在这 32 段上没有替换任何输出,CER 没有改善。oracle 用参考文本选出候选集中编辑距离最小的结果,只表示这批候选的可选空间。后续若调整权重和门槛,需要另外划分开发集。

早期还通过高层 faster-whisper 接口比较过 turbo 与 large-v3,使用了不同的 decoding 和数字日期格式化路径。上表的消融保持相同的 N-best decoder,以其第一名作为基线。完整指标和参数已随代码保存。

浏览器音频与连续输入

麦克风走 getUserMedia(),标签页音频走 getDisplayMedia({video: true, audio: true})。浏览器的共享选择器要求视频轨,用户还要在支持音频共享的来源上勾选音频。取得 stream 后检查 getAudioTracks(),选择器返回零条音轨时直接提示;只有用户选定的音频经过 mono downmix 和 AudioWorklet,视频轨不进入 WebSocket。

录制按钮触发权限申请,页面加载时不采集。停止录制、共享结束或采集失败时,释放持有的音频与视频 tracks。验证同时检查没有音轨的选择是否打开了麦克风或 ASR socket,二者均没有发生。服务默认只监听 loopback;其他设备访问需要 HTTPS 和单独配置的服务入口,浏览器及操作系统的音频共享支持范围也有差异。

连续输入测试把同一段 12.48 秒 FLEURS 音频重复三次,中间加入两段 1 秒静音,共 39.44 秒,检查跨越 30 秒后仍有输出、结束后排空和释放 session。原版 ASR 对照处理了单次 12.48 秒,保留原始转写字段。

浏览器验证使用 Chromium 147:麦克风路径输入指定的公开测试 WAV,标签页路径实际共享播放该音频的页面;两条路径都得到了混写结果,切换回谚文和停止后释放 tracks 的检查通过。390 像素视口验证了字幕界面的窄屏布局,移动设备的标签页音频采集还没有验证。

复现

仓库提供 hanja-assets 下载固定 revision 的模型与数据,hanja-web 启动字幕页。Linux/V100 的安装命令在 README,包含匹配的 PyTorch wheel 和启动前的 LD_LIBRARY_PATH 设置。干净环境的首次 ASR warmup 曾因 libcudnn_ops.so.9 查找失败而终止;加入当前 Python 环境里的 NVIDIA 动态库目录后,通过了同一组音频测试。

完成安装后,前缀和 N-best 实验分别运行:

mkdir -p outputs/prefix
python -m streaming.validate --root outputs/prefix --phase prefix

python scripts/prepare_fleurs.py --root outputs/audio
python -m streaming.validate_joint --root outputs/audio/joint --skip-live

音频准备脚本检查 32 个文件的 SHA-256,与已记录样本不一致时停止。58 项协议、投影、配置和发布测试无需模型下载,GitHub CI 已在 Python 3.11、3.12 上通过。真实模型测试单独执行,候选选择、显示结果和原转换器的前后 replay 分开保存。

自有代码采用 CC BY-NC-SA 4.0,WhisperLiveKit 等上游组件保留原许可。模型和数据下载入口分别注明其条款,演示中的第三方广播画面单独说明。引用项目时请署名 Qifeng Xu(ORCID)并保留项目链接。

还没做的

  • 用 speaker/source-disjoint 的口语和广播语料建立音频到混写文本的人工金标,并单独标注专名。
  • 在独立开发集上调整 revision margin 和 N-best 权重,记录改对、改错和最终保留错误的次数。
  • 按音频时钟测量首字延迟、最终决定延迟、修订次数和回改距离的分布。
  • 增加跨段落的汉字修订策略,以及词典外和预选候选被截断的评估。

参考文档

流式语音与候选解码

  • WhisperLiveKit:连续音频、LocalAgreement、PCM 接口和前端的上游实现;本次固定的 revision 写在项目配置中。
  • faster-whisper:Whisper 的 CTranslate2 推理接口、CUDA/cuDNN 依赖及动态库路径要求。
  • CTranslate2 Whisper API:generate() 的 beam、num_hypotheses、return_scores 与长度惩罚参数。

浏览器采集与测试数据

  • MDN:getDisplayMedia:用户手势、secure context、视频轨要求和共享音频的可用性。
  • Google FLEURS:本次韩语测试音频及转写的来源,数据集标注 CC BY 4.0。

本项目

  • Hanja WSD:源代码、许可证、候选结果和固定版本的复现入口。
  • 模型下载:run6 检查点、词典、先验计数与模型说明。