Files
zWorkFlow/transcribe_chat.py
Davy fe5cc633ff feat: transcribe_voice 新增 whisper.cpp 后端(macOS Metal GPU 加速) (#78)
* Add transcribe_chat_whisper_cpp.py: macOS whisper.cpp transcription

whisper.cpp variant of transcribe_chat.py for Apple Silicon Macs.

Advantages over transcribe_chat.py:
- Uses whisper-cpp CLI with Metal/ANE GPU acceleration (3-5x faster)
- No PyTorch or openai/whisper Python dependency
- Same idempotent, crash-safe design as transcribe_chat.py
- Auto-detects model from common macOS locations:
  ~/Library/Application Support/whisper-cpp/,
  ~/Library/Application Support/Recordly/whisper/, etc.
- --model-size flag for automatic download if no model found
- Configurable --language (default: zh) and --threads

Usage: python3 transcribe_chat_whisper_cpp.py <input.json> [output.json]

* refactor: 将 whisper.cpp 转为后端选项集成到 mcp_server.py 中

根据 PR #78 review 反馈,将独立的 transcribe_chat_whisper_cpp.py 重构为
mcp_server.py 中的 whisper_cpp 后端,与 PR #66 OpenAl 后端模式对齐。

变更:
- mcp_server.py: 新增 _transcribe_whisper_cpp()、_resolve_whisper_cpp_binary()、
  _resolve_whisper_cpp_model(),更新 _resolve_active_backend()/_cache_signature()/
  _transcribe() 以分发至 whisper_cpp 后端
- transcribe_chat.py: 统一入口 mcp_server._transcribe 自动支持新后端,
  仅补充了 backend 打印信息
- 删除 transcribe_chat_whisper_cpp.py

config.json 启用方式:
  "transcription_backend": "whisper_cpp",
  "whisper_cpp_binary": "...",    # 可选,默认自动检测
  "whisper_cpp_model": "...",     # 可选,默认自动检测
  "whisper_cpp_language": "zh",   # 可选
  "whisper_cpp_threads": 4          # 可选,默认自动检测

* docs: 在语音转录隐私章节补充 whisper.cpp 后端说明

根据 PR #78 review 反馈,在 README.md ⚠️ 语音转录隐私章节
新增 whisper.cpp 后端(macOS Metal GPU 加速)的配置说明、隐私
属性和回退行为,与 OpenAI 后端并列。
2026-05-12 11:42:53 +08:00

112 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
为聊天导出 JSON 中的语音消息补齐转录文本。
用法:
.venv/bin/python3 transcribe_chat.py <input.json> [output.json]
参数:
<input.json> 由 export_chat.py 产出的 JSON。
[output.json] 可选输出路径,默认 "<input>_transcribed.json"
完整流程示例:
.venv/bin/python3 export_chat.py <chat_name> /tmp/chat.json
.venv/bin/python3 transcribe_chat.py /tmp/chat.json /tmp/chat_transcribed.json
行为说明:
- 后端由 config.json 中 transcription_backend 字段控制 (local/openai/whisper_cpp)
与 MCP transcribe_voice 工具共享配置。详见 README "语音转录隐私" 章节。
- 默认 local: 使用本地 Whisper (CPU单线程),首次运行下载 ~145 MB 权重。
- 切到 openai: 语音上传至 OpenAI 服务器转录 (~$0.006/分钟)。
- 切到 whisper_cpp: 使用 whisper-cpp CLI (Metal GPU 加速,仅 macOS)。
- 幂等: 已有 "transcription" 字段的消息会被跳过,因此崩溃/中断后可安全重跑。
- 崩溃安全: 每处理完一条即整体重写输出 JSON进程中断最多丢失当前一条。
需要 WeChat DB 仍然在线/已解密 —— 语音 blob 是从 DB 现场按 local_id 读取的,
不从 JSON 读。
"""
import json
import os
import sys
from datetime import datetime
import mcp_server
def _transcribe_local_id(username, local_id, backend):
row = mcp_server._fetch_voice_row(username, local_id)
if row is None:
return "[not found]"
voice_data, create_time = row
try:
wav_path, _ = mcp_server._silk_to_wav(voice_data, create_time, username, local_id)
except Exception as e:
return f"[decode error: {e}]"
try:
result = mcp_server._transcribe(wav_path, backend)
return result["text"]
except Exception as e:
return f"[transcribe error: {e}]"
def transcribe_export(input_path, output_path):
with open(input_path, encoding="utf-8") as f:
data = json.load(f)
# 优先使用导出 JSON 中已记录的 username避免重新模糊匹配导致同名联系人漂移。
username = data.get("username")
chat_name = data.get("chat", "")
if not username:
username = mcp_server.resolve_username(chat_name)
if not username:
print(f"Could not resolve username for: {chat_name}")
sys.exit(1)
messages = data["messages"]
# Compact format: type is absent for text; transcription is only present when filled.
pending = [m for m in messages if m.get("type") == "voice" and not m.get("transcription")]
total = len(pending)
if total == 0:
print("No voice messages to transcribe.")
return
backend = mcp_server._resolve_active_backend()
print(f"Found {total} voice messages to transcribe.")
print(f"Backend: {backend}")
if backend == "local":
print("Loading Whisper model (first run downloads ~145MB)...")
mcp_server._get_whisper_model()
print("Model ready.\n")
elif backend == "whisper_cpp":
print("Using whisper-cpp with Metal GPU acceleration\n")
else:
print("")
for i, msg in enumerate(pending, 1):
local_id = msg["local_id"]
ts = msg["timestamp"]
ts_str = datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if isinstance(ts, (int, float)) else ts
print(f"[{i}/{total}] local_id={local_id} ({ts_str}) ... ", end="", flush=True)
result = _transcribe_local_id(username, local_id, backend)
msg["transcription"] = result
print(repr(result[:60]) if result else '""')
# Save after each transcription so progress isn't lost on crash
with open(output_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"\nDone. Written to {output_path}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python3 transcribe_chat.py <input.json> [output.json]")
sys.exit(1)
inp = sys.argv[1]
base, ext = os.path.splitext(inp)
out = sys.argv[2] if len(sys.argv) > 2 else f"{base}_transcribed{ext}"
transcribe_export(inp, out)