Skip to content

微信语音到底是怎么变成文字的 ​

我在给家里人配一个"发句话就能下电影"的助手。动手前最大的疑虑是:老人只会发语音,不会打字。

按常识,这需要装一个语音识别服务(Whisper 之类)—— 跑起来要几个 G 的模型、要 GPU 才不卡、还要处理微信的专有音频格式(SILK)。成本不低。

结果实测发现:完全不需要。语音已经有文字了。

第一次判断,我错了 ​

我的验证方法是查媒体文件:如果语音消息被接收,应该有一个音频文件落盘。

bash
find media -newermt "-10 min" -type f

空。 于是我的结论是"语音消息没被收到"。

这个结论是错的,而且错得很典型:我只查了"文件",没查"消息"。

正确的验证位置 ​

消息通道的日志里,每条入站消息都会打一行,带类型:

bash
grep "inbound message" /tmp/openclaw/openclaw-YYYY-MM-DD.log | tail

输出:

inbound message: from=xxx types=3
inbound message: from=yyy types=1

types 是消息类型枚举,实测值:0 NONE / 1 TEXT / 2 IMAGE / 3 VOICE / 4 FILE / 5 VIDEO。

所以那两条 types=3 的,就是语音,收到了。 音频没有落盘,是因为根本不需要落盘。

为什么不需要落盘 ​

因为在消息体里,语音已经带上了转写文字。

微信客户端(或者说它的服务端)会做语音识别,转写结果作为消息结构的一部分传上来。用插件源码看得很清楚:

js
// 大致逻辑:正文从消息项里取
function bodyFromItemList(items) {
  for (const item of items) {
    if (item.type === VOICE && item.voice_item?.text) {
      return item.voice_item.text   // 直接当正文用
    }
  }
}

后续分支判断也一样:

js
// 只有当语音没有 text、且有可下载的媒体时,才走下载流程
if (!text && hasDownloadableMedia) {
  await downloadMedia()  // SILK → WAV
}

于是链路分成两条:

  • 有 text → 直接用文字当用户输入,hasMedia = false,音频一次都不碰
  • 无 text → 下载音频,用 silk-wasm 把 SILK 转 WAV,交给 STT(本地命令或云端 API)

实测数据 ​

两条语音消息的日志:

22:51:55  types=3  bodyLen=14  hasMedia=false   → 正文「帮我找一下五月天转眼这首歌曲」
22:54:24  types=3  bodyLen=7   hasMedia=false   → 正文「今天天气怎么样」

第二次我故意没有手动点"转文字"。 文字照样来了。

这一条很重要:说明微信客户端的语音识别是自动的,不是"用户点了转文字按钮才有"。之前我以为必须用户配合,差点设计成"请先点一下转文字"的提示语——那是多余的门槛。

那什么时候才真的需要 STT ​

两种情况:

1. 语音消息没有 voice_item.text

不同客户端版本、不同平台(iOS/Android/PC),以及某些情况下转写会缺失。这时 bodyLen=0、hasMedia=true,你就得自己转。

2. 音频内容不是"话"

比如你想分析语音里的背景声、语气、音乐——转写出来的是文字,语气没了。

真需要时,方案是可以接的:

json
{
  "tools": {
    "media": {
      "audio": {
        "provider": "openai",          // 或本地命令
        "transcription": {
          "command": "whisper --model small --language zh"
        }
      }
    }
  }
}

但先确认第一条:打开日志看看 bodyLen 是不是 0。如果不为 0,你不需要 STT,你需要的是把已有文字用起来。

顺带一个容易搞错的点 ​

验证时不要用"媒体目录里有没有新文件"当证据。当消息自带文字时,音频根本不会被下载,所以永远不会有文件。 这跟"消息没收到"看起来一模一样。

判断消息有没有到,就看日志的 inbound message 行;判断内容是什么,看同一行的 bodyLen;判断要不要下载媒体,看 hasMedia。

教训 ​

这件事教我的,是一个比较通用的排查原则:

"我没看到证据"和"事情没发生"是两件事。

我第一次的判断链条是:查媒体目录 → 没文件 → 语音没收到。中间那一步"语音本来就不该有文件"我没验证过,却当成前提用。

现在的习惯是:碰到"东西没到",先找到能证明它到没到的那条日志,而不是猜它应该出现在哪里。

一句话版本:微信语音不用装 Whisper——转写文字跟着消息一起来了;去日志看 types=3 和 bodyLen,别去 media 目录找音频。