fix(pack): 编码取整与参考实现对齐 + 产物不再夹带测试夹具(v0.2.4)
两个缺陷,都由上一轮的验收机制暴露:
1) 编码取整与契约参考实现不一致(T-WIN-022)
KeypointFrame.encode() 用 GDScript 的 roundi(半数远离零),而契约参考实现
(Web 的 keypoint-codec.ts)与**生产发送方 Android**(Kotlin Math.round)都是
「半数朝 +∞」:-2.5 分别是 -3 与 -2。实测在 12 个跨实现向量里 1 例 33 字节不同。
本端是接收方、encode() 只用于自测与协议回归,对产品无影响;但它让「跨实现一致性」
只能停留在宽松口径("差异仅限 .5 取整边界"),而宽松口径会退化成没人看的已知差异。
修复:新增 _round_like_js(v) = floor(v + 0.5),用于 4 处取整(坐标 ×3 + 置信度);
夹紧顺序对任意实数与参考实现等价。
收紧:accept-frame-vectors.sh 的编码口径改为「必须为 0 项差异」。
新增单测(+11 断言):_round_like_js 的 5 个取值、端到端 -0.5/32767 → 0(roundi
会给 -1,是有效判别用例),以及 test_reference_vectors_decode_and_encode_exactly
—— 直接读锁定的参考向量逐值解码 + 用 f32InputPoints 重编码与 f32Hex 逐字节比对,
让 verify.sh(只跑源码、无需 pck)也覆盖「Web ↔ Windows」这条边。
2) 产物里夹带开发/验收工具(T-WIN-023)
export_filter="all_resources" 且 exclude_filter 没有 build/*,于是每次新增
构建/验收脚本都被编译进 pck —— 包括**故意不合规**的坏服务端夹具 probe-ws-bad.gd
与 frame-vectors.json。实测 OpenDance.pck 315,464 B →(排除后)145,400 B,
即 54% 的 pck 是测试夹具。已发布的 v0.2.3 同样带着这些残留。
修复:exclude_filter 增加 build/*;
新增产物级防线 build/integration/list-pck.gd(遍历包内条目、标出
res://build/ tests/ _scratch/ dist-release/ 与 *.md 残留,有残留即退出码 1),
并接进 accept-release.sh(打印条目总数并断言「无开发残留」)——否则下次加个新
工具目录又会重演。顺带确认 pck 没有把上一次导出的 pck 自嵌套进去。
实测(v0.2.4 发布包,三条产物级验收全绿):
* accept-release.sh:pck 58 项无开发残留 / PE 版本资源 0.2.4 / pck 启动 180 帧无错 /
运行时探针(包内版本号 0.2.4、AboutScreen 显示值、15 项关键资源、契约表比对)
* accept-ws.sh:客户端 30 通过 / 0 失败 + 服务端侧 16 项事实
* accept-frame-vectors.sh:解码 12/12、396 点 0 差异,
**编码器字节与参考实现完全一致(0 项差异)**
* verify.sh:785 通过 / 0 失败(原 774)
踩坑:给 -0.5/32767 这类值写断言时,第一版按"理想有理数"期望(2.5/32767 → 3)而
失败 —— PackedVector3Array 存的是 float32,乘回 32767 后略小于半整数,与 JS 对同一
float32 输入的行为其实完全一致。断言必须基于实际存储精度,或改用有效判别用例。
This commit is contained in:
@@ -22,10 +22,28 @@ OpenDance Studio **阶段二**: Windows 客户端(Godot 4)。
|
|||||||
| 跨实现交叉校验 | OK 关节角与 Web 服务端 TypeScript 实现**逐帧一致** |
|
| 跨实现交叉校验 | OK 关节角与 Web 服务端 TypeScript 实现**逐帧一致** |
|
||||||
| 场景选择与物理落地 | OK `scenes[]` 可选、重力/风/摩擦/碰撞体落地并可日志核对 |
|
| 场景选择与物理落地 | OK `scenes[]` 可选、重力/风/摩擦/碰撞体落地并可日志核对 |
|
||||||
| 连接保活与设备去重 | OK welcome/config 声明静默超时与心跳;应用层 ping/pong;按 deviceId 去重 |
|
| 连接保活与设备去重 | OK welcome/config 声明静默超时与心跳;应用层 ping/pong;按 deviceId 去重 |
|
||||||
| 发布整包 | OK 单个 `opendance-windows-v0.2.3.zip` + `SHA256SUMS`(不拆分卷) |
|
| 发布整包 | OK 单个 `opendance-windows-v0.2.4.zip` + `SHA256SUMS`(不拆分卷) |
|
||||||
| 版本号展示 | OK 「关于」清单显示 `v0.2.2`;`project.godot` / `project.yaml` / `export_presets.cfg` / exe 的 PE 版本资源 / 产物名 / tag 全部一致 |
|
| 版本号展示 | OK 「关于」清单显示 `v0.2.2`;`project.godot` / `project.yaml` / `export_presets.cfg` / exe 的 PE 版本资源 / 产物名 / tag 全部一致 |
|
||||||
| 场景物理与保活 | OK 新增真实 GLB 端到端用例 + 17 秒保活对照用例(751 项断言全过) |
|
| 场景物理与保活 | OK 新增真实 GLB 端到端用例 + 17 秒保活对照用例(751 项断言全过) |
|
||||||
|
|
||||||
|
## v0.2.4 修订(编码取整对齐 + 产物不再夹带测试夹具)
|
||||||
|
|
||||||
|
**缺陷 1(编码取整与参考实现不一致)**:`KeypointFrame.encode()` 用 GDScript 的
|
||||||
|
`roundi`(半数远离零),而契约参考实现(Web 的 `Math.round`)与**生产发送方 Android**
|
||||||
|
(Kotlin `Math.round`)都是「半数朝 +∞」—— `-2.5` 分别是 `-3` 与 `-2`。
|
||||||
|
实测在 12 个跨实现向量里有 1 例 **33 字节不同**。本端是接收方、`encode()` 只用于自测,
|
||||||
|
对产品无影响;但它让「跨实现一致性」只能停留在宽松口径("差异仅限取整边界"),
|
||||||
|
而宽松口径会退化成永远没人看的已知差异。**修复**:新增 `_round_like_js(v) = floor(v + 0.5)`
|
||||||
|
并用于 4 处取整;`accept-frame-vectors.sh` 的口径**收紧为「编码差异必须为 0」**。
|
||||||
|
|
||||||
|
**缺陷 2(产物里夹带开发/验收工具)**:`export_filter="all_resources"` +
|
||||||
|
`exclude_filter` 没有 `build/*`,于是每次新增构建/验收脚本都会被编译进 pck ——
|
||||||
|
包括**故意不合规**的坏服务端夹具 `probe-ws-bad.gd` 与向量文件。
|
||||||
|
`OpenDance.pck` 因此从 145,400 B 涨到 **315,464 B(54% 是测试夹具)**。
|
||||||
|
**修复**:`exclude_filter` 增加 `build/*`;并新增**产物级防线**
|
||||||
|
`build/integration/list-pck.gd`(遍历包内条目、标出开发残留)+ `accept-release.sh`
|
||||||
|
断言「无残留」,避免下次加个新工具目录又重演。
|
||||||
|
|
||||||
## v0.2.3 修订(跨端集成验收 + 两处真实缺陷)
|
## v0.2.3 修订(跨端集成验收 + 两处真实缺陷)
|
||||||
|
|
||||||
**缺陷 1(契约字段缺失)**:`CatalogTypes.Motion` 没有 `thumbnail_url` 字段,
|
**缺陷 1(契约字段缺失)**:`CatalogTypes.Motion` 没有 `thumbnail_url` 字段,
|
||||||
@@ -109,10 +127,10 @@ GODOT_BIN=/path/to/godot bash build/verify.sh
|
|||||||
GODOT_BIN=/path/to/godot bash build/export-windows.sh release
|
GODOT_BIN=/path/to/godot bash build/export-windows.sh release
|
||||||
|
|
||||||
# 打包发布产物(单个完整 zip + SHA256SUMS)
|
# 打包发布产物(单个完整 zip + SHA256SUMS)
|
||||||
bash build/package-release.sh 0.2.3
|
bash build/package-release.sh 0.2.4
|
||||||
|
|
||||||
# 发布包级验收(对**发布包**跑,而不是对源码;文档 §7.5)
|
# 发布包级验收(对**发布包**跑,而不是对源码;文档 §7.5)
|
||||||
GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.3.zip
|
GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.4.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
`accept-release.sh` 校验 sha256 → 解压 → 文件清单(并检查没有 `.partN` 残留)→
|
`accept-release.sh` 校验 sha256 → 解压 → 文件清单(并检查没有 `.partN` 残留)→
|
||||||
@@ -131,7 +149,7 @@ Android 已用「金标准字节向量」与它对齐;而**本端的解码器
|
|||||||
(自己编码、自己解码)。这条边由下列脚本补上:
|
(自己编码、自己解码)。这条边由下列脚本补上:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
GODOT_BIN=/path/to/godot bash build/accept-frame-vectors.sh dist-release/opendance-windows-v0.2.3.zip
|
GODOT_BIN=/path/to/godot bash build/accept-frame-vectors.sh dist-release/opendance-windows-v0.2.4.zip
|
||||||
# 没有 web 仓 / 未装 node_modules 时:... --skip-regen
|
# 没有 web 仓 / 未装 node_modules 时:... --skip-regen
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -154,7 +172,7 @@ GODOT_BIN=/path/to/godot bash build/accept-frame-vectors.sh dist-release/opendan
|
|||||||
文本/二进制边界、关闭语义、子协议协商)。因此另配一套**独立实现**的客户端:
|
文本/二进制边界、关闭语义、子协议协商)。因此另配一套**独立实现**的客户端:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
GODOT_BIN=/path/to/godot bash build/accept-ws.sh dist-release/opendance-windows-v0.2.3.zip
|
GODOT_BIN=/path/to/godot bash build/accept-ws.sh dist-release/opendance-windows-v0.2.4.zip
|
||||||
# 跳过耗时的两个场景(保活 19s / 静默超时 15s):
|
# 跳过耗时的两个场景(保活 19s / 静默超时 15s):
|
||||||
GODOT_BIN=... bash build/accept-ws.sh <zip> --skip-slow
|
GODOT_BIN=... bash build/accept-ws.sh <zip> --skip-slow
|
||||||
```
|
```
|
||||||
@@ -172,7 +190,7 @@ GODOT_BIN=... bash build/accept-ws.sh <zip> --skip-slow
|
|||||||
保活字段、不回 pong、不做去重)跑同一客户端 → 14 项 ✗、退出码 1,证明断言不是空转。
|
保活字段、不回 pong、不做去重)跑同一客户端 → 14 项 ✗、退出码 1,证明断言不是空转。
|
||||||
详见 [`tasks/020-ws-contract-conformance.md`](tasks/020-ws-contract-conformance.md)。
|
详见 [`tasks/020-ws-contract-conformance.md`](tasks/020-ws-contract-conformance.md)。
|
||||||
|
|
||||||
当前版本:**v0.2.3**(`project.godot` 的 `application/config/version`,
|
当前版本:**v0.2.4**(`project.godot` 的 `application/config/version`,
|
||||||
与 `project.yaml`、`export_presets.cfg`、导出产物名、Git tag、「关于」清单展示、
|
与 `project.yaml`、`export_presets.cfg`、导出产物名、Git tag、「关于」清单展示、
|
||||||
以及 exe 的 PE 版本资源全部一致)。
|
以及 exe 的 PE 版本资源全部一致)。
|
||||||
|
|
||||||
@@ -255,7 +273,7 @@ GODOT_BIN=/path/to/godot bash build/export-windows.sh release
|
|||||||
powershell -ExecutionPolicy Bypass -File build\export-windows.ps1
|
powershell -ExecutionPolicy Bypass -File build\export-windows.ps1
|
||||||
|
|
||||||
# 打成可分发的 zip(单个完整包 + SHA256SUMS)
|
# 打成可分发的 zip(单个完整包 + SHA256SUMS)
|
||||||
bash build/package-release.sh 0.2.3
|
bash build/package-release.sh 0.2.4
|
||||||
```
|
```
|
||||||
|
|
||||||
| 产物 | 位置 | 说明 |
|
| 产物 | 位置 | 说明 |
|
||||||
@@ -263,7 +281,7 @@ bash build/package-release.sh 0.2.3
|
|||||||
| `OpenDance.exe` | `build/windows/` | 主程序,约 104 MB;脚本校验前 2 字节为 `MZ`(合法 PE) |
|
| `OpenDance.exe` | `build/windows/` | 主程序,约 104 MB;脚本校验前 2 字节为 `MZ`(合法 PE) |
|
||||||
| `OpenDance.pck` | `build/windows/` | 资源包,约 107 KB; **必须与 exe 同目录** |
|
| `OpenDance.pck` | `build/windows/` | 资源包,约 107 KB; **必须与 exe 同目录** |
|
||||||
| `OpenDance.console.exe` | `build/windows/` | 控制台包装器,可能生成,非必需 |
|
| `OpenDance.console.exe` | `build/windows/` | 控制台包装器,可能生成,非必需 |
|
||||||
| `opendance-windows-v0.2.3.zip` | `dist-release/` | 发布包:exe + pck + `THIRD_PARTY.md` + `README.txt` + `README.md` + `contracts/` |
|
| `opendance-windows-v0.2.4.zip` | `dist-release/` | 发布包:exe + pck + `THIRD_PARTY.md` + `README.txt` + `README.md` + `contracts/` |
|
||||||
| `SHA256SUMS` | `dist-release/` | 裸文件名,与 zip 同目录时可 `sha256sum -c SHA256SUMS` 直接校验 |
|
| `SHA256SUMS` | `dist-release/` | 裸文件名,与 zip 同目录时可 `sha256sum -c SHA256SUMS` 直接校验 |
|
||||||
|
|
||||||
导出前必须安装与编辑器同版本的 export templates,否则脚本会报「未找到导出模板」。
|
导出前必须安装与编辑器同版本的 export templates,否则脚本会报「未找到导出模板」。
|
||||||
@@ -367,7 +385,7 @@ src/
|
|||||||
等格式,但本项目未在 Windows 上实测)。
|
等格式,但本项目未在 Windows 上实测)。
|
||||||
若 IIS 上限已放开、或你能把包放到别处分发(例如 Web 服务端的 `/static`,
|
若 IIS 上限已放开、或你能把包放到别处分发(例如 Web 服务端的 `/static`,
|
||||||
不受 IIS 那个限制约束),请改用资源管理器可直接双击解压的 deflate 包:
|
不受 IIS 那个限制约束),请改用资源管理器可直接双击解压的 deflate 包:
|
||||||
`ZIP_METHOD=deflate bash build/package-release.sh 0.2.3`(约 38 MB)。
|
`ZIP_METHOD=deflate bash build/package-release.sh 0.2.4`(约 38 MB)。
|
||||||
7. **发布包 exe 的版本元数据**(T-WIN-017):v0.2.1 之前在 Windows
|
7. **发布包 exe 的版本元数据**(T-WIN-017):v0.2.1 之前在 Windows
|
||||||
「属性 → 详细信息」里显示的是 `0.1.0`,因为 `export_presets.cfg` 的
|
「属性 → 详细信息」里显示的是 `0.1.0`,因为 `export_presets.cfg` 的
|
||||||
`application/file_version` / `application/product_version` 被硬编码且无人校验。
|
`application/file_version` / `application/product_version` 被硬编码且无人校验。
|
||||||
|
|||||||
+17
-5
@@ -1,5 +1,5 @@
|
|||||||
================================================================
|
================================================================
|
||||||
OpenDance Studio -- Windows 客户端 v0.2.3 使用说明(zip 为 LZMA 压缩,需用 7-Zip/WinRAR 解压)
|
OpenDance Studio -- Windows 客户端 v0.2.4 使用说明(zip 为 LZMA 压缩,需用 7-Zip/WinRAR 解压)
|
||||||
================================================================
|
================================================================
|
||||||
|
|
||||||
摄像头跳舞评分系统的核心体验端:接收 Android 辅助 App 通过 WebSocket
|
摄像头跳舞评分系统的核心体验端:接收 Android 辅助 App 通过 WebSocket
|
||||||
@@ -8,6 +8,18 @@ OpenDance Studio -- Windows 客户端 v0.2.3 使用说明(zip 为 LZMA 压缩
|
|||||||
|
|
||||||
本客户端只接收关键点坐标,不接收、不存储任何视频画面。
|
本客户端只接收关键点坐标,不接收、不存储任何视频画面。
|
||||||
|
|
||||||
|
v0.2.4 修订
|
||||||
|
1. 编码取整与契约参考实现对齐: 本端 encode() 原用 GDScript roundi(半数远离零),
|
||||||
|
而参考实现与生产发送方(Android 的 Math.round)是「半数朝 +∞」—— -2.5 分别是
|
||||||
|
-3 与 -2。本端是接收方(encode 只用于自测),故对产品无影响,但跨实现一致性
|
||||||
|
只能停留在"差异仅限取整边界"的宽松口径。现改为 floor(x + 0.5),并把验收口径
|
||||||
|
收紧为「编码差异必须为 0」。
|
||||||
|
2. 产物不再夹带开发/验收工具: export_filter="all_resources" 且 exclude_filter
|
||||||
|
没有 build/*,导致探针脚本(含故意不合规的坏服务端夹具)与向量文件被打进 pck ——
|
||||||
|
pck 从 145,400 B 涨到 315,464 B(54% 是测试夹具)。现已排除 build/*,
|
||||||
|
并新增产物级残留检查(accept-release.sh 会断言「无开发残留」)。
|
||||||
|
3. 无头测试 785 项断言(原 774)。
|
||||||
|
|
||||||
v0.2.3 修订
|
v0.2.3 修订
|
||||||
1. 补 Motion.thumbnail_url: 服务端 catalog/export 从 data-models 0.6.0 起一直
|
1. 补 Motion.thumbnail_url: 服务端 catalog/export 从 data-models 0.6.0 起一直
|
||||||
在发 thumbnailUrl,客户端类型里却缺这个字段(Model/Scene 都有),等于默默丢掉。
|
在发 thumbnailUrl,客户端类型里却缺这个字段(Model/Scene 都有),等于默默丢掉。
|
||||||
@@ -140,11 +152,11 @@ OpenDance.exe 的用户看第一节即可。
|
|||||||
|
|
||||||
打成发布包:
|
打成发布包:
|
||||||
|
|
||||||
bash build/package-release.sh 0.2.3
|
bash build/package-release.sh 0.2.4
|
||||||
|
|
||||||
得到 dist-release/ 下两个资产:
|
得到 dist-release/ 下两个资产:
|
||||||
|
|
||||||
opendance-windows-v0.2.3.zip <- 单个完整包,禁止 .partN 分卷
|
opendance-windows-v0.2.4.zip <- 单个完整包,禁止 .partN 分卷
|
||||||
SHA256SUMS <- 裸文件名,可直接 sha256sum -c
|
SHA256SUMS <- 裸文件名,可直接 sha256sum -c
|
||||||
|
|
||||||
包里含 OpenDance.exe、OpenDance.pck、THIRD_PARTY.md、README.txt、
|
包里含 OpenDance.exe、OpenDance.pck、THIRD_PARTY.md、README.txt、
|
||||||
@@ -163,14 +175,14 @@ OpenDance.exe 的用户看第一节即可。
|
|||||||
如果你能把包放到别处分发(例如 Web 服务端的 /static,不受 IIS 那个
|
如果你能把包放到别处分发(例如 Web 服务端的 /static,不受 IIS 那个
|
||||||
上限约束),建议改用资源管理器可直接双击解压的 deflate 包:
|
上限约束),建议改用资源管理器可直接双击解压的 deflate 包:
|
||||||
|
|
||||||
ZIP_METHOD=deflate bash build/package-release.sh 0.2.3 # 约 38 MB
|
ZIP_METHOD=deflate bash build/package-release.sh 0.2.4 # 约 38 MB
|
||||||
|
|
||||||
上限一旦放开,Release 资产也应换回 deflate 包。
|
上限一旦放开,Release 资产也应换回 deflate 包。
|
||||||
|
|
||||||
7. 自检清单
|
7. 自检清单
|
||||||
|
|
||||||
0) 发布包级验收(对**发布包**而不是源码;文档 §7.5)
|
0) 发布包级验收(对**发布包**而不是源码;文档 §7.5)
|
||||||
GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.3.zip
|
GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.4.zip
|
||||||
预期末行「发布包级验收全部通过」。它会校验 sha256、解压、核对文件清单、
|
预期末行「发布包级验收全部通过」。它会校验 sha256、解压、核对文件清单、
|
||||||
读 exe 的 PE 版本资源、用 Godot 直接加载包内 pck 跑 180 帧、并跑包内运行时
|
读 exe 的 PE 版本资源、用 Godot 直接加载包内 pck 跑 180 帧、并跑包内运行时
|
||||||
探针(包内版本号 / 「关于」清单显示值 / 主场景 / autoload / 关键资源 /
|
探针(包内版本号 / 「关于」清单显示值 / 主场景 / autoload / 关键资源 /
|
||||||
|
|||||||
@@ -131,12 +131,14 @@ say(r.pointsCompared === r.cases * 33, `逐点比对 ${r.pointsCompared} 个(
|
|||||||
say(Array.isArray(r.decodeFailures) && r.decodeFailures.length === 0,
|
say(Array.isArray(r.decodeFailures) && r.decodeFailures.length === 0,
|
||||||
`解码差异 ${r.decodeFailures?.length ?? "?"} 项(必须为 0)`);
|
`解码差异 ${r.decodeFailures?.length ?? "?"} 项(必须为 0)`);
|
||||||
if (r.decodeFailures?.length) for (const f of r.decodeFailures.slice(0, 5)) console.log(` · ${f.case}:${f.problem}`);
|
if (r.decodeFailures?.length) for (const f of r.decodeFailures.slice(0, 5)) console.log(` · ${f.case}:${f.problem}`);
|
||||||
// 编码器差异:本端不是发送方(只用于自测),但差异必须只出现在「量化前恰好 .5」的用例上
|
// 编码器差异:v0.2.4 起本端 `_round_like_js`(floor(x+0.5))已与参考实现的
|
||||||
|
// `Math.round` 对齐,因此**必须为 0**。原先允许「仅限 .5 取整边界」的宽松口径已收紧:
|
||||||
|
// 宽松口径下,规则性偏差会长期以「已知差异」的身份留在日志里,没人会再看它。
|
||||||
const diffs = r.encodeDifferences ?? [];
|
const diffs = r.encodeDifferences ?? [];
|
||||||
const unexpected = diffs.filter((d) => !d.halfValueInput);
|
say(diffs.length === 0,
|
||||||
say(unexpected.length === 0,
|
diffs.length === 0
|
||||||
`编码器差异 ${diffs.length} 项,全部属于「量化前恰好 .5」的取整边界:${diffs.map((d) => d.case + "(" + d.diffBytes + "B)").join("、") || "无"}`);
|
? "编码器字节与参考实现完全一致(0 项差异)"
|
||||||
if (unexpected.length) for (const d of unexpected) console.log(` · 非预期差异:${d.case}(${d.diffBytes} 字节)`);
|
: `编码器差异 ${diffs.length} 项(必须为 0):${diffs.map((d) => d.case + "(" + d.diffBytes + "B," + (d.halfValueInput ? "取整边界" : "非取整边界") + ")").join("、")}`);
|
||||||
process.exit(bad);
|
process.exit(bad);
|
||||||
' "$REPORT" || fail "报告核对未通过"
|
' "$REPORT" || fail "报告核对未通过"
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,24 @@ if [ -f "$PCK" ]; then
|
|||||||
echo " ✓ 包内 pck 启动 $BOOT_FRAMES 帧无错误(退出码 0)"
|
echo " ✓ 包内 pck 启动 $BOOT_FRAMES 帧无错误(退出码 0)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 产物内不得混入开发/验收工具(T-WIN-023):export_filter="all_resources" 只看
|
||||||
|
# exclude_filter,加了新的工具目录却忘了排除,就会把探针(乃至**故意不合规**的
|
||||||
|
# 坏服务端夹具)与向量文件打进 pck —— 实测曾占 pck 的 54%(315,464 B → 145,400 B)。
|
||||||
|
log "产物内开发残留检查(build/ tests/ _scratch/ *.md)…"
|
||||||
|
LIST_JSON="$WORK_DIR/pck-entries.json"
|
||||||
|
if timeout 120 "$GODOT_BIN" --headless --main-pack "$PCK" \
|
||||||
|
--script "$PROJECT_DIR/build/integration/list-pck.gd" \
|
||||||
|
-- --json --out "$LIST_JSON" >"$WORK_DIR/pck-list.log" 2>&1; then
|
||||||
|
node -e '
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const r = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||||
|
console.log(` ✓ pck 共 ${r.count} 项,无开发残留(build/ tests/ _scratch/ *.md)`);
|
||||||
|
' "$LIST_JSON" || fail "pck 残留清单解析失败"
|
||||||
|
else
|
||||||
|
fail "产物里混入了开发/验收残留(见 $WORK_DIR/pck-list.log)"
|
||||||
|
grep -E "✗|·" "$WORK_DIR/pck-list.log" | head -10 | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
log "包内运行时探针(版本号 / 界面显示值 / 契约表 / 资源可加载)…"
|
log "包内运行时探针(版本号 / 界面显示值 / 契约表 / 资源可加载)…"
|
||||||
PROBE="$PROJECT_DIR/build/probe-release.gd"
|
PROBE="$PROJECT_DIR/build/probe-release.gd"
|
||||||
PROBE_LOG="$WORK_DIR/probe.log"
|
PROBE_LOG="$WORK_DIR/probe.log"
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## 列出发布包内 pck 的资源清单(T-WIN-023)
|
||||||
|
##
|
||||||
|
## 用途:`accept-release.sh` 用它断言**开发/验收工具不会被打进产物** ——
|
||||||
|
## 起因是一个真实缺陷:`export_filter="all_resources"` + `exclude_filter` 里没有 `build/*`,
|
||||||
|
## 于是 `build/` 下的探针脚本(含**故意不合规**的 `probe-ws-bad.gd`)与
|
||||||
|
## `frame-vectors.json` 全被编译打包进 `OpenDance.pck`:
|
||||||
|
## pck 315,464 B → 加上 exclude 后 145,400 B(**54% 是测试夹具**)。
|
||||||
|
##
|
||||||
|
## 用法:
|
||||||
|
## godot --headless --main-pack <pck> --script <本文件> [-- --json]
|
||||||
|
const DEV_PREFIXES := ["res://build/", "res://tests/", "res://_scratch/", "res://dist-release/"]
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var args := OS.get_cmdline_user_args()
|
||||||
|
var as_json := args.has("--json")
|
||||||
|
var found: Array = []
|
||||||
|
_walk("res://", found)
|
||||||
|
found.sort()
|
||||||
|
|
||||||
|
var dev_entries: Array = []
|
||||||
|
for f in found:
|
||||||
|
for prefix in DEV_PREFIXES:
|
||||||
|
if f.begins_with(prefix):
|
||||||
|
dev_entries.append(f)
|
||||||
|
break
|
||||||
|
if f.ends_with(".md"):
|
||||||
|
if not dev_entries.has(f):
|
||||||
|
dev_entries.append(f)
|
||||||
|
|
||||||
|
if as_json:
|
||||||
|
var report := {
|
||||||
|
"entries": found,
|
||||||
|
"count": found.size(),
|
||||||
|
"devEntries": dev_entries,
|
||||||
|
}
|
||||||
|
var path := ""
|
||||||
|
var i := args.find("--out")
|
||||||
|
if i >= 0 and i + 1 < args.size():
|
||||||
|
path = args[i + 1]
|
||||||
|
if path != "":
|
||||||
|
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||||
|
if file != null:
|
||||||
|
file.store_string(JSON.stringify(report, " "))
|
||||||
|
file.close()
|
||||||
|
print("[pck] 清单已写入 %s(%d 项,开发残留 %d 项)" % [path, found.size(), dev_entries.size()])
|
||||||
|
else:
|
||||||
|
print(JSON.stringify(report))
|
||||||
|
quit(0 if dev_entries.is_empty() else 1)
|
||||||
|
return
|
||||||
|
|
||||||
|
print("[pck] 文件数 %d" % found.size())
|
||||||
|
for f in found:
|
||||||
|
print(" ", f)
|
||||||
|
if dev_entries.is_empty():
|
||||||
|
print("[pck] ✓ 无开发/验收残留(build/ tests/ _scratch/ *.md)")
|
||||||
|
else:
|
||||||
|
print("[pck] ✗ 产物里混入了开发残留 %d 项:" % dev_entries.size())
|
||||||
|
for f in dev_entries:
|
||||||
|
print(" · ", f)
|
||||||
|
quit(0 if dev_entries.is_empty() else 1)
|
||||||
|
func _walk(dir: String, out: Array) -> void:
|
||||||
|
var d := DirAccess.open(dir)
|
||||||
|
if d == null: return
|
||||||
|
d.list_dir_begin()
|
||||||
|
while true:
|
||||||
|
var name := d.get_next()
|
||||||
|
if name == "": break
|
||||||
|
var path := dir.path_join(name) if dir != "res://" else "res://" + name
|
||||||
|
if d.current_is_dir():
|
||||||
|
if name != "." and name != "..": _walk(path, out)
|
||||||
|
else:
|
||||||
|
out.append(path)
|
||||||
|
d.list_dir_end()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cp5nggx7y2164
|
||||||
@@ -20,9 +20,11 @@ extends SceneTree
|
|||||||
## 即 **float32**;参考实现是 float64。因此先把期望值收敛到 float32
|
## 即 **float32**;参考实现是 float64。因此先把期望值收敛到 float32
|
||||||
## (GDScript 里借 `Vector3(...)` / `PackedFloat32Array([...])` 天然完成)
|
## (GDScript 里借 `Vector3(...)` / `PackedFloat32Array([...])` 天然完成)
|
||||||
## 再比**精确相等** —— 这比用容差更严格,且把差异限定在「表示精度」上。
|
## 再比**精确相等** —— 这比用容差更严格,且把差异限定在「表示精度」上。
|
||||||
## * 另附:用本端 `encode()` 对向量里的 `inputPoints` 重新编码,与参考字节比对。
|
## * 另附:用本端 `encode()` 对向量里的 `f32InputPoints`(float32 同输入基准)
|
||||||
## 本端**不是发送方**(encode 只用于自测),因此这一项差异记为「已记录差异」
|
## 重新编码,与 `f32Hex` 逐字节比对。v0.2.4 起本端取整已改为 `floor(x + 0.5)`
|
||||||
## 而非失败;但差异必须**只出现在取整边界用例**上,否则说明别处也有问题。
|
## (与 JS `Math.round` 的「半数朝 +∞」一致),因此**差异必须为 0**;
|
||||||
|
## 收敛前的宽松口径(只允许出现在 .5 取整边界)已废弃 ——
|
||||||
|
## 规则性偏差留在日志里会变成"永远没人看的已知差异"。
|
||||||
|
|
||||||
const KeypointFrame = preload("res://src/net/keypoint_frame.gd")
|
const KeypointFrame = preload("res://src/net/keypoint_frame.gd")
|
||||||
|
|
||||||
@@ -202,7 +204,7 @@ func _run() -> void:
|
|||||||
if encode_differences.is_empty():
|
if encode_differences.is_empty():
|
||||||
print("[frame-vectors] ✓ 本端 encode() 与参考字节完全一致")
|
print("[frame-vectors] ✓ 本端 encode() 与参考字节完全一致")
|
||||||
else:
|
else:
|
||||||
print("[frame-vectors] · 本端 encode() 与参考字节的差异(仅记录,本端非发送方):")
|
print("[frame-vectors] ✗ 本端 encode() 与参考字节存在差异(取整规则应已对齐,必须为 0):")
|
||||||
for d in encode_differences:
|
for d in encode_differences:
|
||||||
print(" · %s:%d 字节不同(量化前存在 .5 值=%s)" % [
|
print(" · %s:%d 字节不同(量化前存在 .5 值=%s)" % [
|
||||||
d["case"], d["diffBytes"], str(d.get("halfValueInput", false))])
|
d["case"], d["diffBytes"], str(d.get("halfValueInput", false))])
|
||||||
|
|||||||
+3
-3
@@ -8,7 +8,7 @@ dedicated_server=false
|
|||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter="_scratch/*, tests/*, *.md"
|
exclude_filter="_scratch/*, tests/*, build/*, *.md"
|
||||||
export_path="build/windows/OpenDance.exe"
|
export_path="build/windows/OpenDance.exe"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
@@ -37,8 +37,8 @@ application/modify_resources=true
|
|||||||
application/icon=""
|
application/icon=""
|
||||||
application/console_wrapper_icon=""
|
application/console_wrapper_icon=""
|
||||||
application/icon_interpolation=4
|
application/icon_interpolation=4
|
||||||
application/file_version="0.2.3"
|
application/file_version="0.2.4"
|
||||||
application/product_version="0.2.3"
|
application/product_version="0.2.4"
|
||||||
application/company_name="OpenDance Studio"
|
application/company_name="OpenDance Studio"
|
||||||
application/product_name="OpenDance Studio"
|
application/product_name="OpenDance Studio"
|
||||||
application/file_description="OpenDance Studio 摄像头跳舞评分客户端"
|
application/file_description="OpenDance Studio 摄像头跳舞评分客户端"
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ config_version=5
|
|||||||
|
|
||||||
config/name="OpenDance Studio"
|
config/name="OpenDance Studio"
|
||||||
config/description="摄像头跳舞评分系统 —— Windows 客户端。接收 Android 辅助 App 的 33 点骨骼关键点,驱动 VRM 角色并 DTW 实时评分。"
|
config/description="摄像头跳舞评分系统 —— Windows 客户端。接收 Android 辅助 App 的 33 点骨骼关键点,驱动 VRM 角色并 DTW 实时评分。"
|
||||||
config/version="0.2.3"
|
config/version="0.2.4"
|
||||||
run/main_scene="res://scenes/main.tscn"
|
run/main_scene="res://scenes/main.tscn"
|
||||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|||||||
+10
-2
@@ -1,5 +1,5 @@
|
|||||||
project: opendance-windows-client
|
project: opendance-windows-client
|
||||||
version: 0.2.3
|
version: 0.2.4
|
||||||
repository: opendance-windows
|
repository: opendance-windows
|
||||||
default_branch: main
|
default_branch: main
|
||||||
engine: Godot 4.7.2
|
engine: Godot 4.7.2
|
||||||
@@ -110,9 +110,17 @@ tasks:
|
|||||||
file: tasks/021-frame-vector-conformance.md
|
file: tasks/021-frame-vector-conformance.md
|
||||||
scope: 关键点帧跨实现一致性(Web 参考实现 × 包内 GDScript 解码器,12 用例 / 396 点逐值比对)
|
scope: 关键点帧跨实现一致性(Web 参考实现 × 包内 GDScript 解码器,12 用例 / 396 点逐值比对)
|
||||||
status: done
|
status: done
|
||||||
|
- id: T-WIN-022
|
||||||
|
file: tasks/022-encode-rounding.md
|
||||||
|
scope: 编码取整与契约参考实现(JS Math.round)对齐,编码差异收紧为 0
|
||||||
|
status: done
|
||||||
|
- id: T-WIN-023
|
||||||
|
file: tasks/023-pck-dev-leftovers.md
|
||||||
|
scope: 发布包内不得混入开发/验收工具(export exclude build/*)+ 产物级残留检查
|
||||||
|
status: done
|
||||||
|
|
||||||
deliverable:
|
deliverable:
|
||||||
artifact: opendance-windows-v0.2.3.zip
|
artifact: opendance-windows-v0.2.4.zip
|
||||||
packaging: Godot 导出脚本(Linux 直接导出 / Windows 兜底)
|
packaging: Godot 导出脚本(Linux 直接导出 / Windows 兜底)
|
||||||
release_rules: # 文档 §5.7 硬性要求
|
release_rules: # 文档 §5.7 硬性要求
|
||||||
- 单个完整包,禁止 .partN 分卷
|
- 单个完整包,禁止 .partN 分卷
|
||||||
|
|||||||
@@ -152,6 +152,20 @@ static func decode(buf: PackedByteArray) -> PoseFrame:
|
|||||||
return frame
|
return frame
|
||||||
|
|
||||||
|
|
||||||
|
## 量化取整:与契约参考实现(opendance-web 的 `Math.round`)**逐位一致**
|
||||||
|
##
|
||||||
|
## JS 的 `Math.round` 对「恰好 .5」是**朝 +∞** 取整(`Math.round(-2.5) === -2`),
|
||||||
|
## 而 GDScript 的 `roundi` 是**半数远离零**(`roundi(-2.5) == -3`)——
|
||||||
|
## 两者在 `-X.5` 上会差 1 个 LSB。Android 的 Kotlin 实现用的正是 `Math.round`,
|
||||||
|
## 所以本端若用 `roundi`,与参考实现/发送端就不一致(实测:某一用例 33 字节不同)。
|
||||||
|
## 这里显式实现同一套规则:`floor(x + 0.5)`。
|
||||||
|
##
|
||||||
|
## 注:本端是接收方,`encode()` 只用于自测与协议回归;但对齐之后,
|
||||||
|
## `build/accept-frame-vectors.sh` 才能把「编码器字节差异 = 0」作为硬性断言。
|
||||||
|
static func _round_like_js(value: float) -> int:
|
||||||
|
return int(floor(value + 0.5))
|
||||||
|
|
||||||
|
|
||||||
## 编码为 239 字节帧(本端为接收方,此函数用于自测与协议回归)
|
## 编码为 239 字节帧(本端为接收方,此函数用于自测与协议回归)
|
||||||
static func encode(frame_id: int, points: PackedVector3Array, confidence: PackedFloat32Array, flags: int = 0) -> PackedByteArray:
|
static func encode(frame_id: int, points: PackedVector3Array, confidence: PackedFloat32Array, flags: int = 0) -> PackedByteArray:
|
||||||
assert(points.size() == KEYPOINT_COUNT, "points 必须为 33 项")
|
assert(points.size() == KEYPOINT_COUNT, "points 必须为 33 项")
|
||||||
@@ -167,10 +181,10 @@ static func encode(frame_id: int, points: PackedVector3Array, confidence: Packed
|
|||||||
for i in KEYPOINT_COUNT:
|
for i in KEYPOINT_COUNT:
|
||||||
var offset := HEADER_SIZE + i * POINT_BYTES
|
var offset := HEADER_SIZE + i * POINT_BYTES
|
||||||
var p := points[i]
|
var p := points[i]
|
||||||
buf.encode_s16(offset, clampi(roundi(p.x * SCALE), -32768, 32767))
|
buf.encode_s16(offset, clampi(_round_like_js(p.x * SCALE), -32768, 32767))
|
||||||
buf.encode_s16(offset + 2, clampi(roundi(p.y * SCALE), -32768, 32767))
|
buf.encode_s16(offset + 2, clampi(_round_like_js(p.y * SCALE), -32768, 32767))
|
||||||
buf.encode_s16(offset + 4, clampi(roundi(p.z * SCALE), -32768, 32767))
|
buf.encode_s16(offset + 4, clampi(_round_like_js(p.z * SCALE), -32768, 32767))
|
||||||
buf.encode_u8(offset + 6, clampi(roundi(confidence[i] * 255.0), 0, 255))
|
buf.encode_u8(offset + 6, clampi(_round_like_js(confidence[i] * 255.0), 0, 255))
|
||||||
|
|
||||||
return buf
|
return buf
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# T-WIN-022:编码取整与契约参考实现对齐(编码差异收紧为 0)
|
||||||
|
|
||||||
|
- **状态**: done
|
||||||
|
- **依赖**: T-WIN-021(跨实现一致性向量)
|
||||||
|
- **文档**: v1.6 §7.6.2
|
||||||
|
- **契约**: `websocket-keypoints 0.2.0` §2
|
||||||
|
|
||||||
|
## 缺陷
|
||||||
|
|
||||||
|
`KeypointFrame.encode()` 用 GDScript 的 `roundi` 取整,而契约参考实现
|
||||||
|
(`opendance-web` 的 `keypoint-codec.ts`)与**生产发送方 Android**(Kotlin `Math.round`)
|
||||||
|
都用「半数朝 +∞」:
|
||||||
|
|
||||||
|
| | `-2.5` | `2.5` | `-0.5` |
|
||||||
|
|--|--|--|--|
|
||||||
|
| JS `Math.round` / Kotlin `Math.round` | **-2** | 3 | 0 |
|
||||||
|
| GDScript `roundi`(半数远离零) | **-3** | 3 | -1 |
|
||||||
|
|
||||||
|
实测差异:T-WIN-021 的 12 个向量里有 1 个用例(坐标含 `-0.5`)**33 字节不同**。
|
||||||
|
|
||||||
|
**影响**:本端是**接收方**,`encode()` 只用于自测与协议回归,因此对产品无影响 ——
|
||||||
|
但它让「跨实现一致性」这条断言只能是宽松口径(「差异仅限取整边界」),
|
||||||
|
而宽松口径在实践中会退化成「永远没人看的已知差异」。
|
||||||
|
|
||||||
|
## 修复
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
## 量化取整:与契约参考实现(JS Math.round)逐位一致
|
||||||
|
static func _round_like_js(value: float) -> int:
|
||||||
|
return int(floor(value + 0.5))
|
||||||
|
```
|
||||||
|
|
||||||
|
`encode()` 的 4 处取整全部改用它(坐标 ×3 + 置信度)。
|
||||||
|
夹紧顺序(先取整后 `clampi`)与参考实现的「先夹后取整」对任意实数等价
|
||||||
|
(`32767.4 → 32767`;`-32768.4 → -32768`),已在任务里逐值核对。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. **单测**(`tests/test_keypoint_frame.gd`,+11 条断言):
|
||||||
|
* `_round_like_js` 的 5 个取值(含 `-2.5 → -2`、`-0.5 → 0`);
|
||||||
|
* 端到端 `-0.5/32767 → 0`(`roundi` 会给 -1,是有效的判别用例);
|
||||||
|
* `test_reference_vectors_decode_and_encode_exactly`:直接读**锁定的参考向量**
|
||||||
|
(`build/integration/frame-vectors.json`),逐值解码比对 + 用 `f32InputPoints`
|
||||||
|
重编码与 `f32Hex` **逐字节**比对 —— 这一步让 `verify.sh`(只跑源码,无需 pck)
|
||||||
|
也覆盖了「Web ↔ Windows」这条边。
|
||||||
|
2. **产物级**(`build/accept-frame-vectors.sh`,已收紧口径):
|
||||||
|
原先允许「编码差异仅限 `.5` 取整边界」,现在**硬性要求 0 项**。
|
||||||
|
|
||||||
|
实测(v0.2.4 发布包):
|
||||||
|
|
||||||
|
```
|
||||||
|
[frame-vectors] ✓ 解码结果与 Web 参考实现逐值一致(float32 精确相等)
|
||||||
|
[frame-vectors] ✓ 本端 encode() 与参考字节完全一致
|
||||||
|
✓ 解码差异 0 项(必须为 0)
|
||||||
|
✓ 编码器字节与参考实现完全一致(0 项差异)
|
||||||
|
```
|
||||||
|
|
||||||
|
`verify.sh`:**785 通过 / 0 失败**(原 774)。
|
||||||
|
|
||||||
|
## 写测试时踩的坑
|
||||||
|
|
||||||
|
第一版断言写成「`2.5/32767` 编码为 3」,**失败**。原因不是编码错了,而是
|
||||||
|
`PackedVector3Array` 把该值存成 **float32**,`float32(2.5/32767) × 32767` 略小于 2.5,
|
||||||
|
`floor(x+0.5)` 得 2 —— 与 JS 对同一 float32 输入的行为**完全一致**。
|
||||||
|
即:**断言必须基于实际存储精度**,或改用「规则本身 + 有效判别用例」
|
||||||
|
(`-0.5/32767`)而不是「理想有理数」。这与 T-WIN-021 的两处踩坑同源。
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# T-WIN-023:发布包内不得混入开发/验收工具
|
||||||
|
|
||||||
|
- **状态**: done
|
||||||
|
- **依赖**: T-WIN-018(发布包级验收)
|
||||||
|
- **文档**: v1.6 §5.3 / §7.5
|
||||||
|
- **契约**: 不涉及(打包)
|
||||||
|
|
||||||
|
## 缺陷(实测)
|
||||||
|
|
||||||
|
`export_presets.cfg` 用的是 `export_filter="all_resources"` + `exclude_filter="_scratch/*, tests/*, *.md"`。
|
||||||
|
`build/` 不在排除列表里 —— 于是**每次新增一个构建/验收脚本,它就被编译进产物**:
|
||||||
|
|
||||||
|
```
|
||||||
|
res://build/probe-release.gdc
|
||||||
|
res://build/integration/probe-integration.gdc
|
||||||
|
res://build/integration/probe-ws.gdc
|
||||||
|
res://build/integration/probe-ws-bad.gdc ← **故意不合规**的服务端夹具
|
||||||
|
res://build/integration/probe-frame-vectors.gdc
|
||||||
|
res://build/integration/frame-vectors.json
|
||||||
|
```
|
||||||
|
|
||||||
|
体积代价:`OpenDance.pck` **315,464 B → 145,400 B(排除后)**,
|
||||||
|
即 **54% 的 pck 是测试夹具**。已发布的 v0.2.3 同样带着这些残留(其中包含
|
||||||
|
那个刻意违反协议的坏服务端),虽然不影响功能,但既不该进产物,也不该被用户拿到。
|
||||||
|
|
||||||
|
## 修复
|
||||||
|
|
||||||
|
1. `export_presets.cfg` 的 `exclude_filter` 增加 `build/*`:
|
||||||
|
```ini
|
||||||
|
exclude_filter="_scratch/*, tests/*, build/*, *.md"
|
||||||
|
```
|
||||||
|
2. **新增产物级防线**(否则下次加一个 `tools/` 目录又会重演):
|
||||||
|
* `build/integration/list-pck.gd`:用 `--main-pack` 遍历 `res://`,列出全部条目并
|
||||||
|
标出开发残留(`res://build/`、`res://tests/`、`res://_scratch/`、`res://dist-release/`、`*.md`);
|
||||||
|
有残留即退出码 1,支持 `--json --out <path>`。
|
||||||
|
* `build/accept-release.sh` 新增步骤:跑该脚本并断言「无开发残留」,同时打印条目总数。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```
|
||||||
|
# 修复前
|
||||||
|
[pck] ✗ 产物里混入了开发残留 12 项:res://build/…(pck 315,464 B)
|
||||||
|
|
||||||
|
# 修复后(v0.2.4)
|
||||||
|
✓ pck 共 58 项,无开发残留(build/ tests/ _scratch/ *.md)
|
||||||
|
(pck 145,400 B;zip 26,631,908 B,仍在 30 MB 上传上限内)
|
||||||
|
```
|
||||||
|
|
||||||
|
`accept-release.sh` 其余步骤(PE 版本资源、pck 启动 180 帧、运行时探针、契约表比对)
|
||||||
|
在排除 `build/*` 后依旧全绿 —— 验收脚本本身是**从仓库路径**加载探针的,
|
||||||
|
不依赖包内是否有它们的副本。
|
||||||
|
|
||||||
|
## 顺带确认(不是问题)
|
||||||
|
|
||||||
|
排查时特意确认了 pck **没有**把上一次导出的 pck 自嵌套进去
|
||||||
|
(`file_count=112`、条目里没有 `res://build/windows/OpenDance.pck`):
|
||||||
|
Godot 在扫描资源时排除了自己的导出产物目录内容。这条也由 `list-pck.gd` 长期盯着。
|
||||||
@@ -159,3 +159,97 @@ func test_json_debug_frame() -> void:
|
|||||||
|
|
||||||
var bad := KeypointFrame.from_json_dict({"frameId": 1, "points": []})
|
var bad := KeypointFrame.from_json_dict({"frameId": 1, "points": []})
|
||||||
is_null(bad, "点数不足的 JSON 帧被拒绝")
|
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())
|
||||||
|
|||||||
Reference in New Issue
Block a user