# 实体实时对话接入

本应用源码归属独立 `speaking_motion_lab` 仓库，网页源码在桌面客户端 `src/previews/joyinside/`。SDK 是 Daemon 的唯一实现，不复制 Daemon，不增加按业务消息类型分流的例外。

## 启动

1. 使用安装了最新 SDK 的 Python 环境。仅看到 Daemon 的版本正确还不够，Application 使用的 Python 也必须支持 `expression_runtime.set_audio_follow`。
2. 机器人需具备 `rtc.audio.full_duplex.v1`、`expression.audio_follow.v1`、`expression.audio_follow.v2`、`motion` 和 `motion.action.once.v1`。旧固件在接管媒体前拒绝启动。SDK 的 `set_audio_follow` 必须支持 `emotion` 关键字。串口只用于确认设备、查看日志和必要的设备设置，不承担业务连接。
3. 在桌面仓库运行 `npm run build:joyinside-application`。Windows PowerShell 与 macOS/Linux Shell 入口均委托同一个 Node 核心。默认目的地为 workspace 旁的 `WatcheRobot-Applications/speaking_motion_lab`；其他布局可用 `WATCHER_BEHAVIOR_LAB_ROOT` 指定。不会覆盖 Python 源码。
4. 使用对应 SDK Python 环境的 `watcherobot app run <应用目录>` 启动，打开日志中的本机网页。不得直接运行 `app.py`。启动前确认同一 Daemon 的其他应用没有正在进行的测试。
5. 在页面「机器人连接」中查看实时状态；未连接时打开机器人「SDK 控制」，输入屏幕的六位配对码，点击「连接机器人」。两端必须在同一局域网。搜索/连接中可取消，重连中可停止重连；实际在线且状态为 connected 时，桌面连接（desktop_link）和 SDK 控制（python_sdk）均自动选择实体；已从桌面连接时直接复用，不必再次配对。刷新也从实际状态恢复。
6. 点击「开始实时对话」。勾选机器人麦克风后对设备说话，也可取消勾选并输入文字验证回答、声音和动作。管理服务不可用、离线、非 connected、未知模式或被另一会话占用时不能开始；结束后等待资源释放才能断开。清理未确认时禁止换机，原设备恢复在线后可以再次开始，由原 owner 先重试清理。

连接管理独立于 Device 业务通道。浏览器只访问同源认证 `/api/robot/*`，Application 根据 Daemon 注入的固定 loopback 管理 URL 调用 `/daemon/devices`、`pair`、`pair/cancel`、`disconnect`。请求不能指定代理地址或目标模式，配对只使用 `python_sdk`；状态仅返回白名单，不透传媒体地址或凭据。HTTP 202 只表示配对已开始，后续轮询确认真实在线。当前应用锁保证本页会话 admission 与连接变更互斥；不能约束其他 Desktop/CLI 管理请求。

## 数据链路

- Application 管理 Codex 会话、设备 RTC 信令及动作，通过 Device channel 经 Daemon 转发。
- 浏览器持有 Codex RTC 与 SDK 设备 RTC 两个媒体端点。机器人收音转交 Codex；Codex 输出经 WebAudio 音量控制转交机器人，电脑不重复播放。这是 SDK 协商的媒体路径，不是另建业务控制连接。
- 嘴部使用设备的 `audio_follow`，跟随实际成功播放的 PCM。`LiveBehaviorDriver` 从 Codex 音频和字幕推断状态；实体动作由状态选择本地资源，尚未使用设备播放时钟补偿。
- 头部由 `TimedMotionExecutor.play` 一次调用 `motion.play_action`，使用重新设计的七份 `chat_v1` 完整轨迹（见 [资源合同](motion_assets/README.md)）。运动中只保留最新状态；同一情绪进入播放一次，平静讲话在完整动作后停顿再重复。等待 MCU Job 终态与完整资源时长＋120ms，满足后才发送下一段。数字孪生按同一 JSON 和 ACK 后估算起点采样，姿态明确标记 `estimated`；ADC 才能用于实际到位验收。启动未知位置的归中仍保留全范围物理预算，关闭先停止，再按可确认的窄域归中。表情和实际 PCM 嘴型独立运行。
- 用户最终语音转写和文字输入发送 `userCue`，Application表情导演识别明确表演请求，分句处理否定，保持4秒且优先于助手文本推断。情绪驱动眼形与说话手势mood；表情单独worker，运动等待不阻塞切换。固件 `audio_follow.v2` 在同一实例持久保存0–6情绪，与真实播放PCM嘴型共存；停止重启中性。
- `emotion_target` 表示接受的目标，`emotion_submitted` 是成功提交的renderer更新。二者都不能独立证明LCD已显示；渲染错误计数与LCD/肉眼证据需区分。

