Problem Statement
看外语访谈或长句视频时,字幕经常落后语音大约一整句。短句还跟得上;一旦句子超过大约 15 个词,译文要等上一句说完才翻到眼前。已经解决了「一句话在屏幕上改几十次」的闪烁,现在剩下的是:稳是稳了,但晚了。
不想再凭印象拧参数,也不想换模型或改 Prompt 来装成修了延迟。需要一套能对照数字、一次只动一刀、升了改写率就回滚的做法。
Solution
不换架构、不换翻译引擎。现行链路已经是方案:系统音频 process tap → 端侧识别 → audio-range ledger(已定稿原文 + 可替换的未定稿原文)→ 翻译单元与显示单元分开 → 本机模型三条通道(当前句预览、整句定稿、静音后补历史)。未定稿原文已经参与当前句预览,不是「等整句识别完才开翻」。
先把落后语音的秒数拆到阶段上,并在字幕条上看见。然后按测量结果只改调度策略,一次一刀。每一刀必须用节奏诊断的会话总表对照:一句话变几次、每产出一个字被擦掉多少、落后秒数。变差就回滚那一刀。体验是否变好由使用者用真实语音验收。
User Stories
- As a 听访谈的人, I want 长句译文不要落后一整句, so that 我能边听边读而不是等说完再补看
- As a 听访谈的人, I want 已经出现在当前行上的字不被改写, so that 我不用反复重读同一句
- As a 听访谈的人, I want 整句定稿后进入上一行、当前行翻到下一句, so that 我能分清「刚说完」和「正在说」
- As a 听访谈的人, I want 短句不要为了压延迟而开始乱跳, so that 现在已经可接受的短句体验不被牺牲
- As a 听访谈的人, I want 字幕条三行位置和高度保持不变, so that 画面不会因为有没有译文而跳动
- As a 听访谈的人, I want 当前译文最醒目、上一句降权, so that 我的眼睛停在正在说的那一句
- As a 听访谈的人, I want 识别重复或说反了时先修识别而不是换翻译模型, so that 问题被修在真正出错的那一层
- As a 听访谈的人, I want 暂停或关闭后采集和识别真正停掉, so that 空闲时不占机器
- As a 调节奏的人, I want 字幕条上直接看到落后语音多少秒, so that 我不用只靠「追赶中」三个字和感觉判断
- As a 调节奏的人, I want 设置页运行时也显示落后秒数, so that 字幕条没展开时也能看数字
- As a 调节奏的人, I want 打开节奏诊断后得到按阶段拆开的延迟, so that 我知道慢在等识别、排队、模型、定格还是翻页
- As a 调节奏的人, I want 会话总表里仍有一句话变几次和擦除比, so that 降延迟没有把闪烁换回来
- As a 调节奏的人, I want 诊断默认关闭、用完关掉, so that 日常使用不写盘
- As a 调节奏的人, I want 每一刀改动都有改前改后两份会话总表, so that 我能回滚而不是靠记忆比较
- As a 调节奏的人, I want 改写率或擦除比升高时自动视为失败, so that 不会为了数字好看牺牲可读性
- As a 本机用户, I want 语音内容仍然只发到本机模型, so that 隐私边界不变
- As a 本机用户, I want 不安装虚拟声卡、不出现正在共享屏幕, so that 采集方式保持现在这样
- As an 实现者, I want 开发契约、产品说明和源码对采集、调度、三行布局说同一套话, so that 我不会按过时设计把屏幕共享或可取消定稿加回去
- As an 实现者, I want 正在跑的整句定稿跑完为止, so that 取消后调用方不会永远等不到翻页
- As an 实现者, I want 正在跑的当前句预览也不被下一帧识别取消, so that 字幕不会卡在只有原文
- As an 实现者, I want 已确认的译文只写一次, so that 后续识别或模型响应不能回写历史
- As an 实现者, I want 过期响应按会话和修订号丢弃, so that 旧翻译不能覆盖新字幕
- As an 实现者, I want 翻译单元和显示单元继续分开, so that 切窗不会让当前行凭空变短
- As an 实现者, I want 当前句预览继续只能续写已上屏译文, so that 屏幕上的字物理上不能被模型改写
- As an 实现者, I want 整句定稿仍然是唯一一次能推翻措辞的机会, so that 预览和定稿的职责不被合并成一次请求
- As an 实现者, I want 一次只改一条调度策略, so that 失败时知道该回滚哪一刀
- As an 实现者, I want 调度策略的测试表达不变量而不是写死词数, so that 以后拧参数不会让用例 silently 过期
- As an 实现者, I want 改节奏参数时必须跑状态测试, so that 9c61419 那种词数用例变红却无人发现的事不再发生
- As an 验收的人, I want Agent 不代替我播放测试媒体, so that 体验验收仍然由我签字
- As an 验收的人, I want 实现完成时标明运行态和体验仍待我验收, so that 没有人宣称延迟已经验证改善
- As a 看字幕的人, I want 长句在从句边界就可以出译文, so that 不必等整个自然句定稿才看到字
- As a 看字幕的人, I want 翻页发生在上一句定稿进了上一行之后, so that 当前行不会因为内部切窗而缩水
- As a 看字幕的人, I want 页面太长时才按语义边界裁, so that 翻译还没回来时当前行不会无限变长也不会动不动被剪掉
- As a 看字幕的人, I want 「追赶中」和具体秒数一起出现, so that 橙色标签有数字含义
- As a 维护契约的人, I want 开发说明里「预览互相抢占」改成与源码一致, so that 以后的人不会按文档把不取消 in-flight 改回去
Implementation Decisions
-
冻结现行架构,不重画数据流。 采集只用系统音频 process tap。识别产出已定稿原文与可替换未定稿原文,用音频时间范围对账。翻译单元(语义切窗 → 整句定稿)和显示单元(分页 → 当前句预览)分开。当前句预览的源文是「当前页原文 + 未定稿原文」,不是切窗器里还没切走的待处理原文。三条翻译通道优先级:当前句预览、整句定稿、静音后补历史。过期响应只丢弃。已确认译文只写一次。
-
以现行契约和源码为准,8 月底两份任务设计里与此冲突的段落作废。 包括:屏幕共享流采集、可取消的整句定稿、说话时不要上一行。用户可见布局跟产品说明的固定三行;视觉层级跟开发契约(当前句最高、历史降权、纵向淡入淡出)。
-
正在跑的整句定稿必须跑完。 取消不会触发完成回调,调用方的待办会永远翻不了页。正在跑的当前句预览也不取消:源码已经如此,避免字幕永远只有原文。开发契约里「预览互相抢占」以源码为准改掉,不要按文档把取消加回去。
-
不换翻译引擎,不合并预览和定稿请求,不拧切窗词数作为第一刀。 已有会话总表显示首次出字大约 168ms,模型出字不是主因。系统翻译框架和「预览定稿共用一次请求」会动隐私叙事或取消「定稿才能推翻措辞」。切窗词数历史上拧过,测试写死词数还红过。这些列为后备,本规格第一轮不做。
-
测量门闩先于策略刀。 节奏诊断会话总表已有落后秒数、首次出字、采集到识别、排队、采集到可见、采集到定稿、定格次数、翻页裁剪。第一刀实现是:字幕条 hover 显示落后秒数;若总表还不能区分「定格挡住预览」和「等最后一个窗口才翻页」,补上这两段时长或计数。没有改前对照总表,不动策略。
-
策略刀候选(按测量排序,一次一条)。 最大嫌疑是:整句定格时长直接加在延迟上;长句多窗口时当前行要等最后一个窗口定稿才翻页;in-flight 预览占住车道导致新快照要等。每一刀只改其中一条。改前改后都必须有真实语音会话总表。一句话变几次或擦除比升高 = 该刀失败,回滚。
-
身份键不变。 识别侧用 span 的编号、时间范围、修订和状态。模型侧用会话、片段、修订、种类、音频范围五元组。生命周期代数只服务启动、停止和语言切换,不进请求键。
-
源转录问题停在识别适配层。 定稿边界跟框架的定稿时刻走,不用标点、长度或定时器冒充已定稿。重复或错序先查滚动重叠、时间范围、定稿前沿和 ledger 合并。
-
诊断默认关。 打开才写会话总表,用完关掉。不把写盘变成实时字幕的常驻行为。
-
完成边界。 Agent:实现、状态测试、Release 构建、提供验证步骤。运行态权限/真实音频和体验验收由使用者做。不得宣称延迟改善已验证。
Testing Decisions
好的测试只锁外部不变量:窗口有界且不消费前瞻、不以未闭合尾词收尾、分页不因切窗而缩水、续写才追加否则按阅读门槛 hold、整句定稿不被预览取消、过期响应不写已确认字幕。不要写死某个词数。
模块与既有先例:
- 切窗 / 预览稳定性 / 滞后模式:沿用现有流水线状态测试(不变量断言,不是词数快照)。
- 分页:沿用现有分页测试(切窗不让当前行缩水、上界给翻译往返留余量)。
- 展示规则:沿用现有展示测试(追加 vs 改写 hold、定格挡住下一句预览)。
- 诊断:沿用现有诊断日志测试;若补阶段字段,只断言总表里出现该阶段,不断言具体毫秒。
- 新缝(仅一处): 三条通道的调度行为——整句定稿 in-flight 不可取消、当前句预览 in-flight 不可取消、补历史只在无当前活动时跑、积压有上限。现在状态测试不覆盖这条调度,改车道策略前必须先有它,否则测试全绿也说明不了调度。
运行态验收不是单元测试:打开节奏诊断,跑一段真实长句语音,对比会话总表。默认关日志。
Out of Scope
- 系统翻译框架、Apple Intelligence、云翻译作为预览引擎
- 当前句预览与整句定稿合并为一次模型请求
- 再拧切窗的最少词 / 最多词 / 目标时长 / 前瞻词数作为本轮主改动
- 换 Ollama 模型或改翻译 Prompt 来降延迟
- 麦克风、说话人、会议 SDK、字幕默认写盘导出
- 自定义快捷键、签名公证、Swift 6、用量看板、本机助手
- 复活屏幕共享音频流或说话时隐藏上一行
Further Notes
本规格落实 Issue #3。2026-09-04 已有一轮真实语音:99 秒 34 句,一句话平均变 1.29 次,擦除比 1.13,落后平均 1.56 秒、p90 3.96 秒,15 词以上平均 2.54 秒。首次出字平均 168ms。本轮没有再开 App 录新语料——Agent 不能代替使用者播放测试媒体。实施时用现有诊断字段做改前对照,缺的阶段再补,不要另起一套日志格式。
历史教训必须写进实施约束:把切窗默认词数从 16 降到 12 曾让不变量用例变红且多版本无人发现;旧预览滑窗曾把改写占比做到 94%,展示层追加几乎命不中。节奏数字不会让普通测试变红。
测试缝请确认是否同意:运行态以会话总表为最高缝;代码以现有切窗/分页/展示不变量测试为最高缝;唯一新增缝是三条通道调度。 不同意就在本 Issue 里改,不要另起一套测试金字塔。
Problem Statement
看外语访谈或长句视频时,字幕经常落后语音大约一整句。短句还跟得上;一旦句子超过大约 15 个词,译文要等上一句说完才翻到眼前。已经解决了「一句话在屏幕上改几十次」的闪烁,现在剩下的是:稳是稳了,但晚了。
不想再凭印象拧参数,也不想换模型或改 Prompt 来装成修了延迟。需要一套能对照数字、一次只动一刀、升了改写率就回滚的做法。
Solution
不换架构、不换翻译引擎。现行链路已经是方案:系统音频 process tap → 端侧识别 → audio-range ledger(已定稿原文 + 可替换的未定稿原文)→ 翻译单元与显示单元分开 → 本机模型三条通道(当前句预览、整句定稿、静音后补历史)。未定稿原文已经参与当前句预览,不是「等整句识别完才开翻」。
先把落后语音的秒数拆到阶段上,并在字幕条上看见。然后按测量结果只改调度策略,一次一刀。每一刀必须用节奏诊断的会话总表对照:一句话变几次、每产出一个字被擦掉多少、落后秒数。变差就回滚那一刀。体验是否变好由使用者用真实语音验收。
User Stories
Implementation Decisions
冻结现行架构,不重画数据流。 采集只用系统音频 process tap。识别产出已定稿原文与可替换未定稿原文,用音频时间范围对账。翻译单元(语义切窗 → 整句定稿)和显示单元(分页 → 当前句预览)分开。当前句预览的源文是「当前页原文 + 未定稿原文」,不是切窗器里还没切走的待处理原文。三条翻译通道优先级:当前句预览、整句定稿、静音后补历史。过期响应只丢弃。已确认译文只写一次。
以现行契约和源码为准,8 月底两份任务设计里与此冲突的段落作废。 包括:屏幕共享流采集、可取消的整句定稿、说话时不要上一行。用户可见布局跟产品说明的固定三行;视觉层级跟开发契约(当前句最高、历史降权、纵向淡入淡出)。
正在跑的整句定稿必须跑完。 取消不会触发完成回调,调用方的待办会永远翻不了页。正在跑的当前句预览也不取消:源码已经如此,避免字幕永远只有原文。开发契约里「预览互相抢占」以源码为准改掉,不要按文档把取消加回去。
不换翻译引擎,不合并预览和定稿请求,不拧切窗词数作为第一刀。 已有会话总表显示首次出字大约 168ms,模型出字不是主因。系统翻译框架和「预览定稿共用一次请求」会动隐私叙事或取消「定稿才能推翻措辞」。切窗词数历史上拧过,测试写死词数还红过。这些列为后备,本规格第一轮不做。
测量门闩先于策略刀。 节奏诊断会话总表已有落后秒数、首次出字、采集到识别、排队、采集到可见、采集到定稿、定格次数、翻页裁剪。第一刀实现是:字幕条 hover 显示落后秒数;若总表还不能区分「定格挡住预览」和「等最后一个窗口才翻页」,补上这两段时长或计数。没有改前对照总表,不动策略。
策略刀候选(按测量排序,一次一条)。 最大嫌疑是:整句定格时长直接加在延迟上;长句多窗口时当前行要等最后一个窗口定稿才翻页;in-flight 预览占住车道导致新快照要等。每一刀只改其中一条。改前改后都必须有真实语音会话总表。一句话变几次或擦除比升高 = 该刀失败,回滚。
身份键不变。 识别侧用 span 的编号、时间范围、修订和状态。模型侧用会话、片段、修订、种类、音频范围五元组。生命周期代数只服务启动、停止和语言切换,不进请求键。
源转录问题停在识别适配层。 定稿边界跟框架的定稿时刻走,不用标点、长度或定时器冒充已定稿。重复或错序先查滚动重叠、时间范围、定稿前沿和 ledger 合并。
诊断默认关。 打开才写会话总表,用完关掉。不把写盘变成实时字幕的常驻行为。
完成边界。 Agent:实现、状态测试、Release 构建、提供验证步骤。运行态权限/真实音频和体验验收由使用者做。不得宣称延迟改善已验证。
Testing Decisions
好的测试只锁外部不变量:窗口有界且不消费前瞻、不以未闭合尾词收尾、分页不因切窗而缩水、续写才追加否则按阅读门槛 hold、整句定稿不被预览取消、过期响应不写已确认字幕。不要写死某个词数。
模块与既有先例:
运行态验收不是单元测试:打开节奏诊断,跑一段真实长句语音,对比会话总表。默认关日志。
Out of Scope
Further Notes
本规格落实 Issue #3。2026-09-04 已有一轮真实语音:99 秒 34 句,一句话平均变 1.29 次,擦除比 1.13,落后平均 1.56 秒、p90 3.96 秒,15 词以上平均 2.54 秒。首次出字平均 168ms。本轮没有再开 App 录新语料——Agent 不能代替使用者播放测试媒体。实施时用现有诊断字段做改前对照,缺的阶段再补,不要另起一套日志格式。
历史教训必须写进实施约束:把切窗默认词数从 16 降到 12 曾让不变量用例变红且多版本无人发现;旧预览滑窗曾把改写占比做到 94%,展示层追加几乎命不中。节奏数字不会让普通测试变红。
测试缝请确认是否同意:运行态以会话总表为最高缝;代码以现有切窗/分页/展示不变量测试为最高缝;唯一新增缝是三条通道调度。 不同意就在本 Issue 里改,不要另起一套测试金字塔。