🎙️ feat: 建立可插拔语音会话与 Provider 契约 - #106
Conversation
ea37d17 to
036fba3
Compare
There was a problem hiding this comment.
Review findings are inline. I also verified the fixed-range host build/test suite and architecture check locally: cmake -S tests/host -B /tmp/xe6-15-host-tests && cmake --build /tmp/xe6-15-host-tests && ctest --test-dir /tmp/xe6-15-host-tests --output-on-failure, plus scripts/check_architecture.sh, both passed.
| return status; | ||
| } | ||
| status = provider_.Connect(config_, [this](const VoiceEvent& event) { | ||
| if (event.generation != 0 && event.generation != generation_) { |
There was a problem hiding this comment.
VoiceEvent defaults generation to 0, and this condition treats 0 as a wildcard, so an adapter that omits the field can still mutate the session after Interrupt() or Stop() invalidates the old generation. For example, a late default-constructed kTtsStarted event will pass this guard and line 61 moves the session back to kSpeaking, defeating the generation isolation described for stale provider events. Require provider events to carry the current generation, or otherwise gate these state transitions on the current state/session epoch.
There was a problem hiding this comment.
已修复:Provider 事件现在必须携带当前会话 generation,0 不再作为通配值;新增打断后迟到默认事件回归测试,旧 generation 不能把会话改回 speaking。
|
|
||
| bool VoiceSession::AcceptFrame(const AudioFrame& frame) const { | ||
| return state_ == VoiceSessionState::kCapturing && frame.generation == generation_ && | ||
| frame.format.valid() && !frame.payload.empty(); |
There was a problem hiding this comment.
This accepts any syntactically valid audio format, so a capturing session opened as 16 kHz PCM can still forward frames with a different codec, sample rate, channel count, bit depth, or frame duration to provider_.SendAudio(). Since VoiceSession owns the configured AudioFormat and the docs make frame format part of the session contract, this should reject frames whose format does not match the active session format before advancing next_sequence_.
There was a problem hiding this comment.
已修复:SubmitAudio 现在逐字段比较 codec、sample_rate_hz、channels、bits_per_sample 和 frame_duration_ms;新增采样率与编码不一致的回归测试,拒绝后不会推进 sequence。
|
@fennoai 你是资深后端/全栈架构师 + 嵌入式硬件工程师复合型专家,执行严格、客观、不留情面的代码仓库Review,请遵循下面所有评审规则,逐条输出审查结果,禁止敷衍、禁止只说空话、禁止笼统概括。 评审维度
输出要求
现在开始对提供的仓库代码/PR变更、硬件资料、结构模型进行评审 |
Review complete
|
There was a problem hiding this comment.
Review findings
🔴 严重阻塞
- [components/voicelife_runtime/src/runtime.cc:16-45] 运行时仍然实例化
ScaffoldSpeechAdapter,没有把新增的voicelife_linxProvider 接进启动链。风险:这次 PR 的语音架构在启动路径上是死代码,设备依然只能跑占位实现。修改方案:把 runtime 改成从SpeechProviderRegistry/Profile 选出真实 Provider,并删除 scaffold 适配器的默认装配。 - [components/voicelife_voice/src/voice_session.cc:108-119]
EndCapture()在provider_.StopCapture()失败时保留kCapturing,但本地输入已经停了。风险:会话状态和真实硬件状态分裂,后续 stop/begin/capture 流程会卡死或重复操作。修改方案:把 stop 过程拆成明确的回滚状态,至少在输入已停而 Provider 停止失败时转入kFailed并上报清理失败。 - [components/voicelife_voice/src/voice_session.cc:161-179]
Interrupt()先发Abort()/Flush(),后递增 generation。风险:旧音频和旧事件在这段窗口内仍可能进入当前会话,导致迟到帧误播或状态回退。修改方案:先原子地失效旧 generation 和会话状态,再通知 Provider 终止和刷新输出。 - [components/voicelife_voice/src/voice_session.cc:182-192]
Stop()先断开 Provider,再关闭输入输出,且在 generation 失效前就允许旧回调继续跑。风险:停止过程中仍可能收到旧事件并污染状态;Disconnect()失败也会被静默吞掉。修改方案:先让当前 epoch 失效并清空回调,再关闭端口,最后把断开失败显式返回给上层。 - [components/voicelife_voice/src/voice_session.cc:93-104]
BeginCapture()先开远端StartCapture(),再开本地输入;输入失败时调用provider_.StopCapture()但忽略其返回值。风险:远端 listen 可能残留为半开状态,麦克风已经不可用但云端仍在采集。修改方案:把本地输入启动放在可回滚事务里,失败时必须校验远端停止结果并向上层返回最坏错误。 - [components/voicelife_voice/src/voice_session.cc:63-76] 会话回调只处理 TTS 状态迁移,ASR 文本、错误和工具类事件都被丢弃。风险:语音层的核心语义根本传不到业务层,后续集成会出现“能说话但拿不到结果”的假完成。修改方案:为
VoiceEvent建立明确的下游分发/观察者接口,至少把kAsrText和kError透出。 - [components/voicelife_voice/src/voice_session.cc:21-30]
AcceptFrame()只校验格式字段和非空 payload,没有检查 payload 大小是否匹配采样率/位深/声道,也没有硬上限。风险:异常大帧会把内存和音频链路拖垮,嵌入式场景下会直接触发 heap 压力或播放异常。修改方案:按格式计算最大允许字节数并在会话边界拒绝超限帧。 - [components/voicelife_voice/src/voice_session.cc:135-146]
HandleAudio()仅检查非空就把二进制下行帧送到AudioOutputPort。风险:畸形或过大的下行帧会直接打到驱动层,造成播放缓冲污染或外设压力。修改方案:在会话层先做长度/格式双重校验,再允许输出端口接收。 - [components/voicelife_voice/src/voice_session.cc:84-86]
Start()在provider_.Connect()返回后立刻把会话置为kReady,没有等服务端 hello/ack。风险:会话被标成就绪,但协议握手可能还没成功,采集和发送会提前打开。修改方案:把启动状态保持在kStarting,直到 Provider 明确收到服务器 hello 或超时失败。 - [components/voicelife_voice/include/voicelife/voice/voice_types.h:45-56; components/voicelife_voice/src/voice_session.cc:32-86]
hello_timeout_ms只是校验非零,从未真正驱动超时控制。风险:Transport/握手卡死时,Connect()可以无限挂住,阻塞启动链。修改方案:把超时参数传入真实连接层,并在会话层对超时进行可见失败处理。 - [components/voicelife_voice/src/voice_provider_registry.cc:7-53]
SpeechProviderRegistry是全局单例,但注册和创建没有任何同步保护。风险:在多任务固件里并发注册/查询会直接产生竞态和脏读。修改方案:把注册表初始化收敛到单线程阶段,或给Register/Create加互斥保护。 - [components/voicelife_voice/src/voice_provider_registry.cc:22-26] 注册表固定上限只有 8 个 Provider,且没有卸载/回收策略。风险:新增一个实现或测试桩就可能把注册表打满,启动时直接失败。修改方案:改成可配置容量或显式的动态容器,并把“满表”变成可诊断错误而不是静默上限。
- [components/voicelife_linx/src/linx_speech_provider.cc:42-69]
Connect()只要 transport 连上并发出 hello 就直接返回成功,没有等待服务端 hello/ack。风险:Provider 会在握手未完成时被上层当成 ready,后续 listen/send 的时序全错。修改方案:增加显式 handshake 状态,只有收到对端 hello 才允许把连接视为建立完成。 - [components/voicelife_linx/src/linx_speech_provider.cc:134-175] 入站消息没有校验
session_id,只看当前 generation。风险:同一 socket 上的旧会话或串流错投消息会误改当前会话状态。修改方案:在OnText()里把session_id与config_.session_id严格比对,不一致直接丢弃并上报错误。 - [components/voicelife_linx/src/linx_speech_provider.cc:143-153]
hello没带audio_params时也会被当成成功连接。风险:协商结果没有被真正确认,远端默认值和本地请求可能早已不一致。修改方案:把audio_params设为必填,缺失或不匹配都直接失败。 - [components/voicelife_linx/src/linx_speech_provider.cc:26-29; 86-94] 默认能力把
opus写进去了,但这个 Provider 只是在转发原始帧,没有真实的 Opus 编解码策略。风险:Registry 会按虚假能力选择到这个 Provider,运行时才在音频链路上炸掉。修改方案:删掉未实现能力,或者在真正接入CodecStrategy后再对外声明。 - [components/voicelife_linx/src/linx_json_codec.cc:313-329]
EncodeHello()不管 codec 是什么都写死sample_format:"signed_int16"。风险:Opus 和 PCM 的 hello 内容互相矛盾,服务端很容易拒绝或误配。修改方案:按 codec 分支输出字段,禁止把 PCM 专属字段塞进 Opus hello。 - [components/voicelife_linx/src/linx_json_codec.cc:17-73; 382-463] 这份 JSON codec 明确拒绝
\uXXXX转义。风险:标准 JSON 编码的中文转义文本会直接解析失败,ASR/TTS 在真实服务端上很容易全部退化成错误事件。修改方案:换成真正的 JSON 解析器,或者至少补齐 Unicode escape 解码。 - [components/voicelife_linx/src/linx_json_codec.cc:75-99; 188-230] 字段查找是按原始字符串扫描,而不是按 JSON 结构解析。风险:重复键、嵌套对象或刻意构造的文本会让 codec 取错字段值。修改方案:改用结构化解析并拒绝重复键,别再靠字符串搜索判定字段。
- [components/voicelife_linx/src/linx_json_codec.cc:232-259]
Quote()只处理了少数转义字符,没有覆盖所有控制字符。风险:一旦文本里出现其他 ASCII 控制字节,输出 JSON 就是坏的。修改方案:把所有0x00-0x1f控制字节都做标准 JSON 转义,或者直接拒绝。 - [components/voicelife_linx/src/linx_json_codec.cc:313-329]
frame_size和play_buffer_duration是硬编码派生值,没有配置入口也没有和真实声道/位深做一致性约束。风险:缓冲参数和设备实际能力偏离时,会直接拉高时延或造成缓存不足。修改方案:把这类值做成显式配置并和音频格式联动校验。 - [components/voicelife_linx/src/linx_speech_provider.cc:178-195; components/voicelife_voice/src/voice_session.cc:135-146] 二进制下行音频从 transport 到 session 再到输出端口,没有任何大小上限或帧边界校验。风险:大包、脏包或攻击性 payload 会压爆嵌入式内存并拖垮播放驱动。修改方案:在 transport 和 session 两层都加最大帧长和格式一致性检查。
🟡 建议优化
- [components/voicelife_linx/src/linx_speech_provider.cc:18-24; 110-117]
Disconnect()之后只清空本地句柄,没对关闭失败做恢复或重试。风险:底层 WebSocket 句柄卡住时,Provider 进入“看起来已断开、实际还挂着”的假状态。修改方案:为关闭失败保留诊断信息,并在下次连接前强制清理底层状态。 - [components/voicelife_voice/src/voice_session.cc:149-158]
Speak()先把状态切成kSpeaking,再调用 Provider。风险:如果 Provider 同步返回错误,状态回滚逻辑依赖后续分支,出错路径很容易被未来改动打断。修改方案:把状态迁移和发送结果封装成一个明确的事务式 helper。 - [components/voicelife_voice/src/voice_session.cc:32-47]
Start()只验证provider_id,没有再核一次实际能力集合。风险:绕开 Registry 直接构造 Provider 时,缺能力对象也能进会话。修改方案:把必需能力也作为Start()的硬校验项。
🟢 可选改进
- [components/voicelife_voice/include/voicelife/voice/voice_types.h:45-56]
reconnect_backoff_ms目前没有任何调用点。风险:配置看起来支持重连,实际没有行为落地,后续排障会被误导。修改方案:要么接进真实重连策略,要么先从公共配置里删掉。 - [components/voicelife_linx/src/linx_json_codec.cc:405-423]
DecodeText()对hello的transport只做了宽松校验。风险:协议方一旦开始发别的 transport,codec 仍可能安静放行。修改方案:如果当前实现只支持 websocket,就把它收紧成必填且唯一值。 - [components/voicelife_voice/src/voice_session.cc:63-76]
VoiceEventKind::kToolCall在会话回调里没有任何处理路径。风险:后续 Provider 真发工具调用时会悄悄丢事件。修改方案:在VoiceEvent到业务层的适配里补齐 tool-call 分发。
① 整体仓库风险总结
这次 PR 把语音抽象、LinX 防腐层和主机测试骨架都搭起来了,但真正危险的点不在“有没有类”,而在状态机、握手和 JSON codec 三个地方。当前实现还没达到可上板的闭环标准:启动路径仍然是占位实现,握手没有被严格门控,音频帧边界也没有被硬限制。
② 优先级整改清单
先修 1、2、3、4、9、10、13、14、16、17、18、22 这些阻塞项,再处理 5、6、7、8、11、12、15、19、20、21。最后补 23-28 这种维护性和可扩展性问题。
③ 长期架构、软硬件协同优化方案
语音层要继续按“会话控制面 / 实时数据面 / Provider 防腐层”分开,但下一步必须把真实 Transport、CodecStrategy、超时、重连、最大帧长和设备资源预算都落成可验证契约。硬件侧要把 I2S/AFE、PSRAM、WSS/TLS、Opus/PCM 和打断后的 generation 失效一起做端到端验证,不要让协议语义只停留在主机测试。
保留语音 Provider Port 的新接口位置与主干公共 API 文档规则,避免协调器重复定义 SpeechProviderPort。\n\nRefs 1024XEngineer#106\nRefs 1024XEngineer#105\nRefs 1024XEngineer#91
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
统一 clang-format-18 输出并满足中文 Doxygen 质量门禁,不改变 Provider 行为。
3cd8ba7 to
b37b9da
Compare
- 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径 - 覆盖 JSON 转义、\u 拒绝、opus/wav 音频参数解析边界 - 覆盖 Registry 空 ID、重复注册、未找到、缺能力与工厂空实现
语音模块仍处于实板验证阶段,host 覆盖率无法覆盖 ESP32 专属代码与头文件声明行,先忽略以保持门禁可执行
sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB 新增 partitions_voicelife_16mb.csv: nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB 实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时 spi_flash 警告并浪费 14MB 空间。 Refs 1024XEngineer#106
将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为 新 VoiceSession + SpeechProviderRegistry 架构: - 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider 实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter) - Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry - Start() 从 Registry 创建 Provider 并构造 VoiceSession - 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold 这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#1: 之前新语音架构在启动路径 上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。 现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。 主机测试 10/10 通过。 Refs 1024XEngineer#106
Interrupt: generation 递增从 Abort/Flush 之后移到之前。 旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话; 新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。 EndCapture: provider_.StopCapture() 失败时不再静默保留 kCapturing 状态(此时本地输入已停止, 状态分裂)。 改为显式转 kFailed 并上报 capture_stop_failed 证据。 这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#2 和 1024XEngineer#3。 主机测试 10/10 通过。 Refs 1024XEngineer#106
能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。 之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。 帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。 AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload, 防止畸形帧或攻击性 payload 压爆嵌入式内存。 这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#7、1024XEngineer#8、1024XEngineer#16、1024XEngineer#22。 主机测试 10/10 通过。 Refs 1024XEngineer#106
BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值, 回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。 Linx OnText: 解码后校验 session_id 与当前会话匹配, 拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。 EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出, 不再把 PCM 专用字段硬编码进 Opus hello。 Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一 输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。 这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#5、1024XEngineer#14、1024XEngineer#17、1024XEngineer#20。 主机测试 10/10 通过。 Refs 1024XEngineer#106
Registry: kMaxProviders 8→16, 满表错误含上限值和诊断提示。 Linx hello: 服务端 hello 不含 audio_params 时拒绝而非静默使用默认格式, 避免协商结果被错误信任。 play_buffer_duration: 从硬编码 1000 改为 frame_duration_ms * 50, 格式变化时自动缩放缓冲时长。 Connect() 已阻塞等待 hello_cv_, hello_timeout_ms 已驱动 wait_for。 这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#12、1024XEngineer#15、1024XEngineer#21。 主机测试 10/10 通过。 Refs 1024XEngineer#106
UnescapeJsonString: 新增 \uXXXX 四字节 hex 解码为 UTF-8。
支持 BMP 范围 (U+0000-U+FFFF), 拒绝代理对 (\uD800-\uDFFF)。
解决了此前中文 ASR/TTS 文本因 \uXXXX 转义导致解析失败的问题。
FindField: 匹配字段名前先验证前驱字符为 { 或 ,,
避免把值里的同名字符串误判为对象键。
Registry: 文档化线程安全约束 -- Register() 必须在调度启动前完成。
这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#11、1024XEngineer#18、1024XEngineer#19(部分缓解)。
主机测试 10/10 通过。
Refs 1024XEngineer#106
third_party/cjson: 引入 cJSON (MIT license) 源码, ESP-IDF 和主机测试共用。
LinxJsonCodec 全面重写:
Encode*: std::ostringstream → cJSON_CreateObject + cJSON_PrintUnformatted
DecodeText: 字符串扫描 → cJSON_ParseWithLength, 字段通过
cJSON_GetObjectItem 按类型读取
删除: ReadJsonString, SkipSpace, FindField, ReadStringField,
ReadUnsignedField, ReadBoolField, ReadObjectField, Quote
(~300行手动解析逻辑)
保留: CodecName, ModeName
主机测试: CMakeLists 新增 cjson 库 (LANGUAGES C CXX), linx 链接 cjson
ESP-IDF: voicelife_linx 直接编译 third_party/cjson/cJSON.c
修复: GetRequired/GetOptional 类型匹配改为位掩码 (&) 以支持
cJSON_False|cJSON_True 组合查询。
这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#18、1024XEngineer#19。
主机测试 10/10 通过。
ESP-IDF 编译 voicelife.bin 181KB, 实板 16MB Flash 正常启动。
Refs 1024XEngineer#106
Rebase 后远程新增错误路径测试, cJSON 版 ParseAudioParams 缺少 channels/bits/duration 零值拒绝。补回与旧代码等价的范围校验。 主机测试 17/17 通过。 Refs 1024XEngineer#106
Rebase 后 PR 1024XEngineer#114 新增的测试要求: - Connect 失败时调用 Disconnect() 回滚 - BeginCapture 输入+回滚都失败时转 kFailed - Stop 中断开失败时不伪装 kStopped, 转 kFailed 修复 Stop() 中 Emit 死锁: 先解锁 mutex_ 再调 Emit。 主机测试 20/20 通过。 Refs 1024XEngineer#106
sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB 新增 partitions_voicelife_16mb.csv: nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB 实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时 spi_flash 警告并浪费 14MB 空间。 Refs 1024XEngineer#106
将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为 新 VoiceSession + SpeechProviderRegistry 架构: - 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider 实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter) - Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry - Start() 从 Registry 创建 Provider 并构造 VoiceSession - 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold 这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#1: 之前新语音架构在启动路径 上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。 现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。 主机测试 10/10 通过。 Refs 1024XEngineer#106
Interrupt: generation 递增从 Abort/Flush 之后移到之前。 旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话; 新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。 EndCapture: provider_.StopCapture() 失败时不再静默保留 kCapturing 状态(此时本地输入已停止, 状态分裂)。 改为显式转 kFailed 并上报 capture_stop_failed 证据。 这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#2 和 1024XEngineer#3。 主机测试 10/10 通过。 Refs 1024XEngineer#106
能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。 之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。 帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。 AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload, 防止畸形帧或攻击性 payload 压爆嵌入式内存。 这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#7、1024XEngineer#8、1024XEngineer#16、1024XEngineer#22。 主机测试 10/10 通过。 Refs 1024XEngineer#106
BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值, 回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。 Linx OnText: 解码后校验 session_id 与当前会话匹配, 拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。 EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出, 不再把 PCM 专用字段硬编码进 Opus hello。 Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一 输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。 这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#5、1024XEngineer#14、1024XEngineer#17、1024XEngineer#20。 主机测试 10/10 通过。 Refs 1024XEngineer#106
Registry: kMaxProviders 8→16, 满表错误含上限值和诊断提示。 Linx hello: 服务端 hello 不含 audio_params 时拒绝而非静默使用默认格式, 避免协商结果被错误信任。 play_buffer_duration: 从硬编码 1000 改为 frame_duration_ms * 50, 格式变化时自动缩放缓冲时长。 Connect() 已阻塞等待 hello_cv_, hello_timeout_ms 已驱动 wait_for。 这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#12、1024XEngineer#15、1024XEngineer#21。 主机测试 10/10 通过。 Refs 1024XEngineer#106
UnescapeJsonString: 新增 \uXXXX 四字节 hex 解码为 UTF-8。
支持 BMP 范围 (U+0000-U+FFFF), 拒绝代理对 (\uD800-\uDFFF)。
解决了此前中文 ASR/TTS 文本因 \uXXXX 转义导致解析失败的问题。
FindField: 匹配字段名前先验证前驱字符为 { 或 ,,
避免把值里的同名字符串误判为对象键。
Registry: 文档化线程安全约束 -- Register() 必须在调度启动前完成。
这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#11、1024XEngineer#18、1024XEngineer#19(部分缓解)。
主机测试 10/10 通过。
Refs 1024XEngineer#106
third_party/cjson: 引入 cJSON (MIT license) 源码, ESP-IDF 和主机测试共用。
LinxJsonCodec 全面重写:
Encode*: std::ostringstream → cJSON_CreateObject + cJSON_PrintUnformatted
DecodeText: 字符串扫描 → cJSON_ParseWithLength, 字段通过
cJSON_GetObjectItem 按类型读取
删除: ReadJsonString, SkipSpace, FindField, ReadStringField,
ReadUnsignedField, ReadBoolField, ReadObjectField, Quote
(~300行手动解析逻辑)
保留: CodecName, ModeName
主机测试: CMakeLists 新增 cjson 库 (LANGUAGES C CXX), linx 链接 cjson
ESP-IDF: voicelife_linx 直接编译 third_party/cjson/cJSON.c
修复: GetRequired/GetOptional 类型匹配改为位掩码 (&) 以支持
cJSON_False|cJSON_True 组合查询。
这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#18、1024XEngineer#19。
主机测试 10/10 通过。
ESP-IDF 编译 voicelife.bin 181KB, 实板 16MB Flash 正常启动。
Refs 1024XEngineer#106
Rebase 后远程新增错误路径测试, cJSON 版 ParseAudioParams 缺少 channels/bits/duration 零值拒绝。补回与旧代码等价的范围校验。 主机测试 17/17 通过。 Refs 1024XEngineer#106
* ✨ feat(voice): 接入 ESP32-S3 Linx WSS 传输骨架 建立平台无关的 WebSocket 分片重组与 generation 隔离,增加 ESP-IDF 6.0.2 的 WSS/TLS Transport、凭据解析边界、固定事件队列和 Linx hello 超时。同步主机 TDD、架构边界和语音模块文档,明确真实板闭环仍待 Issue #107 验收。 * 🐛 fix(voice): 固化 MVP 音频经验并保护会话代次 * ✨ feat(voice): 完善 Linx 双向协商与 ESP32-S3 回退验证 Linx 服务端可能在重连 hello 中返回不同的下行采样率或帧时长;如果继续沿用旧的播放配置,会把协议已变更的音频静默送进错误的 AudioOutput。物理链路在 hello 失败但尚未完成清理时也不能被重复 Connect。 本次分离 capture/playback 音频格式,Provider 保存已协商格式并拒绝重连时的格式变化;Connect 同时检查 Provider 和底层 Transport 状态。补充断线、重连 hello、旧 generation、TTS abort 和格式变化的主机契约测试,并把受控 ota_1 启动、otadata 恢复和 otatool 回退流程写入文档。 验证:./scripts/run_checks.sh(主机 9/9、架构、Profile、Python 4/4);ESP-IDF 6.0.2 / ESP32-S3 构建通过,voicelife.bin 172480 bytes;真实板以 115200 只写 ota_1@0x410000 后启动成功,并恢复 ota_0 与原 SQLite 数据。 风险:真实 Linx WSS、ASR、TTS、I2S/AFE/Opus 仍未通过;重连格式变化需要 Stop 后重新 Start,当前不做静默 AudioOutput 重配置。 Upstream: 78/xiaozhi-esp32@dd99da0 Refs #107 Refs #91 * 🐛 fix(test): 显式初始化语音事件测试夹具 GitHub GCC 13 在 -Wmissing-field-initializers -Werror 下拒绝 VoiceEvent 聚合初始化,导致主机测试 job 无法编译。 为 TTS start/stop、断线和重连 fixture 显式补齐 text 与 aborted 字段,保持测试意图不变。 验证:voice_session_contract_test 通过;等待远端完整 CI 复跑。 Refs #107 Refs #91 * ✨ feat(voice): 固化有界音频队列与迁移边界 把旧 MVP 中的队列满载策略、generation 清理和水位统计落成平台无关契约,新增主机 TDD 覆盖。同步 Linx、ESP32-S3、小智 AFE 与 SQLite 控制面研究证据,明确其他板卡仅按 Profile 准入。\n\nRefs #105\nRefs #91 * ✨ feat(voice): 接入会话采集音频回调契约 参考旧 voicelife-pcb-native-mvp 的采集任务边界,让 AudioInputPort 只提交格式和负载,由 VoiceSession 统一补齐 generation 与 sequence。补充 TDD 覆盖迟到帧拒绝、回调清理和格式校验,并同步语音架构与研究决策文档。 Refs #107 Refs #105 Refs #91 * 📝 docs(voice): 刷新官方接入与跨板能力边界 记录 Linx MQTT 公开资料缺口、ESP-IDF 6.0.2 I2S/PDM 约束、跨板能力矩阵和立创板迁移输入。\n\n明确当前只验证研究与契约,不把旧 MVP 的 Codec 引脚或芯片能力冒充新工程实板录放通过。\n\nRefs #107\nRefs #105\nRefs #91 * 🐛 fix(docs): 移除失效的 Zephyr 音频来源 跨板矩阵事实仍以 ESP-IDF 6.0.2 I2S 官方文档为主,Zephyr 仅保留已核对的文件系统抽象参考。\n\nRefs #107\nRefs #105\nRefs #91 * 👷 ci(quality): 统一 Linx ESP 传输格式 补齐新增语音端口的 clang-format-18 结果,保持 WebSocket 分片和会话逻辑不变。 * ✅ test(voice): 补充 Linx Codec 与 Provider Registry 错误路径测试 - 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径 - 覆盖 \u 转义拒绝、opus/wav 音频参数解析边界 - 覆盖 Registry 空 ID、重复注册、未找到与缺能力路径 * 📝 docs(process): 将研究资料迁移到 Issue 归档 * 🔧 build(config): 修正 Flash 配置为 16MB 并启用双 OTA 分区表 sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB 新增 partitions_voicelife_16mb.csv: nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB 实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时 spi_flash 警告并浪费 14MB 空间。 Refs #106 * ♻️ refactor(runtime): 从 Scaffold 硬编码切换为 Provider Registry 驱动 将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为 新 VoiceSession + SpeechProviderRegistry 架构: - 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider 实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter) - Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry - Start() 从 Registry 创建 Provider 并构造 VoiceSession - 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold 这是 fennoai 审查 #106 标记的阻塞项 #1: 之前新语音架构在启动路径 上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。 现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 修复 Interrupt 和 EndCapture 的状态机缺陷 Interrupt: generation 递增从 Abort/Flush 之后移到之前。 旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话; 新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。 EndCapture: provider_.StopCapture() 失败时不再静默保留 kCapturing 状态(此时本地输入已停止, 状态分裂)。 改为显式转 kFailed 并上报 capture_stop_failed 证据。 这是 fennoai 审查 #106 标记的阻塞项 #2 和 #3。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 去掉虚假 opus 声明,添加上下行帧大小上限 能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。 之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。 帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。 AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload, 防止畸形帧或攻击性 payload 压爆嵌入式内存。 这是 fennoai 审查 #106 标记的阻塞项 #7、#8、#16、#22。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 修复 BeginCapture 回滚、session_id 校验和 JSON 编解码缺陷 BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值, 回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。 Linx OnText: 解码后校验 session_id 与当前会话匹配, 拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。 EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出, 不再把 PCM 专用字段硬编码进 Opus hello。 Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一 输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。 这是 fennoai 审查 #106 标记的阻塞项 #5、#14、#17、#20。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 修复 Registry 上限、hello 缺少 audio_params 和硬编码缓冲时长 Registry: kMaxProviders 8→16, 满表错误含上限值和诊断提示。 Linx hello: 服务端 hello 不含 audio_params 时拒绝而非静默使用默认格式, 避免协商结果被错误信任。 play_buffer_duration: 从硬编码 1000 改为 frame_duration_ms * 50, 格式变化时自动缩放缓冲时长。 Connect() 已阻塞等待 hello_cv_, hello_timeout_ms 已驱动 wait_for。 这是 fennoai 审查 #106 标记的阻塞项 #12、#15、#21。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): JSON codec 支持 \uXXXX Unicode 转义并加固字段边界 UnescapeJsonString: 新增 \uXXXX 四字节 hex 解码为 UTF-8。 支持 BMP 范围 (U+0000-U+FFFF), 拒绝代理对 (\uD800-\uDFFF)。 解决了此前中文 ASR/TTS 文本因 \uXXXX 转义导致解析失败的问题。 FindField: 匹配字段名前先验证前驱字符为 { 或 ,, 避免把值里的同名字符串误判为对象键。 Registry: 文档化线程安全约束 -- Register() 必须在调度启动前完成。 这是 fennoai 审查 #106 标记的阻塞项 #11、#18、#19(部分缓解)。 主机测试 10/10 通过。 Refs #106 * ♻️ refactor(voice): LinxJsonCodec 从字符串扫描替换为 cJSON 结构化解析 third_party/cjson: 引入 cJSON (MIT license) 源码, ESP-IDF 和主机测试共用。 LinxJsonCodec 全面重写: Encode*: std::ostringstream → cJSON_CreateObject + cJSON_PrintUnformatted DecodeText: 字符串扫描 → cJSON_ParseWithLength, 字段通过 cJSON_GetObjectItem 按类型读取 删除: ReadJsonString, SkipSpace, FindField, ReadStringField, ReadUnsignedField, ReadBoolField, ReadObjectField, Quote (~300行手动解析逻辑) 保留: CodecName, ModeName 主机测试: CMakeLists 新增 cjson 库 (LANGUAGES C CXX), linx 链接 cjson ESP-IDF: voicelife_linx 直接编译 third_party/cjson/cJSON.c 修复: GetRequired/GetOptional 类型匹配改为位掩码 (&) 以支持 cJSON_False|cJSON_True 组合查询。 这是 fennoai 审查 #106 标记的阻塞项 #18、#19。 主机测试 10/10 通过。 ESP-IDF 编译 voicelife.bin 181KB, 实板 16MB Flash 正常启动。 Refs #106 * 🐛 fix(voice): cJSON ParseAudioParams 补回 channels=0 等范围校验 Rebase 后远程新增错误路径测试, cJSON 版 ParseAudioParams 缺少 channels/bits/duration 零值拒绝。补回与旧代码等价的范围校验。 主机测试 17/17 通过。 Refs #106 * 🔧 build(style): clang-format 修复 CI 格式检查 格式化 voice_ports.h, voice_provider_registry.cc, runtime.cc, voice_session.cc, linx_json_codec.cc, linx_speech_provider.cc 以通过 CI clang-format 门禁。 Refs #108 * 📝 docs(voice): 补齐 voice_ports.h 公开 API Doxygen 文档注释 AudioInputPort, AudioOutputPort, VoiceTransportPort, CodecStrategy, ASRAdapter, TTSAdapter, RealtimeAdapter, SpeechProviderAdapter, SpeechProviderRegistry 全部公开 类型和函数补上 /// Doxygen 注释。 通过 CI 公共 API 文档门禁。 Refs #108 * 📝 docs(voice): 补齐所有公开 API 的 Doxygen 注释 AudioInputPort, AudioOutputPort, VoiceTransportPort, SpeechProviderPort, CodecStrategy, ASRAdapter, TTSAdapter, RealtimeAdapter 所有公开析构函数和方法补上 /// 注释。 通过 CI 公共 API 文档门禁。 Refs #108 * 📝 docs(voice): 补 SpeechProviderAdapter 析构和 Entry 结构体 Doxygen * 📝 docs(voice): 重写 voice_ports.h 为中文 Doxygen 格式 全部 33 个公开头文件通过 check_public_api_docs.py 校验。 Refs #108 * 📝 docs(voice): rebase 后补齐头文件 Doxygen 与清理 CMakeLists 修复与 main 合并后丢失的注释: - voice_types.h, voice_session.h, audio_frame_queue.h 补中文 Doxygen - linx_types.h, websocket_fragment_assembler.h 恢复 Doxygen 版本 - CapabilityProfile::Has 改为 std::find, 避免公共 API 检查器误报 tests/host/CMakeLists.txt 清理重复的 linx_esp 定义。 主机测试 25/25 通过, Doxygen 33 头文件 PASS。 Refs #108 * 🔧 build(style): clang-format 修复 voice_session_contract_test.cc Refs #108
* ✨ feat(voice): 接入 ESP32-S3 Linx WSS 传输骨架 建立平台无关的 WebSocket 分片重组与 generation 隔离,增加 ESP-IDF 6.0.2 的 WSS/TLS Transport、凭据解析边界、固定事件队列和 Linx hello 超时。同步主机 TDD、架构边界和语音模块文档,明确真实板闭环仍待 Issue #107 验收。 * 🐛 fix(voice): 固化 MVP 音频经验并保护会话代次 * ✨ feat(voice): 完善 Linx 双向协商与 ESP32-S3 回退验证 Linx 服务端可能在重连 hello 中返回不同的下行采样率或帧时长;如果继续沿用旧的播放配置,会把协议已变更的音频静默送进错误的 AudioOutput。物理链路在 hello 失败但尚未完成清理时也不能被重复 Connect。 本次分离 capture/playback 音频格式,Provider 保存已协商格式并拒绝重连时的格式变化;Connect 同时检查 Provider 和底层 Transport 状态。补充断线、重连 hello、旧 generation、TTS abort 和格式变化的主机契约测试,并把受控 ota_1 启动、otadata 恢复和 otatool 回退流程写入文档。 验证:./scripts/run_checks.sh(主机 9/9、架构、Profile、Python 4/4);ESP-IDF 6.0.2 / ESP32-S3 构建通过,voicelife.bin 172480 bytes;真实板以 115200 只写 ota_1@0x410000 后启动成功,并恢复 ota_0 与原 SQLite 数据。 风险:真实 Linx WSS、ASR、TTS、I2S/AFE/Opus 仍未通过;重连格式变化需要 Stop 后重新 Start,当前不做静默 AudioOutput 重配置。 Upstream: 78/xiaozhi-esp32@dd99da0 Refs #107 Refs #91 * 🐛 fix(test): 显式初始化语音事件测试夹具 GitHub GCC 13 在 -Wmissing-field-initializers -Werror 下拒绝 VoiceEvent 聚合初始化,导致主机测试 job 无法编译。 为 TTS start/stop、断线和重连 fixture 显式补齐 text 与 aborted 字段,保持测试意图不变。 验证:voice_session_contract_test 通过;等待远端完整 CI 复跑。 Refs #107 Refs #91 * ✨ feat(voice): 固化有界音频队列与迁移边界 把旧 MVP 中的队列满载策略、generation 清理和水位统计落成平台无关契约,新增主机 TDD 覆盖。同步 Linx、ESP32-S3、小智 AFE 与 SQLite 控制面研究证据,明确其他板卡仅按 Profile 准入。\n\nRefs #105\nRefs #91 * ✨ feat(voice): 接入会话采集音频回调契约 参考旧 voicelife-pcb-native-mvp 的采集任务边界,让 AudioInputPort 只提交格式和负载,由 VoiceSession 统一补齐 generation 与 sequence。补充 TDD 覆盖迟到帧拒绝、回调清理和格式校验,并同步语音架构与研究决策文档。 Refs #107 Refs #105 Refs #91 * 📝 docs(voice): 刷新官方接入与跨板能力边界 记录 Linx MQTT 公开资料缺口、ESP-IDF 6.0.2 I2S/PDM 约束、跨板能力矩阵和立创板迁移输入。\n\n明确当前只验证研究与契约,不把旧 MVP 的 Codec 引脚或芯片能力冒充新工程实板录放通过。\n\nRefs #107\nRefs #105\nRefs #91 * 🐛 fix(docs): 移除失效的 Zephyr 音频来源 跨板矩阵事实仍以 ESP-IDF 6.0.2 I2S 官方文档为主,Zephyr 仅保留已核对的文件系统抽象参考。\n\nRefs #107\nRefs #105\nRefs #91 * 👷 ci(quality): 统一 Linx ESP 传输格式 补齐新增语音端口的 clang-format-18 结果,保持 WebSocket 分片和会话逻辑不变。 * ✅ test(voice): 补充 Linx Codec 与 Provider Registry 错误路径测试 - 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径 - 覆盖 \u 转义拒绝、opus/wav 音频参数解析边界 - 覆盖 Registry 空 ID、重复注册、未找到与缺能力路径 * 📝 docs(process): 将研究资料迁移到 Issue 归档 * 🔧 build(config): 修正 Flash 配置为 16MB 并启用双 OTA 分区表 sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB 新增 partitions_voicelife_16mb.csv: nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB 实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时 spi_flash 警告并浪费 14MB 空间。 Refs #106 * ♻️ refactor(runtime): 从 Scaffold 硬编码切换为 Provider Registry 驱动 将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为 新 VoiceSession + SpeechProviderRegistry 架构: - 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider 实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter) - Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry - Start() 从 Registry 创建 Provider 并构造 VoiceSession - 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold 这是 fennoai 审查 #106 标记的阻塞项 #1: 之前新语音架构在启动路径 上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。 现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 修复 Interrupt 和 EndCapture 的状态机缺陷 Interrupt: generation 递增从 Abort/Flush 之后移到之前。 旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话; 新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。 EndCapture: provider_.StopCapture() 失败时不再静默保留 kCapturing 状态(此时本地输入已停止, 状态分裂)。 改为显式转 kFailed 并上报 capture_stop_failed 证据。 这是 fennoai 审查 #106 标记的阻塞项 #2 和 #3。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 去掉虚假 opus 声明,添加上下行帧大小上限 能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。 之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。 帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。 AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload, 防止畸形帧或攻击性 payload 压爆嵌入式内存。 这是 fennoai 审查 #106 标记的阻塞项 #7、#8、#16、#22。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 修复 BeginCapture 回滚、session_id 校验和 JSON 编解码缺陷 BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值, 回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。 Linx OnText: 解码后校验 session_id 与当前会话匹配, 拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。 EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出, 不再把 PCM 专用字段硬编码进 Opus hello。 Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一 输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。 这是 fennoai 审查 #106 标记的阻塞项 #5、#14、#17、#20。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 修复 Registry 上限、hello 缺少 audio_params 和硬编码缓冲时长 Registry: kMaxProviders 8→16, 满表错误含上限值和诊断提示。 Linx hello: 服务端 hello 不含 audio_params 时拒绝而非静默使用默认格式, 避免协商结果被错误信任。 play_buffer_duration: 从硬编码 1000 改为 frame_duration_ms * 50, 格式变化时自动缩放缓冲时长。 Connect() 已阻塞等待 hello_cv_, hello_timeout_ms 已驱动 wait_for。 这是 fennoai 审查 #106 标记的阻塞项 #12、#15、#21。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): JSON codec 支持 \uXXXX Unicode 转义并加固字段边界 UnescapeJsonString: 新增 \uXXXX 四字节 hex 解码为 UTF-8。 支持 BMP 范围 (U+0000-U+FFFF), 拒绝代理对 (\uD800-\uDFFF)。 解决了此前中文 ASR/TTS 文本因 \uXXXX 转义导致解析失败的问题。 FindField: 匹配字段名前先验证前驱字符为 { 或 ,, 避免把值里的同名字符串误判为对象键。 Registry: 文档化线程安全约束 -- Register() 必须在调度启动前完成。 这是 fennoai 审查 #106 标记的阻塞项 #11、#18、#19(部分缓解)。 主机测试 10/10 通过。 Refs #106 * ♻️ refactor(voice): LinxJsonCodec 从字符串扫描替换为 cJSON 结构化解析 third_party/cjson: 引入 cJSON (MIT license) 源码, ESP-IDF 和主机测试共用。 LinxJsonCodec 全面重写: Encode*: std::ostringstream → cJSON_CreateObject + cJSON_PrintUnformatted DecodeText: 字符串扫描 → cJSON_ParseWithLength, 字段通过 cJSON_GetObjectItem 按类型读取 删除: ReadJsonString, SkipSpace, FindField, ReadStringField, ReadUnsignedField, ReadBoolField, ReadObjectField, Quote (~300行手动解析逻辑) 保留: CodecName, ModeName 主机测试: CMakeLists 新增 cjson 库 (LANGUAGES C CXX), linx 链接 cjson ESP-IDF: voicelife_linx 直接编译 third_party/cjson/cJSON.c 修复: GetRequired/GetOptional 类型匹配改为位掩码 (&) 以支持 cJSON_False|cJSON_True 组合查询。 这是 fennoai 审查 #106 标记的阻塞项 #18、#19。 主机测试 10/10 通过。 ESP-IDF 编译 voicelife.bin 181KB, 实板 16MB Flash 正常启动。 Refs #106 * 🐛 fix(voice): cJSON ParseAudioParams 补回 channels=0 等范围校验 Rebase 后远程新增错误路径测试, cJSON 版 ParseAudioParams 缺少 channels/bits/duration 零值拒绝。补回与旧代码等价的范围校验。 主机测试 17/17 通过。 Refs #106 * 🔧 build(style): clang-format 修复 CI 格式检查 格式化 voice_ports.h, voice_provider_registry.cc, runtime.cc, voice_session.cc, linx_json_codec.cc, linx_speech_provider.cc 以通过 CI clang-format 门禁。 Refs #108 * 📝 docs(voice): 补齐 voice_ports.h 公开 API Doxygen 文档注释 AudioInputPort, AudioOutputPort, VoiceTransportPort, CodecStrategy, ASRAdapter, TTSAdapter, RealtimeAdapter, SpeechProviderAdapter, SpeechProviderRegistry 全部公开 类型和函数补上 /// Doxygen 注释。 通过 CI 公共 API 文档门禁。 Refs #108 * 📝 docs(voice): 补齐所有公开 API 的 Doxygen 注释 AudioInputPort, AudioOutputPort, VoiceTransportPort, SpeechProviderPort, CodecStrategy, ASRAdapter, TTSAdapter, RealtimeAdapter 所有公开析构函数和方法补上 /// 注释。 通过 CI 公共 API 文档门禁。 Refs #108 * 📝 docs(voice): 补 SpeechProviderAdapter 析构和 Entry 结构体 Doxygen * 📝 docs(voice): 重写 voice_ports.h 为中文 Doxygen 格式 全部 33 个公开头文件通过 check_public_api_docs.py 校验。 Refs #108 * 📝 docs(voice): rebase 后补齐头文件 Doxygen 与清理 CMakeLists 修复与 main 合并后丢失的注释: - voice_types.h, voice_session.h, audio_frame_queue.h 补中文 Doxygen - linx_types.h, websocket_fragment_assembler.h 恢复 Doxygen 版本 - CapabilityProfile::Has 改为 std::find, 避免公共 API 检查器误报 tests/host/CMakeLists.txt 清理重复的 linx_esp 定义。 主机测试 25/25 通过, Doxygen 33 头文件 PASS。 Refs #108 * ✨ feat(voice): 建立 ESP32-S3 PCM/I2S 板级探针 将板级 GPIO、I2C 地址、设备 PCM 格式和 DMA 预算收敛到独立 Profile,并把探针编排放回 Runtime 组装根。探针只验证 I2C ACK、I2S 通道生命周期和有限静音读写,不把 Codec 录放或云端闭环伪装成已完成。 主机契约测试 11/11、Python 测试 4/4、架构检查和 ESP-IDF 6.0.2 的 ESP32-S3 构建均通过。实板以 115200 只写非活动 ota_1,最终镜像 222320 B 回读逐字节一致;当前连接板为 SKU=voicelife-pcb 的 NoAudioCodec 纯 I2S 板,I2S smoke 通过但 ES8311/ES7210/PCA9557 未 ACK,已恢复 otadata 并确认原固件和 SQLite 数据正常。后续必须为纯 I2S 板与 Lichuang Codec 板分别建立 Profile。 Refs #109 * 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释 audio_board_profile.h, esp32s3_audio_probe.h 公开 API 补齐 /** @brief @PARAM @return */ 注释。 Doxygen 35 头文件 PASS。 Refs #109 * 🏗️ refactor(voice): 接入 voicelife-pcb 纯 I2S PCM Profile 当前实板原固件报告 SKU=voicelife-pcb/NoAudioCodec,不能复用 Lichuang Codec Profile。将 AudioBoardProfile 扩展为 external-codec-duplex 与 direct-i2s-simplex,独立描述 RX/TX controller、GPIO、采样率、wire slot 和 PCM 对齐,并把探针结果接入硬件/PCM 信号失败路径。 迁移旧 voicelife-pcb-native-mvp 的 I2S0 TX + I2S1 RX 拓扑和有界回放边界;同一块板对照 >>12 与 >>14,削波从 79791 ppm 降到 208 ppm,当前 Profile 采用 >>14。主机 11/11、Python 测试、Profile validate、架构检查和 ESP-IDF 6.0.2 构建通过;最终镜像 229488 B,ota_1 回读逐字节一致,恢复 otadata 后原固件从 ota_0 启动且 SQLite 仍加载 7 events/8 reminders/0 notes。 仍未宣称 Codec、AFE、AEC、WakeNet、Opus、WSS、ASR、TTS 或声学播放闭环。 Upstream: 78/xiaozhi-esp32@dd99da0 Refs #111 * 📝 docs(audio): 补 esp32s3_audio_probe.h Impl Doxygen Refs #111 * 🐛 fix(audio): 同步 audio_esp 源文件与新版头文件字段 audio_board_profile.cc, esp32s3_audio_probe.cc 使用 capture_i2s/ playback_i2s 字段; audio_board_profile.h, esp32s3_audio_probe.h 补齐中文 Doxygen 注释。 主机测试 26/26 通过, Doxygen PASS。 Refs #109 * 🔧 build(style): clang-format 修复 audio_esp 与测试文件 runtime.cc, audio_board_profile_contract_test.cc, voice_session_contract_test.cc 等通过 clang-format-18。 Refs #109
* ✨ feat(voice): 接入 ESP32-S3 Linx WSS 传输骨架 建立平台无关的 WebSocket 分片重组与 generation 隔离,增加 ESP-IDF 6.0.2 的 WSS/TLS Transport、凭据解析边界、固定事件队列和 Linx hello 超时。同步主机 TDD、架构边界和语音模块文档,明确真实板闭环仍待 Issue #107 验收。 * 🐛 fix(voice): 固化 MVP 音频经验并保护会话代次 * ✨ feat(voice): 完善 Linx 双向协商与 ESP32-S3 回退验证 Linx 服务端可能在重连 hello 中返回不同的下行采样率或帧时长;如果继续沿用旧的播放配置,会把协议已变更的音频静默送进错误的 AudioOutput。物理链路在 hello 失败但尚未完成清理时也不能被重复 Connect。 本次分离 capture/playback 音频格式,Provider 保存已协商格式并拒绝重连时的格式变化;Connect 同时检查 Provider 和底层 Transport 状态。补充断线、重连 hello、旧 generation、TTS abort 和格式变化的主机契约测试,并把受控 ota_1 启动、otadata 恢复和 otatool 回退流程写入文档。 验证:./scripts/run_checks.sh(主机 9/9、架构、Profile、Python 4/4);ESP-IDF 6.0.2 / ESP32-S3 构建通过,voicelife.bin 172480 bytes;真实板以 115200 只写 ota_1@0x410000 后启动成功,并恢复 ota_0 与原 SQLite 数据。 风险:真实 Linx WSS、ASR、TTS、I2S/AFE/Opus 仍未通过;重连格式变化需要 Stop 后重新 Start,当前不做静默 AudioOutput 重配置。 Upstream: 78/xiaozhi-esp32@dd99da0 Refs #107 Refs #91 * 🐛 fix(test): 显式初始化语音事件测试夹具 GitHub GCC 13 在 -Wmissing-field-initializers -Werror 下拒绝 VoiceEvent 聚合初始化,导致主机测试 job 无法编译。 为 TTS start/stop、断线和重连 fixture 显式补齐 text 与 aborted 字段,保持测试意图不变。 验证:voice_session_contract_test 通过;等待远端完整 CI 复跑。 Refs #107 Refs #91 * ✨ feat(voice): 固化有界音频队列与迁移边界 把旧 MVP 中的队列满载策略、generation 清理和水位统计落成平台无关契约,新增主机 TDD 覆盖。同步 Linx、ESP32-S3、小智 AFE 与 SQLite 控制面研究证据,明确其他板卡仅按 Profile 准入。\n\nRefs #105\nRefs #91 * ✨ feat(voice): 接入会话采集音频回调契约 参考旧 voicelife-pcb-native-mvp 的采集任务边界,让 AudioInputPort 只提交格式和负载,由 VoiceSession 统一补齐 generation 与 sequence。补充 TDD 覆盖迟到帧拒绝、回调清理和格式校验,并同步语音架构与研究决策文档。 Refs #107 Refs #105 Refs #91 * 📝 docs(voice): 刷新官方接入与跨板能力边界 记录 Linx MQTT 公开资料缺口、ESP-IDF 6.0.2 I2S/PDM 约束、跨板能力矩阵和立创板迁移输入。\n\n明确当前只验证研究与契约,不把旧 MVP 的 Codec 引脚或芯片能力冒充新工程实板录放通过。\n\nRefs #107\nRefs #105\nRefs #91 * 🐛 fix(docs): 移除失效的 Zephyr 音频来源 跨板矩阵事实仍以 ESP-IDF 6.0.2 I2S 官方文档为主,Zephyr 仅保留已核对的文件系统抽象参考。\n\nRefs #107\nRefs #105\nRefs #91 * 👷 ci(quality): 统一 Linx ESP 传输格式 补齐新增语音端口的 clang-format-18 结果,保持 WebSocket 分片和会话逻辑不变。 * ✅ test(voice): 补充 Linx Codec 与 Provider Registry 错误路径测试 - 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径 - 覆盖 \u 转义拒绝、opus/wav 音频参数解析边界 - 覆盖 Registry 空 ID、重复注册、未找到与缺能力路径 * 📝 docs(process): 将研究资料迁移到 Issue 归档 * 🔧 build(config): 修正 Flash 配置为 16MB 并启用双 OTA 分区表 sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB 新增 partitions_voicelife_16mb.csv: nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB 实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时 spi_flash 警告并浪费 14MB 空间。 Refs #106 * ♻️ refactor(runtime): 从 Scaffold 硬编码切换为 Provider Registry 驱动 将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为 新 VoiceSession + SpeechProviderRegistry 架构: - 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider 实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter) - Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry - Start() 从 Registry 创建 Provider 并构造 VoiceSession - 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold 这是 fennoai 审查 #106 标记的阻塞项 #1: 之前新语音架构在启动路径 上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。 现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 修复 Interrupt 和 EndCapture 的状态机缺陷 Interrupt: generation 递增从 Abort/Flush 之后移到之前。 旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话; 新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。 EndCapture: provider_.StopCapture() 失败时不再静默保留 kCapturing 状态(此时本地输入已停止, 状态分裂)。 改为显式转 kFailed 并上报 capture_stop_failed 证据。 这是 fennoai 审查 #106 标记的阻塞项 #2 和 #3。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 去掉虚假 opus 声明,添加上下行帧大小上限 能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。 之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。 帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。 AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload, 防止畸形帧或攻击性 payload 压爆嵌入式内存。 这是 fennoai 审查 #106 标记的阻塞项 #7、#8、#16、#22。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 修复 BeginCapture 回滚、session_id 校验和 JSON 编解码缺陷 BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值, 回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。 Linx OnText: 解码后校验 session_id 与当前会话匹配, 拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。 EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出, 不再把 PCM 专用字段硬编码进 Opus hello。 Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一 输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。 这是 fennoai 审查 #106 标记的阻塞项 #5、#14、#17、#20。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 修复 Registry 上限、hello 缺少 audio_params 和硬编码缓冲时长 Registry: kMaxProviders 8→16, 满表错误含上限值和诊断提示。 Linx hello: 服务端 hello 不含 audio_params 时拒绝而非静默使用默认格式, 避免协商结果被错误信任。 play_buffer_duration: 从硬编码 1000 改为 frame_duration_ms * 50, 格式变化时自动缩放缓冲时长。 Connect() 已阻塞等待 hello_cv_, hello_timeout_ms 已驱动 wait_for。 这是 fennoai 审查 #106 标记的阻塞项 #12、#15、#21。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): JSON codec 支持 \uXXXX Unicode 转义并加固字段边界 UnescapeJsonString: 新增 \uXXXX 四字节 hex 解码为 UTF-8。 支持 BMP 范围 (U+0000-U+FFFF), 拒绝代理对 (\uD800-\uDFFF)。 解决了此前中文 ASR/TTS 文本因 \uXXXX 转义导致解析失败的问题。 FindField: 匹配字段名前先验证前驱字符为 { 或 ,, 避免把值里的同名字符串误判为对象键。 Registry: 文档化线程安全约束 -- Register() 必须在调度启动前完成。 这是 fennoai 审查 #106 标记的阻塞项 #11、#18、#19(部分缓解)。 主机测试 10/10 通过。 Refs #106 * ♻️ refactor(voice): LinxJsonCodec 从字符串扫描替换为 cJSON 结构化解析 third_party/cjson: 引入 cJSON (MIT license) 源码, ESP-IDF 和主机测试共用。 LinxJsonCodec 全面重写: Encode*: std::ostringstream → cJSON_CreateObject + cJSON_PrintUnformatted DecodeText: 字符串扫描 → cJSON_ParseWithLength, 字段通过 cJSON_GetObjectItem 按类型读取 删除: ReadJsonString, SkipSpace, FindField, ReadStringField, ReadUnsignedField, ReadBoolField, ReadObjectField, Quote (~300行手动解析逻辑) 保留: CodecName, ModeName 主机测试: CMakeLists 新增 cjson 库 (LANGUAGES C CXX), linx 链接 cjson ESP-IDF: voicelife_linx 直接编译 third_party/cjson/cJSON.c 修复: GetRequired/GetOptional 类型匹配改为位掩码 (&) 以支持 cJSON_False|cJSON_True 组合查询。 这是 fennoai 审查 #106 标记的阻塞项 #18、#19。 主机测试 10/10 通过。 ESP-IDF 编译 voicelife.bin 181KB, 实板 16MB Flash 正常启动。 Refs #106 * 🐛 fix(voice): cJSON ParseAudioParams 补回 channels=0 等范围校验 Rebase 后远程新增错误路径测试, cJSON 版 ParseAudioParams 缺少 channels/bits/duration 零值拒绝。补回与旧代码等价的范围校验。 主机测试 17/17 通过。 Refs #106 * 🔧 build(style): clang-format 修复 CI 格式检查 格式化 voice_ports.h, voice_provider_registry.cc, runtime.cc, voice_session.cc, linx_json_codec.cc, linx_speech_provider.cc 以通过 CI clang-format 门禁。 Refs #108 * 📝 docs(voice): 补齐 voice_ports.h 公开 API Doxygen 文档注释 AudioInputPort, AudioOutputPort, VoiceTransportPort, CodecStrategy, ASRAdapter, TTSAdapter, RealtimeAdapter, SpeechProviderAdapter, SpeechProviderRegistry 全部公开 类型和函数补上 /// Doxygen 注释。 通过 CI 公共 API 文档门禁。 Refs #108 * 📝 docs(voice): 补齐所有公开 API 的 Doxygen 注释 AudioInputPort, AudioOutputPort, VoiceTransportPort, SpeechProviderPort, CodecStrategy, ASRAdapter, TTSAdapter, RealtimeAdapter 所有公开析构函数和方法补上 /// 注释。 通过 CI 公共 API 文档门禁。 Refs #108 * 📝 docs(voice): 补 SpeechProviderAdapter 析构和 Entry 结构体 Doxygen * 📝 docs(voice): 重写 voice_ports.h 为中文 Doxygen 格式 全部 33 个公开头文件通过 check_public_api_docs.py 校验。 Refs #108 * 📝 docs(voice): rebase 后补齐头文件 Doxygen 与清理 CMakeLists 修复与 main 合并后丢失的注释: - voice_types.h, voice_session.h, audio_frame_queue.h 补中文 Doxygen - linx_types.h, websocket_fragment_assembler.h 恢复 Doxygen 版本 - CapabilityProfile::Has 改为 std::find, 避免公共 API 检查器误报 tests/host/CMakeLists.txt 清理重复的 linx_esp 定义。 主机测试 25/25 通过, Doxygen 33 头文件 PASS。 Refs #108 * ✨ feat(voice): 建立 ESP32-S3 PCM/I2S 板级探针 将板级 GPIO、I2C 地址、设备 PCM 格式和 DMA 预算收敛到独立 Profile,并把探针编排放回 Runtime 组装根。探针只验证 I2C ACK、I2S 通道生命周期和有限静音读写,不把 Codec 录放或云端闭环伪装成已完成。 主机契约测试 11/11、Python 测试 4/4、架构检查和 ESP-IDF 6.0.2 的 ESP32-S3 构建均通过。实板以 115200 只写非活动 ota_1,最终镜像 222320 B 回读逐字节一致;当前连接板为 SKU=voicelife-pcb 的 NoAudioCodec 纯 I2S 板,I2S smoke 通过但 ES8311/ES7210/PCA9557 未 ACK,已恢复 otadata 并确认原固件和 SQLite 数据正常。后续必须为纯 I2S 板与 Lichuang Codec 板分别建立 Profile。 Refs #109 * 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释 audio_board_profile.h, esp32s3_audio_probe.h 公开 API 补齐 /** @brief @PARAM @return */ 注释。 Doxygen 35 头文件 PASS。 Refs #109 * 🏗️ refactor(voice): 接入 voicelife-pcb 纯 I2S PCM Profile 当前实板原固件报告 SKU=voicelife-pcb/NoAudioCodec,不能复用 Lichuang Codec Profile。将 AudioBoardProfile 扩展为 external-codec-duplex 与 direct-i2s-simplex,独立描述 RX/TX controller、GPIO、采样率、wire slot 和 PCM 对齐,并把探针结果接入硬件/PCM 信号失败路径。 迁移旧 voicelife-pcb-native-mvp 的 I2S0 TX + I2S1 RX 拓扑和有界回放边界;同一块板对照 >>12 与 >>14,削波从 79791 ppm 降到 208 ppm,当前 Profile 采用 >>14。主机 11/11、Python 测试、Profile validate、架构检查和 ESP-IDF 6.0.2 构建通过;最终镜像 229488 B,ota_1 回读逐字节一致,恢复 otadata 后原固件从 ota_0 启动且 SQLite 仍加载 7 events/8 reminders/0 notes。 仍未宣称 Codec、AFE、AEC、WakeNet、Opus、WSS、ASR、TTS 或声学播放闭环。 Upstream: 78/xiaozhi-esp32@dd99da0 Refs #111 * 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释 audio_board_profile.h, esp32s3_audio_probe.h 公开 API 补齐 /** @brief @PARAM @return */ 注释与 Impl 说明。 Doxygen 35 头文件 PASS。 Refs #111 * 🔧 build(style): clang-format 修复 audio_esp 与测试文件 Refs #111
* ✨ feat(voice): 接入 ESP32-S3 Linx WSS 传输骨架 建立平台无关的 WebSocket 分片重组与 generation 隔离,增加 ESP-IDF 6.0.2 的 WSS/TLS Transport、凭据解析边界、固定事件队列和 Linx hello 超时。同步主机 TDD、架构边界和语音模块文档,明确真实板闭环仍待 Issue #107 验收。 * 🐛 fix(voice): 固化 MVP 音频经验并保护会话代次 * ✨ feat(voice): 完善 Linx 双向协商与 ESP32-S3 回退验证 Linx 服务端可能在重连 hello 中返回不同的下行采样率或帧时长;如果继续沿用旧的播放配置,会把协议已变更的音频静默送进错误的 AudioOutput。物理链路在 hello 失败但尚未完成清理时也不能被重复 Connect。 本次分离 capture/playback 音频格式,Provider 保存已协商格式并拒绝重连时的格式变化;Connect 同时检查 Provider 和底层 Transport 状态。补充断线、重连 hello、旧 generation、TTS abort 和格式变化的主机契约测试,并把受控 ota_1 启动、otadata 恢复和 otatool 回退流程写入文档。 验证:./scripts/run_checks.sh(主机 9/9、架构、Profile、Python 4/4);ESP-IDF 6.0.2 / ESP32-S3 构建通过,voicelife.bin 172480 bytes;真实板以 115200 只写 ota_1@0x410000 后启动成功,并恢复 ota_0 与原 SQLite 数据。 风险:真实 Linx WSS、ASR、TTS、I2S/AFE/Opus 仍未通过;重连格式变化需要 Stop 后重新 Start,当前不做静默 AudioOutput 重配置。 Upstream: 78/xiaozhi-esp32@dd99da0 Refs #107 Refs #91 * 🐛 fix(test): 显式初始化语音事件测试夹具 GitHub GCC 13 在 -Wmissing-field-initializers -Werror 下拒绝 VoiceEvent 聚合初始化,导致主机测试 job 无法编译。 为 TTS start/stop、断线和重连 fixture 显式补齐 text 与 aborted 字段,保持测试意图不变。 验证:voice_session_contract_test 通过;等待远端完整 CI 复跑。 Refs #107 Refs #91 * ✨ feat(voice): 固化有界音频队列与迁移边界 把旧 MVP 中的队列满载策略、generation 清理和水位统计落成平台无关契约,新增主机 TDD 覆盖。同步 Linx、ESP32-S3、小智 AFE 与 SQLite 控制面研究证据,明确其他板卡仅按 Profile 准入。\n\nRefs #105\nRefs #91 * ✨ feat(voice): 接入会话采集音频回调契约 参考旧 voicelife-pcb-native-mvp 的采集任务边界,让 AudioInputPort 只提交格式和负载,由 VoiceSession 统一补齐 generation 与 sequence。补充 TDD 覆盖迟到帧拒绝、回调清理和格式校验,并同步语音架构与研究决策文档。 Refs #107 Refs #105 Refs #91 * 📝 docs(voice): 刷新官方接入与跨板能力边界 记录 Linx MQTT 公开资料缺口、ESP-IDF 6.0.2 I2S/PDM 约束、跨板能力矩阵和立创板迁移输入。\n\n明确当前只验证研究与契约,不把旧 MVP 的 Codec 引脚或芯片能力冒充新工程实板录放通过。\n\nRefs #107\nRefs #105\nRefs #91 * 🐛 fix(docs): 移除失效的 Zephyr 音频来源 跨板矩阵事实仍以 ESP-IDF 6.0.2 I2S 官方文档为主,Zephyr 仅保留已核对的文件系统抽象参考。\n\nRefs #107\nRefs #105\nRefs #91 * 👷 ci(quality): 统一 Linx ESP 传输格式 补齐新增语音端口的 clang-format-18 结果,保持 WebSocket 分片和会话逻辑不变。 * ✅ test(voice): 补充 Linx Codec 与 Provider Registry 错误路径测试 - 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径 - 覆盖 \u 转义拒绝、opus/wav 音频参数解析边界 - 覆盖 Registry 空 ID、重复注册、未找到与缺能力路径 * 📝 docs(process): 将研究资料迁移到 Issue 归档 * 🔧 build(config): 修正 Flash 配置为 16MB 并启用双 OTA 分区表 sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB 新增 partitions_voicelife_16mb.csv: nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB 实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时 spi_flash 警告并浪费 14MB 空间。 Refs #106 * ♻️ refactor(runtime): 从 Scaffold 硬编码切换为 Provider Registry 驱动 将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为 新 VoiceSession + SpeechProviderRegistry 架构: - 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider 实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter) - Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry - Start() 从 Registry 创建 Provider 并构造 VoiceSession - 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold 这是 fennoai 审查 #106 标记的阻塞项 #1: 之前新语音架构在启动路径 上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。 现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 修复 Interrupt 和 EndCapture 的状态机缺陷 Interrupt: generation 递增从 Abort/Flush 之后移到之前。 旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话; 新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。 EndCapture: provider_.StopCapture() 失败时不再静默保留 kCapturing 状态(此时本地输入已停止, 状态分裂)。 改为显式转 kFailed 并上报 capture_stop_failed 证据。 这是 fennoai 审查 #106 标记的阻塞项 #2 和 #3。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 去掉虚假 opus 声明,添加上下行帧大小上限 能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。 之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。 帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。 AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload, 防止畸形帧或攻击性 payload 压爆嵌入式内存。 这是 fennoai 审查 #106 标记的阻塞项 #7、#8、#16、#22。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 修复 BeginCapture 回滚、session_id 校验和 JSON 编解码缺陷 BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值, 回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。 Linx OnText: 解码后校验 session_id 与当前会话匹配, 拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。 EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出, 不再把 PCM 专用字段硬编码进 Opus hello。 Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一 输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。 这是 fennoai 审查 #106 标记的阻塞项 #5、#14、#17、#20。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): 修复 Registry 上限、hello 缺少 audio_params 和硬编码缓冲时长 Registry: kMaxProviders 8→16, 满表错误含上限值和诊断提示。 Linx hello: 服务端 hello 不含 audio_params 时拒绝而非静默使用默认格式, 避免协商结果被错误信任。 play_buffer_duration: 从硬编码 1000 改为 frame_duration_ms * 50, 格式变化时自动缩放缓冲时长。 Connect() 已阻塞等待 hello_cv_, hello_timeout_ms 已驱动 wait_for。 这是 fennoai 审查 #106 标记的阻塞项 #12、#15、#21。 主机测试 10/10 通过。 Refs #106 * 🐛 fix(voice): JSON codec 支持 \uXXXX Unicode 转义并加固字段边界 UnescapeJsonString: 新增 \uXXXX 四字节 hex 解码为 UTF-8。 支持 BMP 范围 (U+0000-U+FFFF), 拒绝代理对 (\uD800-\uDFFF)。 解决了此前中文 ASR/TTS 文本因 \uXXXX 转义导致解析失败的问题。 FindField: 匹配字段名前先验证前驱字符为 { 或 ,, 避免把值里的同名字符串误判为对象键。 Registry: 文档化线程安全约束 -- Register() 必须在调度启动前完成。 这是 fennoai 审查 #106 标记的阻塞项 #11、#18、#19(部分缓解)。 主机测试 10/10 通过。 Refs #106 * ♻️ refactor(voice): LinxJsonCodec 从字符串扫描替换为 cJSON 结构化解析 third_party/cjson: 引入 cJSON (MIT license) 源码, ESP-IDF 和主机测试共用。 LinxJsonCodec 全面重写: Encode*: std::ostringstream → cJSON_CreateObject + cJSON_PrintUnformatted DecodeText: 字符串扫描 → cJSON_ParseWithLength, 字段通过 cJSON_GetObjectItem 按类型读取 删除: ReadJsonString, SkipSpace, FindField, ReadStringField, ReadUnsignedField, ReadBoolField, ReadObjectField, Quote (~300行手动解析逻辑) 保留: CodecName, ModeName 主机测试: CMakeLists 新增 cjson 库 (LANGUAGES C CXX), linx 链接 cjson ESP-IDF: voicelife_linx 直接编译 third_party/cjson/cJSON.c 修复: GetRequired/GetOptional 类型匹配改为位掩码 (&) 以支持 cJSON_False|cJSON_True 组合查询。 这是 fennoai 审查 #106 标记的阻塞项 #18、#19。 主机测试 10/10 通过。 ESP-IDF 编译 voicelife.bin 181KB, 实板 16MB Flash 正常启动。 Refs #106 * 🐛 fix(voice): cJSON ParseAudioParams 补回 channels=0 等范围校验 Rebase 后远程新增错误路径测试, cJSON 版 ParseAudioParams 缺少 channels/bits/duration 零值拒绝。补回与旧代码等价的范围校验。 主机测试 17/17 通过。 Refs #106 * 🔧 build(style): clang-format 修复 CI 格式检查 格式化 voice_ports.h, voice_provider_registry.cc, runtime.cc, voice_session.cc, linx_json_codec.cc, linx_speech_provider.cc 以通过 CI clang-format 门禁。 Refs #108 * 📝 docs(voice): 补齐 voice_ports.h 公开 API Doxygen 文档注释 AudioInputPort, AudioOutputPort, VoiceTransportPort, CodecStrategy, ASRAdapter, TTSAdapter, RealtimeAdapter, SpeechProviderAdapter, SpeechProviderRegistry 全部公开 类型和函数补上 /// Doxygen 注释。 通过 CI 公共 API 文档门禁。 Refs #108 * 📝 docs(voice): 补齐所有公开 API 的 Doxygen 注释 AudioInputPort, AudioOutputPort, VoiceTransportPort, SpeechProviderPort, CodecStrategy, ASRAdapter, TTSAdapter, RealtimeAdapter 所有公开析构函数和方法补上 /// 注释。 通过 CI 公共 API 文档门禁。 Refs #108 * 📝 docs(voice): 补 SpeechProviderAdapter 析构和 Entry 结构体 Doxygen * 📝 docs(voice): 重写 voice_ports.h 为中文 Doxygen 格式 全部 33 个公开头文件通过 check_public_api_docs.py 校验。 Refs #108 * 📝 docs(voice): rebase 后补齐头文件 Doxygen 与清理 CMakeLists 修复与 main 合并后丢失的注释: - voice_types.h, voice_session.h, audio_frame_queue.h 补中文 Doxygen - linx_types.h, websocket_fragment_assembler.h 恢复 Doxygen 版本 - CapabilityProfile::Has 改为 std::find, 避免公共 API 检查器误报 tests/host/CMakeLists.txt 清理重复的 linx_esp 定义。 主机测试 25/25 通过, Doxygen 33 头文件 PASS。 Refs #108 * ✨ feat(voice): 建立 ESP32-S3 PCM/I2S 板级探针 将板级 GPIO、I2C 地址、设备 PCM 格式和 DMA 预算收敛到独立 Profile,并把探针编排放回 Runtime 组装根。探针只验证 I2C ACK、I2S 通道生命周期和有限静音读写,不把 Codec 录放或云端闭环伪装成已完成。 主机契约测试 11/11、Python 测试 4/4、架构检查和 ESP-IDF 6.0.2 的 ESP32-S3 构建均通过。实板以 115200 只写非活动 ota_1,最终镜像 222320 B 回读逐字节一致;当前连接板为 SKU=voicelife-pcb 的 NoAudioCodec 纯 I2S 板,I2S smoke 通过但 ES8311/ES7210/PCA9557 未 ACK,已恢复 otadata 并确认原固件和 SQLite 数据正常。后续必须为纯 I2S 板与 Lichuang Codec 板分别建立 Profile。 Refs #109 * 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释 audio_board_profile.h, esp32s3_audio_probe.h 公开 API 补齐 /** @brief @PARAM @return */ 注释。 Doxygen 35 头文件 PASS。 Refs #109 * 🏗️ refactor(voice): 接入 voicelife-pcb 纯 I2S PCM Profile 当前实板原固件报告 SKU=voicelife-pcb/NoAudioCodec,不能复用 Lichuang Codec Profile。将 AudioBoardProfile 扩展为 external-codec-duplex 与 direct-i2s-simplex,独立描述 RX/TX controller、GPIO、采样率、wire slot 和 PCM 对齐,并把探针结果接入硬件/PCM 信号失败路径。 迁移旧 voicelife-pcb-native-mvp 的 I2S0 TX + I2S1 RX 拓扑和有界回放边界;同一块板对照 >>12 与 >>14,削波从 79791 ppm 降到 208 ppm,当前 Profile 采用 >>14。主机 11/11、Python 测试、Profile validate、架构检查和 ESP-IDF 6.0.2 构建通过;最终镜像 229488 B,ota_1 回读逐字节一致,恢复 otadata 后原固件从 ota_0 启动且 SQLite 仍加载 7 events/8 reminders/0 notes。 仍未宣称 Codec、AFE、AEC、WakeNet、Opus、WSS、ASR、TTS 或声学播放闭环。 Upstream: 78/xiaozhi-esp32@dd99da0 Refs #111 * 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释 audio_board_profile.h, esp32s3_audio_probe.h 公开 API 补齐 /** @brief @PARAM @return */ 注释与 Impl 说明。 Doxygen 35 头文件 PASS。 Refs #111 * ✨ feat(voice): 接入 ESP32-S3 PCM Audio Port 新增硬件 period 到传输帧的组装器、独立采集/投递/播放任务和有界队列,并将 Profile 接入 Runtime。补充主机 TDD、ESP-IDF 构建与真实 voicelife-pcb 实板回退证据;物理声学与 Linx 云端闭环继续留在后续 Issue。 Refs #113 Refs #91 * 🐛 fix(voice): 收紧 Linx 传输生命周期与会话失败回滚 * 🏗️ refactor(voice): 拆分超大语音源文件以通过规模门禁 - esp32s3_pcm_audio_port.cc 670 行拆分为平台无关入口 + esp32s3_pcm_i2s_runtime.cc(I2S 通道与任务循环) - linx_json_codec.cc 拆出 linx_json_reader.{h,cc} - esp_websocket_transport.cc 拆出 impl 与事件处理 - 同步 host 测试 CMakeLists 补齐新源文件 * 🐛 fix(voice): 恢复 Connect/BeginCapture/Stop 失败回滚并补 Doxygen 基于新 #112 重建 PR #114: - voice_session.cc 恢复 Connect 失败 Disconnect 回滚、 BeginCapture 双重失败转 kFailed、Stop 断开失败不伪装 kStopped - pcm_frame_assembler.h, esp32s3_pcm_audio_port.h 补中文 Doxygen 主机测试 27/27 通过, Doxygen PASS, 代码规模 PASS。 Refs #113 * 🔧 build(style): clang-format 修复 audio_esp 与测试文件 Refs #113 * 🔧 chore(build): 移除误提交的 ESP-IDF 构建产物 * 🔧 fix(timing): 链接合并后的主机服务实现
关联
Refs #105
Refs #91
结论
建立 ESP32-S3 优先的语音子架构:实时音频数据面与会话控制面分离,Linx/xiaozhi 通过 Provider 防腐层接入。当前已完成核心会话、Provider Registry,以及 Linx 控制协议和二进制音频的主机可测试实现。
代码变化
AudioInputPort、AudioOutputPort、VoiceTransportPort、CodecStrategy、SpeechProviderAdapter和固定容量SpeechProviderRegistry。VoiceSession的启动回滚、采集/播报状态、generation/sequence 隔离、上下行音频格式校验、打断和幂等停止。voicelife_linx:编码 hello、listen start/stop/detect、abort;解析 hello、STT、TTS、错误事件;映射二进制下行音频。验证
VOICELIFE_HOST_BUILD_DIR=/tmp/voicelife-host-build ./scripts/run_checks.sh:8 个主机测试、架构边界、Profile 校验和 Python 测试通过。voicelife_linx已进入固件依赖图,固件大小0x269c0,最小应用分区剩余 90%。未完成与风险
LinxJsonCodec当前是可移植的严格子集解析器;ESP Transport 接入时应以平台 JSON 库替换并复用同一组协议 fixture。