## 生命周期

只有通过同源检查和首帧运行凭据校验的实体会话可以取得设备。独立网页启动器拒绝实体模式。单个 Application 同时只允许一个实时会话。

结束、断开时通知后台任务取消；已启动设备优先停止 RTC、停止动作并关闭播放嘴型，再等待后台中断与连接收尾。启动尚未完成时先等待启动任务及已发出的同步 SDK 命令结束，由原启动者清理，避免关闭与迟到的启动 ACK 并发。清理不因浏览器断开而直接被取消。每个硬件副作用前复核原设备身份，运动等待也监测身份；设备离线导致 ACK 无法确认时保留 `cleanupPending` 及原 bridge，下一实体会话先重试清理，成功前不创建新owner。

关键资源释放后才考虑归中。正常会话记录已发路径的起点与终点包络，取消途中按包络预算回中；它是开环软件估计，不是位置反馈。故障或启动失败导致位置未知、估算回中超过2000ms时，停在原处并报告 `homeSkipped="position_uncertain"`，避免侵占 SDK Runtime 上限3秒的优雅停止窗口。`cleanupPending=false` 表示资源释放已确认，不保证机械回中或实际到位。初次启动仍使用0–180°最坏跨度和6000ms归中预算。

机器人 ICE 主机候选中的 mDNS 地址使用 Daemon 注入的设备状态 URL 确认设备地址，再选择通往该设备的本机网卡；不使用固定个人 IP 或默认网卡猜测。

设备 ready 应使用固件已有身份服务生成的唯一 `WR-…` ID，与 BLE/配对一致。旧占位身份不能用于多机保护。
bridge 的身份复核以 SDK 当前 ready 缓存为依据，能阻止观察到新身份后的副作用；换设备至新ready到达之间仍有缓存窗口，本合同不保证原子跨设备隔离。更换机器人应先结束当前会话并确认资源释放。

2026-10-13候选已完成新机四种显式表情、真实Codex语音输出、定时运动节奏及后台活跃时Daemon停止测试；停止2.43秒、退出0，重读设备资源确认全部释放。Python118项/客户端30项/SDK150项及固件专项通过，独立本轮软件复审10/10。完整过程、实际采样及限制见[本轮档案](documents/dev_log/2026-10-13/2026-10-13_013655_KID_无Issue_实体表情与顺滑动作/index.md)。新机缺ADC和实体LCD画面，不能据此认证机械顺滑观感或精确到位；真人语音/听感沿用历史事实，不伪称本轮重验。

## 验收

当前分发仓库的连接准入专项：`python -B -m unittest discover -s tests -v`，不访问真实设备或 Codex。历史完整开发测试集（未包含在本仓库）：`python -m pytest tests -q`，网页测试和类型检查：桌面仓库的 `npm run test:joyinside-preview` 与 `npm run typecheck:joyinside-preview`。普通测试不访问 Codex 服务或机器人。

真实 Codex 测试：在测试环境设置 `WATCHER_CODEX_LIVE_TEST=1` 后运行 `python -m pytest tests/test_live_codex.py -q`，需可选 `aiortc`。它验证云端回复、非静音 RTC 输出和合成录音转写，不代替实体麦克风与扬声器验收。

