Add GUI, packaging and export/voice tools

Introduce a tkinter GUI and tooling to produce a single executable and export/convert message data. Adds app_gui.py (GUI launcher that runs decrypt/export/voice subtasks), export_messages.py (export messages to CSV/HTML/JSON), voice_to_mp3.py (extract SILK_V3 voice blobs and convert to MP3 via pilk + ffmpeg), WeChatDecrypt.spec and build.bat (PyInstaller spec and convenience build script), and EXE_USAGE.md (usage for the standalone exe). Update config.py to detect the application base directory when packaged, update README.md to document the GUI and packaging flow, and add pilk/pyinstaller to requirements.txt. Also expand .gitignore with common IDE/build/temp patterns and add output/data directories to ignore. These changes enable one-file packaging and provide end-user tools for decrypting, exporting and converting audio.
This commit is contained in:
xincheng
2026-04-04 06:12:03 +08:00
parent b80e7d1c14
commit a84698b9fc
10 changed files with 1355 additions and 30 deletions

123
voice_to_mp3.py Normal file
View File

@@ -0,0 +1,123 @@
"""从 media_0.db 提取所有语音数据按用户名分目录SILK_V3 转 MP3"""
import sqlite3
import subprocess
import tempfile
import os
import sys
from datetime import datetime
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
import pilk
DB_PATH = r"decrypted\message\media_0.db"
CONTACT_DB_PATH = r"decrypted\contact\contact.db"
OUTPUT_DIR = "data"
def silk_to_mp3(voice_data, output_path):
"""将微信 SILK 语音数据转换为 MP3"""
# 去掉微信格式的 0x02 前缀
if voice_data[0:1] == b'\x02':
silk_data = voice_data[1:]
else:
silk_data = voice_data
if not silk_data.startswith(b'#!SILK_V3'):
print(f" 警告:数据不以 #!SILK_V3 开头,跳过")
return False
# 补上结尾标记
if not silk_data.endswith(b'\xff\xff'):
silk_data += b'\xff\xff'
silk_file = tempfile.mktemp(suffix=".silk")
pcm_file = tempfile.mktemp(suffix=".pcm")
try:
with open(silk_file, "wb") as f:
f.write(silk_data)
pilk.decode(silk_file, pcm_file)
result = subprocess.run([
"ffmpeg", "-y", "-f", "s16le", "-ar", "24000", "-ac", "1",
"-i", pcm_file, output_path
], capture_output=True, encoding="utf-8", errors="replace")
return result.returncode == 0
finally:
if os.path.exists(silk_file):
os.remove(silk_file)
if os.path.exists(pcm_file):
os.remove(pcm_file)
# 1. 读取 Name2Id 映射 (rowid -> user_name)
conn = sqlite3.connect(DB_PATH)
name_map = {}
for rowid, user_name in conn.execute("SELECT rowid, user_name FROM Name2Id"):
name_map[rowid] = user_name
print(f"{len(name_map)} 个用户")
# 2. 读取 contact 信息 (user_name -> {remark, nick_name, alias, ...})
contact_map = {}
try:
cconn = sqlite3.connect(CONTACT_DB_PATH)
for row in cconn.execute("SELECT username, alias, remark, nick_name FROM contact"):
uname, alias, remark, nick_name = row
contact_map[uname] = {"username": uname, "alias": alias or "", "remark": remark or "", "nick_name": nick_name or ""}
cconn.close()
print(f"联系人数据库加载: {len(contact_map)}")
except Exception as e:
print(f"联系人数据库读取失败: {e}")
def display_name(user_name):
"""优先 remark > nick_name > user_name"""
info = contact_map.get(user_name, {})
return info.get("remark") or info.get("nick_name") or user_name
def safe_dirname(name):
"""替换目录名中的非法字符"""
for ch in r'\/:*?"<>|':
name = name.replace(ch, "_")
return name.strip() or "unknown"
# 2. 查询所有语音,按 chat_name_id 关联用户名
rows = conn.execute("SELECT chat_name_id, create_time, local_id, voice_data FROM VoiceInfo ORDER BY chat_name_id, create_time").fetchall()
conn.close()
print(f"{len(rows)} 条语音")
# 3. 遍历转换
success = 0
fail = 0
for chat_name_id, create_time, local_id, voice_data in rows:
user_name = name_map.get(chat_name_id, f"unknown_{chat_name_id}")
dname = safe_dirname(display_name(user_name))
dt = datetime.fromtimestamp(create_time)
filename = dt.strftime("%Y%m%d_%H%M%S") + f"_{local_id}.mp3"
user_dir = os.path.join(OUTPUT_DIR, dname)
os.makedirs(user_dir, exist_ok=True)
# 写入 .info 文件(只写一次)
info_path = os.path.join(user_dir, ".info")
if not os.path.exists(info_path):
info = contact_map.get(user_name, {"username": user_name, "alias": "", "remark": "", "nick_name": ""})
with open(info_path, "w", encoding="utf-8") as f:
f.write(f"username: {info['username']}\n")
f.write(f"alias: {info['alias']}\n")
f.write(f"nick_name: {info['nick_name']}\n")
f.write(f"remark: {info['remark']}\n")
output_path = os.path.join(user_dir, filename)
if os.path.exists(output_path):
success += 1
continue
ok = silk_to_mp3(voice_data, output_path)
if ok:
success += 1
print(f" [{success}/{len(rows)}] {dname}/{filename}")
else:
fail += 1
print(f" 失败: {dname}/{filename}")
print(f"\n完成: 成功 {success}, 失败 {fail}")