Skip to content
Open
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
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,35 @@

> Excel 格式生成 `.xlsx` 文件;聊天记录、朋友圈和收藏会将对应格式文件与必要资源一起打包为 ZIP。

### 本地语音转文字(Whisper)

聊天页可以在保留原语音播放的同时,按需将语音识别为中文;HTML、JSON、TXT 和 Excel 导出也可以选择把转写文字与原语音一起写入归档。转写结果按账号、消息 ID、音频内容和模型缓存在账号目录的 `_cache/voice_transcripts.sqlite3`,重复查看或导出不会再次识别。

该能力是可选依赖,从源码运行时安装:

```powershell
uv sync --no-editable --extra voice-transcription
```

默认配置使用 CPU、`medium` 模型和中文识别,并且不会自动联网下载模型。推荐提前下载模型并指定本地目录:

```powershell
$env:WECHAT_TOOL_WHISPER_MODEL = 'D:\models\faster-whisper-medium'
$env:WECHAT_TOOL_WHISPER_DEVICE = 'cpu'
$env:WECHAT_TOOL_WHISPER_COMPUTE_TYPE = 'int8'
```

也可以明确允许首次识别时下载模型,此操作会访问 Hugging Face,模型文件较大:

```powershell
$env:WECHAT_TOOL_WHISPER_MODEL = 'medium'
$env:WECHAT_TOOL_WHISPER_ALLOW_DOWNLOAD = '1'
```

使用 NVIDIA GPU 时可改为 `WECHAT_TOOL_WHISPER_DEVICE=cuda` 和 `WECHAT_TOOL_WHISPER_COMPUTE_TYPE=float16`。设置 `WECHAT_TOOL_WHISPER_ENABLED=0` 可以完全关闭该能力。隐私模式导出始终禁用语音转写,不会把语音内容写入归档。

应用设置页也可以选择 CPU 或 NVIDIA GPU。GPU 探测、CUDA 初始化失败自动回退 CPU,以及 RTX 5060 验收步骤见 [RTX 5060 faster-whisper CUDA 验收说明](docs/rtx5060-faster-whisper-gpu.md)。

## Windows / macOS 兼容性

| 功能 | Windows | macOS |
Expand Down Expand Up @@ -170,7 +199,7 @@ cd WeChatDataAnalysis

```bash
# 使用uv (推荐)
uv sync
uv sync --no-editable
```

#### 2.3 安装前端依赖
Expand All @@ -185,9 +214,11 @@ npm install
#### 启动后端API服务
```bash
# 在项目根目录
uv run main.py
uv run --no-sync main.py
```

`main.py` 会在任何项目包导入前优先加入当前仓库的 `src`,并在启动日志中打印 `wechat_decrypt_tool` 的代码来源。开发运行时应指向当前仓库的 `src/wechat_decrypt_tool/__init__.py`,而不是 `.venv/Lib/site-packages` 中的旧副本。Windows 中文路径下不要改用 editable 安装;Python 3.11 可能按系统代码页读取 uv 生成的 UTF-8 `.pth`,导致解释器启动失败。

#### 启动前端开发服务器
```bash
# 在frontend目录
Expand Down
15 changes: 15 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,18 @@ Desktop distributions include the platform-specific FFmpeg executable from `ffmp
- Binary builds: https://github.com/ffbinaries/ffbinaries-prebuilt/releases
- Package license: GPL-3.0-or-later
- Distributed license files: `ffmpeg/LICENSE` and `ffmpeg/ffmpeg.LICENSE`

## Optional voice transcription runtime

The optional local voice transcription feature uses the following Python packages. Windows standalone backends built with the `voice-transcription` extra bundle these packages and their required runtime data.

| Package | Upstream project | License |
| --- | --- | --- |
| `faster-whisper` | https://github.com/SYSTRAN/faster-whisper | MIT |
| `CTranslate2` | https://github.com/OpenNMT/CTranslate2 | MIT |
| `PyAV` | https://github.com/PyAV-Org/PyAV | BSD-3-Clause |
| `OpenCC Python Reimplemented` | https://github.com/yichen0831/opencc-python | Apache-2.0 |
| `ONNX Runtime` | https://github.com/microsoft/onnxruntime | MIT |
| `Hugging Face tokenizers` | https://github.com/huggingface/tokenizers | Apache-2.0 |

