diff --git a/.github/workflows/chat-image-quality.yml b/.github/workflows/chat-image-quality.yml new file mode 100644 index 00000000..aefecf55 --- /dev/null +++ b/.github/workflows/chat-image-quality.yml @@ -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 diff --git a/.github/workflows/qwen-gpu-runtime.yml b/.github/workflows/qwen-gpu-runtime.yml new file mode 100644 index 00000000..d17489db --- /dev/null +++ b/.github/workflows/qwen-gpu-runtime.yml @@ -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 diff --git a/desktop/scripts/build-backend.cjs b/desktop/scripts/build-backend.cjs index a0bb2797..4a0d8c3f 100644 --- a/desktop/scripts/build-backend.cjs +++ b/desktop/scripts/build-backend.cjs @@ -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), @@ -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( diff --git a/docs/chat-image-quality-2026-10-06.md b/docs/chat-image-quality-2026-10-06.md new file mode 100644 index 00000000..70756c3a --- /dev/null +++ b/docs/chat-image-quality-2026-10-06.md @@ -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。 diff --git a/docs/qwen-gpu-runtime-2026-10-05.md b/docs/qwen-gpu-runtime-2026-10-05.md new file mode 100644 index 00000000..6da08171 --- /dev/null +++ b/docs/qwen-gpu-runtime-2026-10-05.md @@ -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 显卡、驱动以及无显卡电脑没有做物理换机验收。损坏文件、错误续传位置、路径越界、任务中断及模型下载取消等异常由自动化测试覆盖。 diff --git a/frontend/components/SettingsDialog.vue b/frontend/components/SettingsDialog.vue index 4737ade8..6ea0ff66 100644 --- a/frontend/components/SettingsDialog.vue +++ b/frontend/components/SettingsDialog.vue @@ -364,6 +364,16 @@
{{ model.description }}
{{ model.runtimeReason }}
+
+ 运行组件约 {{ formatBytes(model.runtimeComponent.size) }},下载和解压需约 9 GB 可用空间。 +
+
+

{{ qwenStageText(model) }}

+

+ {{ formatBytes(model.runtimeComponent.job.bytes || 0) }} / {{ formatBytes(model.runtimeComponent.job.total || model.runtimeComponent.size) }} +

+ +
{{ voiceModelDownloadStageText(model) }} @@ -390,17 +400,32 @@
+ +