本次工作概述

本项目实现了一个基于 Milk-V Duo S 开发板的智能语音陪伴机器人系统。系统采用云端协同架构,PC 端作为代理服务器处理核心逻辑,Duo 板负责音频采集和播放,通过 WebSocket 实现实时双向通信。


一、项目架构

1.1 系统组成

┌─────────────┐         ┌──────────────┐         ┌─────────────────┐
│  Milk-V Duo │         │   PC 端代理   │         │   云服务         │
│  (边端设备)  │◄────────►│  (FastAPI)   │◄────────►│  - 阿里云 ASR    │
│             │ WebSocket│              │ HTTP/WS  │  - DashScope LLM │
│ - 音频采集  │          │ - 协议转发   │          │  - Edge TTS      │
│ - 音频播放  │          │ - TTS 生成   │          │                  │
│ - 动作控制  │          │ - 情感分析   │          │                  │
└─────────────┘         └──────────────┘         └─────────────────┘

1.2 技术栈

PC 端(Windows):

  • Python 3.12 + FastAPI
  • DashScope API(阿里云通义千问)
  • Edge TTS(微软免费在线语音合成)
  • websockets(WebSocket 通信)
  • pydub(音频格式转换)

边端(Milk-V Duo S):

  • Python 3 + websocket-client
  • ALSA 音频系统(arecord/aplay)
  • MCP01 USB 音频模块

二、主要工作内容

2.1 核心功能实现

语音交互流程
  1. 语音采集:Duo 板通过 arecord 采集 16kHz 单声道 PCM 音频
  2. 语音识别:音频流经 WebSocket 发送至 PC 端,转发至阿里云 Realtime API 进行 ASR
  3. 智能对话:PC 端调用 DashScope LLM(qwen3.5-flash)生成回复文本和情感分析
  4. 语音合成:PC 端使用 Edge TTS 将回复文本转换为 24kHz PCM 音频
  5. 音频播放:PCM 音频分块传输至 Duo 板,通过 aplay 实时播放
情感与动作控制
  • LLM 返回结构化 JSON,包含 emotion(happy/sad/neutral)和 action(wave/idle/sad 等)
  • Duo 端接收指令后可扩展控制舵机、LED、表情屏等外设
打断机制
  • 支持用户插话打断 AI 发言
  • 检测到新语音输入时自动停止当前播放
自动重连
  • Duo 端实现断线自动重连机制(最多 10 次尝试)
  • PC 端优化 WebSocket 心跳配置,避免连接超时断开

三、遇到的问题及解决方案

问题 1:TTS 音频无法播放(只有噪音)

现象:
Duo 端收到音频数据,但扬声器发出"滋滋啦啦"的噪音,无清晰语音。

原因分析:
Edge TTS 输出的是 MP3 格式音频,而 Duo 端的 aplay 期望接收 PCM S16_LE 格式。直接将 MP3 数据写入 aplay 导致解码错误。

解决方案:
在 PC 端使用 pydub 库将 Edge TTS 生成的 MP3 转换为 24kHz 单声道 16bit PCM 格式:

from pydub import AudioSegment
import io

audio_segment = AudioSegment.from_mp3(io.BytesIO(mp3_data))
audio_segment = audio_segment.set_frame_rate(24000).set_channels(1).set_sample_width(2)
pcm_data = audio_segment.raw_data

依赖安装:

pip install pydub
# 并安装 ffmpeg(音频转换工具)

问题 2:WebSocket 连接频繁断开

现象:
对话 1-2 次后连接断开,报错 keepalive ping timeout 或 AssertionError

原因分析:

  1. LLM 响应时间较长(10-25 秒),超过 WebSocket 默认心跳超时
  2. websockets 库在某些版本存在心跳断言 bug

解决方案:

  1. 禁用 websockets 自动心跳:ping_interval=None, ping_timeout=None
  2. Duo 端增加重连机制,断线后自动重新连接
  3. 优化异常处理,使用 return_exceptions=True 防止单个协程异常导致整体崩溃

问题 3:会话初始化超时

现象:
Duo 端显示"❌ 会话初始化超时",无法进入录音状态。

原因分析:
PC 端在连接阿里云后阻塞等待 session.updated 消息,但阿里云需要收到 Duo 端的配置后才返回该消息,形成死锁。

解决方案:
移除阻塞的初始化等待,让两个协程(接收板子消息、接收阿里云消息)并行运行,阿里云会在收到配置后自动返回 session.updated


问题 4:LLM 响应超时

现象:
LLM 调用超过 15 秒超时,返回 fallback 错误消息。

