From a83e4f6a1fea8a54f44834ac1e4e6830a1c1f62a Mon Sep 17 00:00:00 2001 From: zijunchiang Date: Mon, 14 Sep 2026 20:04:54 +0800 Subject: [PATCH] =?UTF-8?q?docs(win):=20=E4=BB=BB=E5=8A=A1=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E3=80=81DSH=20=E7=BA=A6=E6=9D=9F=E4=B8=8E=E4=BA=A4?= =?UTF-8?q?=E4=BB=98=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DSH.md | 137 ++++++++++++++++++++++++ README.txt | 168 ++++++++++++++++++++++++++++++ THIRD_PARTY.md | 118 +++++++++++++++++++++ project.yaml | 76 ++++++++++++++ tasks/001-scaffold.md | 41 ++++++++ tasks/002-rest-client.md | 44 ++++++++ tasks/003-websocket-receiver.md | 45 ++++++++ tasks/004-keypoint-deserialize.md | 46 ++++++++ tasks/005-vrm-loader.md | 41 ++++++++ tasks/006-skeleton-retarget.md | 45 ++++++++ tasks/007-scoring-dtw.md | 42 ++++++++ tasks/008-game-ui.md | 40 +++++++ tasks/009-source-display.md | 40 +++++++ tasks/010-about-page.md | 34 ++++++ tasks/011-build-script.md | 58 +++++++++++ 15 files changed, 975 insertions(+) create mode 100644 DSH.md create mode 100644 README.txt create mode 100644 THIRD_PARTY.md create mode 100644 project.yaml create mode 100644 tasks/001-scaffold.md create mode 100644 tasks/002-rest-client.md create mode 100644 tasks/003-websocket-receiver.md create mode 100644 tasks/004-keypoint-deserialize.md create mode 100644 tasks/005-vrm-loader.md create mode 100644 tasks/006-skeleton-retarget.md create mode 100644 tasks/007-scoring-dtw.md create mode 100644 tasks/008-game-ui.md create mode 100644 tasks/009-source-display.md create mode 100644 tasks/010-about-page.md create mode 100644 tasks/011-build-script.md diff --git a/DSH.md b/DSH.md new file mode 100644 index 0000000..1fc7cad --- /dev/null +++ b/DSH.md @@ -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 计算) | +| 测试与实现同任务 | 测试单独任务,共享接口摘要 | diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..a60c1ab --- /dev/null +++ b/README.txt @@ -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 点拓扑。 diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md new file mode 100644 index 0000000..58fbe61 --- /dev/null +++ b/THIRD_PARTY.md @@ -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 服务端的来源五元组字段管理并在界面展示。 diff --git a/project.yaml b/project.yaml new file mode 100644 index 0000000..14a07e5 --- /dev/null +++ b/project.yaml @@ -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/ # 当前版本契约快照 diff --git a/tasks/001-scaffold.md b/tasks/001-scaffold.md new file mode 100644 index 0000000..7dbc75e --- /dev/null +++ b/tasks/001-scaffold.md @@ -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 帧无任何运行时错误。 diff --git a/tasks/002-rest-client.md b/tasks/002-rest-client.md new file mode 100644 index 0000000..a461e60 --- /dev/null +++ b/tasks/002-rest-client.md @@ -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/.` 缓存,避免重复下载 10 MB 级 VRM + +## 验收 + +`tests/test_api_integration.gd` 对**真实运行中**的服务端断言: +health / catalog version / catalog export / 关键点序列,服务端不可达时显式跳过。 + +## 结果 + +集成测试通过:目录解析、来源五元组齐备性、`defaultModelId` 解析均符合契约。 diff --git a/tasks/003-websocket-receiver.md b/tasks/003-websocket-receiver.md new file mode 100644 index 0000000..84c06f9 --- /dev/null +++ b/tasks/003-websocket-receiver.md @@ -0,0 +1,45 @@ +# T-WIN-003 接收 Android 关键点帧 + +- **状态**:done +- **依赖**:T-WIN-001 + +## 目标 + +在本端监听 `ws://: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。 diff --git a/tasks/004-keypoint-deserialize.md b/tasks/004-keypoint-deserialize.md new file mode 100644 index 0000000..d2fb237 --- /dev/null +++ b/tasks/004-keypoint-deserialize.md @@ -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 调试帧。 diff --git a/tasks/005-vrm-loader.md b/tasks/005-vrm-loader.md new file mode 100644 index 0000000..6cdabb0 --- /dev/null +++ b/tasks/005-vrm-loader.md @@ -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 不误判 diff --git a/tasks/006-skeleton-retarget.md b/tasks/006-skeleton-retarget.md new file mode 100644 index 0000000..171198c --- /dev/null +++ b/tasks/006-skeleton-retarget.md @@ -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 传播验证。 diff --git a/tasks/007-scoring-dtw.md b/tasks/007-scoring-dtw.md new file mode 100644 index 0000000..3d574e0 --- /dev/null +++ b/tasks/007-scoring-dtw.md @@ -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)、带宽约束、时间伸缩吸收、流式与离线一致、 +低置信度维度不参与距离、角度计算正确性。 diff --git a/tasks/008-game-ui.md b/tasks/008-game-ui.md new file mode 100644 index 0000000..110598f --- /dev/null +++ b/tasks/008-game-ui.md @@ -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` 启动无运行时错误; +主菜单、游戏页、结算页的构造路径均被该启动流程覆盖。 diff --git a/tasks/009-source-display.md b/tasks/009-source-display.md new file mode 100644 index 0000000..e958682 --- /dev/null +++ b/tasks/009-source-display.md @@ -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()`),保证展示组件拿到的数据完整。 diff --git a/tasks/010-about-page.md b/tasks/010-about-page.md new file mode 100644 index 0000000..f69a186 --- /dev/null +++ b/tasks/010-about-page.md @@ -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 含全部条目、纯文本导出可解析。 diff --git a/tasks/011-build-script.md b/tasks/011-build-script.md new file mode 100644 index 0000000..ee84752 --- /dev/null +++ b/tasks/011-build-script.md @@ -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 只在**项目根目录**查找