← 返回报告索引
2026-09-04 · 第三份 GOAL

面向观众的互动:展示、递出,以及「有人在场」这件事

调酒演示里有一个动作:旋转杯身让观众看清酒的状态,然后朝观众推出。 这一条看起来只是「再加一个动作」,实际上它引入了第二个主体——人——并因此要求两件重新设计: 头部控制权归谁,以及安全层怎么理解「人」。这份 GOAL 说明为什么它不能塞进前两份里顺手做。

盘点后最大的发现:这套 HRI 分层三周前就写好了。 face_follow.py(2026-08-14)的设计目标逐条对着这个需求写的。 坏消息是它和策略试跑抢同一条串口总线。
解法已定(§2.1):头部控制不离开策略进程,只按阶段换目标来源 —— 操作阶段锁死、展示阶段跟脸。 这个决定是被数据逼出来的,不是选出来的:50 集里头部两个自由度的 σ 都是 0.0000。
但落地前有个前置:安全层注释里有一处数字错误(§2.2),它正是「头部不设绝对边界」那个决定的依据。

0. 由来

操作者提出的需求(2026-09-03 夜):

还有很重要的就是跟观众互动,比如调酒常有的一个动作是旋转杯身以让观众更好地看到酒的情况, 并且朝着观众的方向推出。

前两份 GOAL 处理的都是机器人和物体的关系。这一条引入了第二个主体:人。


1. 现状盘点(实测,不是推断)

1.1 已经有的:一套完整的 HRI 分层

origin/main 和 china-console 都有,本地也有。 作者 ginagina19992023,2026-08-14。

文件行数做什么
face_perception.py265感知适配器 → 统一的 FaceObservation
face_follow.py330头部跟随一张主脸(pan/tilt 直驱舵机)
face_display.py168ESP32-S3 + OLED 表情脸,硬件无关
test_face_emotion.py113相机 → 人脸/表情 冒烟测试
DEMONSTRATION-INTERFACES.md325示范接口决策文档(见 §1.3)

face_follow.py 的设计目标,是照着「面向观众」写的(文件头原文):

· follow ONE primary face instead of jumping between people;
· proportional motion with a dead zone, smoothing and hard per-tick limits;
· no servo chatter when a face is already near image centre;
· after losing the person, wait, then gently return to centre;
· never share a serial bus with vr_teleop.py or another process.

「只跟一张主脸不来回跳」「死区 + 每帧硬上限」「跟丢了缓慢回中」—— 这正是面向观众时头部控制该有的性质,不是随手写的。实测到的关键常量:

项值出处
头部舵机 IDpan=7, tilt=8,在左臂总线上face_follow.py:49-50
pan / tilt 行程342–3754 / 1479–2617:58-59
中位 / 力矩上限2048 / 200:60-61
死区X 0.10,Y 0.12(归一化画面坐标):72-73
主脸选择choose_primary(faces, previous),粘性:205
跟丢后recenter_tick() 缓慢回中;退出 release() 松力矩:179, :196

检测器用 OpenCV 自带 Haar 级联 —— 不用下载模型。文件头明说后面可以换成 MediaPipe / YOLO-face 而不改头部控制器。这个分层是对的,新工作应该沿用,不要另起。

1.2 缺的:身份识别没有,「观众方位」没接到手臂

顺带更正一条:conversational_ai/vision/face.py 里那个 YuNet 是另一套(2026-09-03 提交,服务对话系统)。仓库里现在有两套并行的人脸检测, 一套 Haar(scripts/)、一套 YuNet(conversational_ai/)。 做这份 GOAL 时要先决定留哪套,别再加第三套。

1.3 已经有的:PRESENT 早就被当成独立技能写进文档了

DEMONSTRATION-INTERFACES.md(2026-08-16)里已经有这一段:

VR is likely strongest for: shaking, larger expressive arm trajectories, coordinated bimanual motion, presentation / serving gestures, motions where human whole-arm movement is part of the intended style.

GRASP / PLACE / POUR → Leader-heavy demonstrations
SHAKE / PRESENT     → VR-heavy demonstrations

也就是说「呈递展示」这个技能,三周前就被判给了 VR 示范。 这份 GOAL 不需要重新论证示范接口的选择,但要注意同一份文档 §4 的成本警告: A/B/C 消融是好论文,演示才是截止日期。


2. 第一个硬冲突:头部控制权

现在三个地方各自直接控制头部:

谁怎么控出处
face_follow.py自己开左臂总线,直写舵机 7/8face_follow.py:49
vr_extras.py同样直写舵机 7/8vr_extras.py:42
策略试跑每帧把头覆盖成启动时的位置policy_safety.py:367

第三条是关键:policy_safety.py 的第 3 步是 mask head → head_motor_1/2 := locked pose, always, silently。 所以策略运行期间,机器人物理上不可能转头看观众。

而且这不只是逻辑冲突,是总线冲突。 头部舵机在左臂总线上,策略跑的时候那条总线归机器人对象所有。 bus_guard.py 存在的原因就是这个 —— 它记录的事故是 2026-08-17 23:16: 一个手敲的 trace_teleop.py 在录制进行时启动,录制以 [TxRxResult] Port is in use! 死掉, 掉了一集,并且双臂砸在桌上。