Whisper model weights are not included in this repository or its pull request. Users download or provide model weights separately and must follow the license and usage terms published with the selected model.
2 changes: 1 addition & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"dev": "node scripts/dev.cjs",
"dev:static": "npm --prefix ../frontend run generate && cross-env ELECTRON_START_URL=http://127.0.0.1:10392 electron .",
"build:ui": "npm --prefix ../frontend run generate && node scripts/copy-ui.cjs",
"build:backend": "uv sync --extra build && node scripts/build-backend.cjs",
"build:backend": "uv sync --no-editable --extra build --extra voice-transcription && node scripts/build-backend.cjs",
"build:icon": "node scripts/build-icon.cjs",
"smoke:mac": "node scripts/smoke-macos-package.cjs",
"dist": "npm run dist:win",
Expand Down
110 changes: 103 additions & 7 deletions desktop/scripts/build-backend.cjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
const fs = require("fs");
const os = require("os");
const path = require("path");
const { spawnSync } = require("child_process");

const repoRoot = path.resolve(__dirname, "..", "..");
const entry = path.join(repoRoot, "src", "wechat_decrypt_tool", "backend_entry.py");
const venvPythonExecutable = path.join(
repoRoot,
".venv",
process.platform === "win32" ? "Scripts/python.exe" : "bin/python",
);

