feat(export_all): add --with-transcriptions flag for voice transcription during export (#89)

This commit is contained in:
Davy
2026-05-12 22:33:36 -07:00
committed by GitHub
parent 187d820bb0
commit 8645fe4210

View File

@@ -1,14 +1,21 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""批量导出所有微信聊天记录为 JSON 文件。 """批量导出所有微信聊天记录为 JSON 文件,可选附带语音转录
此脚本将导出所有会话的聊天记录,输出格式与 export_chat.py 完全一致。 此脚本将导出所有会话的聊天记录,输出格式与 export_chat.py 完全一致。
支持导出到指定目录,默认输出到 ./exported_chats 目录。 支持导出到指定目录,默认输出到 ./exported_chats 目录。
语音转录通过 mcp_server 的 backend 配置驱动config.json 中设置
transcription_backend 为 whisper_cpp / openai / local。未启用 backend
或缺少依赖时仅导出文本消息,不报错。
用法: 用法:
python3 export_all_chats.py [output_dir] python3 export_all_chats.py [output_dir] # 仅导出
python3 export_all_chats.py --with-transcriptions # 导出 + 语音转录
示例: 示例:
python3 export_all_chats.py /path/to/output python3 export_all_chats.py /path/to/output
python3 export_all_chats.py --with-transcriptions
python3 export_all_chats.py --with-transcriptions /path/to/output
""" """
import argparse import argparse
@@ -17,6 +24,7 @@ import os
import re import re
import sqlite3 import sqlite3
import sys import sys
import time
from contextlib import closing from contextlib import closing
from datetime import datetime from datetime import datetime
@@ -24,7 +32,7 @@ import mcp_server
from chat_export_helpers import _extract_content, _msg_type_str, _resolve_sender from chat_export_helpers import _extract_content, _msg_type_str, _resolve_sender
def export_one(username, output_dir, names): def export_one(username, output_dir, names, transcribe=False):
""" """
导出单个会话。 导出单个会话。
@@ -82,6 +90,39 @@ def export_one(username, output_dir, names):
if not messages: if not messages:
return False, 0, "empty" return False, 0, "empty"
# ── 语音转录 ──────────────────────────────────────────────
if transcribe:
transcribed = 0
failed = 0
for msg in messages:
if msg.get("type") != "voice":
continue
lid = msg["local_id"]
try:
row = mcp_server._fetch_voice_row(username, lid)
if row is None:
continue
voice_data, create_time = row
wav_path, _ = mcp_server._silk_to_wav(
voice_data, create_time, username, lid
)
backend = _resolve_backend()
result = mcp_server._transcribe(wav_path, backend)
if result and result.get("text"):
msg["transcription"] = result["text"]
transcribed += 1
os.unlink(wav_path)
except Exception:
failed += 1
if transcribed or failed:
display = names.get(username, username)
voice_total = sum(1 for m in messages if m.get("type") == "voice")
print(
f" 转录: {transcribed}/{voice_total} 条语音"
+ (f" ({failed} 失败)" if failed else "")
)
# ── 写文件 ────────────────────────────────────────────────
output = { output = {
"chat": display_name, "chat": display_name,
"username": username, "username": username,
@@ -100,13 +141,29 @@ def export_one(username, output_dir, names):
return True, len(messages), None return True, len(messages), None
_BACKEND_CACHE = None
def _resolve_backend():
"""解析转录 backend结果缓存以避免重复检测。"""
global _BACKEND_CACHE
if _BACKEND_CACHE is None:
try:
_BACKEND_CACHE = mcp_server._resolve_active_backend()
except Exception:
_BACKEND_CACHE = "local"
return _BACKEND_CACHE
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="批量导出所有微信聊天记录为 JSON 文件", description="批量导出所有微信聊天记录为 JSON 文件,可选附带语音转录",
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=""" epilog="""
示例: 示例:
python3 export_all_chats.py /path/to/output python3 export_all_chats.py /path/to/output
python3 export_all_chats.py --with-transcriptions
python3 export_all_chats.py -t /path/to/output
""", """,
) )
parser.add_argument( parser.add_argument(
@@ -115,11 +172,25 @@ def main():
default=None, default=None,
help="输出目录路径 (默认: ./exported_chats)", help="输出目录路径 (默认: ./exported_chats)",
) )
parser.add_argument(
"-t",
"--with-transcriptions",
action="store_true",
help="导出时一并转录语音消息(依赖 config.json 配置的 backend",
)
args = parser.parse_args() args = parser.parse_args()
script_dir = os.path.dirname(os.path.abspath(__file__)) script_dir = os.path.dirname(os.path.abspath(__file__))
output_dir = args.output_dir or os.path.join(script_dir, "exported_chats") output_dir = args.output_dir or os.path.join(script_dir, "exported_chats")
if args.with_transcriptions:
try:
backend = _resolve_backend()
print(f"语音转录: 启用 (backend={backend})")
except Exception as e:
print(f"语音转录: backend 解析失败: {e}", file=sys.stderr)
args.with_transcriptions = False
if not os.path.exists(mcp_server.DECRYPTED_DIR): if not os.path.exists(mcp_server.DECRYPTED_DIR):
print(f"错误: 解密目录不存在: {mcp_server.DECRYPTED_DIR}", file=sys.stderr) print(f"错误: 解密目录不存在: {mcp_server.DECRYPTED_DIR}", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -128,7 +199,9 @@ def main():
session_db = os.path.join(mcp_server.DECRYPTED_DIR, "session", "session.db") session_db = os.path.join(mcp_server.DECRYPTED_DIR, "session", "session.db")
try: try:
with closing(sqlite3.connect(session_db)) as conn: with closing(sqlite3.connect(session_db)) as conn:
sessions = [u for u, _ in conn.execute("SELECT username, type FROM SessionTable")] sessions = [u for u, _ in conn.execute(
"SELECT username, type FROM SessionTable"
)]
except sqlite3.Error as e: except sqlite3.Error as e:
print(f"会话数据库查询失败: {e}", file=sys.stderr) print(f"会话数据库查询失败: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -140,15 +213,23 @@ def main():
print(f"输出目录: {output_dir}") print(f"输出目录: {output_dir}")
print("=" * 60) print("=" * 60)
t0 = time.time()
ok, skip, err, total = 0, 0, 0, 0 ok, skip, err, total = 0, 0, 0, 0
for i, username in enumerate(sessions, 1): for i, username in enumerate(sessions, 1):
display = names.get(username, username) display = names.get(username, username)
success, count, reason = export_one(username, output_dir, names) success, count, reason = export_one(
username, output_dir, names, transcribe=args.with_transcriptions
)
if success: if success:
ok += 1 ok += 1
total += count total += count
if i <= 10 or i % 100 == 0: if i <= 10 or i % 100 == 0:
print(f"[{i}/{len(sessions)}] {display} - {count} 条消息") elapsed = time.time() - t0
eta = (elapsed / i) * (len(sessions) - i) if i > 0 else 0
print(
f"[{i}/{len(sessions)}] {display} - {count} 条消息"
+ (f" ETA {eta/60:.0f}" if i > 1 else "")
)
else: else:
if "no tables" in str(reason) or "empty" in str(reason): if "no tables" in str(reason) or "empty" in str(reason):
skip += 1 skip += 1
@@ -158,9 +239,13 @@ def main():
err += 1 err += 1
print(f"[{i}/{len(sessions)}] {display} - 失败: {reason}") print(f"[{i}/{len(sessions)}] {display} - 失败: {reason}")
elapsed = time.time() - t0
print() print()
print("=" * 60) print("=" * 60)
print(f"完成! 成功={ok} 跳过={skip} 失败={err} 总消息={total}") print(
f"完成! 成功={ok} 跳过={skip} 失败={err} "
f"总消息={total} 耗时={elapsed/60:.0f}"
)
if __name__ == "__main__": if __name__ == "__main__":