最近看到不少朋友在折腾桌面级语音助手,圈子里热度最高的一个名字就是 Microduck。这玩意说白了就是一个开源机器鸭项目,把大模型塞进一个鸭子造型的硬件里,能对话、能执行简单任务,整条链路从硬件原理图到上位机软件全部开放。我花了两个周末把整套方案复刻了一遍,中间踩了不少坑,这篇文章把完整过程、关键决策和问题排查全部记录下来,希望能给正准备动手的朋友省点时间。
Microduck 本质上解决的是“如何用低成本硬件跑通大模型语音交互”的问题。它不像那些动辄几千块的开发套件,主控用 ESP32-S3 级别就能带动,麦克风阵列、功放、喇叭都有现成模块,软件侧把唤醒词、语音识别、大模型调用、语音合成串成一条流水线。适合三类人来玩:一是想入门嵌入式 AI 的学生,二是做智能家居语音节点的开发者,三是单纯喜欢折腾硬件的极客。
1. 项目整体设计与思路拆解
1.1 为什么选这条技术路线
先聊选型。Microduck 复刻的第一道选择题就是“要不要上 Linux 级主控”。市面上大多数桌面语音助手项目直接上了树莓派或者 RK3588,性能确实强,但成本和体积都压不下来。Microduck 团队选了 ESP32-S3,这很关键。S3 自带向量加速指令,跑 300M 参数以内的量化模型勉强够用,更重要的是它支持原生 USB OTG,可以直接外接 USB 麦克风阵列,省掉 I2S 编码的麻烦。
有人会问,边缘端跑不动大模型怎么办?这就是 Microduck 设计的精髓——混合推理。唤醒词在本地跑,语音识别通过 WebSocket 抛给云端 Whisper 服务,大模型走 OpenAI 兼容接口,TTS 也在云端合成。本地只做音频采集、端点检测和结果播放。这样硬件成本压到百元级,同时体验不打折。复刻的时候我一度想全部本地化,实测下来 S3 跑量化后的 TinyLlama 太吃力,一轮对话要等三四十秒,果断放弃。
另一个让人印象深刻的设计是电源管理。鸭子底座里塞了一颗 IP5306 电源管理芯片,自带充放电管理和电量检测,配合 18650 电池可以连续工作四到五小时。这个芯片在移动电源里非常成熟,用在这里完全够用,避开了专门做 PMIC 的复杂度。复刻时不需要自己设计充电电路,省下的精力全用在软件调试上。
1.2 软硬件分层逻辑
Microduck 的代码结构很清晰,整体分三层。最底层是硬件抽象层,封装了 LED 驱动、按键扫描、音频编解码和 Wi-Fi 管理;中间层是状态机,管理空闲、唤醒、聆听、思考、播报五个状态;最上层是业务逻辑,负责 MQTT 指令解析和远程控制。这种分层方式对后续扩展特别友好。
音频链路是整个项目最关键的部分。Microduck 使用 ReSpeaker 双麦克风阵列做声源定位和波束成形,配合 ESP32-S3 自带的 ADC 做唤醒词检测。这里有个坑必须提前说:S3 的 ADC 采样率上限约 20kHz,而麦克风阵列需要 16kHz 的采样率,两者容易冲突。解决方法是把麦克风阵列通过 USB 接口接入,走 UAC(USB Audio Class)协议,这样 S3 只当音频终端,不需要直接处理模拟信号。
软件侧用 ESP-IDF 作为开发框架,音频处理节点用 ESP-ADF 的 pipeline 模型组织。每个节点负责一个环节,比如audio_hal节点负责采集、wakenet节点负责唤醒词识别、media_router节点负责把音频流转发到网络。调试时可以只跑单节点,定位问题很快。第一次接触 ESP-ADF 的朋友别急着改代码,先跑通仓库里自带的 pipeline 示例,理解数据流的方向和缓冲区的用法,后面改起来会顺手很多。
2. 核心细节解析与实操要点
2.1 硬件选型与 BOM 清单
复刻的第一步是备料。Microduck 原版的 BOM 表很精简,但有些料不太好买,比如定制的鸭子外壳和专用的 FPC 排线。我实际测试下来,可以用标准模块做替代,效果差别不大。下面这份清单是我验证过的替代方案,价格是淘宝散件价。
| 模块 | 原版型号 | 替代方案 | 参考价格 | 备注 |
|---|---|---|---|---|
| 主控 | ESP32-S3-WROOM-1 | 合宙 ESP32-S3 开发板 | 约 25 元 | 选 8MB Flash 版本 |
| 麦克风阵列 | ReSpeaker 双麦 | INMP441 双麦模块 | 约 40 元 | 需要自己做同步 |
| 功放 | MAX98357A | MAX98357A 模块 | 约 8 元 | 3W 输出足够 |
| 喇叭 | 3W 全频扬声器 | 4Ω/3W 内磁喇叭 | 约 6 元 | 尺寸选 36mm 以下 |
| 电源芯片 | IP5306 | IP5306 成品板 | 约 10 元 | 带电量显示 |
| 电池 | 18650 电池 | 18650 带保护板 | 约 15 元 | 容量 2000mAh 以上 |
| 外壳 | 定制 3D 打印 | 现有鸭子玩具壳改造 | 约 20 元 | 内部空间要够 |
这几个替代方案里,风险点主要在麦克风阵列。INMP441 是单声道 I2S 输出,想做双麦波束成形必须用两个模块拼,还得保证时钟同步。我的建议是直接买现成的双麦阵列板,比如 Waveshare 的 Dual INMP441 板,省去飞线的麻烦。如果你只需要单麦方案,INMP441 单模块就够了,效果在安静环境下差距不大。
焊接的时候有两点不能马虎。第一,ESP32-S3 的 GPIO4 和 GPIO5 是 USB 引脚,如果用来接其他外设,会干扰烧录。最好把这些引脚空出来,需要外接时用飞线。第二,MAX98357A 的 SD 引脚默认拉低是静音模式,必须通过 10kΩ 电阻上拉至高电平才能出声音。这个细节不仔细看手册根本发现不了,我一开始没接电阻,折腾了半天没声音,最后翻芯片数据手册才发现问题。
2.2 唤醒词训练的关键参数
Microduck 默认唤醒词是“Hi, Microduck”,如果你不想用默认词,就需要自己训练。这个训练流程是仓库里最容易被忽视的模块,其实花点时间搞清楚它,后面体验会好很多。
唤醒词训练基于 ESP-SR 的 WakeNet 模型,底层是 2D-CNN + FSMN 结构,输入是 16kHz、16bit、单声道的音频特征。训练工具链用的是 ESP-BSP 里内置的wakenet_train脚本,依赖 Python 3.8 和 Pytorch 1.12。安装依赖的时候容易踩版本坑,建议用 conda 建一个独立环境:
conda create -n esp_sr python=3.8 conda activate esp_sr pip install torch==1.12.1+cpu torchvision==0.13.1+cpu -f https://download.pytorch.org/whl/torch_stable.html数据准备是训练的核心。WakeNet 要求正样本(唤醒词)至少 300 条,负样本至少 1000 条,每条时长 1 秒。正样本最好用真实设备录音,不要用 TTS 合成音,因为实际使用中麦克风的频率响应和噪声环境都会影响识别效果。我录了 350 条正样本,分布在客厅、书房、厨房三个环境,背景噪声各不同。负样本则用了一部分通用的语音命令集(比如 Speech Commands 里的随机词),加一部分环境噪声,比如电视声、键盘声、走路声。
训练命令参考如下:
cd esp-sr-train python train.py \ --wakeword "hello_microduck" \ --positive_dir ./dataset/positive \ --negative_dir ./dataset/negative \ --epoch 50 \ --batch_size 32 \ --lr 0.001训练大约需要两到三小时(看 CPU 性能),每个 epoch 结束后会生成一个 checkpoint。重点看验证集上的false_accept(误唤醒率)和false_reject(漏唤醒率)两个指标。我实验下来,误唤醒率在 1% 到 3% 之间比较合适,太低说明阈值太严格,容易漏唤醒;太高则会出现频繁被打断的情况。训练完成后,仓库会导出一个.tflite文件,放到固件里编译即可。
2.3 音频链路的采样率与缓冲配置
音频链路是整个系统最影响体验的部分,配置不对会直接导致声音变调、响应迟钝或者爆音。Microduck 的音频流路径是:麦克风 → I2S/USB → 缓冲 → WakeNet 检测 → 触发后音频上传 → 远端 ASR → 大模型 → TTS → 喇叭播放。
先说采样率。WakeNet 支持 16kHz,而 ASR 端(比如 Whisper 的 API)也要求 16kHz,所以采样率统一设 16kHz 没问题。麻烦的是 I2S 的位深度。INMP441 输出 24bit 数据,但 ESP32-S3 的 I2S DMA 缓冲默认是 32bit 对齐,这中间有个转换。ESP-ADF 的管道节点会自动做截断,但前提是你在初始化时正确设置了bits_per_sample。
缓冲区大小也很关键,Microduck 原版配置是 2400 字节的 DMA buffer,换算下来约 30ms 音频。实测这个值太小了,Wi-Fi 信号差一点就会出现丢包。我最终调到 4800 字节(60ms),延迟增加不明显,但稳定性提升明显。如果你是首次复刻,建议直接把缓冲区调大一倍,后期再优化。
另外,电源噪声是最隐蔽的杀手。如果喇叭播放时麦克风采集到明显的电流声,大概率是电源纹波进入音频模拟域。解决办法有两条路:一是把模拟地和数字地在主控板处单点连接,二是给功放单独加一颗 100µF 的钽电容滤波。实测单点接地效果最明显,但布线时要注意别把模拟地环路绕大。
3. 实操过程与核心环节实现
3.1 固件编译环境搭建
Microduck 的固件是标准的 ESP-IDF 项目,编译前需要先装好工具链。官方文档推荐用 v5.1.2 版本的 ESP-IDF,这个版本比较稳定,ST 的 USB Host 驱动和 UAC 驱动都能正常编译。如果直接用 master 分支,大概率会遇到 API 变动导致的编译错误。
安装步骤我记录一下,少走弯路。先克隆 ESP-IDF:
mkdir -p ~/esp cd ~/esp git clone -b v5.1.2 --recursive https://github.com/espressif/esp-idf.git然后执行安装脚本,官方推荐用install.sh esp32s3只装 S3 的工具链,省时间:
cd ~/esp/esp-idf ./install.sh esp32s3 source export.sh到这里环境就准备好了。接着克隆 Microduck 固件仓库,注意这里有一个常见的坑:仓库是 submodule 管理的,直接git clone拿不到子模块代码,必须加--recursive参数。否则编译时提示缺少components/esp-adf之类的模块,还要回头再拉子模块,很烦。
git clone --recursive https://github.com/open-duck/microduck-firmware.git cd microduck-firmware idf.py set-target esp32s3 idf.py menuconfigmenuconfig里需要关注几个配置项。第一,Wi-Fi 的 SSID 和密码是在这里配置的,原版把网络配置做成了 NVS 存储,编译时需要在Component config → Microduck Configuration里填入默认网络信息。第二,如果不打算用 MQTT,可以关闭Enable MQTT选项,减少内存占用。第三,如果你用的是单麦克风而非双麦阵列,需要在Audio Pipeline → Mic Type里选择Single INMP441。
3.2 云端服务接入方式
Microduck 复刻里比较麻烦的是云端的服务配置,主要是三块:ASR(语音识别)、LLM(大模型)、TTS(语音合成)。原版默认实现是接 OpenAI 的 Whisper 和 GPT-4o-mini,以及 Azure TTS。这个方案效果确实好,但有两个问题——网络延迟高、需要科学支付方式。
实测下来,国内可用的替换方案很多。ASR 可以用百度短语音识别,注册账号后免费额度够日常测试;LLM 可以用阿里云的通义千问或者 Moonshot 的 Kimi,都提供 OpenAI 兼容接口;TTS 可以用微软 Edge 的免费接口,或者腾讯云的语音合成。
以通义千问为例,在main/app_config.h里需要配置三个宏:
#define LLM_API_KEY "sk-xxxxx" #define LLM_API_URL "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation" #define LLM_MODEL_NAME "qwen-turbo-latest"这里有一个细节值得注意。不同厂商的 OpenAI 兼容接口,model字段和messages格式基本一致,但响应里choices[0].message.content的路径可能不同。Microduck 的代码里有针对不同厂商的解析分支,配置时确认你选的模型服务与代码里的parse_response函数匹配。如果你用的是自建 Ollama 服务,还需要额外改一下 base URL 和端口号。
TTS 我直接用的火山引擎,原因很简单——中文发音自然、延迟低、SDK 文档清楚。在代码里,TTS 的处理逻辑是:拿到大模型的文本回复后,先通过 WebSocket 发给 TTS 服务,TTS 返回音频流,然后推入播放队列。这里有个容易踩坑的点:如果 TTS 音频返回的格式是 MP3,而 ESP32-S3 的音频解码器只内置了 AAC 和 WAV 解码,就会播不出声。解决办法是在服务端把编码转成 WAV,或者在固件里集成 libhelix-mp3 解码库。后者改动较大,我偷了个懒,直接用 FFmpeg 在服务端转成 WAV:
ffmpeg -i tts_output.mp3 -ar 16000 -ac 1 -f s16le output.pcm3.3 首次上电与基础功能验证
硬件焊接完成、固件烧录之后,先别急着连 Wi-Fi,用 USB 线供电,打开串口监视器看日志。这一步能筛掉大半硬件问题。串口波特率是 115200,在idf.py monitor下直接看输出。
正常启动的日志应该是这样的顺序:
- ESP-ROM 启动信息
- 分区表加载
- NVS 初始化,读取 Wi-Fi 配置
- 音频硬件初始化,打印
I2S initialized和Codec initialized - WakeNet 模型加载,打印
WakeNet v5.3 model loaded - Wi-Fi 连接成功,打印 IP 地址
如果卡在第四步,说明 I2S 初始化失败,大概率是引脚定义不匹配。如果卡在第五步,说明 WakeNet 模型文件没有正确推送到 Flash 分区,需要检查partitions.csv里的 model 分区地址。
功能验证也按顺序来。第一步测唤醒,对着设备说“Hi, Microduck”,看串口是否打印wakeword detected和声源方向。第二步测录音,唤醒后说一句话,看日志里上传的音频时长是否正确。第三步测试 ASR,看返回的文本是否符合。第四步测大模型,看回复内容是否合理。最后一步测 TTS,确认喇叭有声音输出。按这个流程排查,每一步只解决一个变量,问题定位会很快。
如果你发现唤醒后经常超时没有反应,不一定是网络问题,很可能是录进去的音频静音段太多。Microduck 里有一个语音活动检测(VAD)参数,默认起始阈值是 30ms,终止阈值是 500ms。如果环境噪声大,VAD 会过早结束录音,导致你说的话只录进去一半。可以适当调高终止阈值,比如调到 800ms,代价是响应时间会变慢一点点。
4. 常见问题与排查技巧实录
4.1 编译阶段的高频报错
复刻过程中,编译阶段的问题其实比运行时还多,主要是环境差异和版本不一致引起的。把我实际遇到的几个问题整理成表格,方便对照排查:
| 报错信息 | 原因分析 | 解决办法 |
|---|---|---|
fatal error: driver/adc.h: No such file or directory | ESP-IDF 版本太新,ADC 驱动头文件路径变了 | 切换到 v5.1.2 版本 |
undefined reference to esp_audio_hal | 子模块没拉取完整 | git submodule update --init --recursive |
C++ exception: std::bad_alloc | 内存不足,Flash 分区配置太小 | 增大model分区到 4MB |
CONFIG_ESP_MAIN_TASK_STACK_SIZE太小 | 主任务栈溢出,服务运行时崩溃 | 在 menuconfig 里把栈大小调到 8192 |
Failed to mount SPIFFS | 分区格式不对 | 执行idf.py erase-flash后重新烧录 |
遇到过最坑的是编译时提示CONFIG_UAC_DRIVER未定义。原因是 UAC 驱动在 ESP-IDF 里默认没有启用,需要在menuconfig → Component config → USB Stack里打开Enable UAC Driver选项。如果你没用到 USB 麦克风阵列,可以关掉这个选项,省一点 Flash 空间。
4.2 设备唤醒后无响应或响应延迟严重
唤醒词识别成功、设备进入聆听状态,但后续的 ASR 和 LLM 交互没有反馈。这是我复刻时调试最久的问题。排查路径其实是逐段打日志看卡在哪。
先看唤醒后的串口日志,Microduck 在状态切换时会打印---> EVT_WAKEUP。如果日志停在这里,说明音频上传没有发起,需要检查网络连接是否正常。这里有个隐蔽逻辑——唤醒后,设备并不会立刻连 Wi-Fi,而是直接使用初始化时建立的 Wi-Fi 会话。如果初始化时连网失败,唤醒后也不会重试,需要重启设备才能恢复。
再看 ASR 的返回结果。日志里一般会打印识别文本。如果识别文本为空或者只有开头几个字,说明 VAD 判断有误,音频被截断。解决办法是把录音终止阈值调大,或者调整麦克风灵敏度。Microduck 的麦克风增益默认是 24dB,环境安静时可以提高到 30dB,环境嘈杂时降到 18dB 反而识别率更高。
延迟的问题则要看具体是哪个环节耗时最长。我用串口日志里的时间戳粗略统计过,默认配置下,唤醒耗时约 200ms,录音约 1.5s(用户说话时长),ASR 约 800ms,LLM 流式返回约 1s,TTS 加上播放约 1.2s。总延迟约 5s,属于正常范围。如果你发现 LLM 部分耗时异常高,可能是你选的模型厂商接口没有流式返回,Microduck 不支持非流式响应的多轮对话衔接,建议切换为支持 SSE 的模型服务。
4.3 离线可玩性增强:接入本地模型服务
虽然 Microduck 的设计初衷是云端推理,但我还是想折腾一下完整离线方案,毕竟家里 Wi-Fi 一旦故障,这个鸭子就只能当摆件了。离线方案的核心是把 ASR、LLM、TTS 全部搬到局域网内的 PC 或者小主机上。
ASR 用 whisper.cpp 编译的 CPU 版本,跑base模型,在 i7-1165G7 上大概 1.2 倍实时速度,够用。LLM 可以接 Ollama 跑 qwen2.5:3b,量化后的文件大约 2GB,对内存要求不高。TTS 用 piper,里面内置了中文女声模型,效果中规中矩,但胜在完全离线。
配置上,只需要把app_config.h里的 LLM 地址从云端改成局域网地址:
#define LLM_API_URL "http://192.168.1.100:11434/v1/chat/completions" #define LLM_MODEL_NAME "qwen2.5:3b"实测下来,离线模式下总响应延迟约 3 到 4 秒(含 TTS 播放),比云端还略快一点,因为减少了广域网传输的耗时。这个方案特别适合有隐私需求的场景,比如在家里做语音控制,不想把录音数据传到外部服务器。
5. 场景扩展与进阶玩法
5.1 做一台桌面效率助手
复刻完成后,Microduck 的默认功能其实比较单薄,只有对话和简单的天气查询。但如果结合 MQTT,它可以变成一个桌面信息中枢。我在桌子旁边放了一块小的墨水屏,通过 MQTT 接收 Microduck 推送的“日程提醒”“股票涨跌”“未读邮件数”,每天早上语音问一句“帮我看看今天都有什么会”,鸭子就会把信息同步到墨水屏上。
这块的逻辑不复杂。Microduck 收到语音指令后,通过大模型接口提取意图和槽位,生成结构化 JSON,然后统一推给 MQTT broker。墨水屏端订阅对应主题,就能实时刷新内容。这种组合比单独用智能音箱更灵活,因为显示信息是永久的,不会像语音播报那样说完就没了。
5.2 接入智能家居语音控制
再进一步,Microduck 可以用来做家庭语音控制的中枢网关。因为 ESP32-S3 自带 Wi-Fi 和 BLE,可以直接和米家、涂鸦的智能设备通信。最简单的接法是通过 MQTT 协议对接 Home Assistant,把 HA 的实体状态暴露成可调用的服务。
我的实际测试场景是:对鸭子说“把客厅灯亮度调到 50%”,语音指令经 ASR 转成文本,LLM 提取“客厅灯”“亮度”“50%”三个槽位,MQTT 发布到homeassistant/light/livingroom/set。HA 侧用自动化订阅这个主题,执行实际的调光操作。这个链路实现下来并不复杂,但体验非常好,有一种“赛博管家”的感觉。
不过要注意安全问题。MQTT broker 必须设置账号密码认证,不要用默认的匿名访问,因为语音控制设备一旦暴露到公网,攻击者可以直接通过 MQTT 控制家里的电器,后果很严重。另外建议把 Microduck 的按键功能设置成“静默模式”,防止意外唤醒时误触家电指令。
5.3 用 WebRTC 做音视频门铃
这个玩法稍微偏门一点,但值得一试。ESP32-S3 本身没法跑完整的 WebRTC 协议栈,但可以配合树莓派当信号中转。Microduck 采集的视频流(如果接摄像头模块的话)通过 UDP 发送到树莓派,树莓派上跑 Janus Gateway,把视频流转成 WebRTC,这样手机在任何地方都能看到摄像头画面。
语音方面也是同理。手机端发出的语音经过 Janus 转成 RTP 流,树莓派收到后通过串口转发给 Microduck 的功放播放。这个玩法对网络带宽要求较高,建议在有线网络下使用。我之前测试时,720p 视频的延迟大约 500ms,语音延迟约 300ms,基本能实现流畅对话。
5.4 多设备集群语音交互
最后提一个更进阶的方向,Microduck 之间可以组网做分布式语音交互。比如客厅一只、书房一只、卧室一只,任意一只设备听到语音指令后,可以通过 MQTT 把音频流转给最靠近用户的设备播放。这样用户从客厅走到书房时,对话可以不中断。
实现原理是基于声源定位和 RSSI 信号强度来判断用户位置,然后动态切换音频输出节点。这部分的代码改动量比较大,需要在每只鸭子上部署位置估计线程,并通过 MQTT 同步设备间状态。如果你只是想体验一下,可以用三只鸭子各放一个固定位置,人工指定“主设备”,效果也差不多。
6. 写在最后
这套 Microduck 复刻流程走下来,我最深的体会是:它并不是一个“装上就能用”的项目,而是给了你一张设计图和一盒积木,让你在不修改核心架构的前提下,根据自己的需求做大量自定义。从选一个合适的麦克风阵列,到调通云端大模型服务,再到设计一个离线语音节点,每一步都踩在嵌入式 AI 开发的核心知识点上。
如果你准备动手,我的建议是先别急着追求完美效果。第一版能把“唤醒 → 录音 → ASR → LLM → TTS → 播放”这条链路跑通,这个小目标已经意味着你掌握了从硬件到云端的全套技能。后续再逐步优化音质、响应速度和交互逻辑。踩坑也不可怕,串口日志就是你最好的调试工具,每一步都有迹可循。
最后分享一个我改进后的小技巧:在喇叭外壳四周贴一圈薄泡棉胶,能明显减少共振带来的杂音。这个改进不花一分钱,但音质提升是肉眼可见的。这是我对比过很多方案后觉得最实用的一招。