const distDir = path.join(repoRoot, "desktop", "resources", "backend");
const workDir = path.join(repoRoot, "desktop", "build", "pyinstaller");
Expand All @@ -16,7 +22,36 @@ fs.mkdirSync(specDir, { recursive: true });
const integrityManifest = path.join(repoRoot, "native", "wce_integrity", "Cargo.toml");
const integrityTargetDir = path.join(repoRoot, "native", "wce_integrity", "target", "release");
let integrityNativeBinary = null;
if (process.platform === "darwin" || process.platform === "linux") {
if (process.platform === "win32") {
const nativeBuild = spawnSync(
"powershell.exe",
[
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
path.join(repoRoot, "tools", "build_wce_integrity.ps1"),
],
{
cwd: repoRoot,
env: {
...process.env,
WCE_UI_PUBLIC_DIR: path.join(repoRoot, "frontend", ".output", "public"),
},
stdio: "inherit",
}
);
if ((nativeBuild.status ?? 1) !== 0) {
console.error("Failed to build the Windows wce_integrity module.");
process.exit(nativeBuild.status ?? 1);
}
const windowsNativeDir = path.join(repoRoot, "src", "wechat_decrypt_tool", "native");
integrityNativeBinary = path.join(windowsNativeDir, "wce_integrity.pyd");
if (!fs.existsSync(integrityNativeBinary)) {
console.error("Windows wce_integrity build completed without the canonical wce_integrity.pyd artifact.");
process.exit(1);
}
} else if (process.platform === "darwin" || process.platform === "linux") {
const fileName = process.platform === "darwin" ? "libwce_integrity.dylib" : "libwce_integrity.so";
const nativeBuild = spawnSync(
"cargo",
Expand All @@ -41,17 +76,23 @@ if (process.platform === "darwin" || process.platform === "linux") {
}
}

if (!fs.existsSync(venvPythonExecutable)) {
console.error(`Missing virtual environment Python: ${venvPythonExecutable}`);
process.exit(1);
}
const integrityPreflight = spawnSync(
"uv",
venvPythonExecutable,
[
"run",
"python",
"-c",
[
"from wechat_decrypt_tool.export_integrity import load_wce_integrity_native",
"w=load_wce_integrity_native()",
"required=('chat','sns','records-project','records-generic','contacts')",
"assert all(w.export_css(kind).strip() for kind in required)",
"css=w.export_css('chat')",
"compact=''.join(css.split())",
"assert '.wechat-voice-transcript' in css",
"assert '.wechat-voice-wrapper{display:flex;flex-direction:column' in compact",
"assert callable(w.record_file) and callable(w.seal_export)",
].join(";"),
],
Expand Down Expand Up @@ -132,6 +173,7 @@ if (fs.existsSync(wasmDir)) {
if (process.platform === "win32") {
for (const item of fs.readdirSync(nativeDir, { withFileTypes: true })) {
if (!item.isFile() || !/\.(dll|pyd)$/i.test(item.name)) continue;
if (/^wce_integrity.*\.pyd$/i.test(item.name) && item.name.toLowerCase() !== "wce_integrity.pyd") continue;
fs.copyFileSync(path.join(nativeDir, item.name), path.join(runtimeNativeDir, item.name));
}
} else if (process.platform === "darwin") {
Expand Down Expand Up @@ -162,8 +204,6 @@ if (process.platform === "win32") {
}

const args = [
"run",
"pyinstaller",
"--noconfirm",
"--clean",
"--name",
Expand All @@ -179,6 +219,14 @@ const args = [
pyInstallerAddData(runtimeNativeDir, "wechat_decrypt_tool/native"),
"--add-data",
pyInstallerAddData(skillDir, "skills/wechat-mcp-copilot"),
"--collect-all",
"faster_whisper",
"--collect-all",
"ctranslate2",
"--collect-all",
"av",
"--collect-all",
"opencc",
entry,
];

Expand All @@ -198,11 +246,59 @@ if (integrityNativeBinary) {
);
}

const r = spawnSync("uv", args, { cwd: repoRoot, stdio: "inherit" });
const pyInstallerExecutable = path.join(
repoRoot,
".venv",
process.platform === "win32" ? "Scripts/pyinstaller.exe" : "bin/pyinstaller",
);
if (!fs.existsSync(pyInstallerExecutable)) {
console.error(`Missing PyInstaller executable: ${pyInstallerExecutable}`);
process.exit(1);
}
const r = spawnSync(pyInstallerExecutable, args, { cwd: repoRoot, stdio: "inherit" });
if ((r.status ?? 1) !== 0) {
process.exit(r.status ?? 1);
}

const packagedBackend = path.join(
distDir,
process.platform === "win32" ? "wechat-backend.exe" : "wechat-backend",
);
const openccSmokeDir = fs.mkdtempSync(path.join(os.tmpdir(), "wda-opencc-smoke-"));
try {
const smokeEnv = { ...process.env, PYTHONPATH: "" };
delete smokeEnv.PYTHONHOME;
const smoke = spawnSync(packagedBackend, ["--smoke-opencc"], {
cwd: openccSmokeDir,
env: smokeEnv,
encoding: "utf8",
windowsHide: true,
});
if ((smoke.status ?? 1) !== 0) {
process.stderr.write(smoke.stderr || smoke.stdout || "Packaged OpenCC smoke test failed.\n");
process.exit(smoke.status ?? 1);
}
const outputLines = String(smoke.stdout || "").trim().split(/\r?\n/).filter(Boolean);
let payload;
try {
payload = JSON.parse(outputLines.at(-1) || "");
} catch {
console.error(`Packaged OpenCC smoke test returned invalid JSON: ${smoke.stdout || "<empty>"}`);
process.exit(1);
}
const expected = {
"繁體中文": "繁体中文",
"軟體與資料庫": "软体与资料库",
};
if (!payload.frozen || JSON.stringify(payload.results) !== JSON.stringify(expected)) {
console.error(`Packaged OpenCC smoke test returned an unexpected result: ${JSON.stringify(payload)}`);
process.exit(1);
}
console.log(`Packaged OpenCC smoke test passed: ${JSON.stringify(payload)}`);
} finally {
fs.rmSync(openccSmokeDir, { recursive: true, force: true });
}

// Keep a stable external native folder for packaged runtime to avoid relying on
// onefile temp extraction paths when wcdb_api.dll performs environment checks.
const packagedNativeDir = path.join(distDir, "native");
Expand Down
92 changes: 92 additions & 0 deletions docs/rtx5060-faster-whisper-gpu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# RTX 5060 faster-whisper CUDA 验收说明

状态:实现完成,待 RTX 5060 实机验收。更新:2026-07-29。

## 适用范围

本项目的语音转文字使用 `faster-whisper` 和 CTranslate2,不使用 PyTorch。因此,同类 PyTorch 音色克隆项目采用的 `cu128` wheel 安装路线不能直接复制到这里;本说明只复用其 RTX 50 系显卡驱动、磁盘、模型缓存与验收原则。

默认设备是 CPU `int8`。在有 NVIDIA GPU 的机器上,设置页可选择 NVIDIA GPU;首次识别会使用 CUDA `float16` 加载模型。CUDA 探测或模型初始化失败时,会自动改用 CPU `int8`,不影响语音播放、聊天查看或导出任务。

## 服务器前置检查

先在目标机上执行只读检查:

```bash
nvidia-smi
python3 --version
ffmpeg -version
df -h
```

`nvidia-smi` 必须能列出 RTX 5060。若它不可用,先由服务器管理员修复 NVIDIA driver;不要通过反复重装 Python 包排查驱动问题。

本项目不需要安装 `torch`、`torchaudio` 或复用音色克隆项目的 wheelhouse。安装当前项目的语音依赖:

```bash
uv sync --no-editable --extra voice-transcription
```

当前锁定的 `CTranslate2 4.8.1` 在 Linux GPU 环境需要 NVIDIA driver、CUDA 12.x 和 cuDNN 9 运行库;PyTorch 的 `cu128` wheel 不会为 CTranslate2 提供这些库。先完成只读探测,再由服务器管理员或已获授权的维护人员按 CTranslate2 对应版本的官方安装说明补齐运行库。若采用 PyPI 的用户级 CUDA/cuDNN 包,也应只安装到本项目虚拟环境,并在启动后端的同一用户进程中设置其库路径;不要修改系统 `/usr`、全局 `LD_LIBRARY_PATH` 或共享 CUDA 安装。

## 模型准备

当前默认模型是 `Systran/faster-whisper-medium`,磁盘约 1.43 GiB。生产验收前应将模型放进目标机 Hugging Face 缓存,或将 `WECHAT_TOOL_WHISPER_MODEL` 指向完整的本地模型目录;默认禁止首次识别联网下载。

建议把缓存放在数据盘,例如:

```bash
export HF_HOME=/mnt/sdb/wechat-data-analysis/hf-cache
export WECHAT_TOOL_WHISPER_MODEL=medium
export WECHAT_TOOL_WHISPER_ALLOW_DOWNLOAD=0
```

如果缓存尚未准备好,可在已获联网许可的维护窗口临时把 `WECHAT_TOOL_WHISPER_ALLOW_DOWNLOAD=1`,完成下载后恢复为 `0`。模型与缓存目录包含本地运行资产,不要提交到 Git。

## CUDA 验收

启动后端后,先检查 CTranslate2 是否能识别 CUDA:

```bash
python - <<'PY'
import ctranslate2
print("cuda device count:", ctranslate2.get_cuda_device_count())
PY
```

期望设备数量大于零。然后打开应用的“设置 -> 语音转文字”:

1. 页面应显示检测到的 NVIDIA GPU;RTX 5060 名称会由 `nvidia-smi` 提供。
2. 选择“NVIDIA GPU”。该偏好会保存到应用 `output/runtime_settings.json`。
3. 对一条清晰中文微信语音点击“转文字”,或用同一会话开启“语音转文字”导出。
4. 重新打开设置页,确认“实际”显示为“NVIDIA GPU”。
5. 导出结果仍需包含可播放的 `media/voices/*.mp3` 与中文转写文本。

若 CUDA 探测通过但模型初始化失败,设置页会保留“NVIDIA GPU”为已选设备,并显示自动回退原因;“实际”应显示为 CPU。此时语音转写仍应成功,随后应记录 CTranslate2、NVIDIA driver 和 CUDA 运行库版本再排查。

## 配置优先级

推理设备优先级如下:

```text
WECHAT_TOOL_WHISPER_DEVICE 环境变量
> 设置页保存的设备偏好
> 默认 CPU
```

部署脚本若设置了 `WECHAT_TOOL_WHISPER_DEVICE`,界面会标记为“由启动环境变量固定”,避免用户误以为修改已生效。未设置该环境变量时,使用设置页选择 CPU 或 NVIDIA GPU。

## 验收记录

在 RTX 5060 服务器完成实测后,记录以下信息:

```text
nvidia-smi 的 GPU 名称、Driver Version、CUDA Version
Python / faster-whisper / CTranslate2 版本
CTranslate2 CUDA device count
设置页检测结果、已选设备、实际设备、是否发生回退
单条语音首次识别耗时与缓存后二次识别耗时
导出中的语音数量、成功/失败/缓存统计
```

不要为了制造回退场景而在共享服务器上卸载驱动、修改系统 CUDA、重启机器或停止他人进程。自动回退路径由本地单元测试覆盖;服务器只验证真实 NVIDIA GPU 成功路径。
Loading