契约 §2 的参考实现是 opendance-web 的 keypoint-codec.ts(其 decodeBinaryFrame
注释就是「Windows 端参考实现」)。Android 已用金标准向量与它对齐;而本端的解码器
此前**只跟自己比过**(自己编码、自己解码),两个方向共用同一份错误假设也能一直绿。
新增:
* build/integration/frame-vectors.json —— 由参考实现生成并锁定的向量
(12 用例:i16 最小值 -32768、i16 最大值、±1 全量程、置信度 0/128/255、
flags 低 3 位组合、frame_id 0xFFFFFFFF 与 0x80000000、取整边界)。
* build/integration/probe-frame-vectors.gd —— 用**包内** KeypointFrame 解码同一批字节,
与参考解码结果**逐值精确比对**(坐标/置信度先收敛到 float32 再比精确相等,
因为本端点存 PackedVector3Array、置信度存 PackedFloat32Array)。
* build/accept-frame-vectors.sh —— 编排:用参考实现重新生成向量并与锁定文件比对
(参考实现变了却不同步向量即失败)→ 解包 → 包内解码比对 → 报告核对。
实测(**已发布**的 v0.2.3 资产重新下载后跑):
✓ 锁定的向量与参考实现重新生成的结果逐字节一致
✓ 解码用例 12/12,逐点比对 396 个,解码差异 0 项
✓ 编码器差异 1 项,且全部属于「量化前恰好 .5」的取整边界
关键点帧跨实现一致性验收全部通过
已测量并归类的一处差异(非缺陷):JS Math.round 与 Kotlin(生产发送方)Math.round
对 -2.5 取 -2(半数朝 +∞),本端 roundi 取 -3(半数远离零)。Windows 是接收方,
encode() 只在自测/协议回归里用,故对产品无影响;脚本硬性要求这类差异只能出现在
取整边界用例上,否则视为新问题。若将来本端要当发送方,把 roundi 换成 floor(x+0.5) 即可。
踩坑(两处「看着通过其实没测到」,都写进任务文档):
1. 生成器第一版把「喂进编码器的输入」当成 points 写进向量 → 比对变成「比编码量化」,
靠抽查数值才发现;现在向量同时含 inputPoints / f32InputPoints / f32Hex / points。
2. 编码器比对一开始用 float64 输入编出的 hex 当基准(正数恰好 .5 那例整例 99 字节不同),
实际只是输入从 float64 收敛到 float32 的量化差;改用同输入的 f32Hex 基准后,
差异收敛到唯一一个真正的取整边界用例。教训:跨实现比对必须先统一输入。
package-release.sh 结尾把四条发布前验收一次列全;verify.sh 全绿(774 通过 / 0 失败)。
5.4 KiB
5.4 KiB
T-WIN-021:关键点帧跨实现一致性(Web 参考实现 × 包内 GDScript 解码器)
- 状态: done
- 依赖: T-WIN-004(关键点帧解码)/ T-WEB-006(动作内部标准)
- 文档: v1.6 §7.6.2
- 契约:
websocket-keypoints 0.2.0§2
为什么需要
opendance-web/web-server/src/preprocess/keypoint-codec.ts 是契约 §2 的参考实现
(它的 encodeBinaryFrame 注释就是「Android 端参考实现」,
decodeBinaryFrame 注释是「Windows 端参考实现(T-WIN-004)」):
- Android ↔ Web 已经用「金标准字节向量」对齐(
KeypointFrameTest.goldenVectorMatchesWebImplementation); - Windows 的解码器此前只跟自己比过 —— 自己编码、自己解码, 两个方向用同一份错误假设也能一直绿。这条边是本轮补的。
实现
| 文件 | 角色 |
|---|---|
opendance-web/web-server/scripts/gen-frame-vectors.mts(新增) |
用参考实现生成向量:12 个用例的字节(hex)+参考解码结果(points,float64)+ float32 输入基准(f32InputPoints / f32Hex) |
opendance-web/web-server/test/keypoint-frame-codec.spec.ts(新增,7 项) |
把参考实现对其他两端的承诺显式钉住:i16 最小值边界、取整规则、置信度阈值 128、u32 frame_id、flags 掩码、非法帧拒绝、随机输入往返 |
opendance-windows/build/integration/frame-vectors.json(锁定) |
由参考实现生成并入库的向量(比对基准) |
opendance-windows/build/integration/probe-frame-vectors.gd(新增) |
用包内 KeypointFrame 解码同一批字节并逐值比对,产出 JSON 报告 |
opendance-windows/build/accept-frame-vectors.sh(新增) |
编排:重新生成并比对向量 → 解包 → 包内解码比对 → 报告核对 |
比对口径(关键)
flags/frame_id:精确相等;- 坐标与置信度:本端点存进
PackedVector3Array、置信度存进PackedFloat32Array, 即 float32;参考实现是 float64。因此先把期望值收敛到 float32 (GDScript 里借Vector3(...)/PackedFloat32Array([...])天然完成)再比精确相等 —— 比用容差更严格,并把差异限定在「表示精度」这一点上。
实测(已发布的 v0.2.3 资产重新下载后跑)
[accept-fv] 用参考实现重新生成向量并比对…
✓ 锁定的向量与参考实现重新生成的结果逐字节一致
[accept-fv] 包内解码器 × 参考向量…
[frame-vectors] 用例 12 个;解码用例 12/12,比对点 396 个
[frame-vectors] ✓ 解码结果与 Web 参考实现逐值一致(float32 精确相等)
✓ 解码差异 0 项(必须为 0)
✓ 逐点比对 396 个(期望 396)
✓ 编码器差异 1 项,全部属于「量化前恰好 .5」的取整边界
关键点帧跨实现一致性验收全部通过
覆盖的边界:i16 最小值 -32768(-32768/32767,必须是 0x8000 而不是被夹成 -32767)、
i16 最大值、±1 全量程、置信度 0 / 128 / 255、flags 低 3 位组合、
frame_id 的 0xFFFFFFFF 与 0x80000000、以及取整边界。
一处已测量并归类的差异(非缺陷)
本端 KeypointFrame.encode() 与参考编码器在一个用例上差 33 字节:
| 取整方式 | -X.5 的行为 |
|
|---|---|---|
JS 参考实现(Math.round) |
半数朝 +∞ | -2.5 → -2 |
Android(生产发送方,Math.round) |
同上 | -2.5 → -2 ✅ 与参考一致 |
Windows KeypointFrame.encode()(roundi,半数远离零) |
-2.5 → -3 ❌ 差 1 LSB |
影响:Windows 是接收方,生产链路里不调用 encode()(它只用于本仓自测与协议回归),
因此对产品无影响。accept-frame-vectors.sh 因此把编码差异记为「归类记录」,
但硬性要求差异只能出现在「量化前恰好落在 .5」的用例上 —— 若哪天出现在别处,
就说明还有别的问题。若将来 Windows 需要成为发送方,把 roundi 换成
floor(x + 0.5) 即可与参考实现完全一致。
踩坑记录(两处"看起来通过、其实没测到")
- 期望值写成了输入值:第一版生成器把「喂进编码器的原始点」当成
points写进向量, 于是比对变成了「比编码量化」而不是「比两个解码器是否一致」。 靠抽查数值(-2.5/32767的原值出现在解码结果里)才发现。 → 向量里现在同时有inputPoints(原始)、f32InputPoints/f32Hex(同输入基准) 与points(参考解码结果),并各自注明用途。 - 拿不同输入的字节做比较:编码器比对一开始用
hex(float64 输入编出来的)当基准, 于是「正数恰好 .5」整例 99 字节不同 —— 实际只是输入从 float64 收敛到 float32 的量化差, 与取整规则无关。改用f32Hex(同输入基准)后,差异收敛到唯一一个真正的取整边界用例。 → 教训:跨实现比对必须先统一输入,否则差异无法归因。
未覆盖
- 三端矩阵的另外两条边已有证据:Android ↔ Web(金标准向量,Android 仓 JVM 测试)、 Android → Windows(T-WIN-020 的实时链路验收,真实 WebSocket 发 239 字节帧);
- 真机端到端(相机 → MediaPipe → 编码 → Wi-Fi → Windows 解码 → 驱动模型)仍需设备实测。