extends TestCase ## T-WIN-004:239 字节帧反序列化 —— 契约 contracts/websocket-keypoints.md §2 ## ## 测试向量与 opendance-web 的 TypeScript 参考实现 ## (web-server/src/preprocess/keypoint-codec.ts)保持一致, ## 保证两端对同一字节流的解释完全相同。 const KeypointFrame = preload("res://src/net/keypoint_frame.gd") func _sample_points() -> PackedVector3Array: var points := PackedVector3Array() points.resize(33) for i in 33: points[i] = Vector3( float(i % 33) / 32.0, 1.0 - float(i % 33) / 32.0, float((i % 7) - 3) / 10.0 ) return points func _sample_confidence() -> PackedFloat32Array: var conf := PackedFloat32Array() conf.resize(33) for i in 33: conf[i] = 1.0 if i % 2 == 0 else 0.5 return conf func test_constants_match_contract() -> void: eq(KeypointFrame.BINARY_SIZE, 239, "帧长度常量为 239") eq(KeypointFrame.KEYPOINT_COUNT, 33, "关键点数量为 33") eq(KeypointFrame.HEADER_SIZE + KeypointFrame.KEYPOINT_COUNT * KeypointFrame.POINT_BYTES, 239, "8 + 33×7 = 239") eq(KeypointFrame.MAGIC, 0x4F44, "magic 为 'OD'(0x4F44)") eq(KeypointFrame.PROTOCOL_VERSION, 1, "协议版本为 1") func test_encode_produces_239_bytes_with_correct_header() -> void: var buf := KeypointFrame.encode(42, _sample_points(), _sample_confidence(), 1) eq(buf.size(), 239, "编码结果恰好 239 字节") eq(buf.decode_u16(0), 0x4F44, "magic 正确") eq(buf.decode_u8(2), 1, "version 正确") eq(buf.decode_u8(3), 1, "flags 正确") eq(buf.decode_u32(4), 42, "frame_id 正确") func test_roundtrip_within_fixed_point_precision() -> void: var points := _sample_points() var conf := _sample_confidence() var decoded := KeypointFrame.decode(KeypointFrame.encode(7, points, conf, 0)) not_null(decoded, "解码成功") eq(decoded.frame_id, 7, "frame_id 往返正确") eq(decoded.points.size(), 33, "解码出 33 个点") var max_delta := 0.0 for i in 33: max_delta = maxf(max_delta, absf(decoded.points[i].x - points[i].x)) max_delta = maxf(max_delta, absf(decoded.points[i].y - points[i].y)) max_delta = maxf(max_delta, absf(decoded.points[i].z - points[i].z)) # 定点精度:1/32767 ≈ 3.05e-5,留一倍余量 ok(max_delta < 1e-4, "坐标往返误差小于定点精度", "最大误差 %.8f" % max_delta) var max_conf_delta := 0.0 for i in 33: max_conf_delta = maxf(max_conf_delta, absf(decoded.confidence[i] - conf[i])) ok(max_conf_delta < 1.0 / 255.0 + 1e-6, "置信度往返误差在一个量化步长内", "最大误差 %.8f" % max_conf_delta) func test_rejects_wrong_length() -> void: var short := PackedByteArray() short.resize(238) ne(KeypointFrame.validate(short), "", "238 字节被拒绝") contains(KeypointFrame.validate(short), "239", "错误信息说明期望长度") var long := PackedByteArray() long.resize(240) ne(KeypointFrame.validate(long), "", "240 字节被拒绝") is_null(KeypointFrame.decode(short), "解码失败返回 null") func test_rejects_bad_magic() -> void: var buf := PackedByteArray() buf.resize(239) ne(KeypointFrame.validate(buf), "", "全零帧因 magic 错误被拒绝") contains(KeypointFrame.validate(buf), "magic", "错误信息指出 magic") func test_rejects_unsupported_version() -> void: var buf := KeypointFrame.encode(0, _sample_points(), _sample_confidence()) buf.encode_u8(2, 2) contains(KeypointFrame.validate(buf), "版本", "版本不符被拒绝") func test_extreme_values_do_not_overflow() -> void: var points := _sample_points() points[0] = Vector3(1.0, 1.0, -1.0) points[1] = Vector3(0.0, 0.0, 1.0) var conf := _sample_confidence() conf[0] = 1.0 conf[1] = 0.0 var decoded := KeypointFrame.decode(KeypointFrame.encode(0, points, conf)) almost(decoded.points[0].x, 1.0, 1e-4, "x = 1.0 不溢出 int16") almost(decoded.points[0].z, -1.0, 1e-4, "z = -1.0 不溢出 int16") almost(decoded.points[1].z, 1.0, 1e-4, "z = 1.0 不溢出 int16") almost(decoded.confidence[1], 0.0, 1e-6, "置信度 0 往返正确") func test_flags_helpers() -> void: var frame := KeypointFrame.decode( KeypointFrame.encode(0, _sample_points(), _sample_confidence(), KeypointFrame.FLAG_MIRRORED | KeypointFrame.FLAG_UPPER_BODY_ONLY) ) is_true(frame.is_mirrored(), "镜像标志解析正确") is_true(frame.is_upper_body_only(), "上半身模式标志解析正确") is_false(frame.has_low_confidence(), "未设置低置信度标志") func test_mirrored_frame_x_is_flipped() -> void: var points := _sample_points() var frame := KeypointFrame.decode( KeypointFrame.encode(0, points, _sample_confidence(), KeypointFrame.FLAG_MIRRORED) ) var flipped := frame.unmirrored_points() almost(flipped[5].x, 1.0 - points[5].x, 1e-4, "镜像帧翻转 x") # y 未经翻转,但仍经过 int16 定点往返,容差取定点精度 almost(flipped[5].y, points[5].y, 1e-4, "翻转不影响 y") var normal := KeypointFrame.decode(KeypointFrame.encode(0, points, _sample_confidence(), 0)) almost(normal.unmirrored_points()[5].x, points[5].x, 1e-4, "非镜像帧不翻转 x") func test_confidence_threshold_marks_unreliable() -> void: var conf := _sample_confidence() conf[11] = 0.49 conf[12] = 0.5 var frame := KeypointFrame.decode(KeypointFrame.encode(0, _sample_points(), conf)) is_false(frame.is_reliable(11), "0.49 判为不可信") is_true(frame.is_reliable(12), "0.5 判为可信(阈值含端点)") is_true(KeypointFrame.compute_low_confidence(_sample_points(), conf), "存在低置信度点时置位") func test_json_debug_frame() -> void: var raw_points: Array = [] for i in 33: raw_points.append([0.25, 0.75, -0.1, 0.9]) var frame := KeypointFrame.from_json_dict({ "frameId": 9, "flags": {"mirrored": true, "upperBodyOnly": false}, "points": raw_points, }) not_null(frame, "JSON 调试帧解析成功") eq(frame.frame_id, 9, "frameId 解析正确") is_true(frame.is_mirrored(), "mirrored 标志解析正确") almost(frame.points[0].x, 0.25, 1e-6, "坐标解析正确") var bad := KeypointFrame.from_json_dict({"frameId": 1, "points": []}) is_null(bad, "点数不足的 JSON 帧被拒绝") ## T-WIN-021:编码/解码必须与契约参考实现(JS)**逐位一致** ## ## JS 的 `Math.round` 对「恰好 .5」朝 +∞ 取整(`Math.round(-2.5) === -2`), ## 而 GDScript 的 `roundi` 是半数远离零(`roundi(-2.5) == -3`)—— ## 两者在 `-X.5` 上差 1 个 LSB。Android 的 Kotlin 实现用的正是 `Math.round`, ## 所以本端必须显式实现同一规则(`_round_like_js` = `floor(x + 0.5)`)。 func test_rounding_rule_matches_js_math_round() -> void: eq(KeypointFrame._round_like_js(-2.5), -2, "取整 -2.5 → -2(半数朝 +∞,与 JS 一致)") eq(KeypointFrame._round_like_js(2.5), 3, "取整 2.5 → 3") eq(KeypointFrame._round_like_js(-0.5), 0, "取整 -0.5 → 0(roundi 会给 -1)") eq(KeypointFrame._round_like_js(0.5), 1, "取整 0.5 → 1") eq(KeypointFrame._round_like_js(-1.5), -1, "取整 -1.5 → -1") # 端到端:-0.5/32767 经 float32 后在量化前恰好落在 -.5 上 var points := PackedVector3Array() var conf := PackedFloat32Array() points.resize(33) conf.resize(33) for i in 33: points[i] = Vector3(-0.5 / 32767.0, 0.0, 0.0) conf[i] = 128.0 / 255.0 var buf := KeypointFrame.encode(0, points, conf, 0) eq(buf.decode_s16(8), 0, "坐标 -0.5/32767 编码为 0(roundi 会给 -1)") eq(buf.decode_u8(14), 128, "置信度 128/255 编码为 128") ## 用 Web 参考实现(opendance-web 的 keypoint-codec.ts)生成的**锁定向量**做逐值比对。 ## 这一步让 `verify.sh`(只跑源码,不需要 pck)也能覆盖「Web ↔ Windows」这条边; ## 发布包层面的同一比对由 build/accept-frame-vectors.sh 负责。 func test_reference_vectors_decode_and_encode_exactly() -> void: var path := "res://build/integration/frame-vectors.json" if not FileAccess.file_exists(path): skip("参考向量", "找不到 %s" % path) return var file := FileAccess.open(path, FileAccess.READ) var parsed: Variant = JSON.parse_string(file.get_as_text()) file.close() not_null(parsed, "参考向量可解析") var cases: Array = parsed.get("cases", []) eq(cases.size(), 12, "参考向量用例数") var mismatches: Array = [] var points_compared := 0 for c in cases: var name := str(c.get("name", "?")) var bytes := PackedByteArray() var hex := str(c.get("hex", "")) bytes.resize(hex.length() / 2) for i in bytes.size(): bytes[i] = hex.substr(i * 2, 2).hex_to_int() if KeypointFrame.validate(bytes) != "": mismatches.append("%s: validate 失败" % name) continue var frame = KeypointFrame.decode(bytes) if frame == null: mismatches.append("%s: decode 为 null" % name) continue if int(frame.frame_id) != int(c.get("frameId", 0)): mismatches.append("%s: frame_id" % name) if int(frame.flags) != int(c.get("flags", 0)): mismatches.append("%s: flags" % name) var expect_points: Array = c.get("points", []) for i in expect_points.size(): points_compared += 1 var e: Array = expect_points[i] # 本端点存 PackedVector3Array(float32),参考实现是 float64: # 先把期望值收敛到 float32(Vector3 的分量天然是 float32)再比精确相等 var want := Vector3(float(e[0]), float(e[1]), float(e[2])) var want_conf := PackedFloat32Array([float(e[3])])[0] if frame.points[i] != want: mismatches.append("%s: 点 %d 坐标 %s != %s" % [name, i, str(frame.points[i]), str(want)]) elif frame.confidence[i] != want_conf: mismatches.append("%s: 点 %d 置信度" % [name, i]) # 编码:用同输入基准 f32InputPoints,与 f32Hex 逐字节比对 var f32_pts: Array = c.get("f32InputPoints", []) var f32_hex := str(c.get("f32Hex", "")) if f32_pts.size() == 33 and f32_hex != "": var pts := PackedVector3Array() var cfs := PackedFloat32Array() pts.resize(33) cfs.resize(33) for i in 33: var p: Array = f32_pts[i] pts[i] = Vector3(float(p[0]), float(p[1]), float(p[2])) cfs[i] = float(p[3]) var encoded := KeypointFrame.encode(int(c.get("frameId", 0)), pts, cfs, int(c.get("flags", 0))) if encoded.hex_encode() != f32_hex: mismatches.append("%s: 编码字节与参考不一致" % name) eq(points_compared, cases.size() * 33, "逐点比对数量") eq(mismatches.slice(0, 5), [], "参考向量逐值/逐字节一致(差异 %d 项)" % mismatches.size())