fix: 语音查询支持多分片 media DB + 文件名唯一化
解决 PR #53 review 的阻塞项 #1,顺手修 #3、#6。 #1 `_get_media_db_path()` 硬编码 `media_0.db` - 新增模块级 `MEDIA_DB_KEYS`,镜像 `MSG_DB_KEYS` 的分片发现逻辑 - `_fetch_voice_row` 遍历所有分片,按 `(chat_name_id, local_id)` 首个命中即返回;单条语音在 media DB 家族内唯一,命中即可停 - `get_voice_messages` 从每个分片各取 `LIMIT limit`,合并排序后 截断到 `limit`。选择"每分片取 limit 条再合并"而非"按 max(create_time) 排序后逐个取到 limit 即停止":后者假设分片 间时间不重叠,一旦 WeChat 改分片策略就会静默丢消息;前者工作 量 O(N 分片 × limit),在任何分片布局下都正确 #3 输出文件名冲突 - `_silk_to_wav` 增加 `local_id` 参数,输出 `{user}_{time}_{lid}.wav`, 同一秒内两条语音不会互相覆盖;两个调用方都已在作用域内持有 `local_id` #6 `_fetch_voice_row` 的 `local_id=None` 死分支 - 随 #1 的重写一并删除,`local_id` 改为必填位置参数 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1744,8 +1744,21 @@ def get_chat_images(chat_name: str, limit: int = 20) -> str:
|
||||
|
||||
DECODED_VOICE_DIR = os.path.join(SCRIPT_DIR, "decoded_voices")
|
||||
|
||||
def _get_media_db_path():
|
||||
return _cache.get("message/media_0.db")
|
||||
# media DB 与 message DB 同样会分片(media_0.db、media_1.db…),
|
||||
# 每个分片各有独立的 Name2Id / VoiceInfo 表。
|
||||
MEDIA_DB_KEYS = sorted([
|
||||
k for k in ALL_KEYS
|
||||
if any(v.startswith("message/") for v in key_path_variants(k))
|
||||
and any(re.search(r"media_\d+\.db$", v) for v in key_path_variants(k))
|
||||
])
|
||||
|
||||
|
||||
def _iter_media_db_paths():
|
||||
for rel_key in MEDIA_DB_KEYS:
|
||||
path = _cache.get(rel_key)
|
||||
if path:
|
||||
yield path
|
||||
|
||||
|
||||
def _get_chat_name_id(conn, username):
|
||||
row = conn.execute(
|
||||
@@ -1754,32 +1767,31 @@ def _get_chat_name_id(conn, username):
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
def _fetch_voice_row(username, local_id=None):
|
||||
"""Query VoiceInfo from media_0.db. Returns (voice_data, create_time) or None."""
|
||||
media_db = _get_media_db_path()
|
||||
if not media_db:
|
||||
return None
|
||||
def _fetch_voice_row(username, local_id):
|
||||
"""遍历所有 media DB 分片,返回 (voice_data, create_time);找不到返回 None。"""
|
||||
for media_db in _iter_media_db_paths():
|
||||
with closing(sqlite3.connect(media_db)) as conn:
|
||||
chat_name_id = _get_chat_name_id(conn, username)
|
||||
if chat_name_id is None:
|
||||
return None
|
||||
if local_id is not None:
|
||||
return conn.execute(
|
||||
continue
|
||||
row = conn.execute(
|
||||
"SELECT voice_data, create_time FROM VoiceInfo "
|
||||
"WHERE chat_name_id = ? AND local_id = ?",
|
||||
(chat_name_id, local_id),
|
||||
).fetchone()
|
||||
if row:
|
||||
return row
|
||||
return None
|
||||
|
||||
|
||||
def _silk_to_wav(voice_data, create_time, username):
|
||||
def _silk_to_wav(voice_data, create_time, username, local_id):
|
||||
"""Decode SILK voice blob to WAV file, return output path."""
|
||||
import pysilk
|
||||
data = bytes(voice_data)
|
||||
silk_data = data[1:] if data[0] == 0x02 else data
|
||||
os.makedirs(DECODED_VOICE_DIR, exist_ok=True)
|
||||
time_str = datetime.fromtimestamp(create_time).strftime('%Y%m%d_%H%M%S')
|
||||
out_path = os.path.join(DECODED_VOICE_DIR, f"{username}_{time_str}.wav")
|
||||
out_path = os.path.join(DECODED_VOICE_DIR, f"{username}_{time_str}_{local_id}.wav")
|
||||
inp = io.BytesIO(silk_data)
|
||||
out = io.BytesIO()
|
||||
pysilk.decode(inp, out, 24000)
|
||||
@@ -1809,24 +1821,28 @@ def get_voice_messages(chat_name: str, limit: int = 20) -> str:
|
||||
names = get_contact_names()
|
||||
display_name = names.get(username, username)
|
||||
|
||||
media_db = _get_media_db_path()
|
||||
if not media_db:
|
||||
return "找不到 media_0.db"
|
||||
if not MEDIA_DB_KEYS:
|
||||
return "找不到 media DB"
|
||||
|
||||
# 从每个分片各取最多 limit 条后合并再截断:分片若有时间重叠也不会漏最新消息
|
||||
rows = []
|
||||
for media_db in _iter_media_db_paths():
|
||||
with closing(sqlite3.connect(media_db)) as conn:
|
||||
chat_name_id = _get_chat_name_id(conn, username)
|
||||
if chat_name_id is None:
|
||||
return f"{display_name} 无语音消息"
|
||||
|
||||
rows = conn.execute(
|
||||
continue
|
||||
rows.extend(conn.execute(
|
||||
"SELECT local_id, create_time, length(voice_data) FROM VoiceInfo "
|
||||
"WHERE chat_name_id = ? ORDER BY create_time DESC LIMIT ?",
|
||||
(chat_name_id, limit),
|
||||
).fetchall()
|
||||
).fetchall())
|
||||
|
||||
if not rows:
|
||||
return f"{display_name} 无语音消息"
|
||||
|
||||
rows.sort(key=lambda r: r[1], reverse=True)
|
||||
rows = rows[:limit]
|
||||
|
||||
lines = []
|
||||
for local_id, create_time, size in rows:
|
||||
time_str = datetime.fromtimestamp(create_time).strftime('%Y-%m-%d %H:%M')
|
||||
@@ -1860,7 +1876,7 @@ def decode_voice(chat_name: str, local_id: int) -> str:
|
||||
return f"找不到 local_id={local_id} 的语音消息"
|
||||
|
||||
voice_data, create_time = row
|
||||
out_path, pcm_len = _silk_to_wav(voice_data, create_time, username)
|
||||
out_path, pcm_len = _silk_to_wav(voice_data, create_time, username, local_id)
|
||||
duration_s = pcm_len / (24000 * 2)
|
||||
return (
|
||||
f"解码成功!\n"
|
||||
@@ -1909,7 +1925,7 @@ def transcribe_voice(chat_name: str, local_id: int) -> str:
|
||||
return f"找不到 local_id={local_id} 的语音消息"
|
||||
|
||||
voice_data, create_time = row
|
||||
wav_path, _ = _silk_to_wav(voice_data, create_time, username)
|
||||
wav_path, _ = _silk_to_wav(voice_data, create_time, username, local_id)
|
||||
|
||||
model = _get_whisper_model()
|
||||
result = model.transcribe(wav_path)
|
||||
|
||||
Reference in New Issue
Block a user