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")
|
DECODED_VOICE_DIR = os.path.join(SCRIPT_DIR, "decoded_voices")
|
||||||
|
|
||||||
def _get_media_db_path():
|
# media DB 与 message DB 同样会分片(media_0.db、media_1.db…),
|
||||||
return _cache.get("message/media_0.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):
|
def _get_chat_name_id(conn, username):
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
@@ -1754,32 +1767,31 @@ def _get_chat_name_id(conn, username):
|
|||||||
return row[0] if row else None
|
return row[0] if row else None
|
||||||
|
|
||||||
|
|
||||||
def _fetch_voice_row(username, local_id=None):
|
def _fetch_voice_row(username, local_id):
|
||||||
"""Query VoiceInfo from media_0.db. Returns (voice_data, create_time) or None."""
|
"""遍历所有 media DB 分片,返回 (voice_data, create_time);找不到返回 None。"""
|
||||||
media_db = _get_media_db_path()
|
for media_db in _iter_media_db_paths():
|
||||||
if not media_db:
|
|
||||||
return None
|
|
||||||
with closing(sqlite3.connect(media_db)) as conn:
|
with closing(sqlite3.connect(media_db)) as conn:
|
||||||
chat_name_id = _get_chat_name_id(conn, username)
|
chat_name_id = _get_chat_name_id(conn, username)
|
||||||
if chat_name_id is None:
|
if chat_name_id is None:
|
||||||
return None
|
continue
|
||||||
if local_id is not None:
|
row = conn.execute(
|
||||||
return conn.execute(
|
|
||||||
"SELECT voice_data, create_time FROM VoiceInfo "
|
"SELECT voice_data, create_time FROM VoiceInfo "
|
||||||
"WHERE chat_name_id = ? AND local_id = ?",
|
"WHERE chat_name_id = ? AND local_id = ?",
|
||||||
(chat_name_id, local_id),
|
(chat_name_id, local_id),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
if row:
|
||||||
|
return row
|
||||||
return None
|
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."""
|
"""Decode SILK voice blob to WAV file, return output path."""
|
||||||
import pysilk
|
import pysilk
|
||||||
data = bytes(voice_data)
|
data = bytes(voice_data)
|
||||||
silk_data = data[1:] if data[0] == 0x02 else data
|
silk_data = data[1:] if data[0] == 0x02 else data
|
||||||
os.makedirs(DECODED_VOICE_DIR, exist_ok=True)
|
os.makedirs(DECODED_VOICE_DIR, exist_ok=True)
|
||||||
time_str = datetime.fromtimestamp(create_time).strftime('%Y%m%d_%H%M%S')
|
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)
|
inp = io.BytesIO(silk_data)
|
||||||
out = io.BytesIO()
|
out = io.BytesIO()
|
||||||
pysilk.decode(inp, out, 24000)
|
pysilk.decode(inp, out, 24000)
|
||||||
@@ -1809,24 +1821,28 @@ def get_voice_messages(chat_name: str, limit: int = 20) -> str:
|
|||||||
names = get_contact_names()
|
names = get_contact_names()
|
||||||
display_name = names.get(username, username)
|
display_name = names.get(username, username)
|
||||||
|
|
||||||
media_db = _get_media_db_path()
|
if not MEDIA_DB_KEYS:
|
||||||
if not media_db:
|
return "找不到 media DB"
|
||||||
return "找不到 media_0.db"
|
|
||||||
|
|
||||||
|
# 从每个分片各取最多 limit 条后合并再截断:分片若有时间重叠也不会漏最新消息
|
||||||
|
rows = []
|
||||||
|
for media_db in _iter_media_db_paths():
|
||||||
with closing(sqlite3.connect(media_db)) as conn:
|
with closing(sqlite3.connect(media_db)) as conn:
|
||||||
chat_name_id = _get_chat_name_id(conn, username)
|
chat_name_id = _get_chat_name_id(conn, username)
|
||||||
if chat_name_id is None:
|
if chat_name_id is None:
|
||||||
return f"{display_name} 无语音消息"
|
continue
|
||||||
|
rows.extend(conn.execute(
|
||||||
rows = conn.execute(
|
|
||||||
"SELECT local_id, create_time, length(voice_data) FROM VoiceInfo "
|
"SELECT local_id, create_time, length(voice_data) FROM VoiceInfo "
|
||||||
"WHERE chat_name_id = ? ORDER BY create_time DESC LIMIT ?",
|
"WHERE chat_name_id = ? ORDER BY create_time DESC LIMIT ?",
|
||||||
(chat_name_id, limit),
|
(chat_name_id, limit),
|
||||||
).fetchall()
|
).fetchall())
|
||||||
|
|
||||||
if not rows:
|
if not rows:
|
||||||
return f"{display_name} 无语音消息"
|
return f"{display_name} 无语音消息"
|
||||||
|
|
||||||
|
rows.sort(key=lambda r: r[1], reverse=True)
|
||||||
|
rows = rows[:limit]
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
for local_id, create_time, size in rows:
|
for local_id, create_time, size in rows:
|
||||||
time_str = datetime.fromtimestamp(create_time).strftime('%Y-%m-%d %H:%M')
|
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} 的语音消息"
|
return f"找不到 local_id={local_id} 的语音消息"
|
||||||
|
|
||||||
voice_data, create_time = row
|
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)
|
duration_s = pcm_len / (24000 * 2)
|
||||||
return (
|
return (
|
||||||
f"解码成功!\n"
|
f"解码成功!\n"
|
||||||
@@ -1909,7 +1925,7 @@ def transcribe_voice(chat_name: str, local_id: int) -> str:
|
|||||||
return f"找不到 local_id={local_id} 的语音消息"
|
return f"找不到 local_id={local_id} 的语音消息"
|
||||||
|
|
||||||
voice_data, create_time = row
|
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()
|
model = _get_whisper_model()
|
||||||
result = model.transcribe(wav_path)
|
result = model.transcribe(wav_path)
|
||||||
|
|||||||
Reference in New Issue
Block a user