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
62 changes: 62 additions & 0 deletions .github/workflows/chat-image-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Chat Image Quality Checks

on:
pull_request:
paths:
- '.github/workflows/chat-image-quality.yml'
- 'src/wechat_decrypt_tool/media_helpers.py'
- 'src/wechat_decrypt_tool/chat_export_service.py'
- 'src/wechat_decrypt_tool/routers/media.py'
- 'src/wechat_decrypt_tool/routers/chat*.py'
- 'tests/test_chat_image_local_quality.py'
- 'tests/test_chat_media_image_cache_upgrade.py'
- 'tests/test_media_decrypt_stream_cancel.py'
- 'frontend/lib/chat/chat-history.js'
- 'frontend/lib/chat/message-normalizer.js'
- 'frontend/tests/chat-image-quality.test.js'
- 'pyproject.toml'
- 'uv.lock'
workflow_dispatch:

permissions:
contents: read

jobs:
images:
strategy:
fail-fast: false
matrix:
os: [windows-2022, macos-14]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.11'
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- run: python -m pip install uv
- run: uv sync --locked
# 使用实际编码的 PNG/JPEG/WebP 与 XOR DAT,不需要真实账号、显卡或 CDN。
- name: Local image display, export and predecryption regression
run: >-
uv run --no-sync pytest -q
tests/test_chat_image_local_quality.py
tests/test_chat_media_image_cache_upgrade.py
tests/test_media_decrypt_stream_cancel.py
tests/test_chat_media_file_id_scope.py
tests/test_chat_media_record_video.py
tests/test_chat_large_image_frontend.py
tests/test_chat_image_group_info.py
tests/test_image_key_verified_store.py
tests/test_image_key_resolver.py
tests/test_media_emoticon_catalog.py
tests/test_media_emoji_download_stream.py
- name: Install frontend dependencies
working-directory: frontend
run: npm ci
- name: Image browser cache regression
working-directory: frontend
run: npx vitest run tests/chat-image-quality.test.js
54 changes: 54 additions & 0 deletions .github/workflows/qwen-gpu-runtime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Qwen GPU Runtime Checks

on:
pull_request:
paths:
- '.github/workflows/qwen-gpu-runtime.yml'
- 'src/wechat_decrypt_tool/qwen_gpu_runtime.py'
- 'src/wechat_decrypt_tool/asr_*.py'
- 'src/wechat_decrypt_tool/voice_transcription.py'
- 'src/wechat_decrypt_tool/routers/chat_media.py'
- 'src/wechat_decrypt_tool/resources/qwen_gpu_runtime.json'
- 'tests/test_qwen_gpu_runtime.py'
- 'tests/test_asr_upgrade.py'
- 'tests/test_voice_transcription.py'
- 'frontend/components/SettingsDialog.vue'
- 'frontend/composables/useApi.js'
- 'frontend/tests/voice-model-settings.test.mjs'
- 'desktop/scripts/build-backend.cjs'
- 'tools/*qwen*.py'
- 'pyproject.toml'
- 'uv.lock'
workflow_dispatch:

permissions:
contents: read

