Files
kefu/deploy/review-voice-20260918/voice-audit-report.md
T
2026-09-21 10:34:06 +08:00

107 lines
11 KiB
Markdown
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.
# 语音转文字链路只读审计
审计日期:2026-09-18。范围:`C:/wechat_rpa` 协议与归档代码、`C:/kefu/wechat_rpa` 视觉模式代码,以及已有解密 SQLite 副本的 schema 和计数。未修改产品、未启动原生接口、未触发解密或转录、未下载模型、未打开音频、未读取登录凭证、未查询或输出消息与转写正文。
## 结论
现有软件具备读取企微本地转写缓存的能力,但该能力只接入了归档导出,没有接入客服回复。安装端现有副本里 5 条语音全部已有对应缓存,可在不新增服务的前提下将这些转写接到问答模型。**这不等于具备主动转录任意新语音的能力**:没有发现可调用的 voice2text RPC,也没有自动右键“转文字”的视觉操作;离线 ASR 是未安装依赖、可能下载模型的导出兜底。
建议先统一消息解析层读取经过账号与消息关联校验的缓存,并让协议模式只放行已经取得转写的语音;对尚无转写的消息设置可重查的等待状态。若要求每条未缓存语音自动识别,还需要另行实现并验证客户端转录触发或本地 ASR,不能把本次缓存接入描述为完整的主动语音识别。
## 已有函数与数据契约
| 位置 | 现状与可复用能力 |
| --- | --- |
| `archive_exporter/wxwork_voice2text.py:20` `load_voice2text` | 遍历解密的 `message.db`,读取 `msg_voice2text.message_id,text`;返回 `(user_dir, str(message_id)) -> text`。仅读取已经存在的转写,不触发客户端转录。 |
| 同文件 `:56` `decode_silk_to_wav` | 通过 `pilk.silk_to_wav(..., rate=24000)` 解码,TypeError 时回退 `pilk.decode`。 |
| 同文件 `:73` `asr_missing` | 对 `.silk/.amr` 的未缓存项实例化 `faster_whisper.WhisperModel(model_size='base', device='cpu', compute_type='int8')`,再 `transcribe(...,language='zh',vad_filter=True)`。首次可能下载模型。 |
| `archive_exporter/wxwork_export_final.py:591` | 导出时自动加载缓存;只有 `voice_asr=True` 才运行离线兜底。`:599` 的 `_get_voice_text` 同时允许账号键与全局 `('',sid)`。 |
| `archive_auto_backup.py:1085` `_normalize` / `:1286` | 增量归档预先取得 `voice_texts`,匹配 server_id 后传给 `_semantic_content`,以转写替换归档正文。没有给回复链路回填。 |
| `wxwork_db.py:984` `_parse_message` | 当前只解析 `message_table.content`;无可读内容时产生 `[语音]` 占位符,没有查询 `msg_voice2text`。 |
| `wxwork_db.py:961` `_read_conversation_context` | 各行经过 `_parse_message` 后组合 `messages`、`last_message` 和 `text`,是两种回复模式共享的合适接入位置。 |
| `reply_database.py:158` `get_conversation_context_by_id` | 协议按 account+conv_id 读取;保留失败,不回退剪贴板。GUI/视觉读库通过后台线程队列,默认等待 0.35 秒。不要在这一读库线程做长耗时 ASR。 |
| `protocol_engine.py:35` `_is_text_message` | 只允许类型 0/1/2。仅将语音 `content` 换成文字还不够,类型 4/16 仍被转人工。 |
| `protocol_engine.py:65` `_unanswered_manual_reason` | 检查当前未回复批次,任一非文本即阻断。应保留对没有可靠转写的语音、图片、文件和混合未解析媒体的限制。 |
| `protocol_engine.py:593` `_generate` | 将 `model_context_text(context)` 交给 `ai_chat.get_ai_reply` 或 `model_router.answer`,可沿原有上下文、知识库和裁判链路处理转写,不需要另建问答出口。 |
语音类型在 `wxwork_db.MSG_TYPE_MAP` 和归档映射中为 **4、16**。注意类型 69 是音频通话,不应自动当作可转写语音消息。
实际副本表结构:
```text
msg_voice2text:
message_id INTEGER PRIMARY KEY
voice_id TEXT
text TEXT
collapse INTEGER
voice_silent_transfer INTEGER
```
已验证关联为:同一账号数据库中,`msg_voice2text.message_id = message_table.server_id`。**不是** `message_table.message_id`。`voice_silent_transfer` 的业务语义没有通过接口或客户端实验核实,不应仅凭字段名作为“转录成功”标志。
消息身份应继续保留 `account`、`conv_id`、`sender_id`、`server_id`、`rowid` 和 `dedup_key`。缓存表没有账号列,账号隔离来自选中的数据库连接;先核对消息属于该账号与目标会话,再使用其有效 server_id 查询。不能遍历所有账号数据库找同 sid 的任意转写。
## 本机副本统计
由同目录 `audit_voice_schema_counts.py` 生成 `voice-schema-counts.json`。只查询 schema、COUNT 和是否非空;未 SELECT 正文。账号使用匿名编号。
| 副本位置(有消息账号) | 消息行 | 语音行 | 对方语音 | 非空转写 | 按 server_id 匹配语音 |
| --- | ---: | ---: | ---: | ---: | ---: |
| 源目录 `wxwork_reply_cache` | 657 | 3 | 3 | 3 | 3 |
| 源目录 `archive_auto_backup/decrypted` | 657 | 3 | 3 | 3 | 3 |
| 源目录 `wxwork_decrypted` | 654 | 3 | 3 | 3 | 3 |
| 安装端 `wxwork_reply_cache` | 706 | 5 | 3 | 5 | 5 |
| 安装端 `archive_auto_backup/decrypted` | 706 | 5 | 3 | 5 | 5 |
这些是同一账号的重复副本,不能相加。另一个匿名账号的已有副本均为零消息。所有发现的语音均为类型 16,均为 M:/S: 单聊。
审计连接使用 `mode=ro&immutable=1`、`PRAGMA query_only=ON`,不创建 journal/SHM;因此表格是现存解密副本主文件的快照,不包含未合入副本的 WAL,不能代表实时客户端完整数据。没有扫描原企微 Data/Cache 目录,也没有调用 `active_context`、`load_keys` 或解密入口。
## 原生能力、音频路径与身份边界
`wecom_native_sender.py` 的 `NativeClient.validate`(`:177`)核对指定客户端构建、消息服务与登录 owner、account UID、发送入口;`NativeClient.send` / `NativeSender.send` 只构造并发送文本。`wecom_native_stub.py` 仅有 send、send_with_options、cleanup。没有语音上传、下载或转录调用。`wecom_native_protocol.py` 只是原生试验响应解析和历史结果展示,不提供 voice2text。不能把已有发消息身份验证等同于获得某个未实现语音 RPC 的权限。
现有本机缓存读取不需要新的接口 token,但必须复用当前账号既有、经过验证的读库连接与客户端身份,不读取其他账号、全局 sid 或猜测对端。此审计没有读取密钥内容。
`archive_exporter/wxwork_export_media.py` 的代码约定:原始媒体位于所选账号 `Cache/Voice`,注释说明为按 UUID 命名的 SILK v3;`extract_media_refs` 从消息二进制提取 UUID/URL/文件名,`build_cache_index(account_dir)` 建立该账号缓存索引,`match_media` 按 UUID、文件名、模糊文件名依次尝试。`export_media` 返回按裸 server_id 键控的字典,path 常为相对导出目录的路径。
**没有检验真实音频文件格式或可访问性**,上述格式和目录是代码声明,不能作为已完成实机音频验证。在线问答若走音频路径,至少要求当前账号缓存根路径包含校验、精确 UUID 关联、长度/格式上限与文件变化检测;不要继承“模糊文件名即关联”的归档便利逻辑,也不要自动抓取消息中任意 URL。
离线导出兜底不能原样当在线识别服务:
- `media_map` 只有 server_id,`asr_missing` 查“任意账号相同 sid”并写 `('',sid)`;有跨账号碰撞风险。
- `export_media` 返回相对 out_dir 的 path,但 `asr_missing` 直接将它交给 decoder,缺少明确根目录解析。
- `.amr` 与 `.silk` 均走名为 `decode_silk_to_wav` 的同一分支;格式兼容和回退结果是否为有效 WAV 尚未测试。
- `asr_missing` 的日志包含转写前 50 字,不应直接进入在线生产诊断日志。
- 构建 Python 3.12 的 `pilk`、`faster_whisper`、`ctranslate2`、`av` 均不存在;requirements 也未列这些依赖。不能承诺当前 EXE 已有离线 ASR。
## 视觉模式
源目录 `wechat_bot.py:6455` `_database_chat_text` 已优先从共享消息库取得 context.text,并核对当前标题、目标账号/会话、期望 dedup_key 与时效。因此统一解析层增加缓存转写可复用于视觉模式,不需要重新识别窗口。
`wechat_bot.py:15584` 的纯未转写语音分支直接请求客户补文字;没有自动右键、菜单定位或“转文字”点击实现。`ai_chat.py:531` 只过滤“转文字/正在识别/失败”等 UI 文案,不是转录功能。`:1953` 的视觉回复遇到语音会返回 `VISION_VOICE_NEEDS_TEXT`,只根据已经存在的可见文字决定转写标志;不会从波形、时长推断声音。
接入后应传递可信的 `voice_transcribed` 和来源字段供视觉/协议判断,而不是依赖模型自报“已经转写”。已有转写的文字可以按正常文本进入模型,同时在展示、归档中保留原媒体类型。纯截图分支仍不能读出音频内容。
## 最小实现与测试建议
1. 在 `WXWorkDB` 当前账号连接中按有效 server_id 查询一条缓存;类型仅限 4/16。表缺失、字段缺失、空串、非字符串/畸形值、无效 sid 均返回“未取得转写”,不把异常当成正文。保留原类型和消息 ID,增加 `voice_transcribed=true`、`transcription_source='wecom_cache'`。
2. 协议 eligibility 仅把“语音类型且可信转写非空”的项作为可回答内容。混合批次必须每条语音均有转写,任何剩余媒体继续人工处理;不得因为批次末尾是文本而丢掉前面的语音。
3. 首次无转写时保留任务,后台按有限频率重新读目标 context,不阻塞 GUI/读库线程。`get_new_messages` 只按 message_table.rowid 推进,msg_voice2text 稍后更新不会发新消息事件,必须主动刷新待转写任务。重新读取后仍校验账号、会话、消息 ID、对方/我方方向及发送保护状态。
4. 成功转写后通过现有 `_generate` 进入上下文、知识库、裁判与人工审核流程。超时、错误、缺少主动转录能力时清楚显示原因,保留可重试状态;不能用占位符或猜测文本生成并自动发送。
建议最少覆盖以下隔离回归(未在本次只读任务中实现或执行):
- 两个账号相同 server_id 返回各自转写,账号A绝不使用账号B或全局缓存。
- 相同本地 message_id、不同 server_id,以 server_id 关联;sid为空/0及不匹配返回未转写。
- 类型4与16成功、类型69不冒充语音;保持原 content_type、方向、dedup_key。
- 缺表、空白/状态文案/畸形转写、读库失败不产生可回答文本。
- 单语音、连续两语音、文本+语音与语音+图片批次;缺任何一条转写不部分自动回复。
- 新语音先到、转写后到且 message_table 没新增 rowid,待转写任务能恢复且仅生成一次。
- 等待时收到新消息、人工回复、切换账号、撤销关系或停止监听,旧任务不错误继续。
- AI 调用捕获合成输入,断言包含合成转写和正确历史,保留原知识库调用路径;不发原始媒体二进制或占位符。
- 视觉数据库优先路径使用可信转写;纯截图且无转写继续安全阻断。
- 所有测试临时库、合成转写、模型/发送 mock;无真实转录、下载、网络或客户发送。
本次执行验证仅包括代码静态审计、依赖存在性检查和 SQLite schema/aggregate 查询;未测试真实语音识别准确率、客户端转录触发或模型回答效果。