实体端到端必须逐项记录：

1. 对话设备为实体，Codex 与机器人 RTC 均连接。播放诊断中机器人收包、发包随时间增长。
2. 文字输入得到真实回答，设备实际播出，设备嘴型有变化；头部命令被固件接受且实际运动。
3. 对机器人说话，出现实际输入转写和回答。排查扬声器回声是否导致自我接话。
4. 持续回复期间插话，验证旧音频停止、无长队列残音和过期动作。网页停用麦克风后不再上传机器人声音。
5. 结束、关闭页面、设备断线、失败后重连，确认资源释放和回中，不恢复旧会话。

测试台架设备版本为 ESP32 v0.4.5、STM32 v0.1.3，已配对。Application 和 Daemon 均需使用支持 audio-follow 的 SDK；全局旧 Python 环境可能缺少该方法。当前状态为开发候选，未发布。

2026-10-12 复测：切回本应用后完成文字请求 → 实际 Codex 回答 → RTC → 设备 I²S 播放。现场确认机器人出声，较长回复时嘴型和头部都有变化。发现并修复 Chromium 实体模式未启动远端媒体解码端点的问题；两种模式均启动媒体元素，实体模式电脑端音量固定为零，设备音量由 WebAudio 增益控制。网页回归 26 项通过，类型检查及生产构建通过，Python 70 项通过、2 项跳过。

首个修复后回复的设备 PCM 峰值为 29202，头部目标命令 9 次；较长回复固件记录 203 帧说话嘴型、20 FPS、零渲染更新错误、36 次头部目标命令，设备 PCM 峰值 20572。结束按钮验证 RTC 停止、动作停止和回中、audio-follow 关闭，`cleanupPending=false`。`/api/realtime/status` 增加 SDK 原始资源证据及接收时间、身份、代数，供区分生成帧、命令 ACK 与现场运动。

仍存在启动阶段偶发机器人 RTC 断开，手动重连可恢复；原因未确定，不能标为稳定性验收通过。自然打断和回声尚需独立证据。上述 9/36 是目标命令成功返回次数，不是编码器测得的机械位移。现场观察优先于命令 ACK。

随后真人麦克风验收出现「二十加六」输入转写及语音回复，现场确认机器人回应，声音、嘴型和头部都有变化。字幕曾显示错误答案「三十二」，现场反馈听到「二十六」；保留这项冲突，语音往返通过不等于回答准确性或字幕一致性通过。自然插话、回声抑制及长时间稳定性仍未验收。

页面刷新断开验收：累计 46 次头部目标命令；RTC 为 stopped，执行 active=false、cleanupPending=false、error=null；固件 audio-follow 与动画均关闭，release.complete=true、failures=[]。按钮结束和页面断开均确认清理，应用继续运行等待下一次会话。

## 2026-10-12 晚间物理反馈复测

当前环境已变为 ESP32 v0.5.6 / STM32 v0.1.4，不能沿用早前 v0.4.5/v0.1.3 的版本结论。通过开发 Debug CLI 打开 SDK 控制并自动配对，业务运动仍由 Application Device channel 经 Daemon 下发，串口只做观测。调试固件是本机工程构建，原活动 OTA 镜像与私有配置已备份，未制作正式发布包。

固定保持 1.5 秒的水平 ±6° 目标，原始 ADC 稳态中位跨度约 111；±12° 约 252。俯仰 ±6° 约 125。两轴反馈会随目标改变；五项 450 ms 定时 Job 均完成，MCU 无 ACK 超时、DONE fault 或 CRC 错误。快速目标测试实际间隔约 218–234 ms，存在超调；未证明通信丢失或舵机完全无响应。

