🐛 fix(sparkbot): 稳定表情动画与字幕时序 - #377
Conversation
Codecov Report❌ Patch coverage is @@ Coverage Diff @@
## main #377 +/- ##
==========================================
+ Coverage 90.27% 90.38% +0.10%
==========================================
Files 213 216 +3
Lines 25697 26061 +364
Branches 6728 6859 +131
==========================================
+ Hits 23199 23554 +355
- Misses 1151 1166 +15
+ Partials 1347 1341 -6
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 8 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
审阅了显示渲染、Linx emotion 事件、VoiceSession 时序、Runtime 事件队列以及固件工具链发现逻辑。下面两处事件优先级/状态门控交互会导致真实回合中的字幕丢失或表情残留,建议在合入前修正。已运行 python3 -m unittest tests/python/test_firmware.py tests/python/test_im_wifi_credential_isolation.py(25/25 通过);完整主机构建受当前 GCC 对既有 [[maybe_unused]] 成员的 -Werror=attributes 报错阻塞。
| EnqueueDisplayText(evidence.detail); | ||
| // 首句在首个真实音频包到达后显示,避免字幕领先声音;后续 | ||
| // 句段沿用协议的 sentence-start 顺序,不伪造采样级时间戳。 | ||
| if (!tts_first_audio_seen_) { |
There was a problem hiding this comment.
[P1] Preserve subtitle events until first audio is handled
tts_first_audio and tts_sentence_started are queued as best-effort items, while tts_stopped remains a control item and the event loop always drains control items first. If Linx delivers the first-audio/sentence/stop burst while the loop is busy, tts_stopped is processed first: it clears pending_tts_sentences_ and moves the interaction out of kSpeaking; the queued sentence is then discarded by the stale-state check. This makes an otherwise valid response show no subtitle under normal queue backlog. Keep the first-audio/sentence ordering coupled with TTS completion (or make the gate state part of the ordered control path) so stop cannot overtake the pending subtitle work.
| ESP_LOGI(kTag, "DISPLAY_EMOTION_DEFERRED reason=speaking key=%s", item.emotion_key.c_str()); | ||
| continue; | ||
| } | ||
| snapshot_.mood = item.mood; |
There was a problem hiding this comment.
[P2] Drop deferred emotions from completed speaking turns
Emotion updates are explicitly placed in the best-effort queue, but interrupt/TTS-stop events use the control queue, which is drained first. An emotion received during speaking can therefore be applied only after an interrupt or tts_stopped has already returned the session to standby/ready. This branch then writes the old turn's emotion_key into the standby snapshot, so the robot can remain on a stale happy/thinking animation until a later display update. Associate the mood update with the speaking generation/phase and discard it when that turn is no longer active (or process it in the same ordered queue).
|
IMU 官方实现核对结论(以 SparkBot 官方工厂示例为准) 结论:SparkBot 板载惯性传感器应按 BMI270 接入,不采用早期分支中的自定义加速度计方案。 已核对来源:
官方事实:
实现约束:
验收证据要求:实板必须出现 |
|
WakeNet / MultiNet 唤醒优化核对结论 当前固件走的是 MultiNet7 已完成的可重复评估资产保存在仓库外:
当前板端归一化播放结果:longcheng_v3、longshu_v3、longwan_v3、longxiaochun_v3、longanlingxi 可稳定检出;longanhuan_v3、longyue_v3 目前没有有效检出证据。说明“你好牛牛”确实存在发音覆盖缺口,但不能据此直接把门限继续下调。 建议的优化顺序:
因此本 PR 不会伪称“已用百炼多音色训练模型”;它先保留可复现实验资产和基于测量的阈值/变体优化路径。 |
MultiNet7 多音色实板复测(bb5756a)本次提交只包含:
数据与环境
结果
复核证据
主机侧 |
WakeNet 语料准备与可行性验证(2026-08-27)本轮提交: 已完成:
校验器明确警告:当前 pilot 仅 21 条、3 个说话人、无儿童样本、只有 1 米元数据,低于官方至少 2 万条/推荐 500 说话人(含至少 100 儿童)及 1 米+3 米采集要求。因此当前产物只是提交给乐鑫官方 WakeNet 定制流程的输入,不是可刷写 WakeNet 模型;本地没有把 TTS 音频冒充成模型,也没有改动现有 MultiNet/WakeNet 固件模型。 结论:多音色、多语速/音调的语料生成和可审计打包流程已就绪;要得到“你好牛牛/牛来”的生产级自定义 WakeNet,下一步必须补齐真实说话人、距离和样本规模后提交官方训练/交付,不能在本地直接训练出等价 |
MultiNet7 多音色实板复测与长对话复测(2026-08-27)本次提交
构建与刷写
MultiNet 28 条实板评估数据集包含 7 种百炼音色、语速/音调
按语速/音调组合统计: 完整逐条串口日志和 JSON 报告只保存在本地,不提交仓库: 百炼实板长对话复测固件刷写后使用真实百炼 TTS(
本次结果与明文串口日志:
结论:语音长对话链路本轮通过;MultiNet conditioning 作为输入电平校准已验证有效,但“你好牛牛”多音色/近负样本的最终唤醒灵敏度仍未达标,后续 WakeNet 训练任务保持未完成状态。 |
当前阶段提交与实板回归(2026-08-27)已提交并推送: 本提交包含:
验证:
原始明文串口日志和结果 JSON 仅保留在本机 |
重复提醒根因与修复(0f1aabc)本次“三次提醒”不是 IM 复制发送,而是提醒服务的有界重试链:第一次触发后立即注册下一次任务,间隔 10 分钟,最多 3 次( 旧固件在公众号点击“知道了”后已收到 SSE 命令,但执行 SQLite/FATFS 持久化时 action worker 使用 PSRAM 栈,ESP-IDF 在关闭 Flash cache 时触发 本提交将 action worker 的 8 KiB 栈从 验证:
待补实板闭环:需新建一个短时提醒,只点击一次“知道了”,确认无复位、出现 |
2026-08-27 实板证据:IM 点击确认与设备确认不等价,导致同一提醒发送三次结论本次三次提醒不是微信公众号重复投递,也不是 Gateway 把同一条消息复制发送,而是同一条提醒链的 3 个独立重试任务:
因此当前契约下:
两者在旧链路中不是等价状态。 1. Gateway 生产日志时间线以下时间是 UTC;北京时间分别为次日 07:04、07:14、07:24。 第一次提醒这证明 IM 按钮已经被提交,并且命令已经进入设备 SSE 下发流程。 第二次提醒第三次提醒三条 Delivery 的 2. PostgreSQL 状态证据生产数据库 第一次 IM action 的数据库状态为: 对应的 3. 设备侧失败证据旧固件串口日志顺序如下: 没有出现: 也没有提交 action result。对应的旧固件日志为本机受控目录中的: 使用对应 ESP-IDF 6.0.2 的该位置执行: assert(esp_task_stack_is_sane_cache_disabled());当时设备启动日志仍是: action 收到后会进入 SQLite/FATFS 持久化路径。Flash cache 关闭期间,任务栈必须位于内部 DRAM;旧 action worker 使用 PSRAM 栈,因而在持久化路径触发断言并复位。问题不在微信公众号按钮、SSE 命令内容、MCP 工具目录或网络连接。 4. 本地代码对应关系设备调度器会预注册后续提醒schedule_reminder_service.cc:563-578 在第一次触发时:
当前常量为: kMaximumAttempts = 3
kFollowUpDelay = 10min只有设备成功执行 acknowledge 才会取消后续任务 schedule_reminder_service.cc:373-416 的
旧固件在第 2 步之前已经复位,所以后续两个任务仍然存在。 Gateway 也把设备结果作为终态依据services.ts:1086-1136 的 services.ts:1139-1159 的 因此“IM 页面显示已提交”不能推导出“硬件已经确认并取消重试”。 5. 修复版对照证据
xTaskCreateWithCaps(..., MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)启动日志由: 改为: 修复版实验 数据库中该 action 为: 在后续日志窗口内没有出现 chain-29 的第二次或第三次 6. 当前契约和后续验收门禁这次问题的准确状态模型应记录为: 验收时必须同时满足:
本评论只记录问题和证据,没有修改生产 Gateway,也没有提交本地明文日志或 |
本轮提交与实板验证(2026-08-27)本轮已提交并推送: 修改内容
验证结果
实板摇动实验明文日志: 唯一一次有效摇动证据: 随后同一回合的完整顺序为: 判定:本次动作只产生 1 个 音频统计(摇动窗口及结束后)均为 0: 日志边界说明这份连续串口采集在摇动前的首次 MCP 连接阶段,曾出现一次独立的 请按上述提交和日志复核本 PR。 |
IMU 灵敏度小调(commit
|
IM 闭环复核进展已按本 PR 现有问题记录复核。重复提醒的直接根因仍是 本轮没有再叠加新的 Gateway 状态机改动,原因是现有状态机已经具备所需语义:IM 点击只创建待设备执行的 command;设备回传成功才使 action 进入 本地验证:
完整 Gateway 套件为 仍待实板验收:新建短时强提醒,只点击一次“知道了”,确认 |
f4592f3 to
34c3cd3
Compare
34c3cd3 to
5d2f29d
Compare
结论:本 PR 收敛 SparkBot 的中央说话动画、字幕滚动/时序和 Linx emotion 事件处理,并修复 ESP-IDF 工具链未加载时固件脚本无法构建的问题。当前版本已在 SparkBot 实板、Wi-Fi
zxp、百炼真实 TTS 注入下完成 3/3 回合测试,链路、TTS、字幕和资源加载均通过。请 Review 并按 Issue #376 验收;这是替代 PR #362 的最终实现,PR #362 不再承载后续代码。
Fixes #376
修复内容
speaking.gif;Linxemotion/action只在非 speaking 阶段更新,不会覆盖说话中的牛动画。kLlmEmotion事件,未知 key 回退本地 VoiceMood。tts_first_audio后显示,后续句段按tts_sentence_started顺序更新。scripts/firmware.py自动发现 ESP-IDF 6.x 与匹配 Python 环境,向构建子进程注入IDF_PATH、IDF_TOOLS_PATH、IDF_PYTHON_ENV_PATH、ESP_IDF_VERSION和IDF_VERSION。验证
94/94通过。198通过,1个按条件跳过。6.0.2;清空 ESP-IDF 环境变量后仍可由firmware.py自动发现并构建。voicelife.binSHA2566982e0dda5b4f7f1cae8b625349e59e7401b6c27be3471d314106ff4b4b1d3ce。7bc16f4712b7750bbbb04ca60cea69947c2e00ae7898387c84ef7cbe1b4badcb;model:SHA25687c77c650bbafbfb63f7cca4979f70302ef34078429635d01f716f5ae01b8e5b。98:a3:16:c7:81:b0,Wi-Fizxp,端口/dev/cu.usbmodem14201;刷写启动链、分区表、应用、assets、model,保留 NVS/FATFS。实板百炼日志
测试时间:2026-08-26 20:18--20:20(本地时间);模型
cosyvoice-v3-flash,音色longanhuan_v3。日志:
test-artifacts/v376/after-fix/multiturn-20260826-201823.log结果:
tts_started、tts_first_audio、多个tts_sentence_started和tts_stopped。scroll_observed=true;日志中的scroll_mode=explicit_left、负scroll_end和scroll_duration_ms均符合单向滚动设计。SPARKBOT_GIF_STARTED asset=speaking,Linx emotion 只记录DISPLAY_EMOTION_DEFERRED reason=speaking,没有覆盖 speaking GIF。tools/list分页完整:首包约 2.7 KB,后续 cursor 页约 651/2991/2220 字节,均成功发送。in_drop=0、in_pool_fail=0、out_reject=0、short_write=0、in_i2s_err=0、out_i2s_err=0;无LINX_WS_ERROR_EVENT、LINX_TX_SEND_FAIL、panic 或 RST。结果 JSON:
test-artifacts/v376/after-fix/multiturn-20260826-201823.json随后发起的等价儿童视角复测由操作者中止,未计入门禁,不覆盖上述已完成的 3/3 实板证据。
工作树与敏感信息
test-artifacts/、构建目录或任何 API token/Authorization。