现状
上一篇做的是整句转换:纯谚文输入,Kiwi 提取候选区间,XLM-R 对候选汉字打分,最后选择一组互不重叠的区间。这次把输入接到麦克风和浏览器标签页音频,希望说话过程中就能看到汉谚混写字幕,后面几个词到达后,前面选错的汉字还可以更新。
下面的配置和结果来自 2026 年 9 月 13 日的执行记录。
| 组件 | 本次使用的配置 |
|---|---|
| 汉字消歧 | 已有的 XLM-R base / run6,权重保持不变 |
| ASR | large-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。
演示
下载 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 | 联合打分 CER | N-best oracle CER |
|---|---|---|---|---|
| 前 8 段 | 346 | 14 / 346 = 4.05% | 4.05% | 11 / 346 = 3.18% |
| 新增 24 段 | 1,151 | 27 / 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。