水平指令 90° 时换算反馈约 48°，绝对角度标定明显不能直接用于此台架的到位判据。这里用 ADC 原始变化证明物理响应，不以软件角度、1 ms DONE 或命令计数代替实际位置。闭环到位和每台机器的反馈标定仍需单独实现。

Codex 对话基线的水平目标 84–96°、俯仰 108–111°，ADC 跨度分别为 138/69。加入实体增益后的对话目标为 90–102°/104–113°，ADC 跨度为 174/129，嘴型说话累计 165 帧且零更新错误；RTC 停止、audio-follow 关闭、执行 inactive、cleanupPending=false、release.complete=true 且 failures=[]。相同提示的两次自然回复内容仍不同，不能把这些跨度当作精确增益标定或全姿态覆盖。随后根据 MCU 事件间隔检查进一步将保持窗口改为 ACK 返回后开始计时，最终复测记录见开发档案。

本轮真实输入为网页文字，验证了 Codex 回复至 RTC、设备音频播放、嘴型及实测头部反馈。真人麦克风、自然插话和长期稳定性不以该验证代替。新会话曾一次在 Codex account 阶段失败，重试成功，保留启动稳定性缺口。

最终固定轨迹专项验证：12 秒、33 个流式目标，ACK 后保持最短 251.9 ms、中位 276.9 ms、最长 283.9 ms，实际 ACK 耗时中位 96.2 ms。两轴 ADC 每轴 166 个样本，水平范围 1202–1467、俯仰 1470–1615；stale 与关闭均回中，RTC stopped、audio-follow=false、cleanupPending=false、release.complete=true、failures=[]。普通单调时钟在此 Windows Python 环境使用 15.625 ms 粗时钟，已用失败测试保护并改为高精度 perf_counter。完整 Python 回归 74 项通过、2 项真实服务用例默认跳过。此专项未协商实际音频媒体，不替代正常内容对话验收。

最终策略的首轮正常内容对话后台任务连续失败，语音前台仅朗读了错误消息；随后独立只读文字诊断返回“二十加六等于二十六”，无需修改全局模型供应商或网关配置。此失败原因仍未确定，不能宣称服务稳定性通过。

最终正常内容重试通过：网页文字请求得到问候和“二十加六等于二十六”，语音字幕包含正确答案；设备 RTC connected，PCM 峰值 16884、I²S 累计写入 3,994,240 字节、嘴型 speaking_frames=61、update_errors=0。应用累计 11 次目标返回，串口窗口捕获 10 条 MCU motion_done/result=0，目标水平 90–95°/俯仰 107–114°；每轴 78 个 ADC 样本，水平 1317–1402、俯仰 1539–1582，证明实际头部反馈发生变化。结束后再次确认 RTC stopped、animation/audio-follow=false、cleanupPending=false、release.complete=true 且 failures=[]。本轮未重新真人收音或现场音质评价；原有偶发后台失败仍是稳定性缺口。

## 2026-10-13 完整动作方案的当前状态

chat_v1 候选现已部署应用和动作资源。Application 全量144 passed / 3 skipped；七份资源与固件逐字节一致，manifest SHA 校验通过。七动作和同 ID 重播8/8硬件协议验收成功；真实网页连续故事触发5次完整 talk，开心、无语及重连后伤心、生气正常调用，表情、设备 I2S 输出和预计孪生同期工作。结束后资源释放确认通过。完整脱敏证据见[本轮档案](documents/dev_log/2026-10-13/2026-10-13_213231_KID_无Issue_预置对话动作/index.md)。

当前台架使用不带位置反馈的舵机；本轮不使用 ADC 数字作位置证据。串口目标/PWM/DONE、中断与孪生均不能证明机械到位或现场顺滑观感。两段连续语音观测运动故障为零，第二段累计4次音频队列丢包、2个过期包，内部 RAM 运行中约9KiB；未认证长期稳定性、现场音质和自然打断。此前 ADC 历史只对应当时反馈硬件，不适用于当前舵机。保留能力门禁，旧固件需升级后再使用本合同。
