前言
在微信生态的自动化场景里,文字消息的处理相对简单,但语音消息往往是绕不开的障碍:用户习惯发语音,机器人却只能读懂文字。要实现完整的微信客服机器人或消息归档系统,必须打通"接收语音 → 下载音频 → 语音识别"这条链路。
本文围绕三个核心问题展开:
- 如何通过 HTTP 接口向指定联系人发送语音消息?
- 收到语音消息时,如何从回调中获取音频文件?
- 拿到音频文件后,如何调用语音识别服务实现语音转文字?
文章会给出完整可运行的 Python 示例,覆盖接口调用、音频下载、格式转换与 ASR 接入的全流程。
一、微信语音消息的基本格式
1.1 微信语音文件格式
微信在内部使用 SILK 格式(.slk)存储语音,这是腾讯自研的低码率编解码格式,不同于常见的 MP3、WAV、AAC 等。SILK 文件可直接在微信内播放,但通用播放器和大多数语音识别 API 并不支持,因此在进行语音识别之前往往需要做一次格式转换。
几种常见格式的对比:
| 格式 | 码率 | 识别支持 | 备注 |
|---|---|---|---|
| SILK (.slk) | 极低 | 几乎不支持 | 微信内部格式 |
| PCM (.pcm) | 高 | 广泛支持 | 原始采样,体积大 |
| WAV (.wav) | 高 | 广泛支持 | PCM 加容器头 |
| MP3 (.mp3) | 中 | 广泛支持 | 有损压缩 |
| AAC (.aac) | 低 | 广泛支持 | 效率最佳 |
实际落地时推荐的转换路径是:SILK → PCM → WAV,再送入语音识别服务,这条链路的开源工具最完整、识别效果最稳定。
1.2 语音消息在回调中的结构
当有人向你的微信账号发送语音时,平台会把该事件以 POST 请求的形式推送到你预先设置的回调地址。回调 payload 中与语音相关的字段大致如下(字段名以实际平台文档为准):
json{
"appId": "你的appId",
"fromWxid": "发送方wxid",
"toWxid": "接收方wxid",
"type": 34,
"msgId": "消息唯一ID",
"content": "语音文件的mediaId或URL",
"voiceTime": 12,
"createTime": 1718000000
}
其中 type=34 是微信协议层对语音消息的类型编号,voiceTime 是语音时长(秒),content 字段通常承载文件标识——不同平台的实现方式略有差异,有的直接给下载链接,有的给一个 mediaId 需要再调下载接口换取文件流。
二、发送语音消息
2.1 接口格式说明
发送语音的接口通常是 POST /message/postVoice,请求体为 JSON,需要提供音频文件内容或可访问的 URL,以及语音时长。
示例代码如下(占位符形式,具体字段以官方文档为准):
pythonimport requests
import base64
BASE = "https://你的接口域名" # 注册后在官方文档获取
TOKEN = "你的Token"
APPID = "你的appId"
HEADERS = {"token": TOKEN} # 鉴权字段名以官方文档为准
def send_voice(to_wxid: str, audio_path: str, voice_time: int) -> dict:
"""
发送语音消息
:param to_wxid: 接收方 wxid 或群 id
:param audio_path: 本地 silk 或 wav 文件路径
:param voice_time: 语音时长(秒)
"""
with open(audio_path, "rb") as f:
audio_b64 = base64.b64encode(f.read()).decode()
payload = {
"appId": APPID,
"toWxid": to_wxid,
"voice": audio_b64, # base64 编码的音频内容(字段名以文档为准)
"voiceTime": voice_time,
}
resp = requests.post(f"{BASE}/message/postVoice", json=payload, headers=HEADERS)
return resp.json()
# 示例调用
result = send_voice("wxid_xxxxxx", "./hello.slk", 5)
print(result) # {"ret": 200, "msg": "操作成功", "data": {...}}
代码为示例,具体接口路径、请求字段及返回结构以官方文档为准。
2.2 发送注意事项
- 格式要求:平台通常要求语音文件为 SILK 格式,若本地只有 WAV/MP3,需先转换再上传。
- 时长字段:
voiceTime必须准确,否则接收方显示的播放时长会有偏差。 - 文件大小:一条微信语音最长 60 秒,文件不宜过大,SILK 格式 60 秒通常在 50–100 KB 以内。
三、接收语音消息并下载音频
3.1 搭建回调服务
使用 Flask 快速搭建一个接收回调的 Web 服务:
pythonfrom flask import Flask, request, jsonify
import json
app = Flask(__name__)
VOICE_MSG_TYPE = 34 # 微信语音消息 type,以实际文档为准
@app.route("/callback", methods=["POST"])
def callback():
data = request.get_json(force=True)
msg_type = data.get("type")
if msg_type == VOICE_MSG_TYPE:
handle_voice(data)
return jsonify({"code": 200}) # 必须返回 200,否则平台会重试
def handle_voice(data: dict):
"""处理语音消息"""
msg_id = data.get("msgId")
from_wxid = data.get("fromWxid")
voice_ref = data.get("content") # mediaId 或直接 URL,视文档而定
voice_sec = data.get("voiceTime", 0)
print(f"收到语音消息 from={from_wxid}, time={voice_sec}s, ref={voice_ref}")
# 异步排队下载,避免阻塞回调响应
download_voice_async(msg_id, voice_ref)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
3.2 下载语音文件
下载接口通常为 POST /message/downloadVoice(字段名以文档为准):
pythonimport requests
import os
BASE = "https://你的接口域名"
TOKEN = "你的Token"
APPID = "你的appId"
HEADERS = {"token": TOKEN}
DOWNLOAD_DIR = "/tmp/wechat_voices"
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
def download_voice(msg_id: str, voice_ref: str) -> str:
"""
下载语音文件,返回本地路径
:param msg_id: 消息 ID,用于命名文件
:param voice_ref: 回调中的音频标识(mediaId 或 URL)
"""
payload = {
"appId": APPID,
"msgId": msg_id,
"content": voice_ref, # 字段名以文档为准
}
resp = requests.post(f"{BASE}/message/downloadVoice", json=payload, headers=HEADERS)
result = resp.json()
if result.get("ret") != 200:
raise RuntimeError(f"下载失败: {result}")
# 假设 data.fileUrl 是可访问的临时文件地址(以文档为准)
file_url = result["data"]["fileUrl"]
local_path = os.path.join(DOWNLOAD_DIR, f"{msg_id}.slk")
audio_resp = requests.get(file_url, timeout=30)
with open(local_path, "wb") as f:
f.write(audio_resp.content)
return local_path
实际返回字段视平台文档而定,部分平台直接在下载接口中返回 base64 编码的音频内容而非 URL。
3.3 异步队列设计
语音下载属于 IO 密集型操作,直接在回调函数中同步执行会超时。建议使用 threading.Thread 或消息队列(Redis + Celery)将下载任务放到后台处理:
pythonimport threading
import time
import random
def download_voice_async(msg_id: str, voice_ref: str):
"""后台线程下载,带随机延迟防封"""
def _task():
time.sleep(random.uniform(3, 10)) # 每条间隔 3–10s,防止频率过高
try:
local_path = download_voice(msg_id, voice_ref)
print(f"语音已保存: {local_path}")
# 下载完成后触发语音识别
text = voice_to_text(local_path)
print(f"识别结果: {text}")
except Exception as e:
print(f"处理失败: {e}")
t = threading.Thread(target=_task, daemon=True)
t.start()
四、SILK 转 WAV 格式转换
4.1 silk-v3-decoder 的使用
开源库 silk-v3-decoder 是目前最成熟的 SILK 解码方案,支持将微信语音转换为 PCM/WAV:
bash# 编译(macOS/Linux)
git clone https://github.com/kn007/silk-v3-decoder
cd silk-v3-decoder
make
# 转换:slk → pcm
./decoder input.slk output.pcm
# pcm → wav(借助 ffmpeg)
ffmpeg -y -f s16le -ar 24000 -ac 1 -i output.pcm output.wav
或者在 Python 中通过 subprocess 调用:
pythonimport subprocess
import os
def silk_to_wav(silk_path: str) -> str:
"""
将 SILK 文件转为 WAV,返回 WAV 文件路径
需要提前编译 silk-v3-decoder 并安装 ffmpeg
"""
base = silk_path.rsplit(".", 1)[0]
pcm_path = base + ".pcm"
wav_path = base + ".wav"
# Step 1: SILK → PCM
subprocess.run(
["/path/to/silk-v3-decoder/decoder", silk_path, pcm_path],
check=True, capture_output=True
)
# Step 2: PCM → WAV (24000Hz, mono, 16-bit signed little-endian)
subprocess.run(
["ffmpeg", "-y", "-f", "s16le", "-ar", "24000", "-ac", "1",
"-i", pcm_path, wav_path],
check=True, capture_output=True
)
# 清理中间文件
os.remove(pcm_path)
return wav_path
采样率 24000 Hz 是微信语音的默认值,部分场景可能是 16000 Hz,以实际音频为准。
4.2 pydub 直接处理 PCM
如果环境中已安装 pydub + ffmpeg,也可以用更 Pythonic 的方式处理:
pythonfrom pydub import AudioSegment
def pcm_to_wav_pydub(pcm_path: str, wav_path: str,
sample_rate: int = 24000, channels: int = 1):
"""PCM 原始采样转 WAV"""
audio = AudioSegment.from_raw(
pcm_path,
sample_width=2, # 16-bit = 2 bytes
frame_rate=sample_rate,
channels=channels
)
audio.export(wav_path, format="wav")
4.3 格式转换实操要点
格式转换是整条链路中最容易出错的环节,上线前务必逐一核实以下细节:
- 采样率必须和实际音频一致:微信语音绝大多数为 24000 Hz,但少数场景(如转发的第三方语音)可能是 16000 Hz。若采样率填错,转出来的 WAV 会变调或加速,ASR 识别率会大幅下降。建议在转换前先用
ffprobe探测实际采样率:ffprobe -i input.slk -show_streams。 - SILK 文件头校验:真正的微信 SILK 文件以固定字节
02 23 21 53 49 4C 4B开头(即\x02#!SILK)。若下载的文件头不对,说明下载接口返回的是错误数据,应先排查网络和接口响应,而不是强行走解码流程。 - 中间文件及时清理:PCM 原始采样体积是 WAV 的数倍,批量处理时要在识别完成后立即删除临时文件,否则磁盘空间会快速耗尽。
- 并发安全:多线程同时处理多条语音时,文件命名必须带上
msg_id或随机 UUID,绝不能使用固定文件名,否则并发写入会互相覆盖。
五、语音转文字(ASR 接入)
5.1 方案选择
语音识别(ASR)有离线和在线两条路:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 百度 ASR | 接入简单、中文准确率高 | 有频率限制,付费超量 | 中低频客服机器人 |
| 讯飞开放平台 | 方言识别强 | 文档复杂 | 需要方言支持 |
| OpenAI Whisper(本地) | 完全离线、无费用 | 需要 GPU,速度慢 | 隐私敏感或高频场景 |
| Azure Speech | 企业级稳定 | 价格较高 | 对准确率要求高 |
下面以百度语音识别为例给出完整调用示例(接入其他服务只需替换 SDK 部分)。
5.2 百度 ASR 接入
pythonfrom aip import AipSpeech # pip install baidu-aip
# 百度 AI 平台凭证(需自行注册获取)
BAIDU_APP_ID = "你的AppId"
BAIDU_API_KEY = "你的ApiKey"
BAIDU_SECRET = "你的SecretKey"
asr_client = AipSpeech(BAIDU_APP_ID, BAIDU_API_KEY, BAIDU_SECRET)
def voice_to_text_baidu(wav_path: str) -> str:
"""
百度 ASR:WAV 文件 → 文字
:param wav_path: 已转换的 WAV 文件路径(PCM 编码,16000 或 24000 Hz)
"""
with open(wav_path, "rb") as f:
audio_data = f.read()
result = asr_client.asr(
audio_data,
"wav",
16000, # 采样率,需与文件实际采样率一致
{"dev_pid": 1537} # 1537=普通话,其他 pid 见百度文档
)
if result.get("err_no") == 0:
return "".join(result.get("result", []))
else:
raise RuntimeError(f"百度 ASR 错误: {result}")
5.3 使用 Whisper 做本地离线识别
若对数据隐私要求较高,或语音消息量大导致云服务费用高,可以使用 OpenAI Whisper 在本地做识别:
pythonimport whisper
# 首次运行会自动下载模型(约 1.5 GB for "medium")
_whisper_model = whisper.load_model("medium")
def voice_to_text_whisper(audio_path: str, language: str = "zh") -> str:
"""
Whisper 本地识别,支持 wav/mp3/slk 等多种格式
:param audio_path: 音频文件路径
:param language: 语言代码,中文填 "zh"
"""
result = _whisper_model.transcribe(audio_path, language=language)
return result["text"].strip()
Whisper 的优势在于直接支持 SILK 文件(内部会调 ffmpeg 解码),省去了格式转换步骤。选用 medium 或 large 模型时普通话识别准确率可达 95% 以上。
5.4 完整流水线整合
将前面各步骤串联,形成从回调到识别结果的完整流水线:
pythondef voice_to_text(silk_path: str) -> str:
"""
语音消息完整处理流水线
SILK → WAV → ASR → 文字
"""
try:
# Step 1: 格式转换
wav_path = silk_to_wav(silk_path)
# Step 2: 语音识别(二选一)
# text = voice_to_text_baidu(wav_path) # 云端方案
text = voice_to_text_whisper(wav_path) # 本地方案
return text
finally:
# 清理临时文件
for path in [silk_path, silk_path.replace(".slk", ".wav")]:
if os.path.exists(path):
os.remove(path)
六、在消息机器人中落地
6.1 语音消息自动转文字回复
一个常见场景:收到语音消息后,自动将识别结果以文字形式回复给发送者,方便客服归档或 AI 进一步处理:
pythonimport requests
BASE = "https://你的接口域名"
TOKEN = "你的Token"
APPID = "你的appId"
HEADERS = {"token": TOKEN}
def reply_with_text(to_wxid: str, text: str):
"""将识别结果回复给发送方"""
payload = {
"appId": APPID,
"toWxid": to_wxid,
"content": f"[语音转文字] {text}",
}
resp = requests.post(f"{BASE}/message/postText", json=payload, headers=HEADERS)
return resp.json()
如果需要对接企业内部系统,WechatApi 提供扫码登录、消息收发、好友与群管理等 REST 接口,HTTP 调用即可完成上述语音收发的全部操作,具体能力以官方文档为准。
6.2 批量归档历史语音
对于已积压的历史语音消息,可以批量从消息记录中提取 msgId,依次调用下载接口并识别:
pythonimport time
import random
def batch_archive_voices(voice_records: list[dict]) -> list[dict]:
"""
批量归档语音消息为文字
:param voice_records: [{"msg_id": ..., "from_wxid": ..., "voice_ref": ...}, ...]
"""
results = []
for record in voice_records:
try:
silk_path = download_voice(record["msg_id"], record["voice_ref"])
text = voice_to_text(silk_path)
results.append({
"msg_id": record["msg_id"],
"from": record["from_wxid"],
"text": text,
"status": "ok",
})
except Exception as e:
results.append({
"msg_id": record["msg_id"],
"status": "error",
"error": str(e),
})
# 下载间隔 3–10 秒,避免频率过高
time.sleep(random.uniform(3, 10))
return results
6.3 常见问题排查
| 问题现象 | 可能原因 | 解决方式 |
|---|---|---|
| 下载返回 404 / 失败 | mediaId 已过期(通常 3 天内有效) | 实时下载,不要延迟太久 |
| WAV 转换后杂音严重 | 采样率填错(应为 24000,填了 16000) | 确认 ffmpeg 参数中 -ar 值 |
| ASR 识别率低 | 输入采样率与识别服务不匹配 | 统一转为 16000 Hz 再识别 |
| 回调收不到语音消息 | 回调地址不公网可达,或返回非 200 | 检查公网 IP + 端口,确保返回 {"code":200} |
| silk-v3-decoder 编译失败 | 缺少 gcc / make | apt install build-essential 或 brew install gcc |
总结
语音消息转文字在微信自动化场景中是一个高频需求,核心链路不复杂:回调接收语音 → 异步下载 SILK 文件 → 转换为 WAV → ASR 识别。格式转换是最容易踩坑的环节,关键是保证采样率与 ASR 服务的输入要求匹配。离线 Whisper 和云端百度 ASR 各有适用场景,按实际的并发量和隐私要求选择即可。
落地时有几点值得特别关注:一是异步处理必须做,回调响应时间窗口极短,同步阻塞极易导致平台重推甚至断连;二是mediaId 有时效性,务必在消息到达后尽快触发下载,超过有效期将无法获取文件;三是磁盘管理不能忽略,批量处理语音时 PCM 中间文件体积可观,需要在流水线末尾做好清理;四是ASR 采样率对齐,格式转换和识别调用中的采样率参数必须保持一致,否则识别结果会出现乱码或词序混乱。以上几点做扎实,整条语音处理链路就能稳定运行。