结论:「策略在抓杯子 + 头跟着观众转」在当前架构下做不到。 不是调参问题,是两个进程抢一条总线、外加两套互斥的头部控制权。

2.1 ★ 已定方案:按阶段切换目标来源,控制权始终在策略进程 ★

操作者 2026-09-04 的判断:

就是比如在某些动作的时候相机依然是不动的,在特定动作, 比如最后的展示和推杯环节,相机才需要锁定人脸。

这不是为了省事,是被两个硬约束逼出来的唯一答案。

约束一:训练数据里头一动没动(实测)

量了 xlerobot-team/xlerobot-cup-grasp-20260820-0230 全部 50 集 / 19453 帧:

 12 head_1    σ=0.0000   范围 [-4.308, -4.308]
 13 head_2    σ=0.0000   范围 [49.846, 49.846]

两个自由度都是常数。策略从来没见过「头在动」的画面。 抓取/倾倒阶段让头跟着人转,头部相机输入就跑出训练分布了。

约束二:深度外参绑在头的姿态上

相机装在会动的头上,外参是「头在某个姿态时」的值 —— 头一动,标定全废。 两个约束指向同一个结论:操作阶段头必须不动。

阶段头部目标来源相当于
接近 / 抓取 / 倾倒 / 搅拌锁定常数(启动时读到的位置)就是现在的行为,不变
展示 / 推杯人脸驱动新增
关键点:头部控制始终留在策略进程里,只换目标来源。 这样串口总线只有一个主人,不需要进程间交接 —— §2 那个冲突从根上消失。 原来设想的「两个进程轮流占总线」(时间片)反而更差:交接要断开重连,代价是秒级的。

代价:要改 policy_safety.py 第 3 步的头部掩码 —— 从「always, silently 锁死」改成「按阶段决定锁死还是跟随」。那是安全层,见 §2.2。

2.2 ★ 安全层里有一处事实错误,动头之前必须先解决 ★

policy_safety.py:239 的 HEAD_LOCK_NOTE 写着:

Every frame of xlerobot-cup-grasp-20260820-0230 carries head_motor_2 = 99.649 … 99.649 * 4095/360 + (1479+2617)/2 = 3181 counts … against a calibrated range_max of 2617 — 564 counts, about 50 degrees, past the end of the calibrated span.

实测(2026-09-04):19453 帧全部是 head_motor_2 = 49.846,不是 99.649。 用它自己的公式重算:

head_2counts对 range_max = 2617
99.649(注释写的)3181.5超出 +564.5
49.846(实测)2615.0在范围内,差 2 counts
这条错误有后果。 正因为推出「超出 564 counts 却没卡住」, 那段注释得出「标定文件描述不了头的真实位置」, 于是第 4 步的绝对钳位直接跳过 HEAD_DIMS(原文:step 4 skips HEAD_DIMS)。 前提不成立,这个跳过就失去了依据。

· 现在无害:头被钉死在启动位置,不动,没有边界也不会跑。
· 动头之后有害:§2.1 的方案要让头在展示阶段跟随人脸, 而此时安全层对头部没有任何绝对边界。唯一的保护是 face_follow.py 自己的 HEAD_PAN_LIMITS / HEAD_TILT_LIMITS —— 那是另一条代码路径, 按 §2.1 合并进策略进程之后,那层保护就不在路径上了。

本 GOAL 没有改它(边界条件:不许动安全层)。改不改、怎么改由操作者决定, 但 §2.1 的方案落地前必须先解决它 —— 否则就是在一个前提已被证伪的跳过之上,叠加一个会真的动起来的头。

2.3 顺带:录制时头就坐在标定行程边缘

2615 counts 距 range_max 2617 只差 2 counts。 也就是说 50 集录制期间,头的 tilt 一直贴在标定行程的边缘上。

如果观众是站着的、需要更大幅度抬头,那个方向可能机械上已经没有余量。 (哪个方向对应「抬头」未验证 —— 这是现场一分钟能确认的事,列进阶段 0。)

3. 第二个硬冲突:安全层里「人」的概念是零

grep -in "human|person|旁人|观众" policy_safety.py 只有 3 处命中, 没有一处是「感知到人」:

  1. :49 步长上限来自「人类曾经指令过的最大单帧变化」
  2. :89 SAFETY.md 里「有人守着急停按钮」
  3. :281 2026-08-16 打到旁人那次的事后注释

所以当前的人身安全 = 固定几何包络 + 有人按急停。

而「朝观众推出」是故意朝人伸手 —— 它要求手臂穿过现在这套包络想拦住的方向。 z_floor(pitch) / y_floor(pitch) 那套「离机身多远就拒帧」的思路, 在「把杯子送到人手边」这个目标下不是参数不对,是前提不对。

需要的是另一类东西:知道人在哪、离多近、速度上限随距离收紧。

