8065e2e4bd1299ada56e2ba50223c82ffe2f2ccb3bc1698c0ae11f0101c330400365d7b3626927a3(分支 v0.4.2)一条需求主线:用户观察到「预览(160ms 快模型)已经把内容显示出来了,第二次识别(1920ms 慢模型)好像很少改动」,提出能不能默认不跑第二次识别。
增量的落地方式是「加开关 + 兜底 + 埋点,判据交给数据」,核心改动如下:
| 文件 | 变更 | 说明 |
|---|---|---|
internal/config/config.go | 新增 FinalASRConfig.Enabled bool(json:"enabled"),默认 true | 开关本身;Load() 以 Defaults() 打底再 Unmarshal,老配置缺该字段时保留默认 true(不会静默退回 false) |
cmd/voiceimed/ipc.go | restartRequiredFields 新增 asr.final.enabled | 列为「需重启生效」,与实际实现一致(Session 无通用 SetConfig,deps.Config 构造期固定) |
internal/session/session.go | 新增 recognize()(模式分流)、previewSession.finalize()(预览收尾)、finishPreview()、collectAudio() 增返预览文本、transcribe() 增度量字段 | 主体逻辑 |
internal/session/textdiff.go | 新增 (两行滚动数组 Levenshtein) |
设计要点(正确性正评):
Load() 的默认值打底保证存量配置不被误关;preview_runes / diff_runes(字符数 + 编辑距离),不含任何文本内容,符合项目「日志绝不记识别内容」的红线;你好→你号 被误判为「没改」。codescan_issues_diff,base→head)工具报告 修复 32 条 / 新增 36 条。逐条对照行号后可以判定:这 32 条里没有一条是真正被修掉的缺陷,全部是「同一函数整体下移导致行号漂移」造成的重复计数(下方 §4 有 1:1 配对证据)。真实数据如下:
修复的问题(32 条)
- [Medium/Dependency] 循环导入 @ cmd/voiceimed/ipc.go:1
- [Medium/Complexity] 递归调用 @ internal/session/session.go:499
- [Medium/Complexity] 圈复杂度过高 @ internal/session/session.go:556
- [Low/Maintainability] 重复代码块 @ internal/session/session.go:580
- [Medium/Concurrency] goroutine 泄漏风险 @ internal/session/session.go:642
- [Low/Maintainability] 重复代码块 @ internal/session/session.go:784
- [Medium/Complexity] 圈复杂度过高 @ internal/session/session.go:1137
- [Low/Maintainability] 过度日志 @ internal/session/session.go:1137
- [Low/Bug] 缺少默认分支 @ internal/session/session.go:1138
- [Low/Maintainability] 重复代码块 @ internal/session/session.go:1154
- [Medium/Complexity] 嵌套深度过高 @ internal/session/session.go:1203
- [Medium/Complexity] 函数过长 @ internal/session/session.go:1203
- [Low/Maintainability] 过度日志 @ internal/session/session.go:1203
- [Medium/Complexity] 圈复杂度过高 @ internal/session/session.go:1502
- [Medium/Complexity] 函数过长 @ internal/session/session.go:1502
- [Low/Maintainability] 过度日志 @ internal/session/session.go:1502
- [Low/Bug] 缺少默认分支 @ internal/session/session.go:1572
- [Medium/Complexity] 函数过长 @ internal/session/session.go:1630
- [Low/Maintainability] 过度日志 @ internal/session/session.go:1630
- [Medium/Complexity] 函数过长 @ internal/session/session.go:1724
- [Low/Maintainability] 过度日志 @ internal/session/session.go:1724
- [Low/Maintainability] 过度日志 @ internal/session/session.go:1845
- [Medium/Complexity] 函数过长 @ internal/session/session.go:1963
- [Low/Maintainability] 过度日志 @ internal/session/session.go:1963
- [Low/Maintainability] 过度日志 @ internal/session/session.go:2071
- [Medium/Concurrency] goroutine 泄漏风险 @ internal/session/session.go:2314
- [Low/Maintainability] 过度日志 @ internal/session/session.go:2429
- [Medium/Concurrency] goroutine 泄漏风险 @ internal/session/session.go:2575
- [Medium/Complexity] 圈复杂度过高 @ internal/session/session.go:2655
- [Medium/Complexity] 圈复杂度过高 @ internal/session/watchdog_test.go:68
- [Medium/Complexity] 嵌套深度过高 @ internal/session/watchdog_test.go:68
- [Medium/Complexity] 函数过长 @ internal/session/watchdog_test.go:68
结论:本增量未修复任何被工具标记的质量问题(也没有需要「修复」的历史缺陷入口);这 32 条应视为噪声,不计入「质量改善」。
codescan_issues_diff,base→head)新增的问题(36 条)
- [Medium/Dependency] 循环导入 @ cmd/voiceimed/voiceprint.go:1(circular import 'voiceprint' <-> 'ipc')
- [Low/Maintainability] 重复代码块 @ internal/session/final_off_test.go:22
- [Medium/Dependency] 高耦合 @ internal/session/final_off_test.go:67(TestSession_LogsPreviewVsFinalDiffMetric 依赖 12 函数)
- [Medium/Complexity] 递归调用 @ internal/session/session.go:520(append)
- [Medium/Complexity] 圈复杂度过高 @ internal/session/session.go:577
- [Low/Maintainability] 重复代码块 @ internal/session/session.go:601
- [Medium/Concurrency] goroutine 泄漏风险 @ internal/session/session.go:663
- [Low/Maintainability] 重复代码块 @ internal/session/session.go:805
- [Medium/Complexity] 圈复杂度过高 @ internal/session/session.go:1158
- [Low/Maintainability] 过度日志 @ internal/session/session.go:1158
- [Low/Bug] 缺少默认分支 @ internal/session/session.go:1159
- [Low/Maintainability] 重复代码块 @ internal/session/session.go:1175
- [Medium/Complexity] 嵌套深度过高 @ internal/session/session.go:1224
- [Medium/Complexity] 函数过长 @ internal/session/session.go:1224(startListening 105 行)
- [Low/Maintainability] 过度日志 @ internal/session/session.go:1224
- [Medium/Complexity] 圈复杂度过高 @ internal/session/session.go:1523
- [Medium/Complexity] 函数过长 @ internal/session/session.go:1523(filterVoiceprint 122 行)
- [Low/Maintainability] 过度日志 @ internal/session/session.go:1523
- [Low/Bug] 缺少默认分支 @ internal/session/session.go:1593
- [Medium/Complexity] 函数过长 @ internal/session/session.go:1651(finishAndInject 91 行)
- [Low/Maintainability] 过度日志 @ internal/session/session.go:1651
- [Medium/Complexity] 函数过长 @ internal/session/session.go:1749(deliver 111 行)
- [Low/Maintainability] 行过长 @ internal/session/session.go:1749
- [Low/Maintainability] 过度日志 @ internal/session/session.go:1749
- [Low/Maintainability] 过度日志 @ internal/session/session.go:1870
- [Medium/Dependency] 高耦合 @ internal/session/session.go:1971(transcribe 依赖 11 函数)
- [Medium/Complexity] 函数过长 @ internal/session/session.go:2025(inject 90 行)
- [Low/Maintainability] 过度日志 @ internal/session/session.go:2025
- [Low/Maintainability] 过度日志 @ internal/session/session.go:2212
- [Medium/Concurrency] goroutine 泄漏风险 @ internal/session/session.go:2377
- [Low/Maintainability] 过度日志 @ internal/session/session.go:2492
- [Medium/Concurrency] goroutine 泄漏风险 @ internal/session/session.go:2676
- [Medium/Complexity] 圈复杂度过高 @ internal/session/session.go:2756
- [Medium/Complexity] 圈复杂度过高 @ internal/session/watchdog_test.go:96
- [Medium/Complexity] 嵌套深度过高 @ internal/session/watchdog_test.go:96
- [Medium/Complexity] 函数过长 @ internal/session/watchdog_test.go:96(TestSession_Watchdog 120 行)
把「修复」与「新增」按偏移量配对(session.go 普遍 +21 / +25 行、watchdog_test.go +28 行,正是本次在对应函数上方插入注释与新代码所致),可以一一对上:
| 基线行号(修复) | 目标行号(新增) | 偏移 | 同一问题 |
|---|---|---|---|
| session.go:499 递归 | session.go:520 递归 | +21 | append 递归(误报,实际是 r.pcm = append(...)) |
| session.go:556 圈复杂度 | session.go:577 | +21 | 同一函数 |
| session.go:642 goroutine | session.go:663 | +21 | 同一处 |
| session.go:1137/1138 | session.go:1158/1159 | +21 | 同一处 |
| session.go:1203 三项 | session.go:1224 三项 | +21 | startListening |
| session.go:1502 三项 | session.go:1523 三项 | +21 | filterVoiceprint |
| session.go:1630 | session.go:1651 | +21 | finishAndInject |
| session.go:1724 |
真实新增(4 处,其中 3 处在测试代码里):
internal/session/final_off_test.go:22 重复代码块、:67 高耦合 —— 新测试文件的复制粘贴/依赖面宽,属可接受噪声;internal/session/session.go:1971 transcribe 高耦合(依赖 11 函数) —— 因新增 runeEditDistance 调用越过了阈值,属结构性轻微退化;internal/session/session.go:1749 deliver 单行超 120 字符 —— 新签名加了 previewText string 参数所致(deliver(ctx, pcm, label, limitHit, previewText, marks)),一行超长,建议拆行;cmd/voiceimed/voiceprint.go:1 循环导入 voiceprint ↔ ipc —— 与本增量代码无直接关系(本次 PR 未改 voiceprint.go),更可能是 ipc.go 整体下移后该包级环被重新锚定到相邻文件,判定为存量/漂移。净结论:本增量的「新增问题」= 3 条测试可接受噪声 + 1 条真实可改进(deliver 超长行)+ 1 条结构性轻微退化(transcribe 高耦合)+ 33 条行号漂移。不存在新引入的功能性缺陷级别告警。
工具只报「复杂度/耦合/日志」这类结构化指标,下面的语义问题需要读逻辑才能发现。
位置:internal/session/session.go — finishAndInject() 与新增的 recognize() / collectAudio()
逻辑链:
startListening 内 rec.preview.accept(f.PCM),预览流拿到的是未经任何过滤的全量音频);filterVoiceprint(pcm) 只对 pcm 做「别人的话」剔除,previewText 不受影响;asr.final.enabled=false 时,recognize() 直接 return previewText, nil(session.go 约 1952 行),被过滤后的 pcm 根本不参与识别。后果:
inject() 成功分支仍按 s.voiceprintDropped 显示「已完成(跳过 N 处不像同一个人的话)」,但上屏文本里恰好包含这 N 处——用户被告知「跳过了」,实际一个字没少;建议(任一):
voiceprintDropped > 0 就按「无可用预览文本」走回退 final(与现有兜底同一入口,改动最小、语义最稳);recognize() 里显式判断 s.voiceprintDropped > 0 并跳过预览路径;位置:collectAudio() → finishPreview()(session.go 约 2560–2600 行)
finishAndInject() 的顺序是 pcm, previewText := s.collectAudio(); marks["pcm_ready"] = time.Now(),而 collectAudio() 内部在返回前就执行了 finishPreview()(补 500ms 静音 + stream.Final 解码,上限 previewFinalizeTimeout = 5s)。这意味着:
Final——而这时预览文本只用于度量字段,属于「为埋点付关键路径成本」;该耗时会进入 pack_ms(pcm_ready - keyup),与项目对 pack_ms 的敏感度(docs/17 §1)相冲突;previewFinalizeTimeout 给到 5s,异常时会明显拖长松手后的等待(虽然会先到超时,再 final 照跑)。建议:度量所需文本可改为「复用 HUD 上最后一帧 ps.last」或把 finishPreview 放到 marks["pcm_ready"] 之后(甚至只在 asr.final.enabled=false 时才在关键路径上执行收尾)。
位置:finishPreview()(session.go)与 transcribe()(session.go 约 1996 行)
finishPreview() 先做 text = s.deps.PreviewCorrector(text),随后该校正后的文本被当作 previewText;而用于比较的 res.Text 是 final 的原始输出(未过 Polisher/重整)。因此 diff_runes 是「已词表校正的预览 vs 未加工的 final」,会系统性低估/偏移真实差异——而 diff_runes 恰恰被文档定义为「要不要保留 final」的唯一判据。口径不同源会把结论带偏(文档本身也强调「口径错了会把结论带偏」)。
建议:让比较两侧同源(都用「未校正/未加工」的原始流文本),或在文档里明确 diff_runes 的口径含义。
finishPreview 用 context.Background(),不随会话取消位置:finishPreview()(session.go)
即使会话/进程正在退出(ctx 已取消),预览收尾仍会跑到自身 5s 超时。正常无害,但会轻微拖慢退出路径。建议绑定到会话 ctx 或更短的上限。
config.Load() 以 Defaults() 打底再 Unmarshal,老 config.json 没有 enabled 字段时保留默认 true,不会因为「零值 false」把所有老用户静默切到预览上屏;app.js 的 collectConfig() 基于 structuredClone(savedConfig) 打补丁,只改已知表单项,未在界面暴露的 asr.final.enabled 会被原样带回,不会被重置为 false。| 优先级 | 问题 | 位置 | 建议 |
|---|---|---|---|
| 🔴 高 | 关 final 时声纹过滤被绕过,且 HUD 文案与实际不一致 | session.go recognize() / finishAndInject() | 声纹有跳过时回退 final,或在关 final 时禁用该路径;同步补文档代价清单 |
| 🟠 中 | 预览收尾进入松手关键路径(默认也跑) | session.go collectAudio()/finishPreview() | 移出关键路径或仅关 final 时执行 |
| 🟡 低 | diff_runes 两侧不同源(预览已校正 / final 未加工) | session.go finishPreview()/transcribe() | 统一口径,或在文档标注含义 |
| 🟡 低 | deliver 单行超 120 字符 | session.go:1749 | 拆行 |
| 🟢 提示 | 高耦合(依赖 11 函数,越阈值) |
总体判断:本次增量是一处设计克制、默认安全的功能开关(默认真、带兜底、埋点只出数字),结构化质量基本持平(无真实回归)。唯一需要在发布前处理的是 4.1 声纹过滤被绕过——它在「关掉 final」这一新路径上悄悄削掉了一个已有的隐私/防误录能力,且与 HUD 文案冲突,属于新路径引入的语义缺口,而非既有缺陷。
codescan_git_diff(base=8065e2e4…, head=3bc1698c…)codescan_issues_diff(project=jyk/voice-ime, branch=v0.4.2, base=8065e2e4…, head=3bc1698c…) → 修复 32 / 新增 36internal/session/session.go(recognize/finishAndInject/collectAudio/finishPreview/transcribe/filterVoiceprint/startListening)、internal/config/config.go(Load/Defaults)、internal/asr/sherpa.go(Partial/Final 锁语义)、cmd/voiceimed/ipc.go(restartRequiredFields)、cmd/voiceime-app/frontend/app.js(collectConfig)SherpaEngine/sherpaStream 内部有互斥(s.mu + 引擎锁),预览收尾 Final 与 Partial 会串行化,且预览与 final 是两个独立引擎,未发现数据竞争或死锁(这一项排除了对新路径最担心的并发风险)。runeEditDistance()| 度量 final 相对预览的字符级编辑距离,只出数字 |
internal/session/final_off_test.go | 新增 3 个用例 | 锁「关 final 用预览」「无预览回退 final」「度量字段落日志」 |
internal/session/textdiff_test.go | 新增 | 锁编辑距离口径(含「长度差看不见、编辑距离看得见」的用例) |
docs/10、docs/17、docs/19 | 文档同步 | 配置项说明、经验库、设计章节 §2.2.1 |
| session.go:1749 |
| +25 |
| deliver |
| session.go:2071 | session.go:2212 | +141? | handleRetry 附近过度日志(下移更多) |
| watchdog_test.go:68 三项 | watchdog_test.go:96 三项 | +28 | TestSession_Watchdog |
transcribesession.go:1971 |
| 可接受;后续拆度量字段拼装函数 |
| 🟢 提示 | 33 条「新增」为行号漂移 + 3 条测试噪声 | 全量 | 无需处理;工具应做函数级去重 |