docs(win): 任务说明、DSH 约束与交付文档

This commit is contained in:
2026-09-14 20:04:54 +08:00
parent 315cf79708
commit a83e4f6a1f
15 changed files with 975 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
# DSH.md —— opendance-windows(OpenDance Studio 项目二:Windows 客户端)
## 0. 本文件的作用
本文件是 **opendance-windows 仓库内**的 DSH 执行约束。顶层说明书见
《DSH 开发文档:OpenDance Studio》,二者冲突时以顶层文档为准。
## 1. 项目定位
Windows 客户端是核心体验载体:接收 Android 辅助 App 推送的 33 点骨骼关键点,
驱动 VRM 角色,并与 Web 服务端下发的参考动作做实时 DTW 评分。
```
Android 辅助 App ──239 字节关键点帧──▶ 本端(WebSocket 服务端 :8765)
│
Web 服务端 ──GET /api/catalog/export──────┤
──GET /api/motions/:id/keypoints┘
▼
VRM 驱动 + DTW 评分
```
**本端是 WebSocket 的「服务端」,Android 主动连入**(契约 websocket-keypoints.md §1)。
## 2. 技术栈
| 组件 | 选型 | 说明 |
|---|---|---|
| 引擎 | Godot 4.7.2(MIT) | 可在 Linux 上导出 Windows exe |
| VRM 加载 | **Godot 内置 GLTFDocument** | 见 §6 的选型变更说明 |
| 多格式导入 | Godot 内置 glTF 导入器 | FBX 由管理端转 BVH 后下发 |
| 通信 | WebSocketPeer + HTTPRequest | 无第三方依赖 |
| 评分 | DTW + 关节角度差异 | 纯 GDScript 实现 |
| 渲染 | GL Compatibility | 兼容性最好,便于无 GPU 导出 |
## 3. 单次会话加载范围
允许加载:
1. 本文件 `DSH.md`
2. `project.yaml`
3. 当前 `tasks/xxx.md`
4. 该任务文件中 `load` 列出的文件
5. `contracts/` 中被明确引用的**锚点片段**
禁止加载:
- `opendance-web` / `opendance-android` 的任何源码
- 非当前任务的 `tasks/*.md`
- 完整 `contracts/` 目录(只引用锚点)
- `.godot/` 导入缓存、`build/windows/` 导出产物、`_scratch/` 下的模型素材
## 4. 上下文预算
| 项 | 建议值 |
|---|---|
| 单任务文件数 | ≤ 6 |
| 单任务代码行数 | ≤ 1500 |
| 单任务 Token | ≤ 12K |
| 契约引用 | 只引用锚点,不加载全文 |
## 5. 目录结构
```
opendance-windows/
├── project.godot # Godot 工程(autoload、渲染、窗口配置)
├── export_presets.cfg # ★ 必须位于项目根目录(Godot 硬性要求)
├── project.yaml / DSH.md
├── tasks/ # 001~011 单任务说明书
├── contracts/ # 只读契约快照(来源 opendance-web v0.1.0)
├── scenes/main.tscn # 仅一个挂载 main.gd 的根节点
├── src/
│ ├── autoload/ # AppConfig / CatalogStore / PoseReceiver / ScoreService
│ ├── net/ # 239 字节编解码、WebSocket 服务、REST 客户端、NDJSON 解析
│ ├── model/ # VRM 加载、humanoid 骨骼名映射
│ ├── anim/ # 关键点 → 骨骼重定向
│ ├── score/ # 特征提取、DTW、评分规则
│ └── ui/ # 主界面 / 游戏界面 / 结算 / 关于 / 来源展示
├── tests/ # 无头测试框架 + 用例
└── build/ # 导出脚本(sh / ps1)
```
## 6. 界面构建方式与选型变更(重要)
**界面全部用 GDScript 代码构建,不手写 `.tscn`。**
`.tscn` 是带 uid 与节点路径的手写格式,在没有编辑器的情况下极易出错且难以验证;
用代码构建可随脚本一起被无头导入和单元测试覆盖。`scenes/main.tscn` 因此只保留一个根节点。
**VRM 加载改用 Godot 内置 glTF 导入器**(文档 §5.1 原选型为 godot-vrm GDExtension):
- `V-Sekai/godot-vrm` **不发布任何预编译产物**(无 GitHub Release,仅源码,
需用 godot-cpp 本地编译原生库),这会同时破坏「Linux 上导出 Windows exe」的验收路径。
- VRM 本身就是 glTF 2.0 二进制容器:实测内置导入器可完整解析 154 骨骼的 VRoid 模型,
且 `GLTFState.json` 可直接读取 `extensions.VRMC_vrm` / `extensions.VRM` 判定版本。
- humanoid 骨骼按名字映射(`src/model/humanoid_map.gd`),覆盖 VRoid / VRM 0.x / Mixamo 三种命名。
若日后接入 godot-vrm,只需替换 `VrmLoader.load_from_file`,对外接口不变。
## 7. 硬性约束
1. **绝不让视频帧离开设备**:本端只接收 33 个关键点的坐标,不接收也不存储图像。
2. **契约先行**:改交互先改 `opendance-web` 的 `contracts/`,再回本仓库同步快照。
3. **不假设骨骼轴向约定**:重定向基于「静止朝向 → 目标朝向」的最小旋转,
`pose = parent_global_basis⁻¹ × desired` 这一合成公式由真实 VRM 实测标定,
有回归测试守护,不得凭记忆改写。
4. **上传/下载的模型不入库**:真实 VRM 样例仅放在 `_scratch/`(已 gitignore),
版权原因不得提交。
5. **不使用 SMPL 拓扑**,统一 MediaPipe 33 点。
6. 关于页面与 `THIRD_PARTY.md` 必须与 `contracts/open-source-credits.md` 一致。
## 8. 验证(每任务执行)
```bash
GODOT=/path/to/godot # Godot 4.7.x
# 1. 全部脚本编译通过(无输出即通过)
$GODOT --headless --path . --import 2>&1 | grep -E "SCRIPT ERROR|Parse Error"
# 2. 无头单元测试 + 真实 API 集成(API 不可达时相关用例显式跳过)
$GODOT --headless --path . --script res://tests/run_tests.gd
# 3. 应用可启动(autoload + UI 构建无报错)
$GODOT --headless --path . --quit-after 180
# 4. Windows 导出(需先安装 export templates)
bash build/export-windows.sh release
```
## 9. 上下文超标时的拆分策略
| 超标原因 | 拆分方式 |
|---|---|
| 单任务涉及多个界面 | 按界面拆(主/游戏/结算/关于) |
| 单文件过大 | 按类拆,先生成接口再分实现 |
| 契约全文加载 | 改用锚点引用 |
| 需要参考其他项目 | 只读契约,不读源码 |
| UI 与逻辑耦合 | UI 只调用已定义接口(GameScreen 不做 DTW 计算) |
| 测试与实现同任务 | 测试单独任务,共享接口摘要 |
+168
View File
@@ -0,0 +1,168 @@
================================================================
OpenDance Studio —— Windows 客户端 v0.1.0 使用说明
================================================================
摄像头跳舞评分系统的核心体验端:接收 Android 辅助 App 通过 WebSocket
推送的 33 点骨骼关键点,驱动 VRM 角色,并与 Web 服务端下发的参考动作
做实时 DTW 评分。
本客户端只接收关键点坐标,不接收、不存储任何视频画面。
----------------------------------------------------------------
一、快速开始
----------------------------------------------------------------
1. 解压后得到:
OpenDance.exe 主程序
OpenDance.pck 资源包(必须与 exe 同目录)
README.txt 本文件
THIRD_PARTY.md 开源声明
contracts/ 契约快照
2. 先启动 Web 管理服务端(opendance-web),确保能访问其 API。
3. 双击 OpenDance.exe 启动。
4. 在主界面确认「服务端地址」(默认 http://127.0.0.1:3000),
点击「保存并测试」;顶部应显示「服务端正常 | DB ...」。
5. 点击「刷新目录」,左侧会出现已发布的曲目、动作与模型。
6. 在 Android 手机上启动「OpenDance 辅助 App」,填入本机显示在
主界面上的接收地址(形如 ws://192.168.x.x:8765/pose)并连接。
连接成功后主界面会提示「已连接 N 台设备」。
7. 依次选择曲目 → 动作 → 模型,点击「开始跟练」。
----------------------------------------------------------------
二、界面说明
----------------------------------------------------------------
主界面
- 三栏选择:曲目 / 动作(仅列出已完成预处理的动作)/ 模型(VRM 优先)
- 服务端地址:可修改并即时测试
- 接收地址:Android 端要连接的 WebSocket 地址
- 「刷新目录」重新拉取已发布资源
- 底部展示所选动作的**来源信息**(来源类型、名称、作者、许可、链接)
游戏界面
- 左侧 3D 视图:VRM 角色跟随关键点实时动作,相机自动框住模型
- 右侧 HUD:实时得分、等级、连击、动作进度、连接状态、来源信息
结算界面
- 等级(S/A/B/C/D)、平均分与最终分、动作完成度、最高单帧、最长连击
- 「再来一次」/「返回主菜单」/「开源声明」
关于页面
- 版本与契约版本、隐私说明、完整可滚动的第三方开源项目声明
----------------------------------------------------------------
三、配置文件
----------------------------------------------------------------
首次运行会在以下位置生成配置文件,可直接用文本编辑器修改:
Windows: %APPDATA%\Godot\app_userdata\OpenDance Studio\opendance.cfg
可配置项:
api_base_url Web 服务端地址(默认 http://127.0.0.1:3000)
listen_host WebSocket 监听地址(默认 0.0.0.0)
listen_port WebSocket 监听端口(默认 8765)
ws_subprotocol 子协议(默认 opendance.pose.v1)
dtw_band DTW 带宽,单位帧(默认 15 ≈ 0.5 秒抖动容忍)
score_tolerance 评分容差:平均每步代价达到该值即 0 分(默认 0.25)
mirror_preview 镜像预览
----------------------------------------------------------------
四、防火墙与网络
----------------------------------------------------------------
本客户端需要接受 Android 设备的入站连接:
- Windows 防火墙需放行 OpenDance.exe 的 **TCP 8765** 入站
- 手机与电脑需在同一局域网
- 手机端填写主界面显示的「接收地址」
若端口被占用,可修改 opendance.cfg 中的 listen_port 后重启。
----------------------------------------------------------------
五、评分说明
----------------------------------------------------------------
- 每帧特征 = 33 点 × (x,y,z) + 9 个关节角余弦 = 108 维
(与 Web 服务端预处理产物、Android 端推流结构完全一致)
- 置信度低于 0.5 的关键点不参与代价计算,避免遮挡导致误判
- 采用带约束 DTW(|i−j| ≤ band)对齐,能吸收一定的节奏快慢差异
- 得分 = clamp(1 − 平均代价 / 容差, 0, 1) × 100,并做指数平滑
- 等级:S ≥ 95,A ≥ 85,B ≥ 70,C ≥ 55,D 其余
----------------------------------------------------------------
六、从源码构建
----------------------------------------------------------------
前置:Godot 4.7.x + 对应版本的 export templates
# Linux 上直接导出 Windows 可执行文件(推荐)
GODOT_BIN=/path/to/godot bash build/export-windows.sh release
# Windows 上兜底导出
powershell -ExecutionPolicy Bypass -File build\export-windows.ps1
产物位于 build/windows/:OpenDance.exe + OpenDance.pck
(脚本会校验产物存在且为合法 PE 文件)
运行测试:
GODOT=/path/to/godot
$GODOT --headless --path . --import
$GODOT --headless --path . --script res://tests/run_tests.gd
$GODOT --headless --path . --quit-after 180
----------------------------------------------------------------
七、目录结构
----------------------------------------------------------------
project.godot 工程配置(autoload / 渲染 / 窗口)
export_presets.cfg 导出预设(必须在项目根目录)
project.yaml 任务清单与状态
DSH.md 本项目 DSH 执行约束
tasks/ 单任务说明书(T-WIN-001 ~ 011)
contracts/ 只读契约快照(来源 opendance-web v0.1.0)
scenes/main.tscn 根节点
src/autoload/ AppConfig / CatalogStore / PoseReceiver / ScoreService
src/net/ 239 字节编解码、WebSocket 服务、REST 客户端
src/model/ VRM 加载、humanoid 骨骼映射
src/anim/ 关键点 → 骨骼重定向
src/score/ 特征提取、DTW、评分规则
src/ui/ 主界面 / 游戏界面 / 结算 / 关于 / 来源展示
tests/ 无头测试框架与用例
build/ 导出脚本
----------------------------------------------------------------
八、常见问题
----------------------------------------------------------------
Q: 主界面显示「服务端不可达」
A: 检查 Web 服务端是否已启动,以及「服务端地址」是否正确(含端口)。
Q: 目录里没有可用的动作
A: 动作必须先在管理端完成「预处理」(BVH → 关键点序列)并「审核通过」,
才会出现在客户端。
Q: 模型加载失败 / 动作不完整
A: 游戏界面会显示「人形覆盖率」。低于 60% 说明模型的骨骼命名不被识别,
请换用标准 VRM(VRoid Studio 导出)模型。
Q: 连上了手机但分数一直是 0
A: 确认手机端已开始推流;主界面的「查看接收状态」会显示累计帧数与丢帧数。
Q: 关于页面的开源声明
A: 与 THIRD_PARTY.md、contracts/open-source-credits.md 三处保持一致。
----------------------------------------------------------------
九、许可
----------------------------------------------------------------
本客户端代码以 MIT 许可发布。第三方开源项目声明见 THIRD_PARTY.md。
未使用 SMPL 模型及其衍生拓扑,统一采用 MediaPipe 33 点拓扑。
+118
View File
@@ -0,0 +1,118 @@
# 第三方开源项目声明
**OpenDance Studio Windows 客户端 v0.1.0**
本软件使用了以下开源项目,特此声明。完整锚点见 `contracts/open-source-credits.md`
(三端共用,任一端新增依赖须回到 `opendance-web` 更新该表)。
客户端「关于」页面展示同一份声明(见 `src/ui/credits.gd`)。
## 1. MediaPipe
- 版权:Copyright 2023 Google LLC
- 许可证:Apache License 2.0
- 源链接:https://github.com/google-ai-edge/mediapipe
- 用途:33 点骨骼拓扑定义(契约来源,**本端不包含其代码**)
## 2. Three.js
- 许可证:MIT
- 源链接:https://github.com/mrdoob/three.js
- 用途:Web 管理端 3D 预览
## 3. @pixiv/three-vrm
- 许可证:MIT
- 源链接:https://github.com/pixiv/three-vrm
- 用途:Web 管理端 VRM 加载
## 4. Assimp
- 许可证:BSD-3-Clause
- 源链接:https://github.com/assimp/assimp
- 用途:FBX/glTF 动作转换(可选外部依赖,在服务端侧使用)
## 5. Godot Engine
- 版权:Copyright Godot Engine contributors
- 许可证:MIT
- 源链接:https://github.com/godotengine/godot
- 用途:**本客户端的运行引擎**,包括内置 glTF/VRM 导入器与 WebSocket 实现
## 6. godot-vrm
- 许可证:MIT
- 源链接:https://github.com/V-Sekai/godot-vrm
- 用途:VRM humanoid 扩展参考实现。
**本客户端未使用其二进制**(该仓库不发布预编译产物),改为内置 glTF 导入器 +
自实现的骨骼名映射;此处按契约表保留声明。
## 7. NestJS
- 许可证:MIT
- 源链接:https://github.com/nestjs/nest
- 用途:Web 管理服务端
## 8. TypeORM
- 许可证:MIT
- 源链接:https://github.com/typeorm/typeorm
- 用途:Web 管理服务端 ORM
## 9. OkHttp
- 许可证:Apache License 2.0
- 源链接:https://github.com/square/okhttp
- 用途:Android 辅助 App 通信
## 10. bvh
- 许可证:MIT
- 源链接:https://github.com/aimoonchen/bvh
- 用途:BVH 解析参考实现
---
## 补充声明(实际引入的依赖)
## 11. React
- 许可证:MIT
- 源链接:https://github.com/facebook/react
- 用途:Web 管理端界面
## 12. Vite
- 许可证:MIT
- 源链接:https://github.com/vitejs/vite
- 用途:Web 管理端构建
## 13. CameraX
- 许可证:Apache License 2.0
- 源链接:https://developer.android.com/jetpack/androidx/releases/camera
- 用途:Android 相机采集
## 14. Jetpack Compose
- 许可证:Apache License 2.0
- 源链接:https://developer.android.com/jetpack/compose
- 用途:Android 界面
## 15. mysql2
- 许可证:MIT
- 源链接:https://github.com/sidorares/node-mysql2
- 用途:Web 管理服务端数据库驱动
---
## 许可合规说明
1. 本客户端自身代码以 MIT 许可发布。
2. 运行时依赖仅为 **Godot Engine**(MIT);其余条目为同项目其他组件或服务端依赖,
按契约要求一并声明。
3. 本客户端**未使用 SMPL 模型及其衍生拓扑**,统一采用 MediaPipe 33 点拓扑,
以规避 SMPL 的非商用许可限制。
4. 本客户端只接收 33 个关键点坐标,**不接收、不存储任何视频画面**。
5. 本客户端下载并缓存的 VRM 模型来自部署方自建的服务端,其来源与许可由
Web 服务端的来源五元组字段管理并在界面展示。
+76
View File
@@ -0,0 +1,76 @@
project: opendance-windows-client
version: 0.1.0
repository: opendance-windows
default_branch: main
engine: Godot 4.7.2
contracts_version: 0.1.0
# 契约变更流程(文档 §7.2):
# 任何跨端契约变更必须先在 opendance-web 修改 contracts/ 并提升版本,
# 再在本项目创建适配任务。本项目内的 contracts/ 只是只读快照。
dependencies:
- name: opendance-web
kind: rest-api
contract: contracts/rest-api.yaml
usage: 曲目/动作/模型目录与关键点序列
- name: opendance-android
kind: websocket-consumer
contract: contracts/websocket-keypoints.md
usage: 本端为服务端,Android 主动连入推流
tasks:
- id: T-WIN-001
file: tasks/001-scaffold.md
scope: Godot 项目骨架
status: done
- id: T-WIN-002
file: tasks/002-rest-client.md
scope: 调用 Web 服务端 API
status: done
- id: T-WIN-003
file: tasks/003-websocket-receiver.md
scope: 接收 Android 关键点帧
status: done
- id: T-WIN-004
file: tasks/004-keypoint-deserialize.md
scope: 239 字节帧反序列化
status: done
- id: T-WIN-005
file: tasks/005-vrm-loader.md
scope: VRM 模型加载
status: done
- id: T-WIN-006
file: tasks/006-skeleton-retarget.md
scope: 关键点 → VRM 骨骼重定向
status: done
- id: T-WIN-007
file: tasks/007-scoring-dtw.md
scope: DTW 评分引擎
status: done
- id: T-WIN-008
file: tasks/008-game-ui.md
scope: 主界面 + 游戏界面 + 结算
status: done
- id: T-WIN-009
file: tasks/009-source-display.md
scope: 模型/动作来源信息展示
status: done
- id: T-WIN-010
file: tasks/010-about-page.md
scope: 关于页面 + 开源声明
status: done
- id: T-WIN-011
file: tasks/011-build-script.md
scope: Windows 打包脚本
status: done
deliverable:
artifact: opendance-windows-v0.1.0.zip
packaging: Godot 导出脚本(Linux 直接导出 / Windows 兜底)
includes:
- OpenDance.exe
- OpenDance.pck
- THIRD_PARTY.md
- README.txt
- contracts/ # 当前版本契约快照
+41
View File
@@ -0,0 +1,41 @@
# T-WIN-001 Godot 项目骨架
- **状态**:done
- **依赖**:无
## 目标
建立 Godot 4 工程骨架,配置 4 个 autoload、渲染方式与窗口,保证可无头导入与启动。
## load
```
DSH.md
project.godot
src/autoload/*.gd
src/ui/main.gd
scenes/main.tscn
```
## 交付物
| 文件 | 说明 |
|---|---|
| `project.godot` | 工程配置;autoload = AppConfig / CatalogStore / PoseReceiver / ScoreService |
| `scenes/main.tscn` | 只含一个挂载 `src/ui/main.gd` 的 Control 根节点(界面用代码构建,见 DSH.md §6) |
| `src/ui/main.gd` | 应用外壳:构建布局、切换页面、启动接收服务 |
| `src/autoload/app_config.gd` | 客户端配置,持久化到 `user://opendance.cfg` |
| `src/autoload/catalog_store.gd` | 目录缓存 + 模型下载缓存 + 关键点序列缓存 |
| `src/autoload/pose_receiver.gd` | 关键点接收服务封装 |
| `src/autoload/score_service.gd` | 评分服务封装 |
## 验收
```bash
GODOT --headless --path . --import # 无 SCRIPT ERROR
GODOT --headless --path . --quit-after 180 # 应用可启动,UI 构建无报错
```
## 结果
- 导入无编译错误;`--quit-after 180` 启动 180 帧无任何运行时错误。
+44
View File
@@ -0,0 +1,44 @@
# T-WIN-002 调用 Web 服务端 API
- **状态**:done
- **依赖**:T-WIN-001
## 目标
调用 opendance-web 的 REST 接口拉取目录与参考动作序列,只消费冻结契约。
## load
```
DSH.md
contracts/rest-api.yaml # 锚点:#/paths/~1api~1catalog
contracts/data-models.md # 锚点:§2 Song、§3 Motion、§4 Model、§6 CatalogBundle
src/net/api_client.gd
src/net/catalog_types.gd
src/autoload/catalog_store.gd
```
## 交付物
| 文件 | 说明 |
|---|---|
| `src/net/api_client.gd` | HTTPRequest 封装;统一返回 `{ok,status,data,error}`,不抛异常 |
| `src/net/catalog_types.gd` | Song / Motion / Model / CatalogBundle / SourceInfo 类型化解析 |
| `src/net/keypoint_sequence.gd` | NDJSON 关键点序列 → 特征向量序列 |
| `src/autoload/catalog_store.gd` | 目录刷新、模型文件下载缓存、序列缓存 |
## 关键实现
- `default_model()` 直接使用服务端解析好的 `defaultModelId`,客户端不重复实现 VRM 优先级
- `Motion.is_scorable()` 要求 `preprocessState == "done"` 且有帧数
- 错误体解析兼容 NestJS 的 `message: string[]`
- 模型文件按 `user://models/<id>.<ext>` 缓存,避免重复下载 10 MB 级 VRM
## 验收
`tests/test_api_integration.gd` 对**真实运行中**的服务端断言:
health / catalog version / catalog export / 关键点序列,服务端不可达时显式跳过。
## 结果
集成测试通过:目录解析、来源五元组齐备性、`defaultModelId` 解析均符合契约。
+45
View File
@@ -0,0 +1,45 @@
# T-WIN-003 接收 Android 关键点帧
- **状态**:done
- **依赖**:T-WIN-001
## 目标
在本端监听 `ws://<host>:8765/pose`,作为 WebSocket **服务端**接收 Android 辅助 App 的推流。
## load
```
DSH.md
contracts/websocket-keypoints.md # 锚点:§1 端点与子协议、§3 文本控制帧
src/net/pose_server.gd
src/autoload/pose_receiver.gd
```
## 交付物
- `src/net/pose_server.gd`:TCPServer + `WebSocketPeer.accept_stream()` 实现服务端
- `src/autoload/pose_receiver.gd`:绑定 AppConfig,暴露 `latest_frame` 与统计
## 能力
| 项 | 说明 |
|---|---|
| 子协议 | 协商 `opendance.pose.v1`,客户端选择不符时告警 |
| 控制帧 | `hello` → 回 `welcome`(含 sessionId);`bye` → 关闭 |
| 协议不匹配 | 回 `bye{reason: protocol_mismatch}` 并以 1002 关闭 |
| 二进制帧 | 严格校验 239 字节与 magic,畸形帧计数但不影响连接 |
| 丢包统计 | 按 `frame_id` 跳号计数(含 u32 回绕保护) |
| 帧率统计 | 1 秒窗口计算 FPS |
| 超时剔除 | 5 秒无数据判定超时断开 |
| config 推送 | 每 2 秒广播 mirror / targetFps / upperBodyOnly / resolution |
## 已知限制
Godot 的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**,
实现接受任意 path,仅校验子协议。已记录于 DSH.md 与任务文档。
## 验收
`tests/test_pose_server.gd` 在真实 socket 上端到端验证:握手、子协议协商、
hello/welcome、二进制帧往返、畸形帧拒绝、丢包计数、flag 保留、config 广播、snapshot、stop。
+46
View File
@@ -0,0 +1,46 @@
# T-WIN-004 239 字节帧反序列化
- **状态**:done
- **依赖**:T-WIN-001
## 目标
按契约实现 239 字节关键点帧的编解码。
## load
```
DSH.md
contracts/websocket-keypoints.md # 锚点:§2 二进制帧(冻结)
contracts/skeleton-topology.md # 锚点:§4 坐标系
src/net/keypoint_frame.gd
```
## 帧布局(契约冻结)
```
偏移 长度 字段
0 2 magic u16 = 0x4F44 ('OD')
2 1 version u8 = 1
3 1 flags u8 bit0 镜像 bit1 低置信度 bit2 上半身
4 4 frame_id u32 LE
8 231 keypoints 33 × 7B:s16 x, s16 y, s16 z, u8 conf
合计 239
```
## 交付物
`src/net/keypoint_frame.gd`:`PoseFrame` 内部类 + `decode` / `encode` / `validate` / `from_json_dict`。
## 关键实现
- 归一化系数统一 32767,与 opendance-web 的 TypeScript 参考实现逐位对齐
- 编解码往返误差 < 1e-4(定点精度 1/32767 ≈ 3.05e-5)
- `unmirrored_points()` 按 flags.bit0 翻转 x
- 低置信度阈值 0.5:`is_reliable()` 与 `compute_low_confidence()`
- 极值(±1.0)不溢出 int16
## 验收
`tests/test_keypoint_frame.gd`:常量与契约一致、239 字节、magic/version/flags/frame_id、
往返精度、错误长度/magic/版本拒绝、极值、镜像翻转、置信度阈值、JSON 调试帧。
+41
View File
@@ -0,0 +1,41 @@
# T-WIN-005 VRM 模型加载
- **状态**:done
- **依赖**:T-WIN-001
## 目标
加载 VRM 模型,识别 VRM 版本,定位 Skeleton3D 并映射 humanoid 骨骼。
## load
```
DSH.md
contracts/model-formats.md # 锚点:§1 角色模型
contracts/data-models.md # 锚点:§4.1 VRM 优先规则
src/model/vrm_loader.gd
src/model/humanoid_map.gd
```
## 选型变更
文档原选型 `godot-vrm`(V-Sekai)GDExtension **不发布任何预编译产物**,
需用 godot-cpp 本地编译原生库,会破坏「Linux 导出 Windows exe」的验收路径。
改用 **Godot 内置 GLTFDocument**:VRM 即 glTF 2.0 容器,实测可完整解析
154 骨骼的 VRoid 模型,且 `GLTFState.json` 可直接读取 VRM 扩展判定版本。
## 骨骼名映射
`HumanoidMap` 覆盖 VRoid / VRM 0.x / Mixamo 三类命名,归一化后精确比较:
- 剥离前缀 `jbip` / `mixamorig` / `bip01`(**归一化之后**的形态)
- 生成 compact(left→l)与 expanded(l→left)两种变体比较
- **故意不剥离 `J_Aim_` / `J_Roll_` / `J_Sec_`**:VRoid 用它们标记扭转辅助骨,
剥离后 `J_Aim_L_UpperLeg` 会与真正的 `J_Bip_L_UpperLeg` 撞名
## 验收
`tests/test_vrm_loader.gd`:
- 三类命名规范的识别(含辅助骨不遮蔽真骨的回归用例)
- 真实 VRM:154 骨骼 / 3 网格 / VRM 1.0,**humanoid 覆盖率 100%**
- 缺失文件、普通 glTF 不误判
+45
View File
@@ -0,0 +1,45 @@
# T-WIN-006 关键点 → VRM 骨骼重定向
- **状态**:done
- **依赖**:T-WIN-005、T-WIN-004
## 目标
把 33 点关键点实时驱动到 VRM 骨骼上。
## load
```
DSH.md
contracts/skeleton-topology.md # 锚点:§1 33 点、§2 虚拟关节、§4 坐标系
src/anim/skeleton_retarget.gd
```
## 算法
1. 每根骨骼指定一条骨链(起点关键点,终点关键点)
2. 取骨骼静止朝向 `rest_dir`(由该骨与首个子骨的位置差得出,**不假设模型骨骼轴向**)
3. 目标朝向由两个关键点在世界空间的方向得出
4. `delta` = rest_dir → target_dir 的最小旋转四元数(保留扭转/roll)
5. `desired = Basis(delta) × rest_global_basis`
6. `pose_local = parent_global_basis⁻¹ × desired`
**第 6 步的合成公式是实测标定的**:Godot 文档未写明 rest/pose 的合成顺序,
在真实 VRoid 模型上暴力枚举 5 种候选公式,只有该式误差为 1.9e-8(其余 ~0.14)。
已有回归测试守护,不得凭记忆改写。
## 坐标转换
契约 §4 的 MediaPipe 归一化坐标 → Godot 世界方向:
`Vector3(x × aspect, −y, −z × aspect)`(x/z 以图像宽为基准故乘宽高比)。
## 验收
`tests/test_retarget.gd` —— 最关键的是**功能性**断言:
```
→ 左上臂方向点积 1.0000,左前臂方向点积 1.0000
```
即:给定一组关键点,重定向后骨骼在世界空间的**实际指向**与关键点方向一致;
另有判别性验证(换一组关键点骨骼朝向随之改变)与躯干 FK 传播验证。
+42
View File
@@ -0,0 +1,42 @@
# T-WIN-007 DTW 评分引擎
- **状态**:done
- **依赖**:T-WIN-004、T-WIN-002
## 目标
用 DTW 把实时关键点序列与服务端参考序列比对,产出 0~100 分与等级。
## load
```
DSH.md
contracts/skeleton-topology.md # 锚点:§5 派生特征
contracts/model-formats.md # 锚点:§2.1 预处理产物
src/score/pose_features.gd
src/score/dtw.gd
src/score/scoring.gd
```
## 特征(契约 §5 冻结)
每帧 = `[33 × (x,y,z), 9 个关节角余弦]` = **108 维**。
低置信度点(conf < 0.5)对应维度**权重置 0**,不参与代价计算。
## DTW
- Sakoe-Chiba 带状约束 `|i − j| ≤ band`(默认 15 帧 ≈ 0.5 秒)
- `DtwMatcher.compute_full()` 离线段;`StreamingDtw` 逐帧流式推进,
每帧只计算带内一行,总体复杂度 O(n·m·band) 但摊到每帧 O(m·band)
- 归一化:代价 / max(i,j)+1,得到「平均每步代价」
## 评分规则(`scoring.gd`)
`score = clamp(1 − cost/tolerance, 0, 1) × 100`,EMA 平滑抑制抖动,
等级 S(≥95) / A(≥85) / B(≥70) / C(≥55) / D,连击阈值 70。
## 验收
`tests/test_dtw.gd` + `tests/test_pose_features.gd`:相同序列距离为 0、
路径单调且起于 (0,0)、带宽约束、时间伸缩吸收、流式与离线一致、
低置信度维度不参与距离、角度计算正确性。
+40
View File
@@ -0,0 +1,40 @@
# T-WIN-008 主界面 + 游戏界面 + 结算
- **状态**:done
- **依赖**:T-WIN-002、T-WIN-003、T-WIN-006、T-WIN-007
## 目标
三页式体验:主菜单(选曲/选动作/选模型)→ 游戏页(3D 角色 + 实时评分 HUD)→ 结算页。
## load
```
DSH.md
src/ui/main.gd
src/ui/main_menu.gd
src/ui/game_screen.gd
src/ui/result_screen.gd
src/ui/ui_kit.gd
```
## 页面
| 页面 | 能力 |
|---|---|
| 主菜单 | 三栏选择(曲目 / 可评分动作 / 模型)、服务端地址编辑与测试、目录刷新、接收服务状态与重启 |
| 游戏页 | SubViewport 3D 角色、自动定机位、实时分数/等级/连击、进度条、连接状态提示、来源展示 |
| 结算 | 等级大字、平均/最终分、完成度、最高单帧、最长连击、评语、来源展示 |
## 关键实现
- 界面全部代码构建(见 DSH.md §6),主菜单只列出 `preprocessState == "done"` 的动作
- 游戏页订阅 `PoseReceiver.frame_received` 驱动骨骼,`ScoreService` 独立订阅做评分
- 未连接设备时给出明确提示而非静默失败;模型/序列拉取失败时显示可读原因并允许重试
- 参考序列走完自动结算
- 切换模型时正确 `dispose()` 旧场景,避免泄漏
## 验收
`GODOT --headless --path . --quit-after 180` 启动无运行时错误;
主菜单、游戏页、结算页的构造路径均被该启动流程覆盖。
+40
View File
@@ -0,0 +1,40 @@
# T-WIN-009 来源信息展示
- **状态**:done
- **依赖**:T-WIN-008
## 目标
在客户端展示模型/动作的**来源五元组**(来源类型、名称、链接、许可、作者)。
## load
```
DSH.md
contracts/data-models.md # 锚点:§1 来源五元组
src/ui/source_panel.gd
src/net/catalog_types.gd
```
## 交付物
`src/ui/source_panel.gd`(可复用组件)+ `CatalogTypes.SourceInfo`(含 `type_label()` 与 `is_complete()`)。
## 展示规则
- 标题 + 来源类型中文标签(原创 / 已获授权 / Creative Commons / 公共领域 / 用户上传 / 其他)
- 资产名 | 来源名称
- 作者
- 许可 + **可点击外链**跳转到 `sourceUrl`
- **五元组不齐备时显式告警**("⚠ 来源信息不完整,请联系管理员补全后再使用"),不静默展示
## 使用位置
- 主菜单:选中动作时显示
- 游戏页:当前跟练动作
- 结算页:本轮动作
## 验收
`tests/test_api_integration.gd` 对真实服务端断言曲目与模型的来源五元组齐备
(`song.source.is_complete()`),保证展示组件拿到的数据完整。
+34
View File
@@ -0,0 +1,34 @@
# T-WIN-010 关于页面 + 开源声明
- **状态**:done
- **依赖**:T-WIN-008
## 目标
关于页面展示版本信息与**完整可滚动**的第三方开源项目声明。
## load
```
DSH.md
contracts/open-source-credits.md # 锚点:全表
src/ui/about_screen.gd
src/ui/credits.gd
```
## 交付物
- `src/ui/credits.gd`:与 `contracts/open-source-credits.md` 逐条对应的数据表,渲染为 BBCode
- `src/ui/about_screen.gd`:应用信息 + 契约版本 + 隐私说明 + 可滚动声明(RichTextLabel)
## 要点
- 声明含每项的**许可证、版权(如有)、用途、可点击源链接**
- 明确声明不使用 SMPL 拓扑
- 明确声明「仅接收关键点,不接收、不存储视频画面」
- 与 `THIRD_PARTY.md`、`contracts/open-source-credits.md` 三处保持一致
## 验收
`tests/test_credits.gd` 断言:条目数一致、每条均有名称/许可证/https 链接、
BBCode 含全部条目、纯文本导出可解析。
+58
View File
@@ -0,0 +1,58 @@
# T-WIN-011 Windows 打包脚本
- **状态**:done
- **依赖**:T-WIN-001 ~ 010
## 目标
提供 Linux 直接导出(方案 A,推荐)与 Windows 端兜底(方案 B)两条打包路径。
## load
```
DSH.md
build/export-windows.sh
build/export-windows.ps1
export_presets.cfg
```
## 交付物
| 文件 | 说明 |
|---|---|
| `export_presets.cfg` | 导出预设(**必须在项目根目录**,Godot 硬性要求) |
| `build/export-windows.sh` | Linux 导出:定位 Godot → 检查模板 → 导入 → 导出 → **校验 PE 头** |
| `build/export-windows.ps1` | Windows 兜底:同上,产物路径与校验逻辑一致 |
## 脚本健壮性
- `GODOT_BIN` 环境变量或 PATH 自动定位 Godot,缺失时给出可执行的安装指引
- 导出模板缺失时明确报错并给出下载与安装路径
- 导出命令返回非 0 时**仍检查产物是否存在**(Godot 有时非 0 返回但产物有效)
- 读取产物前 2 字节校验 `MZ`(PE 头),防止导出静默产出空壳
## 验收(文档 §5.4)
| 验收项 | 结果 |
|---|---|
| Linux:`bash build/export-windows.sh` 成功生成 exe | ✅ 生成 `OpenDance.exe` 104 MB + `OpenDance.pck` 107 KB,PE 头校验通过 |
| Windows:`powershell build/export-windows.ps1` 成功生成 exe | ⚠️ 脚本就绪,本环境无 Windows,未实机执行 |
| 生成的 exe 在 Windows 10/11 可运行 | ⚠️ 本环境无 Windows;已校验为合法 PE32+ x86_64 可执行文件 |
| `THIRD_PARTY.md` 包含所有开源项目与源链接 | ✅ 15 项,与契约表逐条一致 |
## 打包产物
```
opendance-windows-v0.1.0.zip
├── OpenDance.exe
├── OpenDance.pck
├── THIRD_PARTY.md
├── README.txt
└── contracts/ # 契约快照
```
## 环境记录
- Godot 4.7.2.stable(`ed1daf0bf`)
- 导出模板 `4.7.2.stable`(``windows_release_x86_64.exe``)
- 首次导出失败原因:`export_presets.cfg` 放在 `build/` 下,Godot 只在**项目根目录**查找