jobs:
runtime:
strategy:
fail-fast: false
matrix:
os: [windows-2022, macos-14]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.11'
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- run: python -m pip install uv
# 公共 runner 不依赖显卡或 3 GB 的组件下载,验证续传、隔离、取消和发布清单。
- run: uv sync --locked --extra voice-transcription
- run: >-
uv run --no-sync pytest -q
tests/test_qwen_gpu_runtime.py
tests/test_asr_upgrade.py
tests/test_voice_transcription.py
- name: External runtime standard library discovery
run: uv run --no-sync python tools/qwen_runtime_stdlib.py
- name: Voice settings contracts
run: node --test frontend/tests/voice-model-settings.test.mjs
8 changes: 8 additions & 0 deletions desktop/scripts/build-backend.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,8 @@ function main() {
"sherpa_onnx",
"--add-data",
pyInstallerAddData(path.join(repoRoot, "src/wechat_decrypt_tool/resources/voice_models.json"), "wechat_decrypt_tool/resources"),
"--add-data",
pyInstallerAddData(path.join(repoRoot, "src/wechat_decrypt_tool/resources/qwen_gpu_runtime.json"), "wechat_decrypt_tool/resources"),
"--collect-all",
"watchfiles",
...aiPackagingArgs(repoRoot),
Expand All @@ -667,6 +669,12 @@ function main() {
} else {
args.splice(args.length - 1, 0, "--exclude-module", "torch", "--exclude-module", "transformers");
}
// 下载后的推理库会动态导入标准库,冻结分析无法从排除的 Torch 源码推导这些依赖。
if (process.platform === "win32") {
const stdlib = spawnSync("uv", ["run", "--no-sync", "python", path.join(repoRoot, "tools/qwen_runtime_stdlib.py")], { cwd: repoRoot, encoding: "utf8", windowsHide: true });
if (stdlib.status !== 0) throw new Error(`Qwen runtime standard library discovery failed: ${stdlib.stderr}`);
for (const moduleName of JSON.parse(stdlib.stdout)) args.splice(args.length - 1, 0, "--hidden-import", moduleName);
}

if (process.platform === "win32") {
args.splice(
Expand Down
40 changes: 40 additions & 0 deletions docs/chat-image-quality-2026-10-06.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 本地大图显示、导出与预解密验收

## 原因与修复

`resource` 中已有有效缩略图时,普通图片请求与导出会直接使用该文件;手动“尝试加载大图”则重新寻找、解密并比较本地变体。预解密也按 MD5 跳过已有缓存,使先处理的缩略图阻止后续原图参与比较;并发处理同一 MD5 的文件还可能互相覆盖。这解释了本地原图存在、无需联网却必须逐张手动补图的现象。

2.3.0 的代码已有类似缓存分支,因此这里只确认并修复可复现的缓存问题,不将整个问题归因于某个 2.8.0 提交。

- 默认显示和导出比较实际像素面积,兼容 JPEG、PNG、GIF、WebP,不能只按 `_b`、`_h` 等文件名或文件大小判断;即使 hardlink 指向另一个目录中的缩略图,首次显示也会比较当前会话已有的原图。
- 缓存修复只查询 hardlink、已解析源文件的同名变体和当前会话的标准附件目录,不调用 CDN 或递归扫描整个账号。
- 缓存写入采用同一 MD5 的串行比较与原子替换,已有大图不会被缩略图降低分辨率;读取和导出复制也保护换格式时的缓存路径。
- 保留旧版带序号/额外后缀的文件名与点号变体;当前会话的图片目录使用有界索引,新增文件后自动重建。预解密只跳过已经参与比较且未变化的源文件。状态包含源文件与密钥的摘要、缓存文件状态,不保存明文密钥或原始路径;已有旧缓存第一次仍会重新比较。默认显示也复用该状态,避免反复解密未变化的大图。
- 图片使用 ETag 重新验证,普通图片和转发图片 URL 更新缓存版本,以避开旧版本仍有效的浏览器缩略图缓存。

## 真实启动与验收

Windows 上使用正式 `desktop/scripts/dev.cjs` 启动 Nuxt、Electron 和 CPU 依赖的源码后端,采用项目配置的官方生产原生组件及原有签名/完整性校验。

从本机真实微信目录复制三组加密 DAT 文件,源文件字节保持不变。隔离验收账号使用测试用联系人和消息数据库,并从真实图片制作 120×120 缓存来复现旧缓存状态;没有把真实聊天数据库、图片、账号标识或密钥提交到仓库。

| 验收入口 | 结果 |
| --- | --- |
| 普通图片请求,无 `prefer_live`、`deep_scan`、`fetch_remote` | 三张均恢复为 4032×3024,与本地解密原图 SHA-256 一致 |
| HTML 完整导出,导出前重置旧缩略图缓存 | 三张均为 4032×3024,与本地原图内容一致 |
| JSON 完整导出,导出前独立重置旧缓存 | 三张均为 4032×3024,与本地原图内容一致 |
| 串行预解密 | 六个真实源文件均成功,三个缓存均保留 4032×3024 |
| SSE 预解密,并发 10 | 六个真实源文件均成功,三个缓存均保留 4032×3024 |
| 原图未变化时的 ETag 重新验证 | 三张均返回 304 |
| Electron 聊天页与图片查看器 | 图片正常显示,未点击“尝试加载大图” |

测试未调用 CDN 补图;没有切断 Windows 网络,也未把缺少本地原图的图片恢复作为通过条件。此修复优先使用实际存在的最高分辨率本地文件,不能恢复本机已经缺失的原图。

## 自动化回归

- 图片显示、导出媒体入口、预解密、并发换格式、缓存重新验证、媒体定位和图片密钥:78 项及 8 个子测试通过。
- 使用官方生产原生组件运行已有完整 HTML、消息图片、转发记录和增量导出回归:29 项及 13 个子测试通过,没有替换导出签名或完整性实现。
- 完整前端 Vitest:390 项通过,包含普通/转发图片缓存版本回归。
- PR 中原有 Qwen 组件及转写回归:95 项及 10 个子测试通过。

新增 Windows/macOS 图片 CI,使用实际编码的图片与 XOR DAT 覆盖同样的缓存问题,公共 runner 不需要真实账号或 CDN。
56 changes: 56 additions & 0 deletions docs/qwen-gpu-runtime-2026-10-05.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Qwen GPU 组件按需安装与真实验收

## 当前行为

设置中的 Qwen GPU 卡片提供“下载并启用”,已有模型但缺少依赖时提供“安装 GPU 组件并启用”。组件下载、校验、解压、显卡检查、模型检查全部通过后才修改语音模型设置。失败或暂停时保留原选择。

组件安装在应用数据目录的 `voice_runtimes/<版本>/cp311` 等目录,下载缓存独立保留。Windows x64 的 Python 3.11、3.12、3.13 有各自的固定 wheel 清单;下载总量约 2.91 GB(界面按二进制单位显示约 2.71 GB),初次安装预留约 9 GB 可用空间。普通安装包无需预先内置 Torch,也不要求用户安装系统 Python 或 CUDA Toolkit。

后台任务支持暂停、重试和 HTTP 续传。应用重启时,未完成任务显示暂停,用户点击继续才恢复。语音工作进程和 CUDA 检测进程加载外置依赖;主进程不会把组件加入全局导入路径。组件按发布清单校验 SHA-256,解压时拒绝越界路径和符号链接,组件目录拒绝 Windows 目录联接。

接口:

- `POST /api/chat/media/voice/transcription/qwen-gpu/prepare`:准备组件及模型,验证后启用。
- `POST /api/chat/media/voice/transcription/qwen-gpu/pause`:暂停,保留组件下载缓存。
- `GET /api/chat/media/voice/transcription/qwen-gpu/status`:读取状态及进度。
- 原有模型下载接口继续只下载模型,保持已有调用的语义。

模型下载阶段的暂停会取消原模型下载任务,组件 wheel 支持续传;恢复时未完成的模型下载遵循现有模型管理器行为。完成的模型不会因为暂停而删除。

## 真实验收证据

本机 Windows x64、CPython 3.11.3、RTX 4070 SUPER 12 GB、NVIDIA 驱动 596.49。测试使用独立的数据和浏览器目录,没有修改真实账号的转写缓存或语音设置。语音来源为此前验收保留的真实 SILK 子集,未使用合成音频。

| 项目 | 实际结果 |
| --- | --- |
| 官方组件下载 | 下载全部 34 个 wheel,总计 2,907,393,190 字节,逐文件 SHA-256 通过;中断后保留的下载继续完成 |
| 冻结程序加载外置组件 | 排除内置 Torch、Transformers 的冻结程序成功加载下载目录中的 Torch 2.9.1+cu128,执行 CUDA 张量运算并加载 Qwen 模型 |
| 真实桌面操作 | 在 Electron 应用内看到“模型已下载 · 缺少组件”,点击安装后能读到实际安装任务;点击暂停后任务停止,原模型仍为 CTC |
| 重启恢复 | 停止并重新启动验收应用,任务仍为暂停;界面显示“继续安装并启用”,继续后完成实际解压、CUDA 检查和模型加载 |
| 自动启用 | 组件任务返回 done,界面选择 Qwen GPU,设备显示 NVIDIA GPU / RTX 4070 SUPER |
| 真实语音 | 经真实冻结后端的正式语音 HTTP 接口处理 20 条 SILK,20/20 成功,实际设备 CUDA、精度 BF16 |
| 缓存复查 | 对每条语音再次请求,20/20 命中缓存,文字与首次结果一致 |
| 最终构建复查 | 最终代码重新冻结后,重启隔离应用,20/20 持久化缓存通过;另外强制执行 3 条真实 SILK,全部使用 CUDA / BF16 成功 |

20 条音频总长 202.54 秒,强制转写合计 52.95 秒,包括首次模型加载。这是本机该语料单轮耗时,不代表其他硬件或录音的性能保证,也不等于识别准确率测评。

提交 PR 前,又将本次改动迁入最新主分支 `754b49f3`,在独立工作树中安装锁定的 CPU 依赖(不安装内置 Torch / Transformers),通过项目正式 `desktop/scripts/dev.cjs` 启动 Nuxt、Electron 和后端。桌面语音设置实际显示 Qwen GPU 已选和 RTX 4070 SUPER;后端从已下载的外置组件执行第二轮 20 条真实 SILK 转写及 20 次缓存复查,全部通过,强制转写合计 61.68 秒。隔离语料只有转写所需的媒体与最小数据库标记,不用于验收聊天列表。

验收程序和日志保存在 `tmp/qwen-gpu-acceptance/`:`real-api-summary.json`、`real-api.log`、`final-binary-summary.json`、`final-binary.log`、`prepare-result.json`、`frozen-model-check.json`、构建日志及隔离数据。`transcripts.private.json` 保留逐条文字,仅用于本机检查,不进入本说明或 Git。

## 自动化检查与构建

- 后端组件、语音升级及转写相关检查:94 项通过,另有 6 项子测试通过。
- 前端现有语音设置契约:10 项通过。
- Nuxt 静态构建通过;本次设置组件机械检测无新增结果。
- 最新主分支的完整前端 Vitest:29 个文件、388 项测试通过。
- 项目正式 CPU 后端构建通过,冻结 OpenCC、watchfiles、AI 运行检查通过。
- 冻结验证中发现并修正了动态标准库导入缺失,以及 `TorchVersion` 经 Pipe 反序列化时要求父进程导入 Torch 的问题。

组件清单由 `tools/generate_qwen_gpu_manifest.py` 根据 `uv.lock` 生成,新增或升级组件时应更新发布版本 ID 并重新进行冻结程序验收。`tools/qwen_runtime_stdlib.py` 保留外置依赖需要的可用标准库,使 CPU 冻结程序也能加载下载后的推理库。

## 未完成或未实际覆盖的项目

正式安装程序未完成签名验收。本机缺少 `WCE_WINDOWS_CLIENT_CERT_THUMBPRINT` 所需的发布签名证书配置,正式 electron-builder 流程在签名阶段失败,没有关闭签名要求或修改验证策略。桌面功能验收使用已生成的未签名应用目录;将正式后端构建产生、且通过现有校验的 native 资源补入测试目录后运行。此目录仅用于本机验收,不作为正式安装包发布。

Python 3.12/3.13 的 wheel 兼容标签和清单已有自动检查,但没有在这些解释器下执行真实模型推理;其他 NVIDIA 显卡、驱动以及无显卡电脑没有做物理换机验收。损坏文件、错误续传位置、路径越界、任务中断及模型下载取消等异常由自动化测试覆盖。
Loading
Loading