docs(win): 任务说明、DSH 约束与交付文档
This commit is contained in:
@@ -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 帧无任何运行时错误。
|
||||
@@ -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` 解析均符合契约。
|
||||
@@ -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。
|
||||
@@ -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 调试帧。
|
||||
@@ -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 不误判
|
||||
@@ -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 传播验证。
|
||||
@@ -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)、带宽约束、时间伸缩吸收、流式与离线一致、
|
||||
低置信度维度不参与距离、角度计算正确性。
|
||||
@@ -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` 启动无运行时错误;
|
||||
主菜单、游戏页、结算页的构造路径均被该启动流程覆盖。
|
||||
@@ -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()`),保证展示组件拿到的数据完整。
|
||||
@@ -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 含全部条目、纯文本导出可解析。
|
||||
@@ -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 只在**项目根目录**查找
|
||||
Reference in New Issue
Block a user