extends SceneTree ## 关键点帧跨实现一致性探针(T-WIN-021) ## ## 用**发布包里的** `KeypointFrame` 解码由 opendance-web 参考实现(TypeScript) ## 生成的 239 字节向量,逐值比对。用法(由 build/accept-frame-vectors.sh 调用): ## ## godot --headless --main-pack <包内 OpenDance.pck> \ ## --script /绝对路径/build/integration/probe-frame-vectors.gd \ ## -- --vectors /绝对路径/frame-vectors.json --report /tmp/frame-vectors-report.json ## ## 为什么需要:`web-server/src/preprocess/keypoint-codec.ts` 是契约 §2 的 ## **参考实现**(其 `decodeBinaryFrame` 的注释就是「Windows 端参考实现(T-WIN-004)」), ## Android 的 Kotlin 实现已用「金标准字节向量」与它对齐;而**本端的 GDScript 解码器 ## 此前从未与它逐值比对过** —— 只有本端自己的单测(自己编码、自己解码)。 ## ## 比对口径(重要): ## * `flags` / `frame_id`:**精确相等**; ## * 坐标与置信度:本端把点存进 `PackedVector3Array`、置信度存进 `PackedFloat32Array`, ## 即 **float32**;参考实现是 float64。因此先把期望值收敛到 float32 ## (GDScript 里借 `Vector3(...)` / `PackedFloat32Array([...])` 天然完成) ## 再比**精确相等** —— 这比用容差更严格,且把差异限定在「表示精度」上。 ## * 另附:用本端 `encode()` 对向量里的 `f32InputPoints`(float32 同输入基准) ## 重新编码,与 `f32Hex` 逐字节比对。v0.2.4 起本端取整已改为 `floor(x + 0.5)` ## (与 JS `Math.round` 的「半数朝 +∞」一致),因此**差异必须为 0**; ## 收敛前的宽松口径(只允许出现在 .5 取整边界)已废弃 —— ## 规则性偏差留在日志里会变成"永远没人看的已知差异"。 const KeypointFrame = preload("res://src/net/keypoint_frame.gd") var _vectors_path := "" var _report_path := "" var _done := false func _initialize() -> void: var args := OS.get_cmdline_user_args() var i := 0 while i < args.size(): match args[i]: "--vectors": _vectors_path = args[i + 1]; i += 2 "--report": _report_path = args[i + 1]; i += 2 _: i += 1 _run() func _process(_delta: float) -> bool: return _done func _hex_to_bytes(hex: String) -> PackedByteArray: var out := PackedByteArray() out.resize(hex.length() / 2) for i in out.size(): out[i] = hex.substr(i * 2, 2).hex_to_int() return out ## 把 float64 收敛成 float32:GDScript 的 Vector3 分量 / PackedFloat32Array 都是 float32 static func _to_f32(value: float) -> float: return Vector3(value, 0.0, 0.0).x func _run() -> void: if _vectors_path == "" or not FileAccess.file_exists(_vectors_path): print("[frame-vectors] ✗ 找不到向量文件:%s" % _vectors_path) quit(2) return var file := FileAccess.open(_vectors_path, FileAccess.READ) var parsed: Variant = JSON.parse_string(file.get_as_text()) file.close() if typeof(parsed) != TYPE_DICTIONARY: print("[frame-vectors] ✗ 向量文件不是 JSON 对象") quit(2) return var cases: Array = parsed.get("cases", []) print("[frame-vectors] 参考实现:%s" % parsed.get("source", "(未标注)")) print("[frame-vectors] 用例 %d 个" % cases.size()) var decoded_cases := 0 var decode_failures: Array = [] var encode_differences: Array = [] var total_points := 0 for c in cases: var name := str(c.get("name", "(未命名)")) var bytes := _hex_to_bytes(str(c.get("hex", ""))) var expect_frame_id := int(c.get("frameId", 0)) var expect_flags := int(c.get("flags", 0)) var expect_points: Array = c.get("points", []) # ---- 校验 + 解码 ---- var problem := KeypointFrame.validate(bytes) if problem != "": decode_failures.append({"case": name, "problem": "validate: " + problem}) continue var frame = KeypointFrame.decode(bytes) if frame == null: decode_failures.append({"case": name, "problem": "decode 返回 null"}) continue decoded_cases += 1 if int(frame.frame_id) != expect_frame_id: decode_failures.append({ "case": name, "problem": "frame_id 期望 %d,实际 %d" % [expect_frame_id, frame.frame_id], }) if int(frame.flags) != expect_flags: decode_failures.append({ "case": name, "problem": "flags 期望 %d,实际 %d" % [expect_flags, frame.flags], }) # ---- 逐点比对(float32 精确相等)---- for i in expect_points.size(): total_points += 1 var e: Array = expect_points[i] var want := Vector3(_to_f32(float(e[0])), _to_f32(float(e[1])), _to_f32(float(e[2]))) var want_conf := PackedFloat32Array([float(e[3])])[0] var got: Vector3 = frame.points[i] var got_conf: float = frame.confidence[i] if got != want: decode_failures.append({ "case": name, "problem": "点 %d 坐标不符:期望 %s,实际 %s" % [i, str(want), str(got)], }) elif got_conf != want_conf: decode_failures.append({ "case": name, "problem": "点 %d 置信度不符:期望 %s,实际 %s" % [i, str(want_conf), str(got_conf)], }) # ---- 附带:本端 encode() 与参考字节的差异(本端不是发送方,记录不判死)---- # 必须用 **f32InputPoints**(已收敛到 float32)来重编码:本端点/置信度就是 float32, # 若拿 float64 的原始输入比,差异会同时来自「输入精度」与「取整规则」,无法归因。 var input_points: Array = c.get("f32InputPoints", c.get("inputPoints", [])) # 与**同输入**基准比:`f32Hex` 是用 f32InputPoints 编出来的字节。 # 拿 `hex`(float64 输入)比会把「输入精度差异」也算进来(第一版就是这么误判的: # 「正数恰好 .5」整例 99 字节不同,其实只是输入从 float64 收敛到 float32 后的量化差)。 var baseline_hex := str(c.get("f32Hex", c.get("hex", ""))) var baseline := _hex_to_bytes(baseline_hex) if input_points.size() == 33: var pts := PackedVector3Array() var conf := PackedFloat32Array() pts.resize(33) conf.resize(33) for i in 33: var p: Array = input_points[i] pts[i] = Vector3(float(p[0]), float(p[1]), float(p[2])) conf[i] = float(p[3]) var re_encoded := KeypointFrame.encode(expect_frame_id, pts, conf, expect_flags) var diff_offsets: Array = [] for i in re_encoded.size(): if re_encoded[i] != baseline[i]: diff_offsets.append(i) if diff_offsets.size() > 0: # 归类:本端 `roundi` 对「.5」的进位方向与 JS `Math.round` 不同 # (-2.5:GDScript → -3,JS → -2)。**同输入**下,这类差异只可能 # 出现在量化前恰好落在 X.5 的用例上 —— 这里直接**算**出来, # 而不是靠用例名猜(第一版按名字猜,漏判了「置信度 128/255」, # 因为那一例的 y 也取了 -0.5)。 var has_half := false for p in input_points: for value in [p[0], p[1], p[2]]: var scaled: float = float(value) * 32767.0 var a := absf(scaled) if absf(a - floorf(a) - 0.5) < 1e-9: has_half = true var c_scaled: float = float(p[3]) * 255.0 var ac := absf(c_scaled) if absf(ac - floorf(ac) - 0.5) < 1e-9: has_half = true encode_differences.append({ "case": name, "diffBytes": diff_offsets.size(), "halfValueInput": has_half, }) var report := { "source": parsed.get("source", ""), "cases": cases.size(), "decodedCases": decoded_cases, "pointsCompared": total_points, "decodeFailures": decode_failures, "encodeDifferences": encode_differences, } if _report_path != "": var out := FileAccess.open(_report_path, FileAccess.WRITE) if out != null: out.store_string(JSON.stringify(report, " ")) out.close() print("[frame-vectors] 解码用例 %d/%d,比对点 %d 个" % [decoded_cases, cases.size(), total_points]) if decode_failures.is_empty(): print("[frame-vectors] ✓ 解码结果与 Web 参考实现逐值一致(float32 精确相等)") else: print("[frame-vectors] ✗ 解码差异 %d 项:" % decode_failures.size()) for f in decode_failures.slice(0, 8): print(" · %s:%s" % [f["case"], f["problem"]]) if encode_differences.is_empty(): print("[frame-vectors] ✓ 本端 encode() 与参考字节完全一致") else: print("[frame-vectors] ✗ 本端 encode() 与参考字节存在差异(取整规则应已对齐,必须为 0):") for d in encode_differences: print(" · %s:%d 字节不同(量化前存在 .5 值=%s)" % [ d["case"], d["diffBytes"], str(d.get("halfValueInput", false))]) _done = true quit(1 if decode_failures.size() > 0 else 0)