docs: 消除 Gitea 模棱两可 Unicode 告警(全角标点紧邻 ASCII 时改为半角)

依据 Gitea v1.27.3 modules/charset/escape_stream.go 的判定规则
(analyzeDetectResults:仅当左右相邻字符都不是非 ASCII 时才标记,
且 _common 表恒定生效),把说明文档中会被标记的全角标点
(,:;?!() 等)改为半角,并保证成对括号宽度一致。
中文语境中的全角标点(左右均为中文)保持原样,不动代码与契约语义。
This commit is contained in:
opendance
2026-09-15 21:45:51 +08:00
parent 8c1d24865c
commit 4913760a8b
20 changed files with 153 additions and 153 deletions
+6 -6
View File
@@ -1,4 +1,4 @@
# DSH.md -- opendance-windows(OpenDance Studio 项目二:Windows 客户端)
# DSH.md -- opendance-windows(OpenDance Studio 项目二:Windows 客户端)
## 0. 本文件的作用
@@ -11,7 +11,7 @@ Windows 客户端是核心体验载体:接收 Android 辅助 App 推送的 33
驱动 VRM 角色,并与 Web 服务端下发的参考动作做实时 DTW 评分。
```
Android 辅助 App ──239 字节关键点帧──▶ 本端(WebSocket 服务端:8765)
Android 辅助 App ──239 字节关键点帧──▶ 本端(WebSocket 服务端:8765)
│
Web 服务端 ──GET /api/catalog/export──────┤
──GET /api/motions/:id/keypoints┘
@@ -25,7 +25,7 @@ Web 服务端 ──GET /api/catalog/export──────┤
| 组件 | 选型 | 说明 |
|--|--|--|
| 引擎 | Godot 4.7.2(MIT) | 可在 Linux 上导出 Windows exe |
| 引擎 | Godot 4.7.2(MIT) | 可在 Linux 上导出 Windows exe |
| VRM 加载 | **Godot 内置 GLTFDocument** | 见 6 的选型变更说明 |
| 多格式导入 | Godot 内置 glTF 导入器 | FBX 由管理端转 BVH 后下发 |
| 通信 | WebSocketPeer + HTTPRequest | 无第三方依赖 |
@@ -66,7 +66,7 @@ opendance-windows/
├── export_presets.cfg # ★ 必须位于项目根目录(Godot 硬性要求)
├── project.yaml / DSH.md
├── tasks/ # 001~011 单任务说明书
├── contracts/ # 只读契约快照(来源 opendance-web v0.1.0)
├── contracts/ # 只读契约快照(来源 opendance-web v0.1.0)
├── scenes/main.tscn # 仅一个挂载 main.gd 的根节点
├── src/
│ ├── autoload/ # AppConfig / CatalogStore / PoseReceiver / ScoreService
@@ -76,7 +76,7 @@ opendance-windows/
│ ├── score/ # 特征提取、DTW、评分规则
│ └── ui/ # 主界面 / 游戏界面 / 结算 / 关于 / 来源展示
├── tests/ # 无头测试框架 + 用例
└── build/ # 导出脚本(sh / ps1)
└── build/ # 导出脚本(sh / ps1)
```
## 6. 界面构建方式与选型变更(重要)
@@ -121,7 +121,7 @@ $GODOT --headless --path. --script res://tests/run_tests.gd
# 3. 应用可启动(autoload + UI 构建无报错)
$GODOT --headless --path. --quit-after 180
# 4. Windows 导出(需先安装 export templates)
# 4. Windows 导出(需先安装 export templates)
bash build/export-windows.sh release
```
+19 -19
View File
@@ -1,6 +1,6 @@
# opendance-windows
OpenDance Studio **阶段二**:Windows 客户端(Godot 4)。
OpenDance Studio **阶段二**: Windows 客户端(Godot 4)。
接收 Android 辅助 App 通过 WebSocket 推送的 33 点骨骼关键点,驱动 VRM 角色,
并与 Web 管理服务端下发的参考动作做实时 DTW 评分。
@@ -9,14 +9,14 @@ OpenDance Studio **阶段二**:Windows 客户端(Godot 4)。
## 状态
**阶段二已完成**:T-WIN-001 -> T-WIN-011 全部落地。
**阶段二已完成**: T-WIN-001 -> T-WIN-011 全部落地。
| 验收项 | 结果 |
|--|--|
| 脚本编译 | OK 全部通过(无 SCRIPT ERROR) |
| 无头测试 | OK **580 项全部通过**(8 个测试套件) |
| 脚本编译 | OK 全部通过(无 SCRIPT ERROR) |
| 无头测试 | OK **580 项全部通过**(8 个测试套件) |
| 应用启动 | OK 180 帧无运行时错误 |
| Windows 导出 | OK 生成 `OpenDance.exe`(104 MB,PE 头校验通过)+ `OpenDance.pck` |
| Windows 导出 | OK 生成 `OpenDance.exe`(104 MB, PE 头校验通过)+ `OpenDance.pck` |
| 真实 VRM 验证 | OK 154 骨骼 VRoid 模型,humanoid 覆盖率 **100%** |
| 骨骼重定向 | OK 关键点方向与骨骼实际指向点积 **1.0000** |
| 跨实现交叉校验 | OK 关节角与 Web 服务端 TypeScript 实现**逐帧一致** |
@@ -45,10 +45,10 @@ bash build/package-release.sh 0.1.0
| 项 | 要求 |
|--|--|
| 操作系统 | 运行客户端:Windows 10/11(x86_64)。开发与导出:Windows 或 Linux 均可 |
| Godot | **4.7.x**(`project.godot` 声明特性 `4.7`,本仓库按 4.7.2 验证)。使用**标准版**(GDScript),**不需要 .NET / C# 版** |
| 操作系统 | 运行客户端: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`) |
| 导出模板 | **仅打包导出时需要**:必须安装与编辑器**完全同版本**的 export templates(`.tpz`) |
| 磁盘与网络 | Godot 编辑器 + 导出模板约数百 MB;导出产物 `OpenDance.exe` 约 104 MB、`OpenDance.pck` 约 107 KB。本端需能访问 Web 服务端 API(默认 TCP 3000),并与 Android 设备处于同一局域网、放行本端 **TCP 8765 入站** |
### 2. 获取代码
@@ -75,7 +75,7 @@ cd opendance-windows
- 位置:主界面顶部「**服务端地址**」输入框,填好后点「**保存并测试**」。
- 配置字段名(`opendance.cfg`):`api_base_url`。
- **端口区分**:客户端只访问 Web 服务端的 **API 端口(默认 3000)**;管理端页面在**管理端口(默认 3001)**,客户端不使用该端口。
- **端口区分**:客户端只访问 Web 服务端的 **API 端口(默认 3000)**;管理端页面在**管理端口(默认 3001)**,客户端不使用该端口。
- 示例占位符:
```
@@ -89,8 +89,8 @@ http://192.0.2.10:3000
| 对端 | 方向 | 本端配置 | 说明 |
|--|--|--|--|
| 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`) | 需先在服务端**发布**曲目 / 动作 / 模型,动作还须**完成关键点预处理**,否则目录为空或动作不可选 |
| 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`) | 需先在服务端**发布**曲目 / 动作 / 模型,动作还须**完成关键点预处理**,否则目录为空或动作不可选 |
- **Android 侧无本端可配置项**:接收地址由 Android App 自己填写;本端只监听固定端口 8765(可在 `opendance.cfg` 改 `listen_port` 后点「重新连接接收服务」生效)。
- 已知限制:Godot 的 `WebSocketPeer` 不暴露握手请求路径,本端**接受任意 path**,只校验子协议 `opendance.pose.v1`(见「已知限制」1)。
@@ -111,8 +111,8 @@ bash build/package-release.sh 0.1.0
| 产物 | 位置 | 说明 |
|--|--|--|
| `OpenDance.exe` | `build/windows/` | 主程序,约 104 MB;脚本校验前 2 字节为 `MZ`(合法 PE) |
| `OpenDance.pck` | `build/windows/` | 资源包,约 107 KB;**必须与 exe 同目录** |
| `OpenDance.exe` | `build/windows/` | 主程序,约 104 MB;脚本校验前 2 字节为 `MZ`(合法 PE) |
| `OpenDance.pck` | `build/windows/` | 资源包,约 107 KB; **必须与 exe 同目录** |
| `OpenDance.console.exe` | `build/windows/` | 控制台包装器,可能生成,非必需 |
| `opendance-windows-v0.1.0.zip` | `dist-release/` | 发布包:exe + pck + `THIRD_PARTY.md` + `README.txt` + `contracts/` |
@@ -133,9 +133,9 @@ bash build/package-release.sh 0.1.0
| 现象 | 原因 | 处理 |
|--|--|--|
| 游戏界面提示「模型加载失败:缺少骨骼」或「骨骼绑定失败,模型可能不受支持」 | 模型没有 `Skeleton3D`,或骨骼命名不在 humanoid 映射表内;驱动至少需要 hips / spine / neck / head + 四肢共 16 根核心骨骼 | 换用标准 VRM(VRoid Studio 导出);看「人形覆盖率」,低于 60% 会提示「骨骼覆盖偏低,动作可能不完整」;必要时在 `src/model/humanoid_map.gd` 增加命名映射 |
| 游戏界面提示「模型加载失败:缺少骨骼」或「骨骼绑定失败,模型可能不受支持」 | 模型没有 `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\` |
| 导出失败:「未找到导出模板」 | 未安装 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` |
@@ -143,10 +143,10 @@ bash build/package-release.sh 0.1.0
| 组件 | 选型 |
|--|--|
| 引擎 | Godot 4.7.x(MIT) |
| 引擎 | Godot 4.7.x(MIT) |
| VRM 加载 | Godot 内置 GLTFDocument |
| 通信 | WebSocketPeer(服务端)+ HTTPRequest |
| 评分 | DTW + 关节角度差异(纯 GDScript) |
| 评分 | DTW + 关节角度差异(纯 GDScript) |
## 架构
@@ -173,7 +173,7 @@ src/
## 依赖的契约(opendance-web v0.1.0 冻结)
- [`contracts/websocket-keypoints.md`](contracts/websocket-keypoints.md) - 239 字节关键点帧与文本控制消息(本端为**接收方**)
- [`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 优先规则
@@ -185,7 +185,7 @@ src/
## 已知限制
1. Godot 的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**,
1. Godot 的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**,
实现接受任意 path,仅校验子协议 `opendance.pose.v1`。
2. 文档原选型 `godot-vrm` GDExtension 不发布预编译产物,改用 Godot 内置 glTF 导入器
+ 自实现骨骼名映射;理由见 `DSH.md` 6。
+17 -17
View File
@@ -43,7 +43,7 @@ OpenDance.exe 的用户看第一节即可。
1. 前置条件
- 操作系统:Windows 10/11(x86_64)。开发与导出可在 Windows 或 Linux。
- 操作系统:Windows 10/11(x86_64)。开发与导出可在 Windows 或 Linux。
- Godot 4.7.x 标准版(GDScript)。不需要 .NET / C# 版。
- 渲染后端为 GL Compatibility,运行需 OpenGL 3.3 级别的显卡与驱动。
- 导出模板:仅打包导出时需要,必须与编辑器完全同版本。
@@ -68,7 +68,7 @@ OpenDance.exe 的用户看第一节即可。
4. 配置服务端地址
主界面顶部「服务端地址」填好后点「保存并测试」,配置字段名为 api_base_url:
主界面顶部「服务端地址」填好后点「保存并测试」,配置字段名为 api_base_url:
http://<服务器地址>:3000
http://192.0.2.10:3000
@@ -81,7 +81,7 @@ OpenDance.exe 的用户看第一节即可。
- Android 辅助 App -> 本端 WebSocket:本端是服务端,默认监听
0.0.0.0:8765,路径 /pose,子协议 opendance.pose.v1。Android 端填写
本端主界面显示的「接收地址」,形如 ws://<本机局域网地址>:8765/pose。
本端可在 opendance.cfg 改 listen_host / listen_port / ws_subprotocol,
本端可在 opendance.cfg 改 listen_host / listen_port / ws_subprotocol,
改后点「重新连接接收服务」生效。
- Web 服务端:需先在服务端发布曲目 / 动作 / 模型,动作还须完成关键点
预处理,客户端才有可选内容。
@@ -94,7 +94,7 @@ OpenDance.exe 的用户看第一节即可。
# Windows(兜底)
powershell -ExecutionPolicy Bypass -File build\export-windows.ps1
产物在 build/windows/:OpenDance.exe(约 104 MB,PE 头 MZ 校验通过)
产物在 build/windows/: OpenDance.exe(约 104 MB, PE 头 MZ 校验通过)
+ OpenDance.pck(约 107 KB,必须与 exe 同目录);可能附带
OpenDance.console.exe。
@@ -121,7 +121,7 @@ OpenDance.exe 的用户看第一节即可。
8. 部署相关常见问题
Q: 模型加载失败:缺少骨骼
A: 模型无 Skeleton3D,或骨骼命名不在映射表内;换用标准 VRM(VRoid
A: 模型无 Skeleton3D,或骨骼命名不在映射表内;换用标准 VRM(VRoid
Studio 导出)。人形覆盖率低于 60% 会提示「骨骼覆盖偏低」。
Q: 连不上服务端 / 显示「服务端不可达」
@@ -130,7 +130,7 @@ OpenDance.exe 的用户看第一节即可。
Q: 导出失败:未找到导出模板
A: 安装与编辑器同版本的 export templates。Linux 放
~/.local/share/godot/export_templates/<版本>.stable/;
~/.local/share/godot/export_templates/<版本>.stable/;
Windows 放 %APPDATA%\Godot\export_templates\<版本>.stable\。
Q: 接收服务启动失败(端口 8765 可能被占用)
@@ -169,12 +169,12 @@ OpenDance.exe 的用户看第一节即可。
可配置项:
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)
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)
score_tolerance 评分容差:平均每步代价达到该值即 0 分(默认 0.25)
mirror_preview 镜像预览
--
@@ -196,9 +196,9 @@ OpenDance.exe 的用户看第一节即可。
- 每帧特征 = 33 点 x (x,y,z) + 9 个关节角余弦 = 108 维
(与 Web 服务端预处理产物、Android 端推流结构完全一致)
- 置信度低于 0.5 的关键点不参与代价计算,避免遮挡导致误判
- 采用带约束 DTW(|i-j| <= band)对齐,能吸收一定的节奏快慢差异
- 采用带约束 DTW(|i-j| <= band)对齐,能吸收一定的节奏快慢差异
- 得分 = clamp(1 - 平均代价 / 容差, 0, 1) x 100,并做指数平滑
- 等级:S >= 95,A >= 85,B >= 70,C >= 55,D 其余
- 等级:S >= 95, A >= 85, B >= 70, C >= 55, D 其余
--
七、从源码构建
@@ -212,7 +212,7 @@ OpenDance.exe 的用户看第一节即可。
# Windows 上兜底导出
powershell -ExecutionPolicy Bypass -File build\export-windows.ps1
产物位于 build/windows/:OpenDance.exe + OpenDance.pck
产物位于 build/windows/: OpenDance.exe + OpenDance.pck
(脚本会校验产物存在且为合法 PE 文件)
运行测试:
@@ -230,8 +230,8 @@ OpenDance.exe 的用户看第一节即可。
export_presets.cfg 导出预设(必须在项目根目录)
project.yaml 任务清单与状态
DSH.md 本项目 DSH 执行约束
tasks/ 单任务说明书(T-WIN-001 ~ 011)
contracts/ 只读契约快照(来源 opendance-web v0.1.0)
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 客户端
@@ -255,7 +255,7 @@ A: 动作必须先在管理端完成「预处理」(BVH -> 关键点序列)
Q: 模型加载失败 / 动作不完整
A: 游戏界面会显示「人形覆盖率」。低于 60% 说明模型的骨骼命名不被识别,
请换用标准 VRM(VRoid Studio 导出)模型。
请换用标准 VRM(VRoid Studio 导出)模型。
Q: 连上了手机但分数一直是 0
A: 确认手机端已开始推流;主界面的「查看接收状态」会显示累计帧数与丢帧数。
+2 -2
View File
@@ -11,7 +11,7 @@
- 版权:Copyright 2023 Google LLC
- 许可证:Apache License 2.0
- 源链接:https://github.com/google-ai-edge/mediapipe
- 用途:33 点骨骼拓扑定义(契约来源,**本端不包含其代码**)
- 用途:33 点骨骼拓扑定义(契约来源,**本端不包含其代码**)
## 2. Three.js
@@ -109,7 +109,7 @@
## 许可合规说明
1. 本客户端自身代码以 MIT 许可发布。
2. 运行时依赖仅为 **Godot Engine**(MIT);其余条目为同项目其他组件或服务端依赖,
2. 运行时依赖仅为 **Godot Engine**(MIT);其余条目为同项目其他组件或服务端依赖,
按契约要求一并声明。
3. 本客户端**未使用 SMPL 模型及其衍生拓扑**,统一采用 MediaPipe 33 点拓扑,
以规避 SMPL 的非商用许可限制。
+3 -3
View File
@@ -5,7 +5,7 @@ Web 服务端是唯一权威定义,本文件是另两端消费的**稳定视
## 1. 通用约定
- 所有资源返回 `id: number`(BIGINT,超出 2^53 前不会发生)。
- 所有资源返回 `id: number`(BIGINT,超出 2^53 前不会发生)。
- 所有时间为 ISO 8601 UTC 字符串,例:`2026-09-14T10:00:00.000Z`。
- 列表接口统一分页响应:
@@ -117,7 +117,7 @@ rejected 已驳回
```
0. 环境变量 DEFAULT_MODEL_ID 指定,且该模型存在且已发布
1. 源码内写死的默认模型种子(web-server/src/config/default-model.ts 的
DEFAULT_MODEL_SEED):按 **sourceSha256(上传压缩包的 sha256)** 匹配已发布模型,
DEFAULT_MODEL_SEED):按 **sourceSha256(上传压缩包的 sha256)** 匹配已发布模型,
换库/重建库后仍然有效;没有压缩包时退回按产物 sha256,最后按名称兜底。
⚠ 不能用产物 sha256 当主键:实测 Assimp 的 GLB 导出不是字节级确定的,
同一份 zip 每次转换得到的文件 sha256 都不同(BIN chunk 有十几字节浮动)
@@ -145,7 +145,7 @@ rejected 已驳回
| `comment` | string\|null | 审核意见 |
| `createdAt` | string | |
## 6. `CatalogBundle`(客户端一次性拉取,`GET /api/catalog/export`)
## 6. `CatalogBundle`(客户端一次性拉取,`GET /api/catalog/export`)
```json
{
+11 -11
View File
@@ -3,11 +3,11 @@
## 环境
- 类型:MySQL 兼容实例(外部提供,含 MariaDB 10.5+),TypeORM
使用 `mysql` driver(`mysql2`)连接,SQL 语法限制在 MySQL 5.7 / MariaDB 10.5+
使用 `mysql` driver(`mysql2`)连接,SQL 语法限制在 MySQL 5.7 / MariaDB 10.5+
共同子集内,不使用 MySQL 8 专有语法(窗口函数 CTE 递归除外)。
- 数据库名:由部署方提供,通过 `DB_NAME` 注入(本文档示例统一用 `opendance`)
- 数据库名:由部署方提供,通过 `DB_NAME` 注入(本文档示例统一用 `opendance`)
- 字符集:`utf8mb4` / `utf8mb4_general_ci`(沿用现有实例默认排序规则)
- 迁移工具:TypeORM migration,**不使用 `DB_SYNCHRONIZE`**
- 迁移工具:TypeORM migration, **不使用 `DB_SYNCHRONIZE`**
## 连接信息(由部署方提供)
@@ -25,10 +25,10 @@ DB_LOGGING=false
## 约定
- 所有表名使用 `snake_case`,复数形式(`songs` / `motions` / `models`)
- 所有表名使用 `snake_case`,复数形式(`songs` / `motions` / `models`)
- 主键统一为 `BIGINT AUTO_INCREMENT`,字段名 `id`
- 时间字段统一 `DATETIME`,`created_at` / `updated_at`,`updated_at` 由应用层维护
- 软删除统一使用 `deleted_at DATETIME NULL`(`@DeleteDateColumn`)
- 时间字段统一 `DATETIME`, `created_at` / `updated_at`, `updated_at` 由应用层维护
- 软删除统一使用 `deleted_at DATETIME NULL`(`@DeleteDateColumn`)
- 布尔字段使用 `TINYINT(1)`
- 来源(出处)字段为 5 元组,任何资产入库必须齐备:
`source_type` / `source_name` / `source_url` / `license` / `author`
@@ -36,24 +36,24 @@ DB_LOGGING=false
- 每个 migration 必须可逆(提供 `down` 方法)
- 迁移文件名格式:`<timestamp>-<kebab-name>.ts`
## 增量迁移(v0.2.0 / v0.3.0)
## 增量迁移(v0.2.0 / v0.3.0)
| 迁移 | 变更 |
|--|--|
| `AddModelConversionFields` | `models` 增加 `source_asset_id BIGINT NULL`(上传的压缩包,用于溯源)与 `conversion_note VARCHAR(500) NULL`(转换说明,见 model-formats.md 1.1) |
| `AddModelConversionFields` | `models` 增加 `source_asset_id BIGINT NULL`(上传的压缩包,用于溯源)与 `conversion_note VARCHAR(500) NULL`(转换说明,见 model-formats.md 1.1) |
| `AddModelStats` | `models` 增加 `stats TEXT NULL`(模型分析结果:面数、骨骼树、humanoid 覆盖、可驱动性),供审核界面展示 |
## 表清单(v0.1.0)
## 表清单(v0.1.0)
| 表 | 用途 | 任务 |
|--|--|--|
| `songs` | 曲目元数据 | T-WEB-003 |
| `assets` | 上传文件元数据(去重、sha256、物理路径) | T-WEB-004 |
| `motions` | 动作数据(BVH/FBX...)+ 来源 + 预处理产物 | T-WEB-004 / 006 |
| `motions` | 动作数据(BVH/FBX...)+ 来源 + 预处理产物 | T-WEB-004 / 006 |
| `models` | 角色模型(VRM 优先)+ 来源 + 压缩包转换溯源 | T-WEB-005 |
| `review_records` | 上传审核流水 | T-WEB-007 |
## 状态机(`status` 列,v0.1.0)
## 状态机(`status` 列,v0.1.0)
```
draft ──submit──▶ pending ──approve──▶ published
+29 -29
View File
@@ -1,21 +1,21 @@
# contracts/model-formats.md - 模型与动作格式契约 v0.4.0
## 1. 角色模型(models)
## 1. 角色模型(models)
**系统以 GLB / VRM 为主力格式**(两个客户端都原生加载),其余格式一律在服务端转换。
| 格式 | 扩展名 | 支持级别 | 服务端行为 |
|--|--|--|--|
| glTF Binary | `.glb` | **主力(P0)** | **直接导入**,不重编码 |
| VRM | `.vrm` | **主力(P0)** | **直接导入**;= GLB + VRM 扩展(humanoid / 表情 / springBone / meta) |
| glTF | `.gltf` (+ `.bin`, 贴图) | P1 | 直接导入(分离文件在 zip 内可用;单文件上传会缺.bin) |
| glTF Binary | `.glb` | **主力(P0)** | **直接导入**,不重编码 |
| VRM | `.vrm` | **主力(P0)** | **直接导入**; = GLB + VRM 扩展(humanoid / 表情 / springBone / meta) |
| glTF | `.gltf` (+ `.bin`, 贴图) | P1 | 直接导入(分离文件在 zip 内可用;单文件上传会缺.bin) |
| MMD 压缩包 | `.zip` | P1 | **自动搜索包内可用模型**:优先直接导入 `.vrm`/`.glb`/`.gltf`,否则转换 `.pmx`/`.fbx`,见 1.1 |
| MMD 模型 | `.pmx` | P2(输入) | 服务端转换(优先 GLB、其次 VRM);单文件上传无贴图 |
| FBX | `.fbx` | P2(输入) | 服务端转换(优先 GLB、其次 VRM) |
| FBX | `.fbx` | P2(输入) | 服务端转换(优先 GLB、其次 VRM) |
### 1.1 上传与转换策略
**`.glb` / `.vrm` / `.gltf` 直接导入**(不重编码,零损失);**`.pmx` / `.fbx` 由服务端转换**:
**`.glb` / `.vrm` / `.gltf` 直接导入**(不重编码,零损失);**`.pmx` / `.fbx` 由服务端转换**:
**优先 GLB,其次 VRM**。
#### 单文件上传
@@ -28,7 +28,7 @@
#### 压缩包上传(自动搜索)
不再"只找 `.pmx`",而是**按格式优先级自动搜索包内可用模型**:
不再"只找 `.pmx`",而是**按格式优先级自动搜索包内可用模型**:
```
.vrm ->.glb ->.gltf (直接导入,优先)
@@ -38,7 +38,7 @@
同一优先级内:`MODEL_CONVERT_PMX` 指定的文件名优先,其次目录最浅,最后文件最大。
压缩包原件以 `model-archive` 资产留存并写入 `models.source_asset_id`。
#### 转换与校验(PMX / FBX)
#### 转换与校验(PMX / FBX)
| 步骤 | 规则 |
|--|--|
@@ -46,7 +46,7 @@
| 2. glTF 打包 | 上一步失败时 `assimp export ->.gltf`,由 `gltf-packer.ts` 把 `.gltf` + `.bin` + 贴图自行合成 GLB |
| 3. VRM 封装 | `MODEL_CONVERT_FORMAT=vrm` 时,在 GLB 之上补 `VRMC_vrm` 扩展(humanoid 骨骼来自骨骼名映射,见 `model-inspector.ts` 的候选表,与 Godot 客户端一致);**humanoid 不全则回退交付 GLB**,缺失骨骼写进 `conversion_note` |
| 回读校验 | 每一步产物都要 `assimp info` 回读。Assimp 会"写出损坏文件"(实测 FBX 导出退出码 11、产物回读报 `FBX-DOM no Objects dictionary found`),只看退出码会把坏文件入库 |
| 说明 | 每次尝试都写进 `models.conversion_note`(例如 `Hatsune Miku.pmx -> GLB,已内嵌 2 张贴图`) |
| 说明 | 每次尝试都写进 `models.conversion_note`(例如 `Hatsune Miku.pmx -> GLB,已内嵌 2 张贴图`) |
| 失败 | 包内没有可用模型、Assimp 不可用、两条路径都失败 -> **HTTP 422**,不产生模型记录 |
#### 入库后(自动)
@@ -63,14 +63,14 @@
| 项 | 规则 |
|--|--|
| 确定性 | **转换产物不是字节级确定的**:同一份 zip 连续转换 3 次得到 3 个不同 sha256(大小相同,BIN chunk 有十几字节浮动)。因此默认模型种子以**上传压缩包的 sha256** 为匹配键,产物 sha256 只作记录 |
| 安全 | 解压前逐条校验路径,拒绝绝对路径与 `..`(zip-slip);拒绝加密条目;只支持 STORE / DEFLATE |
| 安全 | 解压前逐条校验路径,拒绝绝对路径与 `..`(zip-slip);拒绝加密条目;只支持 STORE / DEFLATE |
**VRM 优先**的选取顺序见 `data-models.md` 4.1。
VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)或
`extensions.VRMC_vrm`(1.0),写入 `models.vrm_version`。
VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)或
`extensions.VRMC_vrm`(1.0),写入 `models.vrm_version`。
## 2. 动作数据(motions)
## 2. 动作数据(motions)
| 格式 | 扩展名 | 预处理支持 | 说明 |
|--|--|--|--|
@@ -82,7 +82,7 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)
### 2.1 预处理产物(冻结)
- 格式:NDJSON(`application/x-ndjson`),每行一帧,UTF-8 无 BOM
- 格式:NDJSON(`application/x-ndjson`),每行一帧,UTF-8 无 BOM
- 一行结构:
```json
@@ -93,9 +93,9 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)
- `angles` 为 `skeleton-topology.md` 5 定义的 9 个关节角余弦,顺序固定:
`[shoulder_L, shoulder_R, elbow_L, elbow_R, hip_L, hip_R, knee_L, knee_R, spine]`
- 帧率:保留源帧率;`fps` 写入 `motions.fps`,缺省 30
- 坐标系换算:所有 3D 动作数据统一转到 **Y 轴向上、右手系、骨盆为原点**,
- 坐标系换算:所有 3D 动作数据统一转到 **Y 轴向上、右手系、骨盆为原点**,
再投影到 MediaPipe 归一化图像空间:
`x_img = 0.5 + x_world * scale`,`y_img = 0.5 - y_world * scale`,`z_img = -z_world * scale`,
`x_img = 0.5 + x_world * scale`, `y_img = 0.5 - y_world * scale`, `z_img = -z_world * scale`,
`scale = 0.35 / max(|x|,|y|,|z|)`(按整段动作的最大范围自适应,保证不出画)
- 缺失关节(BVH 中不存在对应骨骼):`confidence = 0.0`,坐标沿用最近可用父节点
@@ -122,18 +122,18 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)
未映射到的点按 `skeleton-topology.md` 5 的层级由父节点推算,
并置 `confidence` 降低(`0.5`),表示估算而非实测。
### 2.3 VMD(MikuMikuDance)解析约定
### 2.3 VMD(MikuMikuDance)解析约定
VMD 是二进制格式:文件头 `Vocaloid Motion Data 0002`(30 字节)+ 模型名(20 字节,
Shift-JIS)+ 骨骼关键帧数组(每条 **111 字节**:15 字节骨骼名、uint32 帧号、
VMD 是二进制格式:文件头 `Vocaloid Motion Data 0002`(30 字节)+ 模型名(20 字节,
Shift-JIS)+ 骨骼关键帧数组(每条 **111 字节**: 15 字节骨骼名、uint32 帧号、
float32x3 位移、float32x4 旋转四元数、64 字节贝塞尔插值参数)+ morph / 相机 /
灯光 / 自阴影 / IK / 可见性段落。**VMD 不含骨骼长度**,只有旋转(少数骨骼带位移),
因此服务端必须自带一套标准 MMD 骨架才能做 FK。
处理链路:`vmd-parser.ts`(二进制 + Shift-JIS + 贝塞尔插值)-> `mmd-skeleton.ts`
(标准骨架)-> `mmd-pose.ts`(FK + 腿部 IK)-> `retarget.ts` 的
(标准骨架)-> `mmd-pose.ts`(FK + 腿部 IK)-> `retarget.ts` 的
`worldFramesToKeypointFrames()`(与 BVH 完全共用骨盆归零 / 自适应缩放 / 图像空间投影 /
9 个关节角)。固定 **30 fps**(MMD 帧率恒定,上传时填写的 `fps` 被忽略)。
9 个关节角)。固定 **30 fps**(MMD 帧率恒定,上传时填写的 `fps` 被忽略)。
| MediaPipe 点 | MMD 标准骨骼 | 说明 |
|--|--|--|
@@ -149,11 +149,11 @@ float32x3 位移、float32x4 旋转四元数、64 字节贝塞尔插值参数)
骨骼名按 `mmd-skeleton.ts` 的别名表归一(含全角 `IK` / 半角 `IK` 两种写法);
未纳入标准骨架的骨骼(裙摆、物理、表情等)被忽略但会计数并写入预处理日志。
**腿部驱动判定**:`足IK` 轨道存在非零位移 -> 走 IK(用两骨骼解析 IK 把踝拉到 IK
**腿部驱动判定**: `足IK` 轨道存在非零位移 -> 走 IK(用两骨骼解析 IK 把踝拉到 IK
目标点,膝盖朝角色正面弯,不可达时收敛为伸直);否则走 FK(部分动作数据把腿烘焙成
FK 旋转)。两份示例文件分别命中两种路径。
**坐标系**:MMD 原生是左手系(正面为 -Z),本项目通路是右手系(正面为 +Z、+X 为角色
**坐标系**: MMD 原生是左手系(正面为 -Z),本项目通路是右手系(正面为 +Z、+X 为角色
左侧)。VMD 的四元数与位移统一做 Z 镜像(`(x,y,z,w) -> (-x,-y,z,w)`、位移 z 取反)后
送入标准骨架。该选择由真实数据对照实验确定:腿部烘焙为 FK 的样本中,`zMirrored` 下
膝盖朝前占比 94.1%,不转换时仅 5.9%(见 `test/mmd-vmd.spec.ts`)。
@@ -167,15 +167,15 @@ FK 旋转)。两份示例文件分别命中两种路径。
| 项 | 值 |
|--|--|
| 单文件上限 | 512 MiB(`UPLOAD_MAX_BYTES` 可覆盖) |
| 单文件上限 | 512 MiB(`UPLOAD_MAX_BYTES` 可覆盖) |
| 允许扩展名 | 上表全部;其他一律 422 |
| 存储路径 | `${STORAGE_DIR}/{kind}/{yyyy}/{mm}/{sha256前2位}/{sha256}.{ext}` |
| 去重 | 相同 sha256 复用同一 `assets` 记录,不重复落盘 |
| 文件名 | 服务端不使用原始文件名做路径,只存 `assets.original_name` |
## 4. 开源依赖与许可证(引用 `open-source-credits.md`)
## 4. 开源依赖与许可证(引用 `open-source-credits.md`)
- `bvh`(MIT)- BVH 解析参考实现
- `Assimp`(BSD-3-Clause)- FBX/glTF 转换,**可选外部依赖**
- `@pixiv/three-vrm`(MIT)- 管理端 VRM 预览
- `three`(MIT)- 管理端 3D 预览
- `bvh`(MIT)- BVH 解析参考实现
- `Assimp`(BSD-3-Clause)- FBX/glTF 转换,**可选外部依赖**
- `@pixiv/three-vrm`(MIT)- 管理端 VRM 预览
- `three`(MIT)- 管理端 3D 预览
+7 -7
View File
@@ -47,7 +47,7 @@
| 33 | neck | `midpoint(left_shoulder(11), right_shoulder(12))` |
| 34 | pelvis | `midpoint(left_hip(23), right_hip(24))` |
## 3. 父节点表(`parent[i]`,-1 表示世界根)
## 3. 父节点表(`parent[i]`, -1 表示世界根)
对称树,便于骨骼重定向与关节角计算:
@@ -91,16 +91,16 @@
## 4. 坐标系
- **归一化图像坐标**:`x, y ∈ [0, 1]`,原点左上角,`y` 向下;`x` 相对图像宽,`y` 相对图像高。
- **深度**:`z` 与 `x` 同尺度,原点位于髋中心,**负值朝向相机**。
- **镜像**:Android 前置摄像头默认输出镜像画面;是否镜像由帧头 `flags.bit0` 声明,
接收端据此决定是否翻转 `x`(`x' = 1 - x`)。
- **置信度**:`visibility ∈ [0, 1]`,落盘/传输为 `uint8`(`round(v * 255)`)。
- **归一化图像坐标**: `x, y ∈ [0, 1]`,原点左上角,`y` 向下;`x` 相对图像宽,`y` 相对图像高。
- **深度**: `z` 与 `x` 同尺度,原点位于髋中心,**负值朝向相机**。
- **镜像**: Android 前置摄像头默认输出镜像画面;是否镜像由帧头 `flags.bit0` 声明,
接收端据此决定是否翻转 `x`(`x' = 1 - x`)。
- **置信度**: `visibility ∈ [0, 1]`,落盘/传输为 `uint8`(`round(v * 255)`)。
归一化比例统一使用 `32767`(见 `websocket-keypoints.md`)。
## 5. 派生特征(DTW 输入,Windows 端实现,服务端预处理产物同构)
服务端 T-WEB-006 预处理产出的关键点序列与 Android 端输出的帧**结构完全一致**,
服务端 T-WEB-006 预处理产出的关键点序列与 Android 端输出的帧**结构完全一致**,
因此 Windows 端 DTW 引擎只有一条输入通路。
关节角特征(弧度,余弦值便于插值):
+6 -6
View File
@@ -10,17 +10,17 @@ ws://<windows-host>:8765/pose
```
- Windows 客户端监听,Android App 主动连接。
- 子协议:`opendance.pose.v1`(`Sec-WebSocket-Protocol`)
- 子协议:`opendance.pose.v1`(`Sec-WebSocket-Protocol`)
- 二进制帧传输关键点,文本帧传输控制消息(JSON)。
- 默认 30 FPS;发送端允许丢帧,接收端必须按 `frame_id` 检测丢包并插值补齐。
## 2. 二进制帧:239 字节(冻结)
**小端序(little-endian)**。
**小端序(little-endian)**。
```
偏移 长度 字段 类型 说明
0 2 magic u16 LE 固定 0x4F44(ASCII "OD")
0 2 magic u16 LE 固定 0x4F44(ASCII "OD")
2 1 version u8 固定 0x01
3 1 flags u8 bit0 = 镜像画面(mirrored)
bit1 = 本帧含低置信度点(<0.5)
@@ -32,7 +32,7 @@ ws://<windows-host>:8765/pose
合计 239
```
### 单个关键点(7 字节,`i` 为 0..32)
### 单个关键点(7 字节,`i` 为 0..32)
```
偏移 长度 字段 类型 换算
@@ -43,7 +43,7 @@ ws://<windows-host>:8765/pose
```
- 点索引与坐标语义见 `skeleton-topology.md`。
- 编码:`x = clamp(round(x_norm * 32767), -32768, 32767)`,`y`、`z` 同理。
- 编码:`x = clamp(round(x_norm * 32767), -32768, 32767)`, `y`、`z` 同理。
- 低置信度点(`conf < 128`)坐标无意义,接收端必须插值,不得直接使用。
- `i16` 为有符号数:`0x8000` 表示 `-32768`。
@@ -68,7 +68,7 @@ func decode(buf: PackedByteArray) -> Dictionary:
return {"flags": flags, "frame_id": frame_id, "points": pts}
```
## 3. 文本控制帧(JSON,UTF-8)
## 3. 文本控制帧(JSON, UTF-8)
### 3.1 `hello` - Android -> Windows,连接后立即发送
+2 -2
View File
@@ -1,6 +1,6 @@
# T-WIN-001 Godot 项目骨架
- **状态**:done
- **状态**: done
- **依赖**:无
## 目标
@@ -22,7 +22,7 @@ scenes/main.tscn
| 文件 | 说明 |
|--|--|
| `project.godot` | 工程配置;autoload = AppConfig / CatalogStore / PoseReceiver / ScoreService |
| `scenes/main.tscn` | 只含一个挂载 `src/ui/main.gd` 的 Control 根节点(界面用代码构建,见 DSH.md 6) |
| `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` | 目录缓存 + 模型下载缓存 + 关键点序列缓存 |
+2 -2
View File
@@ -1,7 +1,7 @@
# T-WIN-002 调用 Web 服务端 API
- **状态**:done
- **依赖**:T-WIN-001
- **状态**: done
- **依赖**: T-WIN-001
## 目标
+4 -4
View File
@@ -1,7 +1,7 @@
# T-WIN-003 接收 Android 关键点帧
- **状态**:done
- **依赖**:T-WIN-001
- **状态**: done
- **依赖**: T-WIN-001
## 目标
@@ -18,7 +18,7 @@ src/autoload/pose_receiver.gd
## 交付物
- `src/net/pose_server.gd`:TCPServer + `WebSocketPeer.accept_stream()` 实现服务端
- `src/net/pose_server.gd`: TCPServer + `WebSocketPeer.accept_stream()` 实现服务端
- `src/autoload/pose_receiver.gd`:绑定 AppConfig,暴露 `latest_frame` 与统计
## 能力
@@ -36,7 +36,7 @@ src/autoload/pose_receiver.gd
## 已知限制
Godot 的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**,
Godot 的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**,
实现接受任意 path,仅校验子协议。已记录于 DSH.md 与任务文档。
## 验收
+6 -6
View File
@@ -1,7 +1,7 @@
# T-WIN-004 239 字节帧反序列化
- **状态**:done
- **依赖**:T-WIN-001
- **状态**: done
- **依赖**: T-WIN-001
## 目标
@@ -24,20 +24,20 @@ src/net/keypoint_frame.gd
2 1 version u8 = 1
3 1 flags u8 bit0 镜像 bit1 低置信度 bit2 上半身
4 4 frame_id u32 LE
8 231 keypoints 33 x 7B:s16 x, s16 y, s16 z, u8 conf
8 231 keypoints 33 x 7B:s16 x, s16 y, s16 z, u8 conf
合计 239
```
## 交付物
`src/net/keypoint_frame.gd`:`PoseFrame` 内部类 + `decode` / `encode` / `validate` / `from_json_dict`。
`src/net/keypoint_frame.gd`: `PoseFrame` 内部类 + `decode` / `encode` / `validate` / `from_json_dict`。
## 关键实现
- 归一化系数统一 32767,与 opendance-web 的 TypeScript 参考实现逐位对齐
- 编解码往返误差 < 1e-4(定点精度 1/32767 ~ 3.05e-5)
- 编解码往返误差 < 1e-4(定点精度 1/32767 ~ 3.05e-5)
- `unmirrored_points()` 按 flags.bit0 翻转 x
- 低置信度阈值 0.5:`is_reliable()` 与 `compute_low_confidence()`
- 低置信度阈值 0.5: `is_reliable()` 与 `compute_low_confidence()`
- 极值(±1.0)不溢出 int16
## 验收
+9 -9
View File
@@ -1,7 +1,7 @@
# T-WIN-005 VRM 模型加载
- **状态**:done
- **依赖**:T-WIN-001
- **状态**: done
- **依赖**: T-WIN-001
## 目标
@@ -19,23 +19,23 @@ src/model/humanoid_map.gd
## 选型变更
文档原选型 `godot-vrm`(V-Sekai)GDExtension **不发布任何预编译产物**,
文档原选型 `godot-vrm`(V-Sekai)GDExtension **不发布任何预编译产物**,
需用 godot-cpp 本地编译原生库,会破坏「Linux 导出 Windows exe」的验收路径。
改用 **Godot 内置 GLTFDocument**:VRM 即 glTF 2.0 容器,实测可完整解析
改用 **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 用它们标记扭转辅助骨,
- 剥离前缀 `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`:
`tests/test_vrm_loader.gd`:
- 三类命名规范的识别(含辅助骨不遮蔽真骨的回归用例)
- 真实 VRM:154 骨骼 / 3 网格 / VRM 1.0,**humanoid 覆盖率 100%**
- 真实 VRM: 154 骨骼 / 3 网格 / VRM 1.0, **humanoid 覆盖率 100%**
- 缺失文件、普通 glTF 不误判
+6 -6
View File
@@ -1,7 +1,7 @@
# T-WIN-006 关键点 -> VRM 骨骼重定向
- **状态**:done
- **依赖**:T-WIN-005、T-WIN-004
- **状态**: done
- **依赖**: T-WIN-005、T-WIN-004
## 目标
@@ -18,20 +18,20 @@ src/anim/skeleton_retarget.gd
## 算法
1. 每根骨骼指定一条骨链(起点关键点,终点关键点)
2. 取骨骼静止朝向 `rest_dir`(由该骨与首个子骨的位置差得出,**不假设模型骨骼轴向**)
2. 取骨骼静止朝向 `rest_dir`(由该骨与首个子骨的位置差得出,**不假设模型骨骼轴向**)
3. 目标朝向由两个关键点在世界空间的方向得出
4. `delta` = rest_dir -> target_dir 的最小旋转四元数(保留扭转/roll)
4. `delta` = rest_dir -> target_dir 的最小旋转四元数(保留扭转/roll)
5. `desired = Basis(delta) x rest_global_basis`
6. `pose_local = parent_global_basis⁻¹ x desired`
**第 6 步的合成公式是实测标定的**:Godot 文档未写明 rest/pose 的合成顺序,
**第 6 步的合成公式是实测标定的**: Godot 文档未写明 rest/pose 的合成顺序,
在真实 VRoid 模型上暴力枚举 5 种候选公式,只有该式误差为 1.9e-8(其余 ~0.14)。
已有回归测试守护,不得凭记忆改写。
## 坐标转换
契约 4 的 MediaPipe 归一化坐标 -> Godot 世界方向:
`Vector3(x x aspect, -y, -z x aspect)`(x/z 以图像宽为基准故乘宽高比)。
`Vector3(x x aspect, -y, -z x aspect)`(x/z 以图像宽为基准故乘宽高比)。
## 验收
+4 -4
View File
@@ -1,7 +1,7 @@
# T-WIN-007 DTW 评分引擎
- **状态**:done
- **依赖**:T-WIN-004、T-WIN-002
- **状态**: done
- **依赖**: T-WIN-004、T-WIN-002
## 目标
@@ -30,9 +30,9 @@ src/score/scoring.gd
每帧只计算带内一行,总体复杂度 O(n m band) 但摊到每帧 O(m band)
- 归一化:代价 / max(i,j)+1,得到「平均每步代价」
## 评分规则(`scoring.gd`)
## 评分规则(`scoring.gd`)
`score = clamp(1 - cost/tolerance, 0, 1) x 100`,EMA 平滑抑制抖动,
`score = clamp(1 - cost/tolerance, 0, 1) x 100`, EMA 平滑抑制抖动,
等级 S(>=95) / A(>=85) / B(>=70) / C(>=55) / D,连击阈值 70。
## 验收
+3 -3
View File
@@ -1,11 +1,11 @@
# T-WIN-008 主界面 + 游戏界面 + 结算
- **状态**:done
- **依赖**:T-WIN-002、T-WIN-003、T-WIN-006、T-WIN-007
- **状态**: done
- **依赖**: T-WIN-002、T-WIN-003、T-WIN-006、T-WIN-007
## 目标
三页式体验:主菜单(选曲/选动作/选模型)-> 游戏页(3D 角色 + 实时评分 HUD)-> 结算页。
三页式体验:主菜单(选曲/选动作/选模型)-> 游戏页(3D 角色 + 实时评分 HUD)-> 结算页。
## load
+4 -4
View File
@@ -1,7 +1,7 @@
# T-WIN-009 来源信息展示
- **状态**:done
- **依赖**:T-WIN-008
- **状态**: done
- **依赖**: T-WIN-008
## 目标
@@ -26,7 +26,7 @@ src/net/catalog_types.gd
- 资产名 | 来源名称
- 作者
- 许可 + **可点击外链**跳转到 `sourceUrl`
- **五元组不齐备时显式告警**("⚠ 来源信息不完整,请联系管理员补全后再使用"),不静默展示
- **五元组不齐备时显式告警**("⚠ 来源信息不完整,请联系管理员补全后再使用"),不静默展示
## 使用位置
@@ -37,4 +37,4 @@ src/net/catalog_types.gd
## 验收
`tests/test_api_integration.gd` 对真实服务端断言曲目与模型的来源五元组齐备
(`song.source.is_complete()`),保证展示组件拿到的数据完整。
(`song.source.is_complete()`),保证展示组件拿到的数据完整。
+3 -3
View File
@@ -1,7 +1,7 @@
# T-WIN-010 关于页面 + 开源声明
- **状态**:done
- **依赖**:T-WIN-008
- **状态**: done
- **依赖**: T-WIN-008
## 目标
@@ -19,7 +19,7 @@ src/ui/credits.gd
## 交付物
- `src/ui/credits.gd`:与 `contracts/open-source-credits.md` 逐条对应的数据表,渲染为 BBCode
- `src/ui/about_screen.gd`:应用信息 + 契约版本 + 隐私说明 + 可滚动声明(RichTextLabel)
- `src/ui/about_screen.gd`:应用信息 + 契约版本 + 隐私说明 + 可滚动声明(RichTextLabel)
## 要点
+10 -10
View File
@@ -1,7 +1,7 @@
# T-WIN-011 Windows 打包脚本
- **状态**:done
- **依赖**:T-WIN-001 ~ 010
- **状态**: done
- **依赖**: T-WIN-001 ~ 010
## 目标
@@ -20,7 +20,7 @@ export_presets.cfg
| 文件 | 说明 |
|--|--|
| `export_presets.cfg` | 导出预设(**必须在项目根目录**,Godot 硬性要求) |
| `export_presets.cfg` | 导出预设(**必须在项目根目录**, Godot 硬性要求) |
| `build/export-windows.sh` | Linux 导出:定位 Godot -> 检查模板 -> 导入 -> 导出 -> **校验 PE 头** |
| `build/export-windows.ps1` | Windows 兜底:同上,产物路径与校验逻辑一致 |
@@ -28,15 +28,15 @@ export_presets.cfg
- `GODOT_BIN` 环境变量或 PATH 自动定位 Godot,缺失时给出可执行的安装指引
- 导出模板缺失时明确报错并给出下载与安装路径
- 导出命令返回非 0 时**仍检查产物是否存在**(Godot 有时非 0 返回但产物有效)
- 读取产物前 2 字节校验 `MZ`(PE 头),防止导出静默产出空壳
- 导出命令返回非 0 时**仍检查产物是否存在**(Godot 有时非 0 返回但产物有效)
- 读取产物前 2 字节校验 `MZ`(PE 头),防止导出静默产出空壳
## 验收(文档 5.4)
## 验收(文档 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,未实机执行 |
| 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 项,与契约表逐条一致 |
@@ -53,6 +53,6 @@ opendance-windows-v0.1.0.zip
## 环境记录
- Godot 4.7.2.stable(`ed1daf0bf`)
- 导出模板 `4.7.2.stable`(``windows_release_x86_64.exe``)
- Godot 4.7.2.stable(`ed1daf0bf`)
- 导出模板 `4.7.2.stable`(``windows_release_x86_64.exe``)
- 首次导出失败原因:`export_presets.cfg` 放在 `build/` 下,Godot 只在**项目根目录**查找