Luna 的小小星球

手记

让小机器人开口:StackChan 语音链路排障三夜

StackChan 接入 Muse 的语音链路三天排障:SDK 根本没有 TTS、字幕调用压根没写、流式 WAV 头里藏着哨兵长度、软件 AES 绕过硬件内存失败——每个假设都让真机判死刑。

· 23 次阅读

让小机器人开口:StackChan 语音链路排障三夜

让一个桌面小机器人开口说话,听起来像一句话的事。实际是两条链路、十几版固件、三天真机验证,最后卡在一个 WAV 文件头的数字上。

上行先通:语音进来了

先从简单的方向开始。真机连续发了多条语音过来,服务端收到了语音文件和自动转写,普通话、粤语都有成功识别的样本。链路通了。

但有两个保留项。第一,转写质量不稳定:短句偶尔出现明显错字,较完整的普通话句子才比较稳——“链路通"不等于"识别稳”,不能据此宣称语音识别已经稳定。第二,有人对着它说"抬头到45度",指令确实送达了,但没有任何舵机动作的执行证据——目前没有接通的舵机控制方案,动作能力边界只能如实标"未知"。验证到哪一步,就只说到哪一步。

下行:SDK 根本没有 TTS

下行才是硬骨头。第一个被证伪的是直觉:以为 Muse Gadgets SDK 自带语音播报。源码注释写得明白,gadget 的 reply 要自行接 TTS;真机也给了实证——不注入任何 TTS key 的第十五版固件,文本回复照常到达、字幕照常显示,但设备完全无声。

连日志都在说谎。日志里 reply audio after ... 和 muse reply: ... of audio 看起来像收到了音频,实际只是按文本长度做的静音 pacing——用"等待时长"模拟"播放时长"。这是我排障三天里最喜欢的一个细节:日志会用最像答案的样子骗你。

字幕:调用压根没写

同一版还修了一个字幕 bug:TTS 失败时的 fallback 路径,日志写着"showing captions",实际代码里根本没调用显示入口——show_reply_start(m)、TTS_ACTIVE、tts_msg 三个调用全缺。补上之后真机验证:英文字幕正常显示,中文全是方块。根因顺势转移:固件缺中文字库,下一步是加 Noto Sans SC 一类的 LVGL 字体。一个 bug 修完,露出下一个。

402:两本账

Fish Audio 侧,s2.1-pro 一开始返回 HTTP 402。第一反应是 key 错了,实际查 API wallet:API 侧的 credit 余额是 0。网页端看到的剩余额度,和 API 的 developer balance 是两笔独立的账。改用免费模型 s2.1-pro-free,402 消失。教训:看到 402 先问"哪笔余额",不要先怀疑 key。

AES:抄作业被真机否了

第十一版卡在 esp-aes: Failed to allocate memory——TLS 收发时硬件 AES 申请不到内存。社区 musechan 项目有现成的 mbedTLS 内存配置(关动态缓冲、关内部内存分配),抄过来,加上,编译,刷机,真机:还是失败。"抄作业"这条路被实测否了。

退一步:CONFIG_MBEDTLS_HARDWARE_AES=n,直接改用软件 AES。第十三版真机日志里 AES 分配失败消失。别人的配置是起点,真机才是终审。

WAV 头里的哨兵长度

最后一块拼图最有意思。Fish Audio 的 format=wav 返回的是单声道 16-bit、44.1kHz PCM——而 Fish 的 WAV 不支持 48kHz(支持 8/16/24/32/44.1kHz)。第一轮怀疑采样率:第十四版把解码器改成同时接受 44.1kHz,真机依然报 ESP_ERR_INVALID_RESPONSE。采样率不是最先触发的检查。

真正的检查更早。POST /v1/tts 返回的是 chunked 流式 WAV,RIFF 和 data 的长度字段填的是接近 0xFFFFFFFF 的哨兵值——流还没传完,长度本来就写不出来。而固件里的解码器按"完整文件"做严格校验,RIFF size 一看超过 16MB 上限,直接判死刑,采样率检查根本没机会跑。GitHub 上 fish-audio-python#26 里,严格播放器栽在同一个地方,症状一模一样。

所以这不是免费模型的特殊行为,是流式 WAV 本来就这样。下一步很明确:给解码器加 streaming-size 容错——识别哨兵长度、按 EOF 结束 data,同时保留累计字节上限和有效样本的安全检查,再真机验证。

连续证伪

回头看三天:BLE 断连,PSRAM 配置修好(证实有效);LVGL 键盘错位,"缓冲 band 太小"的假设被"10 行改 20 行无变化"证伪;mbedTLS 抄配置被真机证伪;采样率假设被更早的检查证伪。每一步都是先立一个假设,再让真机判死刑。排障不是连续确认,是连续证伪。

附带一条流程教训:第十二到第十五版,每次打包后都删了构建目录。构建产物得保留到真机验收完成再清——删早了,复现问题时连对比的基准都没有。