336 lines
12 KiB
Markdown
336 lines
12 KiB
Markdown
# 蚂蚁阿福接入小智 ESP32 — 实施方案
|
||
|
||
## 项目目标
|
||
|
||
将蚂蚁阿福 App 的 AI 能力接入小智 ESP32 硬件终端,用户通过 ESP32 设备语音对话,
|
||
后端对接蚂蚁阿福代替自建 LLM,省去 GPU 资源(两张 RTX 3090 + Qwen3-32B)。
|
||
|
||
---
|
||
|
||
## 系统架构
|
||
|
||
### 方案A:文字接入(自定义 LLM Provider)
|
||
|
||
```
|
||
ESP32 设备 PlugAI 服务端 手机
|
||
┌──────────┐ WebSocket ┌──────────────────┐ HTTP/SSE ┌─<E2948C><E29480><EFBFBD>────────────┐
|
||
│ 麦克风 │ ──────────────→│ ASR (FunASR) │ │ 蚂蚁阿福 App │
|
||
│ 唤<><E594A4><EFBFBD>词 │ │ 语音→文字 │ │ + Frida 注入 │
|
||
│ AEC/NS │ │ │ GET /chat?q= │ │
|
||
│ │ │ AntafLLM Provider│──────────────→│ HTTP Bridge │
|
||
│ │ │ (新增) │←──────────────│ (port 18900) │
|
||
│ │ │ │ SSE 流式回答 │ │
|
||
│ 喇叭 │←───────────────│ TTS (EdgeTTS) │ │ │
|
||
│ │ WebSocket │ 文字→语音 │ │ │
|
||
└──────────┘ └──────────────────┘ └──────────────┘
|
||
```
|
||
|
||
**数据流**: ESP32 音频 → FunASR(语音转文字) → AntafLLM(文<><E69687><EFBFBD>发给阿福) → EdgeTTS(回答转语音) → ESP32 播放
|
||
|
||
### 方案B:语音直通(替代整个 ASR+LLM+TTS)
|
||
|
||
```
|
||
ESP32 设备 PlugAI 服务端 手机
|
||
┌──────────┐ WebSocket ┌─────<E29480><E29480><EFBFBD>────────────┐ TCP 二进制 ┌──<E29480><E29480><EFBFBD>───────────┐
|
||
│ 麦克风 │ ──────────────→│ 音频转发模块(新增) │ │ 蚂蚁<E89A82><E89A81>福 App │
|
||
│ │ │ Opus解码 │ PCM注入mic │ + Frida <20><>入 │
|
||
│ │ │ 重采样 24k→48k │──────────────→│ Voice Bridge │
|
||
│ │ │ │ PCM speaker │ (port 18901) │
|
||
│ 喇叭 │←───────────────│ 重采样 24k→24k │←──────────────│ libantaudio │
|
||
│ │ WebSocket │ Opus编码 │ │ │
|
||
└──────────┘ └──────────────────┘ └──────────────┘
|
||
```
|
||
|
||
**数据流**: ESP32 音频 → 解码+重采样 → 注入阿福麦克风 → 阿福完整处理(ASR+LLM+TTS) → 捕获音频 → 编码 → ESP32 播放
|
||
|
||
---
|
||
|
||
## 可行性评估
|
||
|
||
| 维度 | 方案A (文字接入) | 方案B (语音直通) |
|
||
|------|-----------------|-----------------|
|
||
| 可行性 | **高** | **中低** |
|
||
| 实现难度 | 低 (1个Python文件) | 高 (改JS+写转发模块) |
|
||
| 改动范围 | 新增 LLM Provider + 改配置 | 改 voice_bridge.js + 新增转发模块 |
|
||
| 延迟 | 中 (ASR+网络+TTS 各一轮) | 低 (音频直通) |
|
||
| 音质 | EdgeTTS (微软高质量) | 阿福原生 TTS |
|
||
| GPU 依赖 | 无 (省掉 Qwen3-32B) | 无 |
|
||
| 手机依赖 | 需要 (App+Frida+adb) | 需要 (App+Frida+adb) |
|
||
| 核心风险 | 低 | **voice_bridge 当前不支持音频注入** |
|
||
|
||
**结论**: 先实施方案A,验证通过后再做方案B。
|
||
|
||
---
|
||
|
||
## 方案A 详细实施
|
||
|
||
### 前置条件
|
||
|
||
| 组件 | 状态 | 说明 |
|
||
|------|------|------|
|
||
| ESP32 设备 | 已就绪 | 固件已烧录,WiFi+服务端已配置 |
|
||
| 小智服务端 | 已就绪 | ws://14.18.247.51:8010 运行中 |
|
||
| ASR (FunASR) | 已就绪 | CPU 模式 |
|
||
| TTS (EdgeTTS) | 已就绪 | 微软免费 |
|
||
| 蚂蚁阿福 HTTP Bridge | 已就绪 | http_bridge_stream.js (port 18900) |
|
||
| Frida + 手机 | 需部署 | 手机需连到服务端可达的网络 |
|
||
|
||
### 第1步:创建 AntafLLM Provider
|
||
|
||
文件路径:`backend/main/xiaozhi-server/core/providers/llm/antaf/antaf.py`
|
||
|
||
```python
|
||
import requests
|
||
from config.logger import setup_logging
|
||
from core.providers.llm.base import LLMProviderBase
|
||
|
||
TAG = __name__
|
||
logger = setup_logging()
|
||
|
||
|
||
class LLMProvider(LLMProviderBase):
|
||
"""
|
||
蚂蚁阿福 LLM Provider
|
||
通过 Frida HTTP Bridge (port 18900) 对接蚂蚁阿福 App 的文字对话 API。
|
||
Bridge 运行在手机上,通过 adb forward 或网络暴露 SSE 流式接口。
|
||
"""
|
||
|
||
def __init__(self, config):
|
||
self.bridge_url = config.get("bridge_url", "http://127.0.0.1:18900")
|
||
self.timeout = config.get("timeout", 60)
|
||
logger.bind(tag=TAG).info(
|
||
f"AntafLLM 初始化: bridge={self.bridge_url}, timeout={self.timeout}s"
|
||
)
|
||
|
||
def response(self, session_id, dialogue, **kwargs):
|
||
"""
|
||
流式返回蚂蚁阿福的回答。
|
||
1. 从 dialogue 取最后一条用户消息
|
||
2. GET {bridge_url}/chat?q={query}
|
||
3. 解析 SSE 流,yield 每个 delta 文本
|
||
"""
|
||
# 提取最后一条用户消息
|
||
query = ""
|
||
for msg in reversed(dialogue):
|
||
if msg.get("role") == "user":
|
||
query = msg.get("content", "")
|
||
break
|
||
|
||
if not query:
|
||
logger.bind(tag=TAG).warning("对话中没有用户消息")
|
||
yield "抱歉,我没有收到您的问题。"
|
||
return
|
||
|
||
logger.bind(tag=TAG).info(f"AntafLLM 请求: {query[:50]}...")
|
||
|
||
try:
|
||
url = f"{self.bridge_url}/chat"
|
||
resp = requests.get(
|
||
url,
|
||
params={"q": query},
|
||
stream=True,
|
||
timeout=self.timeout
|
||
)
|
||
resp.encoding = "utf-8"
|
||
|
||
for line in resp.iter_lines(decode_unicode=True):
|
||
if not line:
|
||
continue
|
||
if line.startswith("data: "):
|
||
data = line[6:] # 去掉 "data: " 前缀
|
||
if data == "[DONE]":
|
||
break
|
||
if data and len(data.strip()) > 0:
|
||
yield data
|
||
|
||
except requests.exceptions.ConnectionError:
|
||
logger.bind(tag=TAG).error("无法连接蚂<E68EA5><E89A82>阿福 Bridge,请检查手机和 Frida 状态")
|
||
yield "抱歉,蚂蚁阿福服务暂时不可用。"
|
||
except requests.exceptions.Timeout:
|
||
logger.bind(tag=TAG).error(f"蚂蚁阿福 Bridge 超时 ({self.timeout}s)")
|
||
yield "抱歉,回答超时了。"
|
||
except Exception as e:
|
||
logger.bind(tag=TAG).error(f"AntafLLM 异常: {e}")
|
||
yield "抱歉,发生了错误。"
|
||
```
|
||
|
||
### 第2步:修改服务端配置
|
||
|
||
编辑 `backend/main/xiaozhi-server/data/.config.yaml`:
|
||
|
||
```yaml
|
||
selected_module:
|
||
LLM: antaf # 改为<E694B9><E4B8BA><EFBFBD>蚁阿福
|
||
|
||
LLM:
|
||
antaf:
|
||
type: antaf
|
||
bridge_url: http://<手机IP>:18900 # 手机的 HTTP Bridge 地址
|
||
timeout: 60 # SSE 流超时时间
|
||
```
|
||
|
||
也可以保留原来的 Qwen3 配置,方便切换:
|
||
|
||
```yaml
|
||
LLM:
|
||
antaf:
|
||
type: antaf
|
||
bridge_url: http://<手机IP>:18900
|
||
timeout: 60
|
||
Qwen3:
|
||
type: openai
|
||
model_name: Qwen3-32B
|
||
url: http://127.0.0.1:30000/v1
|
||
api_key: EMPTY
|
||
```
|
||
|
||
### 第3步:网络打通
|
||
|
||
手机的 Frida Bridge 端口需要让 PlugAI 服<><E69C8D><EFBFBD>器能访问到。有两种方式:
|
||
|
||
#### 方式1:手机直连局域网(推荐)
|
||
|
||
如果手机和 PlugAI 服务器在同一网络(或手机有公网可达 IP):
|
||
```bash
|
||
# 手机上启动 bridge 后,服务端直接访问
|
||
# bridge_url: http://<手机内网IP>:18900
|
||
curl http://<手机IP>:18900/chat?q=hello
|
||
```
|
||
|
||
#### 方式2:adb forward + SSH 隧道
|
||
|
||
手机通过 USB 连接一台中间机器,再通过 SSH 隧道暴露<E69AB4><E99CB2><EFBFBD>
|
||
```bash
|
||
# 中间机器上
|
||
adb forward tcp:18900 tcp:18900
|
||
|
||
# PlugAI 上建 SSH 隧道
|
||
ssh -L 18900:127.0.0.1:18900 user@中间机器IP
|
||
# bridge_url: http://127.0.0.1:18900
|
||
```
|
||
|
||
### 第4步:启动与测试
|
||
|
||
```bash
|
||
# 1. 手机端:启动 Frida + HTTP Bridge
|
||
frida -U -p <PID> -l http_bridge_stream.js
|
||
|
||
# 2. 先测 bridge 连通性
|
||
curl -N 'http://<手机IP>:18900/chat?q=你好'
|
||
|
||
# 3. PlugAI 服务端:重启小智服务
|
||
cd /home/ZeroStack/xiaozhi/xiaozhi-esp32-server/main/xiaozhi-server
|
||
source /home/ZeroStack/xiaozhi/venv/bin/activate
|
||
python app.py
|
||
|
||
# 4. ESP32 设备:唤醒测试
|
||
# 说 "你好小智" → 提问 → 应该听到蚂蚁阿福的回答(EdgeTTS 合成的语音)
|
||
```
|
||
|
||
---
|
||
|
||
## 方案B 详细实施(后续)
|
||
|
||
### 核心改造:voice_bridge.js 支持音频注入
|
||
|
||
当前 voice_bridge.js 的 `MFAntAudio3AV2Filter::process` hook 只**读取** micIn 缓冲区。
|
||
需要改造为可以从外部**写入** micIn 缓冲区,替换真实麦克风输入。
|
||
|
||
#### 改造要点
|
||
|
||
```javascript
|
||
// voice_bridge.js 新增功能
|
||
var injectBuffer = null; // 外部注入的 PCM 数据
|
||
|
||
// 新增 inject 命令:接收外部 PCM 音频帧
|
||
// 客户端发送: [4字节长度][type=3][960字节PCM数据]
|
||
// type 3 = inject audio
|
||
|
||
Interceptor.attach(processAddr, {
|
||
onEnter: function(args) {
|
||
var micIn = args[1]; // 麦克风输入缓冲区 (960 bytes)
|
||
var frameSize = args[4]; // 960
|
||
|
||
if (injectBuffer !== null) {
|
||
// 用注入数据覆盖真实麦克风输入
|
||
micIn.writeByteArray(injectBuffer);
|
||
injectBuffer = null;
|
||
}
|
||
}
|
||
});
|
||
```
|
||
|
||
#### 采样率转换
|
||
|
||
| 来源 | 格式 | 需转换为 |
|
||
|------|------|---------|
|
||
| ESP32 → 服务端 | Opus 24kHz mono | PCM 48kHz mono (阿福 mic) |
|
||
| 阿福 speaker 输出 | PCM 24kHz stereo | Opus 24kHz mono (ESP32) |
|
||
|
||
服务端需要:
|
||
- libopus 解码/编码
|
||
- resampy 或 scipy 做采样率转换
|
||
- 960字节帧对齐(20ms @ 48kHz)
|
||
|
||
#### 新增音频转发模块
|
||
|
||
文件<EFBFBD><EFBFBD>径:`backend/main/xiaozhi-server/core/providers/asr/antaf_voice/antaf_voice.py`
|
||
|
||
这是一个特殊的 ASR Provider,它不做语音识别,而是:
|
||
1. 接收 ESP32 的 Opus 音频流
|
||
2. 解码为 PCM,重采样 24k→48k
|
||
3. 通过 TCP 发送到 voice_bridge (port 18901) 的 inject 命令
|
||
4. 接收 voice_bridge 的 speaker 输出
|
||
5. 重采样 24k stereo → 24k mono,Opus 编码
|
||
6. 直接发回 ESP32(跳过 LLM 和 TTS)
|
||
|
||
#### 方案B 风险点
|
||
|
||
1. **帧时序同步**: ESP32 音频帧和阿福 process() 调用频率可能不一致
|
||
2. **延迟累积**: 网络传输 + 两次重采样 + 注入延迟
|
||
3. **VAD 冲突**: 阿福自带 VAD 可能与注入音频不匹配
|
||
4. **回声消除失效**: 注入 mic 数据后,阿<EFBC8C><E998BF>的 AEC 参考信号(spkRef)对不上
|
||
5. **对话控制**: 何时 open_voice / close_voice 需要与 ESP32 唤醒状态同步
|
||
|
||
---
|
||
|
||
## 依赖清单
|
||
|
||
### 方案A(新增依赖)
|
||
- `requests` — Python HTTP 库(服务端 venv 中应已有)
|
||
|
||
### 方案B(新增<E696B0><E5A29E>赖)
|
||
- `opuslib` 或 `pyogg` — Opus 编解码
|
||
- `resampy` 或 `scipy.signal` — 采样率转换
|
||
- `numpy` — 音频数据处理
|
||
|
||
---
|
||
|
||
## 文件清单
|
||
|
||
### 方案A
|
||
| 操<><E6938D> | 文件 |
|
||
|------|------|
|
||
| 新增 | `backend/main/xiaozhi-server/core/providers/llm/antaf/__init__.py` |
|
||
| 新增 | `backend/main/xiaozhi-server/core/providers/llm/antaf/antaf.py` |
|
||
| 修改 | `backend/main/xiaozhi-server/data/.config.yaml` |
|
||
|
||
### 方案B(额外)
|
||
| 操作 | 文件 |
|
||
|------|------|
|
||
| 修改 | `antaf/voice_bridge.js` (新增 inject 命令) |
|
||
| 新增 | `backend/main/xiaozhi-server/core/providers/asr/antaf_voice/antaf_voice.py` |
|
||
| 新增 | `backend/main/xiaozhi-server/core/utils/audio_resample.py` |
|
||
|
||
---
|
||
|
||
## 里程碑
|
||
|
||
| 阶段 | 目标 | 预期产出 |
|
||
|------|------|---------|
|
||
| M1 | 方案A 代码实现 | AntafLLM Provider + 配置 |
|
||
| M2 | 网络打通 | PlugAI ↔ 手机 Bridge 连通 |
|
||
| M3 | 端到端测试 | ESP32 唤醒→阿福回答→语音播报 |
|
||
| M4 | 方案B 原型 | voice_bridge 音频注入验证 |
|
||
| M5 | 方案B 集成 | 全语音直通链路 |
|