★ 边界条件(最重要的一条) ★
z_floor(pitch) / y_floor(pitch) 是 2026-09-03 刚接好的, 回归 40 集拒帧率 96.6% → 0.49%。
这份 GOAL 不许直接改它。 面向人的安全策略要并行设计、独立验证, 通过之后再谈怎么和现有那层合并。那一层刚从一个错误里救回来,不要在它上面叠新的未验证逻辑。

4. 这条需求改变了 B0/B1/B2 的排序依据

深度整合 GOAL 里那三条路, 原来是按当前代价排的。观众互动加进来之后,应该多一个评分项:能不能带着走。

迁移到「多物体 + 有人在场」为什么
B0(物体 token 当先验)最好 不编码任何任务专属量,只按物体重组视觉输入。场景变成「瓶→杯→人」三实体加关系时,正是它的主场
B2(深度注入)中 几何对「杯口在哪、离人多远」有用,但玻璃高脚杯 + 液体是深度相机最差场景
B1(算 3D 坐标喂流程)最差 要手写的东西从「杯子在哪」膨胀成「杯子在哪 + 人在哪 + 杯口朝向 + 推到哪停」,每加一个动作再加一批
还有一条独立的、更硬的论据:开放词表检测加一个文本 prompt 就同时覆盖 "a cup" 和 "a person", 而手写几何规则根本做不出「人」 —— 你没法用「减桌面找凸起」找出观众。 这不是效果差异,是能力有无。

同时,VLA 的语言条件在这里第一次真正有用: 「把酒展示给左边那位」这种指令,手写坐标表达不出来。

5. 计划

阶段 0:盘清楚再动手 纯远程

这一步的产物是一份清单,不是代码。 三周前已经有一套 HRI 分层了, 再写一套是重复劳动 —— 只不过这次的「上游」是自己三周前的提交。

内容
0.1通读四个文件 + DEMONSTRATION-INTERFACES.md,出「已有 / 缺 / 冲突」清单
0.2决定留哪套人脸检测:Haar 还是 YuNet。两套并存迟早出事
0.3定头部控制权仲裁方案 已定,见 §2.1。改成:先解决 §2.2 那处安全层事实错误,那是方案落地的前置
0.4查文献:人机递交(human-robot handover)是有成熟研究的方向,我们没查过。按规矩先查再定方案
0.5现场一分钟:确认 head tilt 的 range_max 方向是抬头还是低头,从录制姿态还剩多少余量(见 §2.3)

验收:0.3 的安全层问题有明确处置;0.4 至少三篇带链接的出处;0.5 有现场确认的答复。

阶段 1:面向人的安全策略 纯远程

独立于现有安全层设计,不改它。

内容
1.1定义「人在场」的状态量:方位、距离、置信度、丢失多久算没人
1.2定义速度/距离约束:离人越近上限越低。要给出拒绝条件,不只是钳位
1.3拿已录的 30 集 + 人脸检测离线回放,看这套约束会拒掉多少帧
1.4★ 人工看图核对 ★ —— 和深度那份同一条纪律,数字好看不算数

验收(事先写死):在有人入画的回放片段上,人的方位判对 ≥90%(人工核对 ≥30 帧); 约束在无人时的拒帧率 <1%(不能因为加了这层就把正常抓取搞挂)。

阶段 2:PRESENT 技能的示范采集 需要现场

前置:阶段 0.3 和阶段 1 通过。

★ 先量一次展示动作的关节速度 ★
MEASURED_STEP_CAP_DEG 是从抓取数据量出来的 (policy_safety.py:147 注释写明来源是 xlerobot-cup-grasp-20260820-0230), wrist_roll 上限 10.4°/帧,而且是静默钳位不是拒绝 (policy_safety.py:55:a too-large step just gets slowed)。
旋转杯身的速度分布和抓取不同,先量一次,别录完一整批才发现被悄悄削平了。

6. 传感器现实(做方案前必须知道)

有没有
三路 RGB:头 240×424 + 左右腕各 480×640力/力矩传感器
头部深度(Orbbec)身份识别
17 维关节位置(observation.state,纯位置)电流/负载反馈进入观测

observation.state 是 [17] 纯关节位置 —— 实测自 xlerobot-team/xlerobot-cup-grasp-20260820-0230 的 info.json。 递交动作(把杯子交到人手上、感知对方接手了没有)在没有力觉的情况下只能靠视觉判断, 这是个硬约束,方案设计时不能假装它不存在。


7. 一句话总结

观众互动不是「再加一个检测目标」,是两件重新设计:头部控制权归谁、 以及安全层怎么理解「人」。

好消息是 HRI 分层三周前就写好了(face_follow.py 的设计目标逐条对着这个需求), 坏消息是它和策略试跑抢同一条串口总线,现在不可能同时跑。

头部控制权的仲裁已经定了(§2.1,按阶段换目标来源,控制权不离开策略进程), 而且它是被数据逼出来的:50 集里头的两个自由度 σ 都是 0.0000,策略没见过头在动。

所以第一步不是选人脸模型,是先修掉 §2.2 那处安全层事实错误 —— 那才是地基。
2026-09-04 · 仓库内同一份:GOAL-AUDIENCE-INTERACTION.md · 并列的两份:深度整合 GOAL · 试验数据管理 GOAL · 前一夜的工作记录