在复刻小智AI的过程中,本次重点围绕WebSocket协议的核心流程展开,旨在深入剖析实时通信的底层实现机制。文章将涵盖以下核心内容:基于Arduino框架的复刻背景、WebSocket协议在小智AI通信中的关键作用与数据类型,以及建立连接的详细步骤与配置参数。

前言
本次继续沿用复刻小智AI的技术路线,但将开发框架切换为Arduino。
上周已经完成了VSCode + PlatformIO + Arduino环境下ESP32-S3、ESP-SR、ESP-TTS整套开发环境的搭建(详细过程可参阅上一篇文章),并成功实现了语音唤醒、命令识别以及文本转语音功能。接下来的核心任务就是对接小智AI服务端的WebSocket协议。
原项目78/xiaozhi-esp32功能虽然完整,但代码结构较为复杂,阅读门槛相对较高,且实在不想重新搭建IDF编译环境。后续发现了一个Python实现的桌面客户端huangjunsen0406/py-xiaozhi,它不仅带有图形界面,还支持手动与自动对话模式的切换,是一个很好的学习样本,可以一边研究PC端轻量级语音识别的实现,一边深入理解WebSocket通信。
通信过程
小智AI客户端与服务端之间的通信可以选择WebSocket或MQTT,这里我们选择更易于上手的WebSocket协议进行实现。
协议概述
WebSocket负责客户端与服务器之间的实时双向通信,主要涉及以下几类数据:
- 控制指令:用于开始/停止监听、中断TTS播放等操作。
- 文本信息:包含LLM响应内容、情绪指令、配置信息等。
- 音频数据:
- 客户端 → 服务器:传输录制好的Opus编码音频流。
- 服务器 → 客户端:推送TTS生成的Opus编码音频流。
- 状态同步:用于同步TTS播放开始/结束等状态。
通信格式分为两种:JSON格式用于传输文本、控制和状态信息;Binary格式用于传输Opus编码的音频数据。
建立连接
客户端发起连接:根据配置文件中的
WEBSOCKET_URL地址,向服务器发起WebSocket连接请求。发送头部信息:连接时需附带必要的HTTP头部信息,具体包括:
Authorization:Bearer(来源于配置中的WEBSOCKET_ACCESS_TOKEN)Protocol-Version:1Client-Id: 客户端标识Device-Id: 设备标识(通常使用MAC地址)
目前除了
Device-Id需要客户端自行生成外,其他参数均可直接使用示例中的固定值:"WEBSOCKET_URL": "wss://api.tenclass.net/xiaozhi/v1/", "WEBSOCKET_ACCESS_TOKEN": "test-token", "CLIENT_ID": "1dd91545-082a-454e-a131-1c8251375c9c",
服务器响应:服务器接受连接请求。
客户端发送
hello:连接成功后,客户端需要发送一条JSON格式的hello消息:hello_message = { "type": "hello", "version": 1, "transport": "websocket", "audio_params": { "format": AudioConfig.FORMAT, "sample_rate": AudioConfig.SAMPLE_RATE, "channels": AudioConfig.CHANNELS, "frame_duration": AudioConfig.FRAME_DURATION, } }这里预设的音频编码参数问题不大,服务器后续会推送其实际接受的音频配置。
服务器响应hello:服务器返回会话ID和初始配置信息:
{ "type": "hello", "version": 1, "transport": "websocket", "audio_params": { "format": "opus", "sample_rate": 24000, "channels": 1, "frame_duration": 20 }, "session_id": "a1f81xs89" }这里有两点需要特别注意:客户端必须保存返回的
session_id,后续所有需要会话标识的消息都会用到它;此外,还需根据audio_params更新本地的Opus编码设置,以确保编解码参数一致。
服务端认证
首次连接到小智AI官方后台时,需要在控制台中添加设备。具体操作非常便捷:当客户端成功连接服务器并发送第一条语音消息时,服务器会返回一条语音,并附带一个6位数的验证码,使用该验证码即可在后台完成设备添加。
至此,WebSocket连接已成功建立,接下来就可以进行后续的对话流程了。
客户端消息
主动发起对话的机制与之前一致——要么通过手动触发录音,要么通过唤醒词触发。
listen (JSON)
用于控制音频监听(录音)的状态。
- 开始监听:
{ "session_id": "session-id", "type": "listen", "state": "start", "mode": "manual" | "auto" | "realtime" // 监听模式 } - 停止监听:
{ "session_id": "session-id", "type": "listen", "state": "stop" }
wake_word (JSON)
如果通过唤醒词启动对话,需要发送另一种listen消息,告知服务器已检测到唤醒词。这样服务器会立即返回一条语音消息作为回应。
- 格式:
{ "session_id": "session-id", "type": "listen", "state": "detect", "text": "你好小智" // 根据实际唤醒词修改 }
abort (JSON)
用于请求服务器中断当前正在进行的操作(主要是TTS语音播放)。
- 格式:
{ "session_id": "session-id", "type": "abort", "reason": "wake_word_detected" // (可选)中断原因 }
这个功能主要用在当服务器正在输出一段较长的语音,而用户希望重新开始新对话的场景。
audio (Binary)
用于发送录制的音频数据。
- 格式:二进制数据帧 (Binary Frame)。
- 内容:根据
session_info中audio_config约定的格式(默认为Opus)编码的音频数据块。
IoT 消息
这部分内容暂时不涉及,留待后续研究具体格式。
服务端消息
小智AI服务端返回的消息同样分为JSON和Binary两种格式,JSON类型通过type字段来区分实际内容。示例JSON格式如下:
{
"type": "tts",
"state": "start",
"sample_rate": 24000,
"session_id": "session-id"
}
type字段的可能取值包括llm、tts、stt等。
type=tts (JSON)
这是服务端返回的核心消息类型,其中包含了情绪、语音输出以及语音识别结果等信息。可以说,小智AI的整个交互流程,主要工作都由服务端承担,客户端的实现相对轻量。
根据state字段的不同值,需要进行针对性处理:
state=start
服务器接收到客户端的语音数据后,生成了LLM聊天对话内容,并开始返回语音数据。同时会提供音频sample_rate参数,客户端可根据此参数同步更新播放配置。
{
"type": "tts",
"state": "start",
"sample_rate": 24000,
"session_id": "session-id"
}
state=sentence_start
表示对话中的一句话开始,text字段包含该句话的文本内容。
{
"type": "tts",
"state": "sentence_start",
"text": "感觉你心情不太好,发生了什么事吗?",
"session_id": "session-id"
}
state=sentence_end
表示一句话结束。
{
"type": "tts",
"state": "sentence_end",
"text": "感觉你心情不太好,发生了什么事吗?",
"session_id": "session-id"
}
state=stop
表示服务端对之前收到的语音数据生成的响应内容已整体结束,客户端可以继续录音操作。
{
"type": "tts",
"state": "stop",
"session_id": "session-id"
}
type=llm (JSON)
用于返回大模型回复时需要表达的情绪状态。text字段是一个Emoji表情,emotion字段对应情绪单词。在无法显示Emoji的设备上,可以通过单词映射到对应的图片进行展示。
{
"type": "llm",
"text": "