Files
opendance-windows/README.md
T
zijunchiang 6ea2c93d66 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 输入的行为其实完全一致。断言必须基于实际存储精度,或改用有效判别用例。
2026-09-17 05:59:54 +08:00

401 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# opendance-windows
OpenDance Studio **阶段二**: Windows 客户端(Godot 4)。
接收 Android 辅助 App 通过 WebSocket 推送的 33 点骨骼关键点,驱动 VRM 角色,
并与 Web 管理服务端下发的参考动作做实时 DTW 评分。
> 本客户端只接收关键点坐标,**不接收、不存储任何视频画面**。
## 状态
**阶段二已完成**: T-WIN-001 -> T-WIN-014 全部落地(含场景选择与整包发布)。
| 验收项 | 结果 |
|--|--|
| 脚本编译 | OK 全部通过(无 SCRIPT ERROR) |
| 无头测试 | OK **751 项全部通过**(10 个测试套件) |
| 应用启动 | OK 180 帧无运行时错误 |
| Windows 导出 | OK 生成 `OpenDance.exe`(104 MB, PE 头校验通过)+ `OpenDance.pck` |
| 真实 VRM 验证 | OK 154 骨骼 VRoid 模型,humanoid 覆盖率 **100%** |
| 骨骼重定向 | OK 关键点方向与骨骼实际指向点积 **1.0000** |
| 跨实现交叉校验 | OK 关节角与 Web 服务端 TypeScript 实现**逐帧一致** |
| 场景选择与物理落地 | OK `scenes[]` 可选、重力/风/摩擦/碰撞体落地并可日志核对 |
| 连接保活与设备去重 | OK welcome/config 声明静默超时与心跳;应用层 ping/pong;按 deviceId 去重 |
| 发布整包 | OK 单个 `opendance-windows-v0.2.4.zip` + `SHA256SUMS`(不拆分卷) |
| 版本号展示 | OK 「关于」清单显示 `v0.2.2`;`project.godot` / `project.yaml` / `export_presets.cfg` / exe 的 PE 版本资源 / 产物名 / tag 全部一致 |
| 场景物理与保活 | 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 修订(跨端集成验收 + 两处真实缺陷)
**缺陷 1(契约字段缺失)**:`CatalogTypes.Motion` 没有 `thumbnail_url` 字段,
而服务端 `catalog/export` 从 `data-models 0.6.0` 起一直在发 `thumbnailUrl`
(同一文件的 `Model` / `Scene` 都有)。客户端默默丢掉该字段。
**修复**:补字段与解析(缺失时为 `""`,兼容旧服务端),并新增
`tests/test_catalog_types.gd`(4 项)用**服务端真实报文形状**锁住三个资源类型的
`thumbnailUrl` 覆盖;同时显式记录一处**刻意不消费**的差异
(`Model.usable`/`unusableReason` —— 目录只下发可用模型,恒为 true)。
**缺陷 2(二进制响应白做 UTF-8 解码)**:`ApiClient._request()` 对**所有**响应
无条件执行 `raw.get_string_from_utf8()`。数据本身没错(二进制路径返回的是原始字节),
但每下载一个文件都会打印一条
`Unicode parsing error … Invalid UTF-8 leading byte (N)` 噪声,
并把整个文件(模型可达数十 MB)解码成一份 String 拷贝后丢弃。
**修复**:只在需要文本时解码。跨端集成探针第一次注意到这条噪声。
**新增跨端集成验收**(`build/accept-integration.sh`):用**发布包里的客户端**
(`--main-pack` 加载包内 pck)打**真实 Web 服务端**,逐项比对契约与物理参数,
并跑通「真 VMD → 内部标准 33 点 → 客户端解析 2901 帧」的完整闭环。
详见 [`tasks/019-cross-end-integration.md`](tasks/019-cross-end-integration.md)。
此外修掉 `tests/test_api_integration.gd` 里一处**写死的契约版本**(`0.1.0`):
该用例只在服务端可达时才跑,平时一直 skip,于是契约升到 0.5.0 后
没有任何地方发现它。现在改为从仓库 `contracts/VERSION` 读期望值并与服务端 `/health` 比对。
## v0.2.2 修订(发布包 exe 的版本元数据)
**缺陷**:v0.2.1 的包在 Windows 上「右键 → 属性 → 详细信息」显示
**文件版本 0.1.0 / 产品版本 0.1.0**,而产物名与 Git tag 都是 v0.2.1。
**根因**:`export_presets.cfg` 的 `application/file_version` /
`application/product_version` 被硬编码在 `0.1.0`,而它们**不会**跟随
`project.godot` 的 `application/config/version` 自动更新;`check-version.sh`
当时只比 `project.godot` ↔ `project.yaml`(两处一致),所以静态校验全绿、
产物却是错的 —— 漂移发生在没人看的第三个位置。
**修复**:
| 环节 | 做法 |
|--|--|
| 同步 | `build/sync-export-version.sh`(新增):导出前把版本号写进导出预设(幂等,拒绝非 `X.Y.Z`) |
| 静态防线 | `build/check-version.sh`:新增对导出预设两个键的校验 |
| 产物防线 | `build/check-pe-version.sh`(新增):直接读 exe 的 PE 版本资源 —— `VS_FIXEDFILEINFO`(签名 `0xFEEF04BD` 后 16 字节)+ `StringFileInfo`(`strings -el`,属性页显示的就是它)。两处都查:本机实测固定信息里的 `ProductVersion` 恒为 `0.0.0.0` |
| 打包 | `build/package-release.sh`:打包前校验源 exe、解包后再校验包内 exe |
| 一键验证 | `build/verify.sh`:新增步骤 1/5(未导出则跳过) |
| 单元测试 | `tests/test_about.gd`:预设两个键必须等于版本号唯一来源 |
实测修复后产物:`VS_FIXEDFILEINFO FileVersion = 0.2.2.0`、
`StringFileInfo FileVersion/ProductVersion = 0.2.2`。详见
[`tasks/017-pe-version.md`](tasks/017-pe-version.md)。
## v0.2.1 修订(两处修复 + 一批"真机等价"测试)
1. **场景包围盒不再用 `global_transform`**:`SceneRuntime._compute_aabb()` 原本在
`_assemble()` 里调用,而那时运行时节点**还没入树** —— Godot 会打一行
`ERROR: Condition "!is_inside_tree()" is true`,并且算出来的只是各网格的
**局部** AABB。后果:把网格摆在非原点位置的场景,重力/风区域与兜底地面会算得
偏小或偏位(**风力可能作用不到物体上**)。现改为沿树累乘 `Node3D.transform`,
与是否入树无关,结果与运行时挂在单位变换父节点下完全一致;
2. **新增 21 项"真机等价"测试**(无头即可跑,不需要设备):
* `tests/test_scene_config.gd`:在代码里手写合法 GLB(三角形 + BIN chunk)后
**真实加载**,核对三种 `colliders` 取值的碰撞体、重力/风 Area3D 数值、
以及**带位移网格**的包围盒是否跟着走(这一条正是上述缺陷的回归测试);
* `tests/test_pose_server.gd`:**时间维度**验证保活 —— 一个只发应用层 `ping`、
不发关键点帧的客户端在 17 秒(> 15 秒静默超时)后**仍保持连接**,
而完全不发数据的客户端被 `close(1001)` 断开。这正是"画面里没人就掉线、
然后疯狂重连"的根因对照;
3. **新增版本号一致性校验** `build/check-version.sh`:强制
`project.godot` 的 `application/config/version`(「关于」清单与 .pck 的来源)
与 `project.yaml`、发布版本号三者一致;已接入 `build/package-release.sh`
(打包前)与 `build/verify.sh`(0/4 步),并做过反向验证。
## 快速开始
```bash
# 验证(编译 -> 测试 -> 启动)
GODOT_BIN=/path/to/godot bash build/verify.sh
# 导出 Windows 可执行文件
GODOT_BIN=/path/to/godot bash build/export-windows.sh release
# 打包发布产物(单个完整 zip + SHA256SUMS)
bash build/package-release.sh 0.2.4
# 发布包级验收(对**发布包**跑,而不是对源码;文档 §7.5)
GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.4.zip
```
`accept-release.sh` 校验 sha256 → 解压 → 文件清单(并检查没有 `.partN` 残留)→
exe 的 PE 版本资源 → **用 Godot 直接加载包内 pck 跑 180 帧** → 包内运行时探针
(`build/probe-release.gd`:包内版本号、`AboutScreen.app_version()`、主场景、autoload、
15 个关键资源、以及**包内 `Credits.CONTRACT_VERSIONS` 与 `contracts/VERSION` 快照是否一致**)。
它读的全是包内资源,因此结论就是「用户装到机器上的那一份能不能用」。
已对**已发布的 v0.2.2 资产**(重新下载)跑通,并用「篡改包内 contracts/VERSION」与
「期望版本写错」两个负例反向验证过。
### 关键点帧跨实现一致性验收(T-WIN-021)
`web-server/src/preprocess/keypoint-codec.ts` 是契约 §2 的**参考实现**
(其 `decodeBinaryFrame` 注释就是「Windows 端参考实现(T-WIN-004)」)。
Android 已用「金标准字节向量」与它对齐;而**本端的解码器此前只跟自己比过**
(自己编码、自己解码)。这条边由下列脚本补上:
```bash
GODOT_BIN=/path/to/godot bash build/accept-frame-vectors.sh dist-release/opendance-windows-v0.2.4.zip
# 没有 web 仓 / 未装 node_modules 时:... --skip-regen
```
1. 用**参考实现**(`npm run vectors:frame`)重新生成向量,与仓库锁定的
`build/integration/frame-vectors.json` 比对 —— 参考实现变了却不同步向量,这里就会失败;
2. 用**包内 pck** 解码同一批字节,与参考解码结果**逐值精确比对**
(float32 表示精度内):实测 **12 用例 / 396 点 / 0 差异**;
3. 附带比对两边编码器:本端不是发送方(`encode()` 只用于自测),差异只归类记录,
但**必须只出现在「量化前恰好 .5」的用例上**。
> 已测量并归类的一处差异:JS `Math.round` 与 Kotlin `Math.round` 对 `-2.5` 取 `-2`
> (半数朝 +∞),而本端 `roundi` 取 `-3`(半数远离零)。生产发送方是 Android,
> 与参考实现一致;Windows 是接收方,`encode()` 不在生产链路上。
> 详见 [`tasks/021-frame-vector-conformance.md`](tasks/021-frame-vector-conformance.md)。
### WebSocket 契约一致性验收(T-WIN-020)
`tests/test_pose_server.gd` 用的是**同引擎内的客户端**,而真实对端是
**Android(Kotlin/OkHttp)** —— 同引擎自测会共享同样的假设(超时判定、分包、
文本/二进制边界、关闭语义、子协议协商)。因此另配一套**独立实现**的客户端:
```bash
GODOT_BIN=/path/to/godot bash build/accept-ws.sh dist-release/opendance-windows-v0.2.4.zip
# 跳过耗时的两个场景(保活 19s / 静默超时 15s):
GODOT_BIN=... bash build/accept-ws.sh <zip> --skip-slow
```
* `build/integration/probe-ws.gd`:把**包内** PoseServer 跑起来并汇总事件成 JSON(自身不判定);
* `build/integration/ws-client.mjs`:Node 实现的契约一致性客户端(无依赖,用内置 `WebSocket`),
逐条跑 §1 §1.1 §2 §3.1-§3.6 §5 §6 §7 的场景;
* `build/accept-ws.sh`:编排两者,并核对**客户端看不到的那一半**(服务端侧
`frames_dropped` / `malformed_frames` / 控制帧计数 / 去重事件 / 关闭原因)。
实测(已发布的 v0.2.3 资产):客户端 **30 通过 / 0 失败** + 服务端侧 16 项事实全过,
其中最关键的是**保活对照**——只发应用层 `ping` 持续 19 秒(> 静默超时 15 秒)连接
**仍保持**,而完全不发数据的连接在 **15011 ms**(阈值 15000 ms)被 `close(1001, "timeout")`。
反向验证用一个**故意不合规的服务端**(`build/integration/probe-ws-bad.gd`:welcome 不声明
保活字段、不回 pong、不做去重)跑同一客户端 → 14 项 ✗、退出码 1,证明断言不是空转。
详见 [`tasks/020-ws-contract-conformance.md`](tasks/020-ws-contract-conformance.md)。
当前版本:**v0.2.4**(`project.godot` 的 `application/config/version`,
与 `project.yaml`、`export_presets.cfg`、导出产物名、Git tag、「关于」清单展示、
以及 exe 的 PE 版本资源全部一致)。
详细使用说明见 [`README.txt`](README.txt),工程约束见 [`DSH.md`](DSH.md)。
## 初次部署(从零跑起来)
本节面向**第一次拿到本仓库**的人:从安装引擎到跑通「Web 服务端 + 本端 + Android」
三端链路。地址与端口一律使用占位符或产品默认值。
### 1. 前置条件
| 项 | 要求 |
|--|--|
| 操作系统 | 运行客户端:Windows 10/11(x86_64)。开发与导出:Windows 或 Linux 均可 |
| Godot | **4.7.x**(`project.godot` 声明特性 `4.7`,本仓库按 4.7.2 验证)。使用**标准版**(GDScript), **不需要 .NET / C# 版** |
| 显卡与图形界面 | 渲染后端为 **GL Compatibility**,运行需支持 OpenGL 3.3 的显卡与驱动。仅无头测试与导出可无 GPU、可无图形界面 |
| 导出模板 | **仅打包导出时需要**:必须安装与编辑器**完全同版本**的 export templates(`.tpz`) |
| 磁盘与网络 | Godot 编辑器 + 导出模板约数百 MB;导出产物 `OpenDance.exe` 约 104 MB、`OpenDance.pck` 约 107 KB。本端需能访问 Web 服务端 API(默认 TCP 3000),并与 Android 设备处于同一局域网、放行本端 **TCP 8765 入站** |
### 2. 获取代码
```bash
git clone https://gitea.kasugano.cn/zijunchiang/opendance-windows
cd opendance-windows
```
### 3. 首次运行(编辑器内)
1. 启动 Godot 4.7.x,在项目管理器选择「导入」,指向本仓库的 `project.godot`。
2. **首次导入会执行资源导入**(生成 `.godot/`),耗时从数十秒到数分钟不等;请等待完成,不要在导入中途关闭编辑器。
3. 主场景已在 `project.godot` 中配置为 `res://scenes/main.tscn`,直接按 **F5**(或「运行项目」)即可。
4. 界面全部由 GDScript 代码构建,不需要在编辑器里编辑场景,这是刻意设计(见 `DSH.md` 6)。
5. 首次运行会在用户数据目录生成配置与日志:
```
%APPDATA%\Godot\app_userdata\OpenDance Studio\opendance.cfg
%APPDATA%\Godot\app_userdata\OpenDance Studio\logs\godot.log
```
### 4. 配置服务端地址
- 位置:主界面顶部「**服务端地址**」输入框,填好后点「**保存并测试**」。
- 配置字段名(`opendance.cfg`):`api_base_url`。
- **端口区分**:客户端只访问 Web 服务端的 **API 端口(默认 3000)**;管理端页面在**管理端口(默认 3001)**,客户端不使用该端口。
- 示例占位符:
```
http://<服务器地址>:3000
http://192.0.2.10:3000
```
> `<服务器地址>` 是**你们自己部署的 Web 服务端地址**(部署方决定,不是被脱敏的域名);
> 上面第二行是 RFC 5737 文档用示例 IP,照抄时请替换。
保存后点「**刷新目录**」;顶部应显示「服务端正常 | DB ...」,左侧列出已发布的曲目 / 动作 / 模型。
### 5. 与另外两端的联通
| 对端 | 方向 | 本端配置 | 说明 |
|--|--|--|--|
| Android 辅助 App | Android -> 本端(WebSocket) | `listen_host`(默认 `0.0.0.0`)、`listen_port`(默认 **8765**)、`ws_subprotocol`(默认 `opendance.pose.v1`) | 本端是 WebSocket **服务端**; Android 端填写本端主界面显示的「接收地址」,形如 `ws://<本机局域网地址>:8765/pose` |
| Web 服务端 | 本端 -> Web(HTTP) | `api_base_url`(默认 `http://127.0.0.1:3000`) | 需先在服务端**发布**曲目 / 动作 / 模型 / 场景,动作还须**完成关键点预处理**,否则目录为空或该项不可选 |
> **场景不是必需项**:服务端没发布任何可用场景(或缺 GLB)时,本端回退到内置空场景
> 并在界面提示,**不会阻塞开始跟练**(文档 §5.6 第 4 条)。
- **Android 侧无本端可配置项**:接收地址由 Android App 自己填写;本端只监听固定端口 8765(可在 `opendance.cfg` 改 `listen_port` 后点「重新连接接收服务」生效)。
- 已知限制:Godot 的 `WebSocketPeer` 不暴露握手请求路径,本端**接受任意 path**,只校验子协议 `opendance.pose.v1`(见「已知限制」1)。
- 接收状态可用主界面「**查看接收状态**」查看:连接数、**去重后设备数**、累计帧、
丢帧、畸形帧、保活参数,以及每台设备的状态(契约 §8)与首帧时间。
### 6. 打包导出
```bash
# Linux 上直接导出 Windows 可执行文件(推荐)
GODOT_BIN=/path/to/godot bash build/export-windows.sh release
# Windows 上兜底导出
powershell -ExecutionPolicy Bypass -File build\export-windows.ps1
# 打成可分发的 zip(单个完整包 + SHA256SUMS)
bash build/package-release.sh 0.2.4
```
| 产物 | 位置 | 说明 |
|--|--|--|
| `OpenDance.exe` | `build/windows/` | 主程序,约 104 MB;脚本校验前 2 字节为 `MZ`(合法 PE) |
| `OpenDance.pck` | `build/windows/` | 资源包,约 107 KB; **必须与 exe 同目录** |
| `OpenDance.console.exe` | `build/windows/` | 控制台包装器,可能生成,非必需 |
| `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` 直接校验 |
导出前必须安装与编辑器同版本的 export templates,否则脚本会报「未找到导出模板」。
### 7. 自检清单
| 检查 | 命令 | 预期输出 |
|--|--|--|
| 一键验证(编译 + 测试 + 启动) | `GODOT_BIN=/path/to/godot bash build/verify.sh` | 末行「验证全部通过」,退出码 0 |
| 无头单元 / 集成测试 | `$GODOT --headless --path . --script res://tests/run_tests.gd` | `== 结果:687 通过 / 0 失败 ==` 与 `全部通过`(仓库基线 687 项 / 10 个套件;缺 VRM 素材或服务端不可达时相关用例显式跳过,数字会减少) |
| 启动冒烟(180 帧) | `$GODOT --headless --path . --quit-after 180` | 无 `SCRIPT ERROR` / `ERROR:`,进程退出码 0 |
| 日志检查 | 打开 `%APPDATA%\Godot\app_userdata\OpenDance Studio\logs\godot.log` | 无 `SCRIPT ERROR`、无 `ERROR:` |
| 导出产物校验 | `bash build/export-windows.sh release` | 输出「PE 头校验通过」,且 `build/windows/OpenDance.exe` 前 2 字节为 `4d5a` |
| 服务端联通 | 主界面「服务端地址」->「保存并测试」 | 顶部显示「服务端正常 | DB ...」 |
### 8. 常见问题
| 现象 | 原因 | 处理 |
|--|--|--|
| 游戏界面提示「模型加载失败:缺少骨骼」或「骨骼绑定失败,模型可能不受支持」 | 模型没有 `Skeleton3D`,或骨骼命名不在 humanoid 映射表内;驱动至少需要 hips / spine / neck / head + 四肢共 16 根核心骨骼 | 换用标准 VRM(VRoid Studio 导出);看「人形覆盖率」,低于 60% 会提示「骨骼覆盖偏低,动作可能不完整」;必要时在 `src/model/humanoid_map.gd` 增加命名映射 |
| 顶部「服务端不可达」或「目录拉取失败」 | Web 服务端未启动、地址或端口写错(误填管理端口 3001)、或网络不通 | 确认 `api_base_url` 为 `http://<服务器地址>:3000`;浏览器访问 `http://<服务器地址>:3000/health` 应返回 200;再点「保存并测试」 |
| 导出失败:「未找到导出模板」 | 未安装 export templates,或版本 / 路径与编辑器不一致 | 编辑器「编辑器 -> 管理导出模板 -> 下载并安装」;Linux 放 `~/.local/share/godot/export_templates/<版本>.stable/`, Windows 放 `%APPDATA%\Godot\export_templates\<版本>.stable\` |
| 提示「接收服务启动失败(端口 8765 可能被占用)」 | TCP 8765 被占用或未放行 | 改 `opendance.cfg` 的 `listen_port` 后点「重新连接接收服务」,并同步修改 Android 端填写的接收地址 |
| 手机已连接但分数一直是 0 | Android 端未开始推流,或帧内关键点置信度都低于 0.5 | 「查看接收状态」确认累计帧在增长、畸形帧为 0;确认 Android 端子协议为 `opendance.pose.v1` |
## 技术栈
| 组件 | 选型 |
|--|--|
| 引擎 | Godot 4.7.x(MIT) |
| VRM 加载 | Godot 内置 GLTFDocument |
| 通信 | WebSocketPeer(服务端)+ HTTPRequest |
| 评分 | DTW + 关节角度差异(纯 GDScript) |
## 架构
```
Android 辅助 App ──239 字节关键点帧──▶ 本端 WebSocket 服务端:8765
│
Web 服务端 ──GET /api/catalog/export────────┤
──GET /api/motions/:id/keypoints ┘
▼
VRM 驱动 + DTW 评分
```
```
src/
├── autoload/ AppConfig / CatalogStore / PoseReceiver / ScoreService
├── net/ 239 字节编解码、WebSocket 服务、REST 客户端、NDJSON 解析
├── model/ VRM 加载、humanoid 骨骼名映射
├── anim/ 关键点 -> 骨骼重定向
├── scene/ 场景实例化与物理落地(重力/风/碰撞/环境/机位)
├── score/ 特征提取、DTW、评分规则
└── ui/ 主界面 / 游戏界面 / 结算 / 关于 / 来源展示 / 场景参数面板
```
界面**全部用 GDScript 代码构建**(不手写 `.tscn`),因此可被无头导入与单元测试覆盖。
## 依赖的契约(opendance-web 冻结,逐项独立版本)
| 契约 | 版本 |
|--|--|
| `rest-api` | 0.5.0 |
| `data-models` | 0.7.0 |
| `websocket-keypoints` | 0.2.0 |
| `skeleton-topology` | 0.2.0 |
| `model-formats` | 0.5.0 |
| `database` | 0.4.0 |
- [`contracts/websocket-keypoints.md`](contracts/websocket-keypoints.md) - 239 字节关键点帧与文本控制消息(本端为**接收方**)
- [`contracts/skeleton-topology.md`](contracts/skeleton-topology.md) - MediaPipe 33 点拓扑、父节点表、9 个关节角特征
- [`contracts/rest-api.yaml`](contracts/rest-api.yaml) - 目录与关键点序列 API
- [`contracts/data-models.md`](contracts/data-models.md) - 资源字段与 VRM 优先规则
- [`contracts/model-formats.md`](contracts/model-formats.md) - 模型与动作格式、关键点序列结构
- [`contracts/database.md`](contracts/database.md) - 数据库表结构(客户端只读快照,不直连数据库)
`contracts/` 是**只读快照**。任何跨端契约变更必须先回到
[`opendance-web`](https://gitea.kasugano.cn/zijunchiang/opendance-web) 修改并提升版本,
再在本仓库创建适配任务。
## 已知限制
1. Godot 的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**,
实现接受任意 path,仅校验子协议 `opendance.pose.v1`。
2. 文档原选型 `godot-vrm` GDExtension 不发布预编译产物,改用 Godot 内置 glTF 导入器
+ 自实现骨骼名映射;理由见 `DSH.md` 6。
3. `build/export-windows.ps1` 未在真实 Windows 上执行过(本环境无 Windows)。
两条路径共用同名导出预设与相同的 PE 头校验逻辑。
4. 真实 VRM 样例仅放在 `_scratch/`(已 gitignore),版权原因不入库;
测试在找不到模型时显式跳过而非伪装通过。
5. **Gitea 附件上传上限**:本项目 Gitea 前置 IIS 的默认
`maxAllowedContentLength = 30,000,000` 字节(≈28.6 MiB),实测 16.4 MB 通过、
30.5 MB / 37 MB / 40 MB 上传返回 **HTTP 413**。`v0.1.0` 因此被拆成
`zip.part0/1/2`(无法直接解压)。现改为**只上传单个完整包**,超限时打包脚本
直接告警并要求运维放宽上限,不再回退分卷;做法见
`opendance-web/deploy/proxy/README.md` 第九节。
6. **v0.2.0 / v0.2.1 / v0.2.2 的 zip 用的是 LZMA 压缩**(`ZIP_METHOD=lzma`
打出来的),因为这几次发布时 IIS 上限都尚未放开:整包从约 38 MB 降到约 26 MB
才能传上去。**代价:Windows 10 的资源管理器不支持 LZMA(method 14) 的 zip,
必须用 7-Zip / WinRAR / PeaZip 解压**(Win11 24H2+ 起资源管理器原生支持 7z
等格式,但本项目未在 Windows 上实测)。
若 IIS 上限已放开、或你能把包放到别处分发(例如 Web 服务端的 `/static`,
不受 IIS 那个限制约束),请改用资源管理器可直接双击解压的 deflate 包:
`ZIP_METHOD=deflate bash build/package-release.sh 0.2.4`(约 38 MB)。
7. **发布包 exe 的版本元数据**(T-WIN-017):v0.2.1 之前在 Windows
「属性 → 详细信息」里显示的是 `0.1.0`,因为 `export_presets.cfg` 的
`application/file_version` / `application/product_version` 被硬编码且无人校验。
v0.2.2 起由 `build/sync-export-version.sh` 在导出前同步、由
`build/check-pe-version.sh` 直接读 exe 的 PE 版本资源复核
(`VS_FIXEDFILEINFO` + `StringFileInfo` 两处都查),
`check-version.sh` / `package-release.sh` / `verify.sh` / `test_about.gd` 均已接入。
## 第三方开源声明
见 [`THIRD_PARTY.md`](THIRD_PARTY.md),或客户端「关于」页面。
未使用 SMPL 模型及其衍生拓扑,统一采用 MediaPipe 33 点拓扑。