原因分析:
默认的 qwen3.5-plus 模型响应较慢,且超时时间设置过短。

解决方案:

  1. 切换到更快的模型:.env 中设置 DASHSCOPE_MODEL=qwen3.5-flash
  2. 调整超时时间为 20 秒:LLM_TIMEOUT=20.0
  3. 简化 Prompt,减少 token 数量

问题 5:reply_text 与 tts_text 不一致

现象:
屏幕显示的回复内容与扬声器播放的内容不同。

原因分析:
LLM 生成的 JSON 中 reply_text 和 tts_text 字段内容不一致。

解决方案:

  1. 优化 Prompt,明确要求两个字段必须完全相同
  2. 在 PC 端代码中添加兜底逻辑:如果 tts_text 为空,则使用 reply_text
if not tts_text or len(tts_text.strip()) == 0:
    tts_text = reply_text

问题 6:Milk-V Duo 环境限制

现象:
尝试使用 apt-get install espeak-ng 失败,提示命令不存在。

原因分析:
Milk-V Duo 运行的是 Buildroot 系统,不是标准的 Debian/Ubuntu,不支持 apt-get。

解决方案:
改用纯 Python 方案(Edge TTS),通过 pip 安装依赖,避免系统级包管理。


问题 7:打断后无声音

现象:
按回车打断后,后续对话能收到消息但无声音播放。

原因分析:
打断时发送了 response.cancel 给阿里云,取消了正在生成的 TTS 音频,但 PC 端逻辑未正确处理这种情况。

解决方案:
修改 Duo 端打断逻辑,只停止本地播放,不发送 cancel 给服务器,让 TTS 继续生成完成。


四、关键代码片段

4.1 PC 端 TTS 生成与转换

async def generate_tts_audio(text: str) -> bytes:
    """使用 Edge TTS 生成音频并转换为 PCM"""
    communicate = edge_tts.Communicate(text, "zh-CN-XiaoxiaoNeural")
    mp3_data = b""
    async for chunk in communicate.stream():
        if chunk["type"] == "audio":
            mp3_data += chunk["data"]
    
    from pydub import AudioSegment
    audio_segment = AudioSegment.from_mp3(io.BytesIO(mp3_data))
    audio_segment = audio_segment.set_frame_rate(24000).set_channels(1).set_sample_width(2)
    return audio_segment.raw_data

4.2 Duo 端音频播放

class AlsaPlayer:
    def start_process(self):
        self.proc = subprocess.Popen(
            ["aplay", "-D", self.device, "-f", "S16_LE", "-r", "24000", "-c", "1"],
            stdin=subprocess.PIPE,
            stderr=subprocess.DEVNULL
        )
    
    def write(self, data):
        self.proc.stdin.write(data)
        self.proc.stdin.flush()

4.3 机器人控制接口

# 在 Duo 端 on_message 中接收 agent.action
if msg_type == "agent.action":
    action = data.get("action", "")  # wave/idle/sad/happy
    emotion = data.get("emotion", "")  # happy/sad/neutral
    
    # 示例:控制舵机
    if action == "wave":
        control_servo("hand", angle=90)
    
    # 示例:控制 LED
    if emotion == "happy":
        set_led_color(0, 255, 0)  # 绿色


五、性能优化

优化项 优化前 优化后 说明
LLM 模型 qwen3.5-plus qwen3.5-flash 响应时间从 25s 降至 7-10s
超时设置 30s 20s 平衡响应速度与稳定性
音频分块 4800 字节/块 实现流式播放,降低延迟
WebSocket 心跳 默认 禁用 避免 long-polling 导致的断连
TTS 方案 阿里云 TTS Edge TTS 音质更好,无需额外 API 调用

六、待优化方向

  1. 流式 TTS:当前需等待完整音频生成后再播放,可改为边生成边传输
  2. 本地缓存:缓存常用回复的音频,减少重复 TTS 调用
  3. 多语言支持:扩展 Edge TTS 支持其他语言
  4. 离线模式:研究本地 TTS 引擎(如 Coqui TTS)以实现完全离线
  5. 动作同步:实现语音播放与机器人动作的精确同步

七、总结

成功实现了基于 Milk-V Duo 的智能语音机器人系统,解决了以下核心技术难点:

  1. 跨平台音频处理:MP3 到 PCM 的格式转换
  2. 实时流式通信:WebSocket 双向音频流传输
  3. 云端协同架构:合理分配计算任务,平衡性能与成本
  4. 鲁棒性设计:自动重连、异常处理、超时控制

系统目前已能稳定运行,支持自然的多轮对话、情感识别和语音交互,后续会扩展机器人动作控制。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