Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,7 @@ pro-shots/

# 隔离验收数据库、真实聊天资料和本地状态快照不得进入版本库。
/tmp/deepagents-migration/

# 语音验收包含真实聊天音频、转写、模型权重及本地运行环境。
/tmp/stt-benchmark-20260921/
/tmp/stt-integration-20260921/
2 changes: 2 additions & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
"main": "src/main.cjs",
"scripts": {
"dev": "node scripts/dev.cjs",
"dev:gpu": "cross-env WECHAT_TOOL_QWEN_GPU=1 node scripts/dev.cjs",
"dev:static": "npm --prefix ../frontend run generate && cross-env WECHAT_TOOL_STATIC_UI=1 electron .",
"build:ui": "npm --prefix ../frontend run generate && node scripts/copy-ui.cjs",
"build:backend": "uv sync --no-editable --extra build --extra voice-transcription && node scripts/build-backend.cjs",
"build:backend:gpu": "uv sync --no-editable --extra build --extra voice-transcription --extra voice-transcription-gpu && node scripts/build-backend.cjs --qwen-gpu",
"build:icon": "node scripts/build-icon.cjs",
"build:mac:image-helper": "node scripts/build-macos-image-helper.cjs",
"verify:mac:native": "node scripts/verify-macos-native.cjs --arch arm64 --require-host-arch",
Expand Down
11 changes: 11 additions & 0 deletions desktop/scripts/build-backend.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -652,11 +652,22 @@ function main() {
"--collect-all",
"opencc",
"--collect-all",
"sherpa_onnx",
"--add-data",
pyInstallerAddData(path.join(repoRoot, "src/wechat_decrypt_tool/resources/voice_models.json"), "wechat_decrypt_tool/resources"),
"--collect-all",
"watchfiles",
...aiPackagingArgs(repoRoot),
entry,
];

// CUDA/PyTorch 体积较大,仅在明确构建 Qwen GPU 版本时收集。
if (process.argv.includes("--qwen-gpu")) {
args.splice(args.length - 1, 0, "--collect-all", "torch", "--collect-all", "transformers");
} else {
args.splice(args.length - 1, 0, "--exclude-module", "torch", "--exclude-module", "transformers");
}

if (process.platform === "win32") {
args.splice(
args.length - 1,
Expand Down
4 changes: 3 additions & 1 deletion desktop/src/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2174,7 +2174,9 @@ function startBackend() {
// The desktop backend only needs runtime dependencies. Letting `uv run`
// include the default dev group can block Electron startup on an unrelated
// pytest/Pygments download before Python is even launched.
backendProc = spawn("uv", ["run", "--no-dev", "main.py"], {
const voiceExtras = ["--extra", "voice-transcription"];
if (env.WECHAT_TOOL_QWEN_GPU === "1") voiceExtras.push("--extra", "voice-transcription-gpu");
backendProc = spawn("uv", ["run", "--no-dev", ...voiceExtras, "main.py"], {
cwd: repoRoot(),
env,
stdio: "inherit",
Expand Down
3 changes: 2 additions & 1 deletion desktop/tests/native-core-runtime.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,8 @@ test("desktop startBackend clears legacy WCDB state and never starts the sidecar
const startBackend = mainSource.match(/function startBackend\(\) \{([\s\S]*?)\n\}/)?.[1] || "";
assert.match(startBackend, /configureNativeCoreRuntime\(env\)/);
assert.match(startBackend, /clearLegacyWcdbEnvironment\(env\)/);
assert.match(startBackend, /spawn\("uv", \["run", "--no-dev", "main\.py"\]/);
assert.match(startBackend, /spawn\("uv", \["run", "--no-dev", \.\.\.voiceExtras, "main\.py"\]/);
assert.match(startBackend, /voiceExtras = \["--extra", "voice-transcription"\]/);
assert.match(startBackend, /PYTHONIOENCODING:\s*"utf-8"/);
assert.match(startBackend, /WECHAT_TOOL_NODE_EXECUTABLE:\s*process\.execPath/);
assert.match(startBackend, /WECHAT_TOOL_NODE_MODE:\s*"electron-run-as-node"/);
Expand Down
83 changes: 83 additions & 0 deletions docs/stt-benchmark-2026-09-21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# 本机 STT 模型对照测试(2026-09-21)

已完成 12 个运行配置。样本为项目已有的 20 条真实微信语音,共 202.54 秒、5 个会话。

## 对升级方案的修订

- **低配:优先 Zipformer CTC INT8。** 本批语音约 2.74 秒,Tiny 约 22.17 秒,快约 8.1 倍;与微信转写的差异率从 26.44% 降至 10.22%。CTC 进程峰值约 390 MiB,比 Tiny 的 279 MiB 高,不能宣称内存也更省。
- **主流 CPU:Qwen3-ASR 0.6B ONNX INT4 值得接入。** 约 61.59 秒,相比 Medium CPU 的 163.63 秒快约 2.66 倍,差异率从 8.11% 降至 4.35%;代价是进程峰值从约 1.63 GiB 增至 3.65 GiB。建议先面向 16 GB 内存设备,4 GB 老电脑不以它作为默认。
- **GPU:保留 Turbo 极速选项,增加 Qwen 质量优先选项。** Qwen 0.6B 为 38.57 秒、差异率 3.64%;1.7B 为 44.16 秒、2.94%;Turbo 为 7.87 秒、6.82%;原 Large v3 为 20.51 秒、6.93%。标准 Transformers 路径没有带来 GPU 提速,1.7B 是本批样本与参考最接近的配置,但耗时约为 Large v3 的 2.15 倍。
- **不单列 Transducer 均衡档。** 本批结果 3.61 秒、差异率 11.16%,没有体现相对 CTC 的价值。20 条样本不足以判定它在其他数据上一定较差。
- 这些结果足以调整工程接入顺序,不能证明真实准确率已经提升。下一步需要听原音校对参考,以及在真实低配设备上验收;当前只有本机 CPU 路径测试。

## 方法与适用边界

- 本机:Windows、Ryzen 5 5600X(6 核 12 线程)、32 GB 内存、RTX 4070 SUPER 12 GB,驱动 596.49。
- 从 828 条已有微信转写的候选中,以固定种子 20260921 抽取。按 0.8–3、3–8、8–20、20–60 秒各选五条;实际最长时长见本地清单。仅有转写的语音会入选,存在选择偏差。
- 统一解码为 16 kHz 单声道,所有模型使用完全相同的 WAV;音频和文本未发送到外部 ASR 服务。
- 每模型独立进程、CPU 4 线程、单条串行,预热一条后随机顺序运行两轮。耗时是两轮热运行均值,包含特征提取和识别,不含 SILK 解码、下载、模型加载及应用界面开销。
- 指定推理引擎使用 4 个 CPU 线程;这不等同于模拟低端 CPU,也不保证第三方库与整台电脑总共只有 4 个线程。部分测试期间后台仍在下载权重,速度作为本机初筛结果。
- 原 Whisper 参数沿用项目:中文、beam_size=5、vad_filter=True、condition_on_previous_text=False。Qwen 指定中文、贪心解码;各模型按对应部署路径运行,并非相同架构/解码算法的微基准。
- 差异率采用字符编辑距离 / 参考字符数;统一简繁、全半角、大小写,去空白和标点。参考是未人工校对的微信机器转写,因此该数值不能当作真实错误率,更不能用 100% 减去它声称准确率。
- 不提供低配 CPU 的外推保证:本机仅在 CPU 路径上测试,并非 N100 或 4 GB 老电脑实测。样本量不足以证明所有方言、噪声和人名场景的整体提升。
- 进程内存是 50 ms 采样的峰值工作集,包含推理库。Qwen 显存为 PyTorch 分配峰值;该值不等于整张显卡占用,不能与未测得显存的数据直接比较。
- 测试用 faster-whisper 1.2.1 与项目一致;测试 CTranslate2 为 4.8.2,项目原环境为 4.8.1。其他依赖和模型 revision 已留档;因此这是沿用项目参数的独立环境对照,并非原应用端到端计时。

## 实测汇总

| 配置 | 203 秒音频耗时 | RTF↓ | 单条 P95 | 与微信转写差异率↓ | 峰值进程内存 | Qwen 分配显存峰值 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| whisper-tiny-cpu | 22.17 s | 0.109 | 5.11 s | 26.44% | 279 MiB | 未测 |
| whisper-base-cpu | 21.95 s | 0.108 | 1.83 s | 17.04% | 407 MiB | 未测 |
| whisper-small-cpu | 57.80 s | 0.285 | 5.29 s | 10.58% | 593 MiB | 未测 |
| whisper-medium-cpu | 163.63 s | 0.808 | 13.75 s | 8.11% | 1674 MiB | 未测 |
| zipformer-ctc | 2.74 s | 0.014 | 0.34 s | 10.22% | 390 MiB | 未测 |
| zipformer-rnnt | 3.61 s | 0.018 | 0.43 s | 11.16% | 420 MiB | 未测 |
| qwen-onnx-cpu | 61.59 s | 0.304 | 7.87 s | 4.35% | 3737 MiB | 未测 |
| whisper-medium-cuda | 13.66 s | 0.067 | 1.52 s | 10.22% | 1607 MiB | 未测 |
| whisper-turbo-cuda | 7.87 s | 0.039 | 0.80 s | 6.82% | 1676 MiB | 未测 |
| whisper-large-v3-cuda | 20.51 s | 0.101 | 2.42 s | 6.93% | 3082 MiB | 未测 |
| qwen-06-cuda | 38.57 s | 0.190 | 4.61 s | 3.64% | 2401 MiB | 1.67 GiB |
| qwen-17-cuda | 44.16 s | 0.218 | 5.16 s | 2.94% | 4800 MiB | 4.01 GiB |

RTF = 识别耗时 / 音频时长,越低越快。不同模型资源档位不代表质量必然单调上升。

每轮使用相同的随机顺序策略。差异率按第一轮输出计算;Tiny、Base 在两轮中分别有 2 条、1 条输出变化,其余配置是否变化可查原始记录。

## 部署中发现的问题

- Zipformer 的原始 ONNX 文件直接处理约 22 秒样本时,CTC 和 Transducer 均出现 Reshape 维度错误;两份原始失败日志保留在测试目录。表中结果是增加最多 15 秒、末段低能量切分后的配置,不能把它描述成无改动即可替换。
- Qwen ONNX 上游示例硬编码的 system/user token ID 与下载模型的分词器不一致。测试入口改为用实际分词器编码提示模板;参考文本没有进入提示词。
- ONNX CPU 候选是社区导出,官方 HF GPU 候选是另一套转换/运行路径,量化和预处理差异需要随发布版本固定。
- Qwen CPU 特征提取与上游 PyTorch 实现做了同一条音频的数值核对:最大绝对差约 4.86e-5、平均绝对差约 3.28e-7;这验证了实现一致性,不等于量化质量验证。

## 冷启动参考

| 配置 | 加载阶段(含库导入) | 首条推理 |
| --- | ---: | ---: |
| whisper-tiny-cpu | 2.37 s | 1.43 s |
| whisper-base-cpu | 0.51 s | 6.21 s |
| whisper-small-cpu | 1.22 s | 1.83 s |
| whisper-medium-cpu | 3.13 s | 5.44 s |
| zipformer-ctc | 0.99 s | 0.02 s |
| zipformer-rnnt | 0.98 s | 0.05 s |
| qwen-onnx-cpu | 8.07 s | 2.49 s |
| whisper-medium-cuda | 2.11 s | 0.77 s |
| whisper-turbo-cuda | 2.16 s | 0.55 s |
| whisper-large-v3-cuda | 3.42 s | 0.71 s |
| qwen-06-cuda | 9.76 s | 1.24 s |
| qwen-17-cuda | 13.30 s | 1.07 s |

首条采用同一短语音;未清空 Windows 文件缓存,因此不能视作重启电脑后的磁盘冷启动。

## 复现与证据

- 入口:`tools/benchmark_stt_local.py`。测试资产、环境版本、模型 revision、逐条结果位于 `tmp/stt-benchmark-20260921/`。
- `summary.json` 不含聊天原文;`manifest.private.json`、`results/*.private.json` 和 `review.private.html` 含本地语音或转写,不纳入公开报告。
- 抽样清单记录 WAV 的 SHA-256;全部 20 条音频、10 组模型资产的大小及 LFS 哈希已重新核对。12 个运行配置共 480 条推理记录,通过独立动态规划编辑距离复算;记录见 `verification.json`。
- `model-lock.json` 记录实际下载的仓库、revision 和文件清单;`requirements-lock.txt` 留存测试环境依赖。项目 Turbo 的原仓库地址当前重定向至 `dropbox-dash/faster-whisper-large-v3-turbo`,测试使用该重定向目标。
- 当前应用模型设置和项目主虚拟环境未更改;测试依赖安装在独立虚拟环境。

```powershell
tmp/stt-benchmark-20260921/venv/Scripts/python.exe tools/benchmark_stt_local.py --root tmp/stt-benchmark-20260921 --model qwen-06-cuda
```
95 changes: 95 additions & 0 deletions docs/stt-integration-2026-09-21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# 语音识别升级接入说明

2026-09-22:软件仅提供 Zipformer CTC、Qwen3-ASR 0.6B CPU、Turbo、Qwen3-ASR 0.6B GPU 四个选项,默认选择 CTC。此次未生成或替换正式安装包。

## 软件中的入口

在设置的“语音识别模型”中下载、选择模型;聊天页的语音转写侧栏使用同一组选项。Tiny、Base、Small、Medium、Large v3 和 Qwen 1.7B 已从列表及下载入口移除。未安装的运行组件会显示原因,不能误选成可用模型。

| 档位 | 选项 | 运行设备 |
| --- | --- | --- |
| 低配极速 | Zipformer CTC INT8 | CPU;中英文、无标点 |
| 中配质量优先 | Qwen3-ASR 0.6B ONNX INT4 | CPU;建议 16 GB 内存 |
| GPU 速度优先 | 原 Whisper Turbo | NVIDIA GPU,保留原 CPU 回退逻辑 |
| GPU 质量优先 | Qwen3-ASR 0.6B | NVIDIA GPU,需单独的 Qwen GPU 运行组件 |

CPU/GPU 版本是独立选项。选中新模型会设置匹配的设备;环境变量锁定设备时不会覆盖。Qwen GPU 失败会给出错误,不会悄悄切换另一模型。新后端单进程串行复用,避免批量并发创建多份模型;取消时终止工作进程,下一条任务可以重新加载。空闲 120 秒后进程自动释放。

此前验收的四个新模型和 Turbo 的文件已安装到 `%APPDATA%/wechat-data-analysis-desktop/voice_models/`,新模型复制前已校验固定版本的 SHA-256。停用模型的文件及历史转写不会自动删除。旧 Whisper CPU 配置读取时转到 CTC,CUDA 配置转到 Turbo;Qwen 1.7B 转到 0.6B GPU。界面显示迁移提示,用户可重新选择。迁移不改写原设置,环境变量固定的设备不会被覆盖;若与模型冲突,界面会提示选择匹配设备。

## 启动及构建

项目普通开发启动会安装 CPU 语音依赖。前端需要 Node 20.19+ 或 22.12+;本机系统 Node 18 太旧,本次测试和构建使用已存在的 Codex Node 运行时。使用符合版本要求的 Node 后,要启用本机已验证的 Qwen GPU,在仓库根目录运行:

```powershell
npm --prefix desktop run dev:gpu
```

本机不更改系统 Node 也可以这样启动:

```powershell
$env:WECHAT_TOOL_QWEN_GPU = '1'
& "$env:USERPROFILE/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node.exe" desktop/scripts/dev.cjs
```

也可以手动安装:

```powershell
uv sync --extra voice-transcription
# 需要 Qwen GPU 时加上该扩展;Windows 锁定官方 PyTorch CUDA 12.8 索引。
uv sync --extra voice-transcription --extra voice-transcription-gpu
```

`desktop` 的 `build:backend` 构建 CPU 版本,包含 sherpa-onnx 和新模型资产清单,排除 PyTorch/Transformers;`build:backend:gpu` 额外收集 Qwen GPU 组件。默认 `dist:win` 仍走 CPU 后端构建,不应据此宣称普通安装包已包含 Qwen GPU。完整 GPU 安装包需要沿用项目正式签名和原生核心构建流程,使用 GPU 后端构建产物。

## 文件及缓存

- `resources/voice_models.json` 固定 Hugging Face 仓库、revision、必要文件、大小和 SHA-256;下载后先校验,再原子发布。
- `asr_models.py` 定义模型目录及能力;`asr_backends.py` 提供三种后端;`asr_worker.py` 隔离模型、内存、取消和 CUDA 探测。
- 新模型缓存键包含模型 ID、revision、后端和缓存版本;旧 Whisper 缓存键保持原样,切换不会误用另一模型的文本。
- 推理只读取本地权重和音频,工作进程启用 Hugging Face 离线模式。Zipformer 最长 15 秒、Qwen 最长 25 秒分段,优先在低能量位置切分。
- Qwen ONNX 按实际 tokenizer 编码角色提示,避免社区示例的固定 token ID 不匹配;CPU 特征提取不依赖 PyTorch。
- Windows Whisper CUDA 可以复用已安装 PyTorch 中的 CUDA 12 DLL,解决只有系统 CUDA 13 时的依赖缺失。

## 初次接入验收(历史数据,包含现已停用的 1.7B)

通过项目正式 `VoiceTranscriptionService.transcribe_voice` 读取并解码 20 条真实 SILK,四模型共 80 次成功;写缓存和批量缓存查询均验证通过。缓存写入测试目录,没有改写原会话的转写缓存。音频总长 202.54 秒。

| 模型 | 20 条总耗时,含加载、解码和缓存 | 与微信机器参考的字符差异率 |
| --- | ---: | ---: |
| Zipformer CTC | 6.202 秒 | 10.58% |
| Qwen 0.6B CPU | 65.125 秒 | 4.47% |
| Qwen 0.6B GPU | 51.954 秒 | 3.53% |
| Qwen 1.7B GPU | 47.384 秒 | 2.82% |

本表是单轮完整链路验收,与之前仅计推理的双轮基准计时不同,不能混算加速倍数。微信机器转写未经人工校对,差异率不是人工标注准确率;低配最低硬件、方言、噪声和长录音仍需更大语料验证。

回归结果:后端 132 通过、1 跳过;前端语音组件 63 通过;设置契约与桌面启动契约 28 通过;前端 Nuxt 生产静态构建成功。真实工作进程取消后约 81 毫秒完成回收,再次识别成功;Turbo 实际 CUDA 推理成功。NumPy 声学特征与上游 PyTorch 版本比较,最大绝对误差小于 0.0001。

界面机械检查仅发现原有进度条的 width 动画告警,没有新增告警。后续启动真实 Electron 开发应用完成桌面验收:设置中选择 Zipformer,聊天消息实际转写并显示结果;批量扫描可以启动和取消;Qwen 1.7B GPU 同样在聊天页转写成功,来源提示显示正确模型。修正了聊天来源提示残留的 Whisper 专属文案。

运行中的应用 HTTP API 对四个新模型各处理 3 条真实语音,并逐条验证缓存命中、模型 ID 与 CPU/GPU 设备。另将原有 20 条 SILK 复制到隔离测试账号,使用正式批量管理器完整转写:20/20 成功、失败 0,配置并发 4 时实际限制为 1;再次扫描 20/20 命中缓存。真实账号的全库扫描仅验证启动和取消,没有等待全部历史语音完成。正式安装包及其冻结工作进程尚未完成验收。

私人音频、参考文本、逐条输出和测试数据库保存在被 Git 忽略的 `tmp/stt-benchmark-20260921/` 与 `tmp/stt-integration-20260921/`;本文不包含会话内容。

## 独立 PR 基线复验(历史状态)

PR 基于上游 `main` 的 `2646cfdf`,只移入本次语音升级。上游尚无开发分支上的导出语音选项,因此没有带入相关导出界面和导出状态改动。

独立工作区后端相关用例为 131 通过、1 跳过、1 失败;唯一失败是 `test_export_option_is_wired_from_dialog_to_backend`,其要求的 `exportTranscribeVoice` 控件在上游不存在。已从未修改的 `origin/main` 提取该测试及其全部输入文件,独立复现相同断言失败;本 PR 不修改这项测试。语音组件 63 项、设置与桌面启动契约 28 项均通过。使用符合前端版本要求的 Node 重新安装锁定依赖后,Nuxt 生产静态构建成功,预渲染 34 个路由。

## 四模型收敛验收(2026-09-22)

移除旧模型折叠入口,模型卡片改为用途及配置说明,不再使用本机测试数据作为产品文案。新增旧设置迁移、停用模型选择及下载拒绝、四项模型目录一致性回归。

独立 PR 工作区后端 152 项通过,另有 14 项子测试通过;原有导出控件契约失败仍可复现。设置及桌面契约 28 项通过,语音组件 63 项通过,Nuxt 静态构建成功。启动实际 Electron 应用检查四项卡片与说明;四个保留模型各完成两条真实语音的应用 HTTP 转写,并逐条验证缓存,旧六项选择接口均返回 invalid_model。

## 合并修复验收(2026-09-22)

补齐导出语音转文字与 HTML 远程缩略图开关,默认关闭;语音转写使用设置中选择的模型,在隐私模式或未选择语音时禁用。此前缺少导出控件的契约测试已通过。

修复 #150:小程序、小游戏的非 URL 缩略图标识通过会话附件目录查找,解密后写入导出媒体,JSON 的 `thumbUrl` 改为导出根目录下的相对路径。仅选链接也会包含这些本地图片,不需要开启远程下载。缺失或无效图片保留原标识并计入缺失数量;缩略图文件名和缓存按会话隔离,避免同秒消息编号冲突。

Windows CI 失败发生在 AI 并行功能测试的 45 秒超时,并非模型推理。用八条同秒消息保留四个工作槽和两轮调度覆盖,功能用例超时调整为 180 秒;覆盖率、去重和续跑断言不变,并行加速仍由独立性能用例验证。

本机独立 PR 工作区:AI 与本地搜索 730 项通过;导出与语音相关 193 项及 27 项子测试通过(包含修复后复验);前端全套 384 项通过,新增远程下载条件后导出专项 8 项通过;Nuxt 静态构建成功。新增小程序缩略图 16 项用例使用真实 SQLite 消息库、加密附件和正式导出管理器,验证 ZIP 中实际图片字节与 JSON 引用,覆盖两种消息类型、标识回退、跨会话、无效文件、缺失、关闭媒体、隐私及 HTTP 地址。
Loading
Loading