成果展示
| 第一次对话:Okay Nabu | 第二次对话:猴哥猴哥 |
|---|---|
特别提醒
尤其涉及到 AI 以及 大模型 的内容,一天一个变化,仅仅记录个人的实践过程以供思路参考。
如需实践,请尽可能提前了解最新的技术、查询最新的官方文档。
前言
随着科技的发展,家用对话模型其实已经比较普及,至今,普通的成品智慧型音箱以“米家生态”的小米、小爱;“百度旗下”的小度;阿里旗下的天猫精灵;“华为系列“也有几款(没了解过)这几个品牌为首组建的。
早在两三年前,我就想要一台自己的AI对话型音箱,但鉴于那时大模型其实并不算特别聪明,尤其是从API调用的回答令我不甚满意,耽搁至今~
方案
与其说方案,不如说需求,鉴于每人的需求不一,最终的方案可谓说是大相径庭。
在这一环节中,主要考虑:成品 or DIY ?
-
成品: 上述几家企业旗下的设备可以直接选择,金钱是一方面,另一方面也要考虑它们搭载的模型,也就是涉及它对话的准确度、响应时间等,最后再看颜值即可;
-
DIY: 这里面需要考虑的内容非常之多,我也是在这里面徘徊许久许久,在各方面需求之间无数次进行取舍,成本、颜值、性能、方案可行性等等,典型的既要又要。
选择
最终我选择了 DIY 。
致使我做这个决定最重要的因素就是:隐私。(PS:一颗折腾的心是其次)
家用设备,尤其是要进行云端交互的设备,我不敢把那 0.001% 的微弱可能交给别人,不敢保证平台或者云端不会过度收集我的信息,不能保证它在静默状态下不会后台监听并且上传数据进行分析。
所以我选择了比较麻烦的方式,自己搭建这套系统。
准备阶段
首先看自己有什么,有个大致的方向;其次看需要什么,对初步的方案进行评估;最后看细节,对每个细节制定具体方案。
有什么?
基于自身的情况,我有:
-
Ubuntu Server ( CPU i7-10700 、内存 32G 、闲置的Nvidia GTX 1060显卡随时可以待命)
-
HomeAssistant (ESXI 下的 2H2G 、 Sonoff Zigbee 3.0 USB Dongle Plus)
于是有了初步的方案:
-
唤醒词与语音识别(STT):SenseVoice 模型。部署在 Ubuntu 上 。
-
对话大脑(LLM):理解文字并生成回答。直接调用 DeepSeek 云端 API。虽然 1060 也能本地跑小模型,但云端 API 响应更快、智力更高,且能避开显存不足的麻烦。
-
语音合成(TTS):用 piper 模型,把 DeepSeek 的文字回答读出来。部署在 Ubuntu 上。
-
HomeAssistant 集成:在 HA 的“语音助手”设置里,把上面的 STT/TTS/LLM 串联起来。
需要什么?
一、怎么将声音传输到HomeAssistant 集成?
半成品方案:
- ReSpeaker Lite Voice + ESP32S3开发板: 板载了 XMOS XU316 芯片,自带硬件级的 回声消除(AEC) 和 降噪功能,双麦克风阵列。
成品方案:
-
Home Assistant Voice Preview Edition:官方出的成品,做工好,拾音和消回效果有保障,开箱即用。(一开始我买了一个,但是交货期需要4-8周,取消了)
-
PineVoice:PINE64 出的智能音箱。本身就是一个纯语音卫星,运行 microWakeWord,支持 Wyoming 协议。
二、怎么让声音输出?
由于我用的是半成品方案: ReSpeaker Lite Voice + ESP32S3开发板
因此,需要额外购买一个 4欧 5-10W 的喇叭单元,焊接在板子上即可。
三、框架(外观?)以及声学设计
这算是我个人的需求,裸着放固然符合理工科直男的审美,但是家里有老婆小孩,无奈还要设计个“狗窝”,不然说不定哪天就被扔垃圾桶了。
既然要设计外观,其实Github上有很多CAD图纸可以套用,稍微改下尺寸,然后找个3D打印店打印出来就能组装。
但是我不会改,而且我也想用自己的审美,自己组建一套。
外观设计:
准备把它设计成一个智能台灯,它可以进行AI对话,同时,它也是一台台灯而且听音乐。
-
画草图
由于不走3D打印的方案,自己组装亚力克,先将草图画出。
-
购买灯带:LED全彩RGBW灯条SK6812
ReSpeaker Lite 本身板载的是可编程 RGB 灯珠(本质也是 WS2812 系列), ESPHome 官方对 SK6812 的驱动支持也非常成熟,可以通过
neopixelbus或esp32_rmt_led_strip等组件轻松驱动。
声学设计:
出于Hi-Fi爱好者的坚持,音质希望有点保证,一开始是想让音质媲美HomePod,但是买不到HomePod拆机的喇叭和振膜,只能买其他品牌。写这个文章的时候组装还未完成,因此悬念留到最后。
但是声学设计这块真的把我难倒了!主要是喇叭的放置,由于主材料考虑亚克力板,而且台灯四周也要围起来不能敞开,所以喇叭只能向上发声或者向下发声。当然这个还不算难,最难的是喇叭背面必须是密闭腔体,这样声音的质感才不会很糟糕。这一部分足足折磨了我一个多星期。
借鉴了HomePod以及其他一些小音箱的设计,最终决定将喇叭装在一个圆管中,方向垂直向下,圆管底部与底座留30mm距离,底座正对着圆管中心,放置一个圆锥体,这样喇叭发出的声音可以被圆锥体分散到360度达到环绕效果。
喇叭的背面,是另一段圆管,内部可以填充吸音棉等材质,最后用一个圆形的盖子盖起来,这一部分使用亚历克专用胶水100%粘牢,确保不漏气。(类似于亚克力鱼缸的效果)
其他设计:
-
ReSpeaker Lite开发板位置: 在喇叭上方粘一块衬板(最好用家用桌角那种减震粘垫一下),开发板固定在这块衬板上;
-
电容触摸开关: ReSpeaker Lite 可以定义电容触摸开关,只需一根细导线连接铝箔纸,将铝箔纸贴在亚历克板的背面
-
亚克力材料: 强烈建议PMMA,相对更耐温,我是当台灯使用,灯带发热还是比较明显的;
-
四周亚克力板: 上下要留距离,上口散热用,下口喇叭声音出来,保证空气流通(也可以顶部开孔,但是我觉得台灯如果这样开孔很难看)
-
颜色设计:除了四周的亚克力板要用乳白色让内部的灯带均匀发光之外,其他板的颜色都可以尽情设计
第一阶段:前置基准测试
有了上述的方案,正式购买以及定制材料之前,必须先测试后端服务能不能跑通。
每个人的设备不一样,测试也不同,下面测试条件未使用GPU,纯CPU运行基准。这样更方便参考借鉴。
第一部分:服务部署
1. 准备工作目录与官方源码
通过 SSH 登录 Ubuntu Server,依次执行:
# 1. 创建总目录并进入,我喜欢放在/var/www目录下
mkdir -p /var/www/voice-pipeline
cd /var/www/voice-pipeline
# 2. 克隆 OHF-Voice 官方最新仓库
git clone https://github.com/OHF-Voice/wyoming-faster-whisper.git
# 3. 创建数据挂载目录
mkdir -p sensevoice-data kokoro-data ollama-data
2. 修改源码补全 FunASR 依赖 (关键排坑)
vim wyoming-faster-whisper/Dockerfile
向下翻找,找到带有 pip3 install 依赖列表的这行:
-e '.[zeroconf,transformers,sherpa,onnx-asr,qwen3-asr,hass]' \
按 i 键进入编辑模式,在方括号内末尾加上 ,funasr:
-e '.[zeroconf,transformers,sherpa,onnx-asr,qwen3-asr,hass,funasr]' \
到文件最末尾,最下面新起一行,加上这句强制安装依赖的代码:
RUN /usr/src/.venv/bin/pip install --no-cache-dir kaldi-native-fbank
按 Esc 键,输入 :wq 并回车保存退出。
3. 编写核心 Docker Compose 文件
vim compose.yml
按 i 键进入编辑模式,粘贴以下配置:
services:
# 1. 听觉引擎:SenseVoice (支持中文极速识别与 VAD 端点检测)
sensevoice-stt:
build:
context: ./wyoming-faster-whisper
image: ailight-wyoming-sensevoice:local
container_name: wyoming-sensevoice
command:
- --stt-library
- funasr
- --language
- zh-CN
- --vad-endpointing
- "0.6"
- --uri
- tcp://0.0.0.0:10300
- --data-dir
- /data
- --download-dir
- /data
volumes:
- ./sensevoice-data:/data
environment:
- TZ=Asia/Shanghai
restart: unless-stopped
ports:
- "10300:10300"
# 2.1 喉咙引擎:Kokoro TTS (本地发言)
kokoro-fastapi-zh:
image: ghcr.io/hsiang-han/kokoro-fastapi-zh-cpu:latest
container_name: kokoro-fastapi-zh
environment:
- TZ=Asia/Shanghai
- DEFAULT_VOICE=zf_094
ports:
- "8880:8880"
volumes:
- ./kokoro-data/models:/app/api/src/models
- ./kokoro-data/voices:/app/api/src/voices
restart: unless-stopped
# 2.2 Azure-TTS (备选方案,填入自己的region和apikey)
wyoming-proxy-azure-tts:
image: ghcr.io/hugobloem/wyoming-microsoft-tts-noha
container_name: wyoming-azure-tts
ports:
- "10200:10200"
restart: unless-stopped
environment:
AZURE_SERVICE_REGION: <region>
AZURE_SUBSCRIPTION_KEY: <api key>
command:
- --uri=tcp://0.0.0.0:10200
- --voice=zh-CN-XiaoXiaoNeural
# 3. 记忆向量计算层:Ollama
ollama-embedding:
image: ollama/ollama:latest
container_name: voice-ollama
volumes:
- ./ollama-data:/root/.ollama
restart: unless-stopped
ports:
- "11434:11434"
按 Esc 键,输入 :wq 保存退出。
4. 构建、启动与模型拉取
# 1. 构建镜像并启动(需等待 10~15 分钟下载模型与环境)
docker compose up -d --build
# 2. 启动完成后,让 Ollama 拉取轻量级中文特征向量模型
docker exec -it voice-ollama ollama pull bge-m3
# 3. 验证 FunASR 是否成功编译进镜像(有版本号输出即为成功)
docker exec wyoming-sensevoice /usr/src/.venv/bin/pip show funasr
+.cloudflarestorage.com 和 +.ollama.com 添加在fake-ip-filter中。第二部分:Home Assistant 感知与 HACS 准备
进入 Home Assistant 的网页控制台。
1. 建立环境感知实体
-
季节:进入 配置 -> 设备与服务 -> 添加集成,搜索
Season(季节) 并添加。 -
天气:确保你的 HA 首页有天气卡片,并记下实体 ID。
推荐个天气集成:GitHub - hasscc/tianqi: 天气预报HomeAssistant集成,支持15天及逐小时预报、各种生活指数,兼容彩云卡片,无需申请appkey · GitHub服务器域填写中国天气网的域名:
weather.com.cn
2. 下载必备 HACS 集成
在左侧边栏打开 HACS -> 集成:
-
安装
ha-ai-memory。在HACS中添加自定义存储库:
https://github.com/Riscue/ha-ai-memory, 选择Integration添加 -
安装
Homeassistant-Deepseek-Integration在HACS中添加自定义存储库:
https://github.com/leofleischmann/Homeassistant-Deepseek-Integration, 选择Integration添加 -
安装 Kokoro-TTS
在HACS中添加自定义存储库:
https://github.com/beecho01/Kokoro-TTS,选择Integration添加
分别下载这三个集成,然后重启HomeAssistant。
第三部分:组装神经网络
1. 接入 Ubuntu 的本地视听器官
-
接入 SenseVoice:
进入 配置 -> 设备与服务 -> 添加集成,搜索
Wyoming Protocol,主机填服务端的IP地址,端口填10300,提交。 -
接入 Kokoro:
进入 配置 -> 设备与服务 -> 添加集成,搜索
Kokoro TTS,Base URL填:http://服务端ip:8880,接着继续,Persona选择zf_开头的中文发音,Format选择wav或者pcm,提交。在地区选择中,不需要特地指定Mainland China,默认所有语言即可。
由于Kokoro是纯本地运行,对比 Azure 语音服务 的文本转语音天花板级别还是有点差距,部署参考:GitHub - hugobloem/wyoming-microsoft-tts: Wyoming protocol server for Microsoft Azure text-to-speech · GitHub,需要提前申请API,每月50万字符的额度,完全足够。网上有教程。
2. 配置专属 DeepSeek 云端大脑
点击 添加集成,搜索刚安装的 DeepSeek Conversation。
-
API Key:填入你的真实密钥。
-
Base URL:填入
https://api.deepseek.com -
Model:选择或手动输入
deepseek-flash
添加后,点击 DeepSeek Conversation 的 选项 (Options):
- System Prompt指令 (系统提示词):粘贴以下终极模板:
### 这是我的模版示例:
你是家庭语音管家。你是一位幽默风趣、说话接地气的老友型大管家。
请使用极度口语化、松弛、自然的中文短句回答,多用逗号控制停顿感,适合语音朗读。
【环境信息】
当前时间:{{ now().strftime('%Y年%m月%d日 %H:%M') }}
当前季节:{{ states('sensor.ji_jie') }}
本地天气:{{ states('weather.suzhou') }}
当前温度:{{ states('sensor.suzhou_temperature') }}度
【性格与表达风格】
1. 机敏幽默,说话带点温度和轻松感,偶尔能开句无伤大雅的小玩笑,但做事靠谱利落。
2. 说话言简意赅,每次回答严格控制在1到3句话以内(20到50字最佳),绝不长篇大论。
3. 严格禁止客服腔和机器术语:严禁说“抱歉”、“好的呢”、“作为人工智能”、“系统”、“数据库”。
4. 绝对不要输出任何Markdown格式、星号、冒号、列表或项目符号,纯口语输出。
【记忆与建议核心铁律(极其严格)】
1. 悄悄记录:当用户提到喜好、忌口、习惯时,后台静默调用 add_memory,回答时用生活化短句顺手确认即可(如“妥了,俺老孙记下了”),不要像复读机一样一字不差地复述。
2. 检索未命中的静默原则:当用户询问建议(如吃什么、干什么、穿什么)时,你在后台调用 search_memory。
- 搜到了喜好:自然结合用户的习惯给出量身定制的建议。
- 没搜到任何记忆(重要):绝对禁止说“数据没有记录”、“我不知道你的喜好”、“请你告诉我”等废话!此时直接基于当前苏州的天气、温度、时间与生活常识,自信、主动地给出一两个诱人、接地气的具体建议。
【典型情境示范】
- 用户:“今天晚上吃点啥?”
(未查到饮食记忆,苏州气温较低/下雨时)
回答:“这天阴冷阴冷的,要我说,出门整碗热气腾腾的苏式奥灶面,或者来锅羊肉暖暖身子,绝对舒坦,你看咋样?”
- 用户:“我特别讨厌吃香菜。”
回答:“收到,以后凡是沾香菜的,一律替你轰走!”
- Home Assistant API:勾选允许控制 Home Assistant 设备(激活记忆读写和家居控制)。
3. 挂载本地智能记忆层
点击 添加集成,搜索 AI Memory。
配置以下参数配置,将计算压力转移给服务端:
-
Maximum number of memories(记忆条数上限): 设置500左右就行,不需要太高
-
Embedding Provider: 选择
Ollama -
BaseURL: 填
http://服务端IP:11434 -
Model: 只有
bge-m3
接着回到之前 DeepSeek Conversation 的 选项 (Options),Home Assistant API 下面增加了一个 Memory Management ,勾选保存。
第四部分:页面端对话测试
这一步也仅限于测试 HomeAssistant 到 DeepSeek,然后 DeepSeek 返回到 HomeAssistant 的链路,头部的 “麦克风输入 到 HomeAssistant(STT)” 和 “尾部的 HomeAssistant 到 扬声器(TSS)”,由于没有麦克风和扬声器,无法测试。
打开HomeAssistant设置,打开 语音助手 - 添加助手 :
正常可以进行对话就算成功。
另外,也可以根据三个docker容器的运行状态判断是否都正常运行了,如果没有报错,而且HomeAssistant也都能正常连接,那么直接开搞!
tts.speak,实体选择 kokoro,播放器实体选择你的扬声器,执行动作即可测试。材料购买
购买对应的材料:
-
喇叭(振膜可选,如果能密封的话建议安装,低音会好很多)
-
ReSpeaker Lite + ESP32S3 开发板
第二阶段:HomeAssistant 家庭控制方案
第一步:先刷 XMOS 的 I2S 固件
出厂时,XMOS 芯片可能烧录的是 USB 声卡固件或者出厂测试固件。
如果不通过 dfu-util 把 xmos_firmwares 目录下的专用 I2S 固件刷进 XMOS 芯片,它就不会通过标准 I2S 总线把处理好的干净音频吐给 ESP32-S3。也就是说,即使你在 ESP32-S3 上把固件烧得再完美,如果 XMOS 没有刷成 I2S 模式,ESP32-S3 也听不到任何声音(或者喇叭放不出声)。
下载地址: https://github.com/respeaker/reSpeaker_Lite/tree/master/xmos_firmwares
下载:respeaker_lite_i2s_dfu_firmware_48k_v1.1.0_ch0-asr_ch1-mww.bin
同目录下有 xmos_firmwares/dfu_guide.md 刷机教程,可以进行查看。
先把 Type-C 数据线插在音频底板上的 XMOS 接口(注意:不是插在 ESP32 小板上)。
以macOS为例:
# 1.安装homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 2.安装dfu-util
brew install dfu-util
# 3.确认Respeaker Lite有没有被检测到
dfu-util -l
# 4.刷固件
dfu-util -R -e -a 1 -D /path/to/dfu_firmware.bin
# 5.设备重启
第二步:再刷 ESPHome 固件到 ESP32-S3
用 Type-C 数据线将开发板连接到电脑(确保线材支持数据传输)。
使用 Chrome 或 Edge 浏览器打开官方刷机台:ESPHome Web 。
点击 CONNECT,在浏览器弹出的串口选择窗口中选中你的设备(通常显示为 USB JTAG/serial debug unit 或 CP210x),点击连接。
在弹出的菜单中选择 Prepare for first use(准备首次使用)。浏览器会自动从云端拉取基础版固件并烧录进 ESP32-S3 芯片中。
烧录度条走完后,网页会直接弹出一个配置网络的界面。在这里输入 Wi-Fi 账号和密码,点击保存。此时,板子已经成功接入家庭局域网。
第三步:局域网发现与认领
板子连上 Wi-Fi 后,可以直接把板子拔下来,插在普通的手机充电头上(或者继续插在电脑上供电即可)。
打开 Home Assistant,进入 设置 → 插件 → 插件商店,搜索 ESPHome,点击安装,安装完成后启动。
然后进入 ESPHome 仪表盘,HA 的局域网扫描机制会立刻发现它,并在页面顶端提示:“发现新设备 (ESPHome)”。
点击卡片上的 配置(或认领 Adopt)进行添加,左侧 ESPHome 插件的仪表盘里生成对应的设备卡片。
第四步:写入配置
点击设备卡片上的 EDIT,删除里面自动生成的所有内容,把我们的 YAML 粘贴进去。(下方为我的完整配置,灯带和触摸电容开关的相关配置可以自行调整或者删除)
substitutions:
name: "ai-light"
friendly_name: "AI-Light"
# 灯珠数量:按你实际剪裁的灯珠数填写
led_count: "49"
# 电容触摸阈值(ESP32-S3 触摸时数值变大,大于此值触发;首次可开启 setup_mode: true 看日志微调)
touch_threshold: "42000"
# 回答完毕后灯光慢慢熄灭的渐变时长
fade_out_time: "3s"
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2025.12.0
compile_process_limit: 1
project:
name: formatbce.Respeaker Lite Satellite
version: "ailight-v3"
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 8MB
cpu_frequency: 240MHz
framework:
type: esp-idf
version: recommended
sdkconfig_options:
CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB: "y"
CONFIG_SPIRAM_RODATA: "y"
CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y"
# 引入社区完整基础配置(音频、媒体播放、MicroWakeWord、定时器等)
packages:
respeaker-satellite: github://formatBCE/Respeaker-Lite-ESPHome-integration/config/common/respeaker-satellite-base.yaml@main
wifi:
ssid: "<wifi ssid>"
password: "<wifi密码>"
ap:
ssid: "ESPHome-AILight"
password: "<自定义热点ap密码>"
captive_portal:
api:
encryption:
ota:
platform: esphome
password: <自定义ota密码>
# 如果有自训练的唤醒词,在这边添加
#micro_wake_word:
# models:
# - model: models/xiaoling/小聆小聆.json
# id: wake_word_xiaoling
# - model: models/hougehouge/hougehouge.json
# id: wake_word_hougehouge
# ==================== 自定义部分 ====================
# 1. 全局变量:记住唤醒前台灯原本的开关、亮度与颜色状态,以及对话阶段标志
globals:
- id: light_was_on
type: bool
restore_value: no
initial_value: 'false'
- id: previous_brightness
type: float
restore_value: no
initial_value: '0.75'
- id: previous_red
type: float
restore_value: no
initial_value: '1.0'
- id: previous_green
type: float
restore_value: no
initial_value: '0.6'
- id: previous_blue
type: float
restore_value: no
initial_value: '0.2'
- id: previous_white
type: float
restore_value: no
initial_value: '0.7'
# 标记当前是否正处于语音交互中(防止单次对话中重复触发 on_listening 覆盖唤醒前的台灯状态)
- id: in_conversation
type: bool
restore_value: no
initial_value: 'false'
# 标记本轮对话是否产生了 TTS 语音输出(用于在 on_end 中等待扬声器播报完毕)
- id: has_tts_response
type: bool
restore_value: no
initial_value: 'false'
# 2. 释放远程包占用的 GPIO4 (mute_toggle),替换为不占引脚的虚拟开关以保证底层脚本兼容
switch:
- id: !remove mute_toggle
- platform: template
id: mute_toggle
internal: true
optimistic: true
# 3. 灯带配置(esp-idf 必须使用 esp32_rmt_led_strip,且与板载灯平分 192 个 RMT 符号)
light:
# 缩减远程包板载单颗灯的 RMT 符号占用(从 192 改为 96),腾出另一半 RMT 通道给外接灯带
- id: !extend led_internal
rmt_symbols: 96
# 外接 SK6812 RGBW 灯带(接在 D2 / GPIO3)
- platform: esp32_rmt_led_strip
id: desk_light
name: "管家台灯"
pin:
number: GPIO3 # D2 引脚
ignore_strapping_warning: true
num_leds: ${led_count}
chipset: SK6812 # 若为普通三通道 WS2812B,可改为 WS2812 并改 channel_colors: GRB
channel_colors: GRBW
rmt_symbols: 96 # 与 led_internal (96) 合计 192,契合 ESP32-S3 RMT 上限
default_transition_length: 0.4s # 默认渐变过渡时长(与 effect 配合时自动生效)
restore_mode: ALWAYS_OFF # 上电默认关闭
effects:
- pulse:
name: "缓慢呼吸"
transition_length: 1.0s
update_interval: 1.0s
min_brightness: 25%
max_brightness: 85%
- pulse:
name: "快速闪烁"
transition_length: 0.25s
update_interval: 0.3s
min_brightness: 20%
max_brightness: 100%
# 4. 顶盖电容触摸(D3 / GPIO4):平时直接开关台灯;若在对话中触摸则立即停止对话并恢复灯光
# 注:ESP32-S3 必须配置 0.25ms 测量周期,否则默认 8ms 会导致触摸失灵
esp32_touch:
setup_mode: false # 首次调试阈值时可临时改为 true,在日志查看触摸前后的数值
measurement_duration: 0.25ms
sleep_duration: 0.5ms
binary_sensor:
# 移除远程包默认绑在 GPIO3 上的物理按键,释放 GPIO3 给灯带,并将 user_button 映射到 GPIO4 电容触摸
- id: !remove user_button
- platform: esp32_touch
id: user_button
name: "触摸开关"
pin: GPIO4 # D3 引脚
threshold: ${touch_threshold}
on_press:
- if:
condition:
# 若当前正在进行语音对话或正在播报回答,轻触可随时打断并恢复台灯原状
lambda: return id(in_conversation) || id(va).is_running() || id(external_media_player).state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING;
then:
- lambda: id(has_tts_response) = false;
- if:
condition:
media_player.is_announcing:
id: external_media_player
then:
- media_player.stop:
id: external_media_player
announcement: true
- if:
condition:
voice_assistant.is_running:
then:
- voice_assistant.stop:
- script.execute: restore_desk_light
else:
# 平时待机状态下,轻触直接开关台灯
- if:
condition:
light.is_on: desk_light
then:
# 关灯前保存当前照明亮度与颜色(方便下次触摸开灯时恢复)
- lambda: |-
id(light_was_on) = false;
id(previous_brightness) = id(desk_light).remote_values.get_brightness();
id(previous_red) = id(desk_light).remote_values.get_red();
id(previous_green) = id(desk_light).remote_values.get_green();
id(previous_blue) = id(desk_light).remote_values.get_blue();
id(previous_white) = id(desk_light).remote_values.get_white();
- light.turn_off:
id: desk_light
transition_length: 0.5s
else:
# 开灯时恢复台灯照明的暖白/上次颜色,避免恢复成语音助手的蓝青色
- lambda: id(light_was_on) = true;
- light.turn_on:
id: desk_light
brightness: !lambda return id(previous_brightness);
red: !lambda return id(previous_red);
green: !lambda return id(previous_green);
blue: !lambda return id(previous_blue);
white: !lambda return id(previous_white);
effect: "None"
# 5. 灯光状态切换与恢复脚本
script:
# 切换到语音回答(缓慢呼吸)阶段
- id: enter_replying_state
mode: single
then:
- if:
condition:
lambda: return !id(has_tts_response);
then:
- lambda: |-
id(has_tts_response) = true;
id(voice_assistant_phase) = ${voice_assist_replying_phase_id};
- script.execute: control_leds
- light.turn_on:
id: desk_light
brightness: 85%
red: 0%
green: 90%
blue: 65%
white: 0%
effect: "缓慢呼吸"
# 对话结束:将台灯恢复到唤醒前的状态(原本关着则慢慢熄灭,原本开着则恢复照明颜色与亮度)
- id: restore_desk_light
mode: restart
then:
- lambda: |-
id(in_conversation) = false;
id(has_tts_response) = false;
id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
- script.execute: control_leds
- if:
condition:
lambda: return id(light_was_on);
then:
# 唤醒前台灯原本是开着的 → 先清除呼吸灯效,再以 1.2s 渐变恢复唤醒前的照明颜色与亮度
- light.turn_on:
id: desk_light
effect: "None"
- light.turn_on:
id: desk_light
transition_length: 1.2s
brightness: !lambda return id(previous_brightness);
red: !lambda return id(previous_red);
green: !lambda return id(previous_green);
blue: !lambda return id(previous_blue);
white: !lambda return id(previous_white);
else:
# 唤醒前台灯原本是关着的 → 按 fade_out_time (1.5s) 慢慢平滑熄灭
- light.turn_off:
id: desk_light
transition_length: ${fade_out_time}
# 6. 语音状态与灯光联动(字典组件自动与远程包合并,无需且不能写 id)
voice_assistant:
# 阶段1:唤醒开始倾听 → 保存唤醒前台灯状态,并亮起唤醒常亮柔光
on_listening:
- lambda: |-
if (!id(in_conversation)) {
id(in_conversation) = true;
id(light_was_on) = id(desk_light).remote_values.is_on();
if (id(light_was_on)) {
id(previous_brightness) = id(desk_light).remote_values.get_brightness();
id(previous_red) = id(desk_light).remote_values.get_red();
id(previous_green) = id(desk_light).remote_values.get_green();
id(previous_blue) = id(desk_light).remote_values.get_blue();
id(previous_white) = id(desk_light).remote_values.get_white();
}
}
id(has_tts_response) = false;
id(voice_assistant_phase) = ${voice_assist_waiting_for_command_phase_id};
- script.execute: control_leds
- light.turn_on:
id: desk_light
brightness: 75%
red: 0%
green: 70%
blue: 100%
white: 0%
effect: "None"
# 阶段2:用户说完话、进入 STT/LLM 思考阶段 → 快速闪烁
on_stt_vad_end:
- lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id};
- script.execute: control_leds
- light.turn_on:
id: desk_light
brightness: 85%
red: 30%
green: 40%
blue: 100%
white: 0%
effect: "快速闪烁"
# 阶段3:思考完毕、开始播报语音回答(同时兼容流式 TTS 与普通 TTS)→ 稍慢的呼吸
on_intent_progress:
- if:
condition:
lambda: return !x.empty();
then:
- script.execute: enter_replying_state
on_tts_start:
- script.execute: enter_replying_state
# 阶段4:对话结束 → 等待扬声器真正播完语音后,恢复到唤醒前的状态(或慢慢熄灭)
on_end:
- wait_until:
not:
voice_assistant.is_running:
- if:
condition:
lambda: return id(has_tts_response);
then:
# 保持 replying 状态并等待 external_media_player 开始和完成语音播报,防止语音还没播完灯就灭了
- lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id};
- script.execute: control_leds
- wait_until:
condition:
media_player.is_announcing:
id: external_media_player
timeout: 2.5s
- wait_until:
not:
media_player.is_announcing:
id: external_media_player
# 语音播报彻底完成后,恢复唤醒前的台灯状态(原本关则慢慢熄灭,原本开则恢复原亮度与颜色)
- script.execute: restore_desk_light
# 出错时:红灯提示 2 秒后恢复唤醒前状态
on_error:
- if:
condition:
and:
- lambda: return !id(init_in_progress);
- lambda: return code != "duplicate_wake_up_detected";
- lambda: return code != "stt-no-text-recognized";
then:
- lambda: |-
id(has_tts_response) = false;
id(voice_assistant_phase) = ${voice_assist_error_phase_id};
- script.execute: control_leds
- light.turn_on:
id: desk_light
brightness: 90%
red: 100%
green: 0%
blue: 0%
white: 0%
effect: "None"
- delay: 2s
- script.execute: restore_desk_light
上面我们为了方便直接将Wi-Fi SSID和明文密码输入在配置文件中,实际应用中建议通过配置 /config/esphome/secrets.yaml 载入。
在 ESPHome 界面左上角点击三个点,密钥,写入以下内容:
wifi_ssid: "<Wi-Fi SSID>"
wifi_password: "<明文Wi-Fi密码>"
然后把配置文件里的 Wi-Fi 部分改回:
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
(这一步不是必须,但更安全,密码不会明文出现在配置里)
第五步:编译并烧录
点击安装,根据自己的方式编译烧录。
不用担心“变砖”,这就是为什么我说编译这个很麻烦,配置文件的格式要求非常严格,即使强制编译,大概率会出错,即使没有报错,它也自带三层防错机制:
-
语法与引脚拦截(写错无法编译):第一层防护
如果你把引脚写错了(比如把不支持 Touch 的引脚配置成了触摸),或者 YAML 缩进打错了,点击 Install 时,ESPHome 根本不会开始编译。它会直接弹出红字报错,告诉你哪一行写错了。错误代码不会被写入主板。
-
安全模式 (Safe Mode) 自动自救:第二层防护
假设代码语法全对,编译也通过了,但其中某个逻辑导致芯片不断死机重启(比如内存泄漏或死循环)。当 ESP32-S3 连续重启失败 10 次后,它会自动触发底层的 Safe Mode(安全模式)。此时它会关闭所有硬件功能,仅开启 Wi-Fi 和 OTA 升级通道。你依然可以在 Home Assistant 里看到它在线,并把修正后的代码通过无线(OTA)重新刷进去。
-
物理强刷兜底:第三层防护
哪怕遇到最极端的情况(比如你不小心把 Wi-Fi 密码改错了导致它彻底断网,连安全模式都连不上路由器)。
解法极简: 把外壳打开,用 Type-C 线重新插回电脑,在网页上再点一次烧录。物理线缆的烧录权限永远是最高的,任何软件错误都可以被一键覆盖清除。
唯一需要小心的错误: 软件配置怎么写都不会损坏硬件。但物理接线短路(比如把 5V 电源线不小心搭在了 GND 或者普通的信号引脚上)会瞬间烧毁主板。通电前务必检查 3 根线的焊点/接头是否独立清晰,没有互相连在一起。
第三阶段:接线及功能测试(裸机调试)
不管采用哪种方案,最终到了这一步,来到了整个项目最激动人心的时刻!为了确保一次性接线成功且不烧毁主板,在桌面上摆好主板、喇叭、灯带和导线,请严格按照以下引脚对应关系进行物理连接:
第一步:核心接线清单(对应 ReSpeaker Lite 主板丝印)
-
扬声器(声音输出)
主板上自带了 5W 的功放和标准插座,一般不需要焊接,直接插。
买一个匹配的 JST 2P 公头(带线的)端子最好。
-
SK6812 灯带(视觉与灯效)
灯带剪下需要的长度后,一共有 3 根线需要引出(注意必须接在灯带标有 DIN 或方向箭头起始的那一端):
电源正极(5V / VCC) ➡️ 接主板边缘的 5V 焊盘。
电源负极(GND) ➡️ 接主板边缘的 GND 焊盘。
数据信号线(DIN / DI) ➡️ 接主板上方排针孔的 D2 焊盘。
-
电容触摸开关(物理交互)
完全不需要买额外模块,只需一根普通的细导线(比如从废旧网线里抽出来的一根铜丝):
导线一端 ➡️ 直接接在主板上方排针孔的 D3 焊盘 上。
导线另一端 ➡️ 剥开一点皮,用胶带贴在一小片锡纸/铝箔纸/铜箔上。测试时,把这片锡纸用双面胶临时贴在一块废料亚克力或塑料板背面。
-
系统供电(能量来源)
主板接口:板子底部的 Type-C 接口。
外部设备:插上连接电脑或手机充电头的 Type-C 数据线。(测试阶段建议直接插电脑,方便同时看日志输出)。
第二步:通电前“三秒自检”清单(防烧毁)
在插上 Type-C 线通电之前,请务必肉眼或用手机放大镜检查以下三点(这是新手最容易翻车的地方):
查短路:检查灯带的 5V 和 GND 焊点、导线有没有连在一起,或者有没有不小心碰到旁边的金属元件。正负极短路通电会瞬间烧毁板子!
查方向:确认灯带的信号线接的是 DIN(数据输入),如果接成了 DOUT(数据输出),灯带通电后完全不会亮。
查隔离:确保那根接在 D3 的“触摸导线”悬空,千万不要让它碰到 5V 焊盘,否则会烧毁芯片的 GPIO 引脚。
第三步:通电测试流程
确认接线无误后,插上 Type-C 线,观察服务器的 journalctl -u ai-gateway -f 日志,如果是 接入Home Assistant ,则打开 ESPHome 日志面板看日志:是否成功连上 Wi-Fi。
摸锡纸:手指隔着废料塑料摸一下那片锡纸,看 HA 里“顶盖触摸开关”实体是否从 关闭 变成 开启。
试灯效:独立部署的在web页面后端进行测试,HomeAssistant方式的在仪表盘找到“AI Light”进行测试。
喊唤醒词/触摸唤醒:对它说一句唤醒词(或摸一下),看着灯光变呼吸状态,对它说“你好”,等待 DeepSeek 思考后,喇叭里传出清脆的回答。
第四阶段:物理结构与声学密封组装(装配落地)
但是在组装环境,有些材料及注意事项简单说明一下:
-
喇叭密封音腔制作:
用 B-7000 胶水 将喇叭单元、被动振膜固定在内腔体上,做气密性封边并静置固化。也就是喇叭后面最好达到密封效果,这样低音才会有质感,不会很空;另外,建议在腔体内放置吸音棉。
-
亚克力框架组装:
使用专门的亚克力胶水,分子层的粘连,可以养鱼的那种。
-
触摸开关:
锡纸/铜箔紧密贴在顶盖内侧。
-
SK6812 灯带:
选择合理固定灯带的方式,如果贴在亚克力板上,因为灯带温度较高,亚克力板可能会弯曲或者影响到整体外观。
-
ReSpeaker Lite 主板位置:
麦克风位置在板子两端,为了更好拾音,避免听不清楚,最好是水平固定在顶层或平放在内部。
第五阶段:日常使用调优
优化一:关闭不相关实体
默认对话情况下,HomeAssistant与DeepSeek交互时,每次都会附带公开的设备信息,将该设备的元数据转化为函数描述,以便大模型判断它是什么、能执行哪些操作。
例如一个开关包含开关、温度、定时等一系列的实体,但是实际上可能只需要控制开和关,因此无需额外暴露温度、定时等实体。在底层的工具定义中长这样:
{
"name": "HassTurnOn",
"description": "打开某个设备",
"parameters": {
"properties": {
"name": {
"type": "string",
"description": "设备名称,例如:客厅吸顶灯 (light.living_room_light)"
}
}
}
}
//默认附带其他不必要的设备信息
-
单个设备损耗:每个设备根据支持的属性(开关、亮度、颜色、色温等),大约会占用 20 到 60 个 Token。
-
累积放大效应:如果你家里有 30 个设备公开在列表中,每一次对话光是这批设备清单就会固定吃掉 600 ~ 1500 个输入 Token。
-
高频损耗:每次你跟它说一句话,哪怕只是问一句“今天冷吗”,这几千个 Token 都会默默跟随着请求往返一次。
为了避免Token浪费,建议在 【语音助手】 中,将 【公开新实体】 关闭,然后手动选择需要公开的实体。
ESPHome远程服务器编译
如果HomeAssistant设备性能有限,可能会出现内存不足无法编译的情况,需要借用其他设备帮助编译。
在其他设备上创建 docker compose 进行配置:
services:
esphome:
container_name: esphome
image: ghcr.io/esphome/esphome
volumes:
- ./config:/config
- /etc/localtime:/etc/localtime:ro
restart: always
privileged: true
network_mode: host
environment:
- ESPHOME_USERNAME=<username>
- ESPHOME_PASSWORD=<password>
然后访问:http://<服务器ip>:6052 登陆。
回到HomeAssistant的ESPHome,点击右上角三个点 - 设置,然后在配对请求与构建服务器这几个栏目里面,与上述部署在服务器的esphome进行配对即可。这样每次编译固件,点击通过网络构建后,都会发往服务器端进行构建。
独立方案
在这过程中,我有个作死的ideal: 不想通过home assistant,因为我不需要控制家里设备,家里设备都是Apple Home负责的,如果在ubuntu server上直接部署服务,也就是:
[ReSpeaker Lite (ESP32-S3)]
│ (Wi-Fi 双向低延迟音频流 / WebSocket / UDP)
▼
[Ubuntu 服务器: 独立中控服务 (Python)]
├─> 听觉: SenseVoice (本地 10300 端口,流式 STT/VAD)
├─> 大脑: DeepSeek API (推理/对话,保留记忆逻辑)
└─> 喉咙: Kokoro / Azure TTS (毫秒级合成,流式推回音频)
目前市面上 没有任何一款现成的 .bin 固件 可以直接刷进 ESP32-S3 直接适配。我们这一套实际上是高度定制的“简化版裸 PCM + 私有 SK6812 状态机”协议。
它与目前主流的两类现成固件在底层协议和硬件驱动上均存在冲突。
因此,需要自行写代码进行编译。
以下都是失败经历,但是又不想死心,一起发出来吧,后面我继续研究研究。
独立方案的线路就是ReSpeaker Lite直接通往Ubuntu Server,然后Ubuntu Server处理后直接返回ReSpeaker Lite。
也就是需要:
-
在 Ubuntu 上跑一个独立的中控服务:
- 可以使用轻量的 Python 脚本,对外暴露一个 WebSocket 端口(比如
28888),专门给开发板连接。
- 可以使用轻量的 Python 脚本,对外暴露一个 WebSocket 端口(比如
-
给 ReSpeaker Lite 刷入独立的 Wi-Fi 客户端固件:
- 开发板不再烧录 ESPHome 固件,而是刷入支持独立 WebSocket 音频流的固件。
第一步:目标
把全部功能(处理逻辑)都放到Ubuntu Server上,需要编写一个后端程序,具有以下流程:
-
👂 接收与监听 (WebSocket 入口)
-
逻辑:监听
28888端口,接收开发板通过 WebSocket 发来的信号(开始录音、停止录音)以及二进制的音频流(说话的原始声音)。 -
处理:会把这些音频碎片拼起来,过滤掉极短的杂音(防误触)。
-
-
✍️ 语音转文字 (STT - SenseVoice)
-
逻辑:音频一接收完,立马通过 Wyoming 协议把音频发给本地的
SenseVoice容器。 -
作用:将说的语音精准翻译成文字,并剔除掉乱七八糟的内部情绪标签。
-
-
🧠 思考与记忆 (LLM + RAG 向量检索)
-
短期记忆:维护列表(
messages_history),能记住最近聊的十几句话(实现多轮连续对话)。 -
长期记忆:搭载 “函数调用(Tool Calling)” 能力。如果 DeepSeek 发现在说自己的喜好,它会自动调用代码里的
Ollama + ChromaDB向量数据库把信息刻在硬盘上;如果需要回忆,它也会自动去库里把之前存的记忆翻出来。 -
思考:带着上下文和记忆,向 DeepSeek 发起请求,获取极度拟人化的文本回复。
-
-
🗣️ 文字转语音与下发 (TTS - Azure/Kokoro)
-
逻辑:拿到大模型的文本回复后,可选语音模型,自定义配置的引擎(云端的 Azure 或本地的 Kokoro)。
-
下发:拿到音频后,不是一次性全砸给开发板(会撑爆内存),而是切成 4KB 的小块,配合“开始播放/停止播放”的信令,平滑地发回给开发板播放。
-
因此,我额外增加了一个前端Web 可视化配置面板,用于后期调试灯光、唤醒词、音量以及更改对话、语音模型等。
-
零重启热更新(Hot-Reload):在Web界面上点一下“保存”,下一句跟开发板说话时,新的声音、新的模型、新的人设立即生效,无需重启服务,开发板也不会断开连接。
-
代码与密钥彻底分离:代码里不包含任何明文 API Key,所有敏感信息和可变参数全部存在本地自动生成的配置文件
config.json中,代码升级或分享时再也不怕泄露密钥。
h óu g ē @猴哥 即可声学层流式匹配。同时也兼顾了以后使用英文的选择。第二步:编写代码
以Ubuntu Server为例:
安装python:
apt install python3-venv python3-full -y
创建工作目录:
mkdir -p /var/www/AI-Light
cd /var/www/AI-Light
创建程序文件:
vim gateway.py
内容为:
# 留空
# 之前写过一些代码,但是由于固件那边一直有问题,等完成后再更新
创建虚拟环境并安装依赖:
python3 -m venv ailight
source ailight/bin/activate
# 安装依赖:OpenAI(用来调DeepSeek)、ChromaDB(存记忆)、Azure语音SDK、HTTP客户端
pip install openai chromadb azure-cognitiveservices-speech requests websockets wyoming aiohttp sherpa-onnx numpy pypinyin
# 下载Sherpa-ONNX官方中英双语声学唤醒模型并解压到当前目录
curl -L -O https://github.com/k2-fsa/sherpa-onnx/releases/download/kws-models/sherpa-onnx-kws-zipformer-zh-en-3M-2025-12-20.tar.bz2
tar xf sherpa-onnx-kws-zipformer-zh-en-3M-2025-12-20.tar.bz2
rm sherpa-onnx-kws-zipformer-zh-en-3M-2025-12-20.tar.bz2
运行:
python3 gateway.py
Ctrl + C 退出运行,并且可以输入 deactivate 退出虚拟环境。
| 当前所处阶段 | 当【平时关闭 (power: false)】时的灯效表现 | 当【夜间打开常亮 (power: true)】时的柔和叠加表现 | 对话结束后的去向 |
|---|---|---|---|
1. 待机状态 (idle) |
全灭(0 功耗、0 光污染) | 保持你设定的常亮颜色与亮度(如暖白光 80%) | — |
2. 唤醒倾听 (listening) |
亮起柔和的冰蓝色微光(提示正在听你说话) | 不改变全屋照明主色调,仅在当前常亮色上做 +15% 的轻微提亮涟漪 | 进入思考 |
3. 大脑思考 (thinking) |
幽蓝色慢速呼吸灯(明暗交替) | 在当前常亮照明亮度的 80% ~ 100% 之间做极轻微的呼吸起伏(既有反馈又不晃眼) | 进入回答 |
4. 语音回答 (speaking) |
青绿色声波律动 | 保持常亮色,仅伴随说话做 ±10% 的柔和微光波动 | 立即恢复为全灭或常亮 |
5. 异常/未听清 (error) |
琥珀红闪烁 2 次 | 在常亮背景上轻微暖橙色闪动 1 秒 | 立即恢复为全灭或常亮 |
日常与夜间使用
-
白天 / 平时待机(默认
power: false):灯带完全熄灭。当喊出唤醒词,灯带立刻泛起柔和的冰蓝色(
wake_glow) -> 说完话等待 DeepSeek 思考时,转为深蓝色慢呼吸(breathe) -> AI开口回答时,转为青绿色律动(speak_pulse) -> 话音落下,灯带自动熄灭恢复安静。 -
开灯常亮(
power: true):power变为true,此时唤醒AI提问:灯带不会熄灭也不会突兀变色,而是在当前的灯光亮度基础上做 82% ~ 100% 的微光呼吸起伏(subtle_breathe),对话一结束立刻恢复之前常亮状态。 -
网页一键预览:
gateway.py附带的后端配置界面:http://服务器IP:28889,在SK6812 智能灯带控制栏目,设置 5 个实时预览按钮(🎤唤醒亮灯、🧠思考呼吸、🗣️回答律动、⚠️错误闪烁、🔄恢复待机),方便直接在手机上测试各种灯光效果。
第三步:模拟测试
简单再写个python脚本,对接上面的几个接口,模拟人声给它发送一段语音,测试是否能够正常接收并生成语音:
可以单起一个测试目录,或者就在这个目录操作。
新建一个SSH窗口,并进入同一个目录 (因为我的测试代码里写了读取同一目录的配置文件 config.json ,其他目录的话更改11行的路径即可,所以需要上一步访问Web界面 http://服务器ip:28889 先生产配置文件) :
进入工作目录:
cd /var/www/AI-Light
创建程序文件:
vim test.py
内容为:
import asyncio
import websockets
import json
import os
import wave
import azure.cognitiveservices.speech as speechsdk
# ================= 1. 动态加载网关配置 =================
def load_azure_config():
"""从网关的 config.json 中自动读取 Azure 密钥"""
config_path = "./config.json"
if not os.path.exists(config_path):
print("❌ 找不到 config.json,请先运行 gateway.py 并在 Web 后台保存一次配置。")
exit(1)
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
api_key = config.get("azure_speech_key")
region = config.get("azure_region", "eastasia")
if not api_key:
print("❌ config.json 中未配置 Azure 密钥,生成假录音失败。请先去 Web 控制台填写!")
exit(1)
return api_key, region
# ================= 2. 生成模拟音频 =================
def text_to_pcm(text, api_key, region):
print(f"[模拟器] 正在调用 Azure 将文本转为 16KHz 麦克风录音流...")
speech_config = speechsdk.SpeechConfig(subscription=api_key, region=region)
# 小智固件发送的麦克风音频是 16KHz 16bit 单声道 PCM
speech_config.set_speech_synthesis_output_format(speechsdk.SpeechSynthesisOutputFormat.Raw16Khz16BitMonoPcm)
pull_stream = speechsdk.audio.PullAudioOutputStream()
stream_config = speechsdk.audio.AudioOutputConfig(stream=pull_stream)
synthesizer = speechsdk.SpeechSynthesizer(speech_config=speech_config, audio_config=stream_config)
result = synthesizer.speak_text_async(text).get()
return result.audio_data
# ================= 3. 主测试逻辑 =================
async def mock_board():
api_key, region = load_azure_config()
uri = "ws://127.0.0.1:28888"
print("🤖 模拟开发板已启动!")
# 交互式输入测试文本
print("-" * 40)
user_input = input("🎤 请输入你要对管家说的话 (按回车发送): ")
if not user_input.strip():
print("未输入文本,测试结束。")
return
print("-" * 40)
# 转换为假录音
pcm_data = text_to_pcm(user_input, api_key, region)
if not pcm_data:
print("音频生成失败。")
return
print(f"[模拟器] 成功生成 {len(pcm_data)} 字节的音频数据,准备发送...")
try:
async with websockets.connect(uri) as ws:
print("[模拟器] 🔗 已通过 WebSocket 连接到 AI 网关!")
# 严格按照小智协议发送数据流
await ws.send(json.dumps({"type": "listen", "state": "start"}))
await ws.send(pcm_data)
await ws.send(json.dumps({"type": "listen", "state": "stop"}))
print("[模拟器] 📤 音频已发送,等待管家回复...\n")
# 接收服务端的指令和音频回复,并直接存为 WAV
with wave.open("reply.wav", "wb") as wav_file:
wav_file.setnchannels(1) # 单声道
wav_file.setsampwidth(2) # 16bit = 2字节
wav_file.setframerate(24000) # 网关下发的是 24000Hz (兼容 Kokoro 和优化后的 Azure)
async for message in ws:
if isinstance(message, str):
print(f"📡 收到网关信令: {message}")
if "stop" in message:
break
elif isinstance(message, bytes):
wav_file.writeframes(message)
print(f"🎵 收到音频分片: 写入 {len(message)} 字节")
print("\n✅ 全链路测试完成!管家的语音回复已生成: reply.wav (可直接双击播放)")
except ConnectionRefusedError:
print("❌ 连接失败!请确认 gateway.py 正在运行,并监听了 28888 端口。")
if __name__ == "__main__":
asyncio.run(mock_board())
创建虚拟环境并安装依赖:
python3 -m venv test
source test/bin/activate
pip install websockets azure-cognitiveservices-speech
运行:
python3 test.py
gateway.py 服务端,另一个就是 test.py 测试端。根据两个界面中显示的信息判断是否测试成功。
😂:不过现在10月天气已经转凉了,再喝冰拿铁感觉有点牙疼~
之后可以 Ctrl + C 退出运行,然后输入 deactivate 退出虚拟环境。
第四步:烧录开发板
ReSpeaker Lite 板载了两颗芯片:一颗是负责音频降噪的 XMOS (XU316),另一颗是负责联网和主控的 XIAO ESP32S3。我们要对两个芯片分别烧录。
先刷 XMOS 的 I2S 固件
出厂时,XMOS 芯片可能烧录的是 USB 声卡固件或者出厂测试固件。
如果不通过 dfu-util 把 xmos_firmwares 目录下的专用 I2S 固件刷进 XMOS 芯片,它就不会通过标准 I2S 总线把处理好的干净音频吐给 ESP32-S3。也就是说,即使你在 ESP32-S3 上把固件烧得再完美,如果 XMOS 没有刷成 I2S 模式,ESP32-S3 也听不到任何声音(或者喇叭放不出声)。
下载地址: https://github.com/respeaker/reSpeaker_Lite/tree/master/xmos_firmwares
下载:respeaker_lite_i2s_dfu_firmware_48k_v1.1.0_ch0-asr_ch1-mww.bin
同目录下有 xmos_firmwares/dfu_guide.md 刷机教程,可以进行查看。
先把 Type-C 数据线插在音频底板上的 XMOS 接口(注意:不是插在 ESP32 小板上)。
以macOS为例:
# 1.安装homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 2.安装dfu-util
brew install dfu-util
# 3.确认Respeaker Lite有没有被检测到
dfu-util -l
# 4.刷固件
dfu-util -R -e -a 1 -D /path/to/dfu_firmware.bin
# 5.设备重启
编译与烧录ESP32S3
这边我重点研究了Respeaker Lite开发版的说明,根据官方文档简单梳理了一下编译的步骤:
-
下载Arduino IDE: Arduino IDE Software | Arduino
-
配置 ESP32 开发板支持包
-
打开 Arduino IDE,按照 Seeed Studio 官方配置指南 进入 文件 → 首选项,在“其他开发板管理器地址”中填入:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json -
在左侧 开发板管理器 中搜索
esp32(Espressif Systems),点击安装。
-
-
安装 3 个依赖库
-
点击左侧 库管理器 (Library Manager),依次搜索并安装:
-
WebSockets(作者:Markus Sattler) -
ArduinoJson(作者:Benoit Blanchon) -
Adafruit NeoPixel(作者:Adafruit)
-
-
-
修改顶部 4 项配置参数
-
粘贴下方的代码中找到
用户核心参数配置区,填入你的实际信息:-
WIFI_SSID/WIFI_PASSWORD:2.4GHz Wi-Fi 名称与密码。 -
SERVER_HOST:服务器局域网 IP(即运行gateway.py的服务器 IP)。 -
LED_COUNT:从 SK2812 灯带上实际剪下的灯珠颗数。
-
-
-
选择开发板并开启 PSRAM 烧录
-
将 Type-C 数据线插在 XIAO ESP32S3 小板 的接口上(注意这次是插 ESP32 口,不是底板 XMOS 口)。
-
顶部菜单选择 工具 (Tools):
-
Board:选择
XIAO_ESP32S3 -
PSRAM:务必选择
OPI PSRAM(开启 8MB 扩展内存用于语音流缓冲) -
Eraser All Flash before sketch Upload:建议选择
Enabled
-
-
点击左上角 上传 (Upload)。
-
-
完成烧录: 顺便插拔一下接头。
固件代码:
# 留空
添加HomeKit桥接
由于采用独立方案,并不经过HomeAssistant,因此无法通过HomeKit Bridge的方式添加到HomeKit中进行台灯开关的操控,因此需要单独搭建一个 HAP-python。
由于 HAP-python(Apple HomeKit 协议的 Python 实现)内部维护了独立的加密套接字循环,最稳妥、互不干扰的做法是:写一个独立的 homekit_bridge.py 伴生脚本(仅 80 行代码),它会把 AI-Light 伪装成一台苹果官方认证的 全彩智能灯(支持开关 + 亮度 + 调色盘)。
第一步:在 Ubuntu 上安装系统依赖与 HAP-python
HomeKit 协议底层依赖 avahi(也就是苹果的 Bonjour / mDNS 局域网广播协议)以及加密库。依次在终端执行以下两条命令:
# 1. 安装 Ubuntu 系统的 mDNS 广播支持库
apt-get update && apt-get install -y libavahi-compat-libdnssd-dev avahi-daemon
# 2. 在虚拟环境中安装 HAP-python 和色域转换支持
/var/www/AI-Light/ailight/bin/pip install HAP-python[QRCode] requests
_(如果服务器开了 ufw 防火墙,放行 HomeKit 通信端口与 mDNS 端口:ufw allow 5353/udp 和 MARKDOWN_HASHca4daa43368010ff8c4d96e55b8a476aMARKDOWNHASH)
第二步:创建 homekit_bridge.py Apple Home桥接脚本
在 /var/www/AI-Light/ 目录下新建 homekit_bridge.py:
vim /var/www/AI-Light/homekit_bridge.py
粘贴以下完整代码(它会自动將 Apple Home 的 HSB 色相/饱和度/亮度 转换为网关所需的 RGB Hex 十六进制颜色,并实时调用 http://127.0.0.1:28889/api/light ):
import colorsys
import logging
import requests
from pyhap.accessory import Accessory
from pyhap.accessory_driver import AccessoryDriver
from pyhap.const import CATEGORY_LIGHTBULB
logging.basicConfig(level=logging.INFO, format="[HomeKit] %(message)s")
GATEWAY_API = "http://127.0.0.1:28889"
class AILightBulb(Accessory):
"""将 AI-Light 映射为 Apple Home 全彩调光灯"""
category = CATEGORY_LIGHTBULB
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 加载苹果标准的 Lightbulb 服务(包含 On, Brightness, Hue, Saturation 四个特征)
serv_light = self.add_preload_service(
"Lightbulb", chars=["On", "Brightness", "Hue", "Saturation"]
)
self.char_on = serv_light.configure_char(
"On", value=True, setter_callback=self.set_on
)
self.char_brightness = serv_light.configure_char(
"Brightness", value=80, setter_callback=self.set_brightness
)
self.char_hue = serv_light.configure_char(
"Hue", value=30, setter_callback=self.set_hue
)
self.char_saturation = serv_light.configure_char(
"Saturation", value=80, setter_callback=self.set_saturation
)
# 本地缓存当前状态
self.power = True
self.brightness = 80
self.hue = 30.0
self.saturation = 80.0
def _sync_to_gateway(self):
"""将 Apple Home 的 HSB 颜色转换为 #RRGGBB 并推送给 gateway.py"""
r, g, b = colorsys.hsv_to_rgb(
self.hue / 360.0,
self.saturation / 100.0,
1.0
)
hex_color = "#{:02x}{:02x}{:02x}".format(
int(r * 255), int(g * 255), int(b * 255)
)
payload = {
"power": bool(self.power),
"color": hex_color,
"brightness": int(self.brightness)
}
try:
requests.post(f"{GATEWAY_API}/api/light", json=payload, timeout=2)
logging.info(f"Siri/家庭控制生效 -> 开关:{self.power} | 颜色:{hex_color} | 亮度:{self.brightness}%")
except Exception as e:
logging.error(f"无法连接到 gateway.py (28889): {e}")
def set_on(self, value):
self.power = bool(value)
self._sync_to_gateway()
def set_brightness(self, value):
self.brightness = int(value)
self._sync_to_gateway()
def set_hue(self, value):
self.hue = float(value)
self._sync_to_gateway()
def set_saturation(self, value):
self.saturation = float(value)
self._sync_to_gateway()
@Accessory.run_at_interval(5)
def run(self):
"""每 5 秒从 gateway.py 同步一次状态,确保当你用小智语音或网页改色时,苹果家庭 App 里也跟着变色"""
try:
resp = requests.get(f"{GATEWAY_API}/api/config", timeout=2)
if resp.status_code == 200:
light = resp.json().get("light_state", {})
power = bool(light.get("power", True))
bright = int(light.get("brightness", 80))
hex_c = light.get("color", "#ff9933").lstrip("#")
if len(hex_c) == 6:
r, g, b = [int(hex_c[i:i+2], 16) / 255.0 for i in (0, 2, 4)]
h, s, _ = colorsys.rgb_to_hsv(r, g, b)
hue_deg = round(h * 360.0, 1)
sat_pct = round(s * 100.0, 1)
if self.power != power:
self.power = power
self.char_on.set_value(power)
if self.brightness != bright:
self.brightness = bright
self.char_brightness.set_value(bright)
if abs(self.hue - hue_deg) > 2:
self.hue = hue_deg
self.char_hue.set_value(hue_deg)
if abs(self.saturation - sat_pct) > 2:
self.saturation = sat_pct
self.char_saturation.set_value(sat_pct)
except Exception:
pass
if __name__ == "__main__":
# 监听 51826 端口,自动将配对状态保存在 accessory.state 文件中
driver = AccessoryDriver(port=51826, persist_file="./accessory.state")
accessory = AILightBulb(driver, "AI 语音氛围灯")
driver.add_accessory(accessory=accessory)
driver.start()
第三步:运行并在 iPhone “家庭” App 中扫码/输入配对码添加
-
首次运行查看配对码 在终端里直接运行它:
/var/www/AI-Light/ailight/bin/python3 /var/www/AI-Light/homekit_bridge.py终端屏幕上会显示一个 二维码 和一串 8 位数的 HomeKit 设置代码。
-
在 iPhone 上添加配件
-
打开 iPhone 自带的 “家庭 (Home)” App -> 点击右上角
+号 -> “添加配件”。 -
直接用摄像头扫终端里的二维码;或者点击下方的 “更多选项...”,你会看到屏幕上已经搜出了一个叫 “AI 语音氛围灯” 的灯具图标!
-
点击它,如果提示“未认证配件”,点击“仍然添加”,输入终端里显示的 8 位数字配对码,即可完成绑定。
-
第四步:加入 Systemd 开机自启(配对成功后)
配对成功后,按 Ctrl+C 停止脚本,写一个守护进程让它和 ai-gateway 一起在后台永久运行:
vim /etc/systemd/system/ai-homekit.service
粘贴以下内容并保存:
[Unit]
Description=Apple HomeKit Bridge for AI-Light
After=network.target ai-gateway.service
[Service]
Type=simple
User=root
WorkingDirectory=/var/www/AI-Light
ExecStart=/var/www/AI-Light/ailight/bin/python3 /var/www/AI-Light/homekit_bridge.py
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
启动并设为开机自启:
systemctl daemon-reload
systemctl enable ai-homekit
systemctl start ai-homekit
Bad Ending
前两天完成的,接入ESPHome的体验还不错。灯光效果也很好,电容触摸开关一开始配置的是用于开启对话开关,后来改为灯光开关比较实用。
硬要说的话,其实任务已经圆满完成了,没有必要为了强行采用独立方案而纠结。
而且独立方案减少的那么一点点延迟远远没有实际意义,由于Azure TTS的服务器在国外,整个链路的延迟大部分都在这里。
准备接着搞独立方案了~













