Files
zWorkFlow/transcribe_chat.py
btc-z edf2c0940a feat: 新增聊天导出与语音转录 CLI 脚本 (#57)
* feat: 新增聊天导出与语音转录 CLI 脚本

新增两个独立 CLI 脚本,用于将单个聊天导出为结构化 JSON、并批量
填充语音消息的 Whisper 转录。区别于 MCP 工具:这些脚本面向离线
导出/归档,适合一次性拉取大量消息,或在会话外喂给其他 LLM/索引
管线使用。

- export_chat.py:跨分片合并某个聊天的全部消息,按时间排序后输出
  紧凑 JSON(type 为 text 时省略,is_group 仅群聊保留等)。复用
  mcp_server 中的消息解析/发送者解析辅助函数。
- transcribe_chat.py:读入 export_chat.py 产出的 JSON,对所有尚
  未转录的 voice 消息调用 Whisper,原地写回 transcription 字段。
  幂等(已有 transcription 的消息跳过)、崩溃安全(每条写回一次
  输出文件)。
- .gitignore:新增 *.json 通配,避免本地导出文件被误提交。
  config.example.json 已被跟踪,不受影响。

修复:transcribe_chat.py 原先调用 _silk_to_wav 时缺少 local_id
参数(commit c149389 将 local_id 加入签名用于文件名唯一化),
本 PR 中已补齐。

* docs: 新增聊天导出 JSON 数据格式文档

新增 docs/chat_export_format.md,描述 export_chat.py 与
transcribe_chat.py 产出的 JSON schema:顶层字段、消息对象的必填/
可选字段、默认值省略规则,以及加载与过滤的 Python 示例。

与现有 docs/macos-*.md 指南风格一致,避免在脚本 docstring 中堆叠
大段表格。export_chat.py 的 docstring 加一行指针指向本文档。

* docs: 聊天导出格式文档翻译为中文

与 docs/macos-*.md 既有指南保持一致的语言风格,将
docs/chat_export_format.md 翻译为中文。JSON 字段名、Python
代码示例等技术标识保持英文不变。

* fix: 回应 PR #57 review — 崩溃处理、幂等性、schema 补全

根据 review (#57) 的反馈:

- export_chat.py: _resolve_chat_context 返回 None 时的崩溃改为友好
  退出,并在 resolve 成功后打印 display_name (username),便于用户
  核对 resolve_username 的模糊匹配结果。
- export_chat.py: _query_messages 的 limit=999999 改为 None,避免
  超长历史被悄悄截断(_query_messages 对 None 会省略 LIMIT 子句)。
- export_chat.py: 输出 JSON 顶层新增 username 字段,让
  transcribe_chat.py 可以跳过二次模糊匹配,避免同名联系人漂移。
- transcribe_chat.py: 优先读取 JSON 顶层的 username,旧导出文件
  (无 username)回退到按 chat 名解析,保持向后兼容。
- transcribe_chat.py: 删除未使用的 import io / import wave,将循环
  内的 import datetime 提至模块顶部。
- export_chat.py: _decode_sticker_desc 的 varint 单字节简化给出
  注释说明局限,以及对 create_time 排序加 "or 0" 防御。
- export_chat.py / transcribe_chat.py: 模块 docstring 翻译为中文,
  与 docs/macos-*.md 保持一致。
- docs/chat_export_format.md: 同步补充 username 字段说明。
- .gitignore: 将 *.json 收窄为 *_export*.json / *_transcribed*.json,
  避免误屏蔽未来的 config/fixtures,同时匹配导出工具实际产出的
  文件名。
2026-04-25 00:16:37 +08:00

103 lines
3.6 KiB
Python
Raw 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
行为说明:
- 使用 OpenAI Whisper (CPU单线程) 对每条语音消息转录。
- 幂等: 已有 "transcription" 字段的消息会被跳过,因此崩溃/中断后可安全重跑。
- 崩溃安全: 每处理完一条即整体重写输出 JSON进程中断最多丢失当前一条。
- 首次运行会下载 Whisper 模型 (~145 MB) 并缓存。
需要 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):
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:
model = mcp_server._get_whisper_model()
result = model.transcribe(wav_path)
return result.get("text", "").strip()
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
print(f"Found {total} voice messages to transcribe.")
print("Loading Whisper model (first run downloads ~145MB)...")
mcp_server._get_whisper_model()
print("Model ready.\n")
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)
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)