9 Commits
Author SHA1 Message Date
zijunchiang 8c1d24865c docs: 全角 | / ? 改 ASCII(Gitea 混淆字符检测的剩余触发项) 2026-09-15 21:36:41 +08:00
zijunchiang 68a5ca0a87 docs: 其余易混淆字符(x/>=/<=/~/OK 取代 × − ≥ ≤ ≈ ✓) 2026-09-15 21:34:50 +08:00
zijunchiang 9f7b926a9a docs: 新增「初次部署(从零跑起来)」;去混淆字符与真实内网信息
- README.md / README.txt 新增初次部署章节:前置条件、获取代码、首次运行、
  配置服务端地址(API 3000 / 管理端 3001 分离)、与 Android 的 WebSocket 联通、
  打包导出、自检清单、常见问题
- 三库同源契约:':=' 写法修回、去掉真实 MariaDB 版本串
- 全文去除易被 Gitea 判为「模棱两可 Unicode」的字符(- - ... -> 取代 — – … § →)
- 真实内网地址与库名改占位符
2026-09-15 21:33:27 +08:00
zijunchiang baec8fce97 chore(contracts): 同步 rest-api(新增 POST /api/models/{id}/reanalyze) 2026-09-15 21:18:21 +08:00
zijunchiang 5a13167739 chore(contracts): 同步契约到 0.4.0/0.3.0(GLB/VRM 主力格式、模型审核与骨骼分析) 2026-09-15 20:37:58 +08:00
zijunchiang c22790f3fe chore(contracts): 同步契约(默认模型改用压缩包 sha256 匹配,补 sourceSha256 与确定性说明) 2026-09-15 19:36:42 +08:00
zijunchiang f4cf2c70de chore(contracts): 同步契约到 rest-api/data-models/model-formats 0.3.0、database 0.2.0(模型压缩包转换与默认模型规则) 2026-09-15 13:55:59 +08:00
zijunchiang 8f303c0595 chore(contracts): 同步契约到 v0.2.0(rest-api / data-models / model-formats)
Web 端需求变更带来的契约字段级改动:
- MotionFormat 枚举新增 vmd(MikuMikuDance 动作数据)
- rest-api 服务地址说明改为双端口语义(软件 API 端口给客户端 / 管理端端口给浏览器)
- model-formats 新增 §2.3 VMD 解析约定(标准 MMD 骨架、腿部 IK/FK 判定、手性转换)

三库契约文件保持逐字节一致。
2026-09-15 08:45:06 +08:00
zijunchiang 3ba8103723 docs(contracts): 服务地址描述由 Docker 部署改为 Linux 业务服务部署(Web 端已去 Docker) 2026-09-15 01:27:50 +08:00
22 changed files with 589 additions and 204 deletions
+17 -17
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┘
@@ -19,14 +19,14 @@ Web 服务端 ──GET /api/catalog/export──────┤
VRM 驱动 + DTW 评分
```
**本端是 WebSocket 的「服务端」,Android 主动连入**(契约 websocket-keypoints.md §1)。
**本端是 WebSocket 的「服务端」,Android 主动连入**(契约 websocket-keypoints.md 1)。
## 2. 技术栈
| 组件 | 选型 | 说明 |
|---|---|---|
|--|--|--|
| 引擎 | Godot 4.7.2(MIT) | 可在 Linux 上导出 Windows exe |
| VRM 加载 | **Godot 内置 GLTFDocument** | 见 §6 的选型变更说明 |
| VRM 加载 | **Godot 内置 GLTFDocument** | 见 6 的选型变更说明 |
| 多格式导入 | Godot 内置 glTF 导入器 | FBX 由管理端转 BVH 后下发 |
| 通信 | WebSocketPeer + HTTPRequest | 无第三方依赖 |
| 评分 | DTW + 关节角度差异 | 纯 GDScript 实现 |
@@ -52,10 +52,10 @@ Web 服务端 ──GET /api/catalog/export──────┤
## 4. 上下文预算
| 项 | 建议值 |
|---|---|
| 单任务文件数 | ≤ 6 |
| 单任务代码行数 | ≤ 1500 |
| 单任务 Token | ≤ 12K |
|--|--|
| 单任务文件数 | <= 6 |
| 单任务代码行数 | <= 1500 |
| 单任务 Token | <= 12K |
| 契约引用 | 只引用锚点,不加载全文 |
## 5. 目录结构
@@ -72,7 +72,7 @@ opendance-windows/
│ ├── autoload/ # AppConfig / CatalogStore / PoseReceiver / ScoreService
│ ├── net/ # 239 字节编解码、WebSocket 服务、REST 客户端、NDJSON 解析
│ ├── model/ # VRM 加载、humanoid 骨骼名映射
│ ├── anim/ # 关键点 → 骨骼重定向
│ ├── anim/ # 关键点 -> 骨骼重定向
│ ├── score/ # 特征提取、DTW、评分规则
│ └── ui/ # 主界面 / 游戏界面 / 结算 / 关于 / 来源展示
├── tests/ # 无头测试框架 + 用例
@@ -85,7 +85,7 @@ opendance-windows/
`.tscn` 是带 uid 与节点路径的手写格式,在没有编辑器的情况下极易出错且难以验证;
用代码构建可随脚本一起被无头导入和单元测试覆盖。`scenes/main.tscn` 因此只保留一个根节点。
**VRM 加载改用 Godot 内置 glTF 导入器**(文档 §5.1 原选型为 godot-vrm GDExtension):
**VRM 加载改用 Godot 内置 glTF 导入器**(文档 5.1 原选型为 godot-vrm GDExtension):
- `V-Sekai/godot-vrm` **不发布任何预编译产物**(无 GitHub Release,仅源码,
需用 godot-cpp 本地编译原生库),这会同时破坏「Linux 上导出 Windows exe」的验收路径。
@@ -99,8 +99,8 @@ opendance-windows/
1. **绝不让视频帧离开设备**:本端只接收 33 个关键点的坐标,不接收也不存储图像。
2. **契约先行**:改交互先改 `opendance-web` 的 `contracts/`,再回本仓库同步快照。
3. **不假设骨骼轴向约定**:重定向基于「静止朝向 → 目标朝向」的最小旋转,
`pose = parent_global_basis⁻¹ × desired` 这一合成公式由真实 VRM 实测标定,
3. **不假设骨骼轴向约定**:重定向基于「静止朝向 -> 目标朝向」的最小旋转,
`pose = parent_global_basis⁻¹ x desired` 这一合成公式由真实 VRM 实测标定,
有回归测试守护,不得凭记忆改写。
4. **上传/下载的模型不入库**:真实 VRM 样例仅放在 `_scratch/`(已 gitignore),
版权原因不得提交。
@@ -113,13 +113,13 @@ opendance-windows/
GODOT=/path/to/godot # Godot 4.7.x
# 1. 全部脚本编译通过(无输出即通过)
$GODOT --headless --path . --import 2>&1 | grep -E "SCRIPT ERROR|Parse Error"
$GODOT --headless --path. --import 2>&1 | grep -E "SCRIPT ERROR|Parse Error"
# 2. 无头单元测试 + 真实 API 集成(API 不可达时相关用例显式跳过)
$GODOT --headless --path . --script res://tests/run_tests.gd
$GODOT --headless --path. --script res://tests/run_tests.gd
# 3. 应用可启动(autoload + UI 构建无报错)
$GODOT --headless --path . --quit-after 180
$GODOT --headless --path. --quit-after 180
# 4. Windows 导出(需先安装 export templates)
bash build/export-windows.sh release
@@ -128,7 +128,7 @@ bash build/export-windows.sh release
## 9. 上下文超标时的拆分策略
| 超标原因 | 拆分方式 |
|---|---|
|--|--|
| 单任务涉及多个界面 | 按界面拆(主/游戏/结算/关于) |
| 单文件过大 | 按类拆,先生成接口再分实现 |
| 契约全文加载 | 改用锚点引用 |
+123 -20
View File
@@ -9,22 +9,22 @@ OpenDance Studio **阶段二**:Windows 客户端(Godot 4)。
## 状态
**阶段二已完成**:T-WIN-001 → T-WIN-011 全部落地。
**阶段二已完成**:T-WIN-001 -> T-WIN-011 全部落地。
| 验收项 | 结果 |
|---|---|
| 脚本编译 | ✓ 全部通过(无 SCRIPT ERROR) |
| 无头测试 | ✓ **580 项全部通过**(8 个测试套件) |
| 应用启动 | ✓ 180 帧无运行时错误 |
| Windows 导出 | ✓ 生成 `OpenDance.exe`(104 MB,PE 头校验通过)+ `OpenDance.pck` |
| 真实 VRM 验证 | ✓ 154 骨骼 VRoid 模型,humanoid 覆盖率 **100%** |
| 骨骼重定向 | ✓ 关键点方向与骨骼实际指向点积 **1.0000** |
| 跨实现交叉校验 | ✓ 关节角与 Web 服务端 TypeScript 实现**逐帧一致** |
|--|--|
| 脚本编译 | OK 全部通过(无 SCRIPT ERROR) |
| 无头测试 | OK **580 项全部通过**(8 个测试套件) |
| 应用启动 | OK 180 帧无运行时错误 |
| Windows 导出 | OK 生成 `OpenDance.exe`(104 MB,PE 头校验通过)+ `OpenDance.pck` |
| 真实 VRM 验证 | OK 154 骨骼 VRoid 模型,humanoid 覆盖率 **100%** |
| 骨骼重定向 | OK 关键点方向与骨骼实际指向点积 **1.0000** |
| 跨实现交叉校验 | OK 关节角与 Web 服务端 TypeScript 实现**逐帧一致** |
## 快速开始
```bash
# 验证(编译 → 测试 → 启动)
# 验证(编译 -> 测试 -> 启动)
GODOT_BIN=/path/to/godot bash build/verify.sh
# 导出 Windows 可执行文件
@@ -36,10 +36,113 @@ bash build/package-release.sh 0.1.0
详细使用说明见 [`README.txt`](README.txt),工程约束见 [`DSH.md`](DSH.md)。
## 初次部署(从零跑起来)
本节面向**第一次拿到本仓库**的人:从安装引擎到跑通「Web 服务端 + 本端 + Android」
三端链路。地址与端口一律使用占位符或产品默认值。
### 1. 前置条件
| 项 | 要求 |
|--|--|
| 操作系统 | 运行客户端:Windows 10/11(x86_64)。开发与导出:Windows 或 Linux 均可 |
| Godot | **4.7.x**(`project.godot` 声明特性 `4.7`,本仓库按 4.7.2 验证)。使用**标准版**(GDScript),**不需要 .NET / C# 版** |
| 显卡与图形界面 | 渲染后端为 **GL Compatibility**,运行需支持 OpenGL 3.3 的显卡与驱动。仅无头测试与导出可无 GPU、可无图形界面 |
| 导出模板 | **仅打包导出时需要**:必须安装与编辑器**完全同版本**的 export templates(`.tpz`) |
| 磁盘与网络 | Godot 编辑器 + 导出模板约数百 MB;导出产物 `OpenDance.exe` 约 104 MB、`OpenDance.pck` 约 107 KB。本端需能访问 Web 服务端 API(默认 TCP 3000),并与 Android 设备处于同一局域网、放行本端 **TCP 8765 入站** |
### 2. 获取代码
```bash
git clone https://<Gitea 地址>/<owner>/opendance-windows
cd opendance-windows
```
### 3. 首次运行(编辑器内)
1. 启动 Godot 4.7.x,在项目管理器选择「导入」,指向本仓库的 `project.godot`。
2. **首次导入会执行资源导入**(生成 `.godot/`),耗时从数十秒到数分钟不等;请等待完成,不要在导入中途关闭编辑器。
3. 主场景已在 `project.godot` 中配置为 `res://scenes/main.tscn`,直接按 **F5**(或「运行项目」)即可。
4. 界面全部由 GDScript 代码构建,不需要在编辑器里编辑场景,这是刻意设计(见 `DSH.md` 6)。
5. 首次运行会在用户数据目录生成配置与日志:
```
%APPDATA%\Godot\app_userdata\OpenDance Studio\opendance.cfg
%APPDATA%\Godot\app_userdata\OpenDance Studio\logs\godot.log
```
### 4. 配置服务端地址
- 位置:主界面顶部「**服务端地址**」输入框,填好后点「**保存并测试**」。
- 配置字段名(`opendance.cfg`):`api_base_url`。
- **端口区分**:客户端只访问 Web 服务端的 **API 端口(默认 3000)**;管理端页面在**管理端口(默认 3001)**,客户端不使用该端口。
- 示例占位符:
```
http://<服务器地址>:3000
http://192.0.2.10:3000
```
保存后点「**刷新目录**」;顶部应显示「服务端正常 | DB ...」,左侧列出已发布的曲目 / 动作 / 模型。
### 5. 与另外两端的联通
| 对端 | 方向 | 本端配置 | 说明 |
|--|--|--|--|
| Android 辅助 App | Android -> 本端(WebSocket) | `listen_host`(默认 `0.0.0.0`)、`listen_port`(默认 **8765**)、`ws_subprotocol`(默认 `opendance.pose.v1`) | 本端是 WebSocket **服务端**;Android 端填写本端主界面显示的「接收地址」,形如 `ws://<本机局域网地址>:8765/pose` |
| Web 服务端 | 本端 -> Web(HTTP) | `api_base_url`(默认 `http://127.0.0.1:3000`) | 需先在服务端**发布**曲目 / 动作 / 模型,动作还须**完成关键点预处理**,否则目录为空或动作不可选 |
- **Android 侧无本端可配置项**:接收地址由 Android App 自己填写;本端只监听固定端口 8765(可在 `opendance.cfg` 改 `listen_port` 后点「重新连接接收服务」生效)。
- 已知限制:Godot 的 `WebSocketPeer` 不暴露握手请求路径,本端**接受任意 path**,只校验子协议 `opendance.pose.v1`(见「已知限制」1)。
- 接收状态可用主界面「**查看接收状态**」查看:连接数、累计帧、丢帧、畸形帧。
### 6. 打包导出
```bash
# Linux 上直接导出 Windows 可执行文件(推荐)
GODOT_BIN=/path/to/godot bash build/export-windows.sh release
# Windows 上兜底导出
powershell -ExecutionPolicy Bypass -File build\export-windows.ps1
# 打成可分发的 zip(可选)
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.console.exe` | `build/windows/` | 控制台包装器,可能生成,非必需 |
| `opendance-windows-v0.1.0.zip` | `dist-release/` | 发布包:exe + pck + `THIRD_PARTY.md` + `README.txt` + `contracts/` |
导出前必须安装与编辑器同版本的 export templates,否则脚本会报「未找到导出模板」。
### 7. 自检清单
| 检查 | 命令 | 预期输出 |
|--|--|--|
| 一键验证(编译 + 测试 + 启动) | `GODOT_BIN=/path/to/godot bash build/verify.sh` | 末行「验证全部通过」,退出码 0 |
| 无头单元 / 集成测试 | `$GODOT --headless --path . --script res://tests/run_tests.gd` | `== 结果:580 通过 / 0 失败 ==` 与 `全部通过`(仓库基线 580 项 / 8 个套件;缺 VRM 素材或服务端不可达时相关用例显式跳过,数字会减少) |
| 启动冒烟(180 帧) | `$GODOT --headless --path . --quit-after 180` | 无 `SCRIPT ERROR` / `ERROR:`,进程退出码 0 |
| 日志检查 | 打开 `%APPDATA%\Godot\app_userdata\OpenDance Studio\logs\godot.log` | 无 `SCRIPT ERROR`、无 `ERROR:` |
| 导出产物校验 | `bash build/export-windows.sh release` | 输出「PE 头校验通过」,且 `build/windows/OpenDance.exe` 前 2 字节为 `4d5a` |
| 服务端联通 | 主界面「服务端地址」->「保存并测试」 | 顶部显示「服务端正常 | DB ...」 |
### 8. 常见问题
| 现象 | 原因 | 处理 |
|--|--|--|
| 游戏界面提示「模型加载失败:缺少骨骼」或「骨骼绑定失败,模型可能不受支持」 | 模型没有 `Skeleton3D`,或骨骼命名不在 humanoid 映射表内;驱动至少需要 hips / spine / neck / head + 四肢共 16 根核心骨骼 | 换用标准 VRM(VRoid Studio 导出);看「人形覆盖率」,低于 60% 会提示「骨骼覆盖偏低,动作可能不完整」;必要时在 `src/model/humanoid_map.gd` 增加命名映射 |
| 顶部「服务端不可达」或「目录拉取失败」 | Web 服务端未启动、地址或端口写错(误填管理端口 3001)、或网络不通 | 确认 `api_base_url` 为 `http://<服务器地址>:3000`;浏览器访问 `http://<服务器地址>:3000/health` 应返回 200;再点「保存并测试」 |
| 导出失败:「未找到导出模板」 | 未安装 export templates,或版本 / 路径与编辑器不一致 | 编辑器「编辑器 -> 管理导出模板 -> 下载并安装」;Linux 放 `~/.local/share/godot/export_templates/<版本>.stable/`,Windows 放 `%APPDATA%\Godot\export_templates\<版本>.stable\` |
| 提示「接收服务启动失败(端口 8765 可能被占用)」 | TCP 8765 被占用或未放行 | 改 `opendance.cfg` 的 `listen_port` 后点「重新连接接收服务」,并同步修改 Android 端填写的接收地址 |
| 手机已连接但分数一直是 0 | Android 端未开始推流,或帧内关键点置信度都低于 0.5 | 「查看接收状态」确认累计帧在增长、畸形帧为 0;确认 Android 端子协议为 `opendance.pose.v1` |
## 技术栈
| 组件 | 选型 |
|---|---|
|--|--|
| 引擎 | Godot 4.7.x(MIT) |
| VRM 加载 | Godot 内置 GLTFDocument |
| 通信 | WebSocketPeer(服务端)+ HTTPRequest |
@@ -48,7 +151,7 @@ bash build/package-release.sh 0.1.0
## 架构
```
Android 辅助 App ──239 字节关键点帧──▶ 本端 WebSocket 服务端 :8765
Android 辅助 App ──239 字节关键点帧──▶ 本端 WebSocket 服务端:8765
│
Web 服务端 ──GET /api/catalog/export────────┤
──GET /api/motions/:id/keypoints ┘
@@ -61,7 +164,7 @@ src/
├── autoload/ AppConfig / CatalogStore / PoseReceiver / ScoreService
├── net/ 239 字节编解码、WebSocket 服务、REST 客户端、NDJSON 解析
├── model/ VRM 加载、humanoid 骨骼名映射
├── anim/ 关键点 → 骨骼重定向
├── anim/ 关键点 -> 骨骼重定向
├── score/ 特征提取、DTW、评分规则
└── ui/ 主界面 / 游戏界面 / 结算 / 关于 / 来源展示
```
@@ -70,14 +173,14 @@ src/
## 依赖的契约(opendance-web v0.1.0 冻结)
- [`contracts/websocket-keypoints.md`](contracts/websocket-keypoints.md) — 239 字节关键点帧与文本控制消息(本端为**接收方**)
- [`contracts/skeleton-topology.md`](contracts/skeleton-topology.md) — MediaPipe 33 点拓扑、父节点表、9 个关节角特征
- [`contracts/rest-api.yaml`](contracts/rest-api.yaml) — 目录与关键点序列 API
- [`contracts/data-models.md`](contracts/data-models.md) — 资源字段与 VRM 优先规则
- [`contracts/model-formats.md`](contracts/model-formats.md) — 模型与动作格式、关键点序列结构
- [`contracts/websocket-keypoints.md`](contracts/websocket-keypoints.md) - 239 字节关键点帧与文本控制消息(本端为**接收方**)
- [`contracts/skeleton-topology.md`](contracts/skeleton-topology.md) - MediaPipe 33 点拓扑、父节点表、9 个关节角特征
- [`contracts/rest-api.yaml`](contracts/rest-api.yaml) - 目录与关键点序列 API
- [`contracts/data-models.md`](contracts/data-models.md) - 资源字段与 VRM 优先规则
- [`contracts/model-formats.md`](contracts/model-formats.md) - 模型与动作格式、关键点序列结构
`contracts/` 是**只读快照**。任何跨端契约变更必须先回到
[`opendance-web`](https://gitea.kasugano.cn/zijunchiang/opendance-web) 修改并提升版本,
[`opendance-web`](https://<Gitea 地址>/<owner>/opendance-web) 修改并提升版本,
再在本仓库创建适配任务。
## 已知限制
@@ -85,7 +188,7 @@ src/
1. Godot 的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**,
实现接受任意 path,仅校验子协议 `opendance.pose.v1`。
2. 文档原选型 `godot-vrm` GDExtension 不发布预编译产物,改用 Godot 内置 glTF 导入器
+ 自实现骨骼名映射;理由见 `DSH.md` §6。
+ 自实现骨骼名映射;理由见 `DSH.md` 6。
3. `build/export-windows.ps1` 未在真实 Windows 上执行过(本环境无 Windows)。
两条路径共用同名导出预设与相同的 PE 头校验逻辑。
4. 真实 VRM 样例仅放在 `_scratch/`(已 gitignore),版权原因不入库;
+143 -40
View File
@@ -1,5 +1,5 @@
================================================================
OpenDance Studio —— Windows 客户端 v0.1.0 使用说明
OpenDance Studio -- Windows 客户端 v0.1.0 使用说明
================================================================
摄像头跳舞评分系统的核心体验端:接收 Android 辅助 App 通过 WebSocket
@@ -8,9 +8,9 @@ OpenDance Studio —— Windows 客户端 v0.1.0 使用说明
本客户端只接收关键点坐标,不接收、不存储任何视频画面。
----------------------------------------------------------------
--
一、快速开始
----------------------------------------------------------------
--
1. 解压后得到:
OpenDance.exe 主程序
@@ -24,19 +24,122 @@ OpenDance Studio —— Windows 客户端 v0.1.0 使用说明
3. 双击 OpenDance.exe 启动。
4. 在主界面确认「服务端地址」(默认 http://127.0.0.1:3000),
点击「保存并测试」;顶部应显示「服务端正常 | DB ...」。
点击「保存并测试」;顶部应显示「服务端正常 | DB...」。
5. 点击「刷新目录」,左侧会出现已发布的曲目、动作与模型。
6. 在 Android 手机上启动「OpenDance 辅助 App」,填入本机显示在
主界面上的接收地址(形如 ws://192.168.x.x:8765/pose)并连接。
主界面上的接收地址(形如 ws://<本机局域网地址>:8765/pose)并连接。
连接成功后主界面会提示「已连接 N 台设备」。
7. 依次选择曲目 → 动作 → 模型,点击「开始跟练」。
7. 依次选择曲目 -> 动作 -> 模型,点击「开始跟练」。
----------------------------------------------------------------
二、界面说明
----------------------------------------------------------------
--
二、初次部署(从零跑起来)
--
本节面向第一次拿到源码、要从零把客户端跑起来的人;只运行已打包好的
OpenDance.exe 的用户看第一节即可。
1. 前置条件
- 操作系统:Windows 10/11(x86_64)。开发与导出可在 Windows 或 Linux。
- Godot 4.7.x 标准版(GDScript)。不需要 .NET / C# 版。
- 渲染后端为 GL Compatibility,运行需 OpenGL 3.3 级别的显卡与驱动。
- 导出模板:仅打包导出时需要,必须与编辑器完全同版本。
- 磁盘:编辑器 + 导出模板数百 MB;导出产物 exe 约 104 MB + pck 约 107 KB。
- 网络:能访问 Web 服务端 API(默认 TCP 3000);与手机同一局域网,
并放行本端 TCP 8765 入站。
2. 获取代码
git clone https://<Gitea 地址>/<owner>/opendance-windows
cd opendance-windows
3. 编辑器内首次运行
- Godot 项目管理器 -> 导入 -> 选择 project.godot。
- 首次导入会执行资源导入(生成 .godot/),可能等待数十秒到数分钟;
不要在导入中途关闭编辑器。
- 主场景已配置为 res://scenes/main.tscn,按 F5 运行。
- 首次运行会生成:
%APPDATA%\Godot\app_userdata\OpenDance Studio\opendance.cfg
%APPDATA%\Godot\app_userdata\OpenDance Studio\logs\godot.log
4. 配置服务端地址
主界面顶部「服务端地址」填好后点「保存并测试」,配置字段名为 api_base_url:
http://<服务器地址>:3000
http://192.0.2.10:3000
注意:客户端只访问 API 端口(默认 3000);管理端页面在管理端口
(默认 3001),客户端不使用。保存后点「刷新目录」。
5. 与另两端的联通
- Android 辅助 App -> 本端 WebSocket:本端是服务端,默认监听
0.0.0.0:8765,路径 /pose,子协议 opendance.pose.v1。Android 端填写
本端主界面显示的「接收地址」,形如 ws://<本机局域网地址>:8765/pose。
本端可在 opendance.cfg 改 listen_host / listen_port / ws_subprotocol,
改后点「重新连接接收服务」生效。
- Web 服务端:需先在服务端发布曲目 / 动作 / 模型,动作还须完成关键点
预处理,客户端才有可选内容。
6. 打包导出
# Linux(推荐)
GODOT_BIN=/path/to/godot bash build/export-windows.sh release
# Windows(兜底)
powershell -ExecutionPolicy Bypass -File build\export-windows.ps1
产物在 build/windows/:OpenDance.exe(约 104 MB,PE 头 MZ 校验通过)
+ OpenDance.pck(约 107 KB,必须与 exe 同目录);可能附带
OpenDance.console.exe。
打成发布包(可选):
bash build/package-release.sh 0.1.0
得到 dist-release/opendance-windows-v0.1.0.zip,内含 OpenDance.exe、
OpenDance.pck、THIRD_PARTY.md、README.txt、contracts/。
7. 自检清单
1) 一键验证:GODOT_BIN=/path/to/godot bash build/verify.sh
预期末行「验证全部通过」。
2) 无头测试:$GODOT --headless --path . --script res://tests/run_tests.gd
预期「== 结果:580 通过 / 0 失败 ==」与「全部通过」(缺 VRM 素材或
服务端不可达时相关用例跳过,数字会减少)。
3) 启动冒烟:$GODOT --headless --path . --quit-after 180
预期无 SCRIPT ERROR / ERROR:,退出码 0。
4) 日志:%APPDATA%\Godot\app_userdata\OpenDance Studio\logs\godot.log
预期无 SCRIPT ERROR。
5) 服务端联通:主界面点「保存并测试」后顶部显示「服务端正常 | DB ...」。
8. 部署相关常见问题
Q: 模型加载失败:缺少骨骼
A: 模型无 Skeleton3D,或骨骼命名不在映射表内;换用标准 VRM(VRoid
Studio 导出)。人形覆盖率低于 60% 会提示「骨骼覆盖偏低」。
Q: 连不上服务端 / 显示「服务端不可达」
A: 确认 Web 服务端已启动,api_base_url 指向 http://<服务器地址>:3000
(不要填管理端口 3001);访问 http://<服务器地址>:3000/health 应返回 200。
Q: 导出失败:未找到导出模板
A: 安装与编辑器同版本的 export templates。Linux 放
~/.local/share/godot/export_templates/<版本>.stable/;
Windows 放 %APPDATA%\Godot\export_templates\<版本>.stable\。
Q: 接收服务启动失败(端口 8765 可能被占用)
A: 改 opendance.cfg 的 listen_port 后点「重新连接接收服务」,
并同步修改 Android 端填写的接收地址。
--
三、界面说明
--
主界面
- 三栏选择:曲目 / 动作(仅列出已完成预处理的动作)/ 模型(VRM 优先)
@@ -56,9 +159,9 @@ OpenDance Studio —— Windows 客户端 v0.1.0 使用说明
关于页面
- 版本与契约版本、隐私说明、完整可滚动的第三方开源项目声明
----------------------------------------------------------------
三、配置文件
----------------------------------------------------------------
--
四、配置文件
--
首次运行会在以下位置生成配置文件,可直接用文本编辑器修改:
@@ -70,13 +173,13 @@ OpenDance Studio —— Windows 客户端 v0.1.0 使用说明
listen_host WebSocket 监听地址(默认 0.0.0.0)
listen_port WebSocket 监听端口(默认 8765)
ws_subprotocol 子协议(默认 opendance.pose.v1)
dtw_band DTW 带宽,单位帧(默认 15 ≈ 0.5 秒抖动容忍)
dtw_band DTW 带宽,单位帧(默认 15 ~ 0.5 秒抖动容忍)
score_tolerance 评分容差:平均每步代价达到该值即 0 分(默认 0.25)
mirror_preview 镜像预览
----------------------------------------------------------------
四、防火墙与网络
----------------------------------------------------------------
--
五、防火墙与网络
--
本客户端需要接受 Android 设备的入站连接:
@@ -86,20 +189,20 @@ OpenDance Studio —— Windows 客户端 v0.1.0 使用说明
若端口被占用,可修改 opendance.cfg 中的 listen_port 后重启。
----------------------------------------------------------------
五、评分说明
----------------------------------------------------------------
--
六、评分说明
--
- 每帧特征 = 33 点 × (x,y,z) + 9 个关节角余弦 = 108 维
- 每帧特征 = 33 点 x (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 其余
- 采用带约束 DTW(|i-j| <= band)对齐,能吸收一定的节奏快慢差异
- 得分 = clamp(1 - 平均代价 / 容差, 0, 1) x 100,并做指数平滑
- 等级:S >= 95,A >= 85,B >= 70,C >= 55,D 其余
----------------------------------------------------------------
六、从源码构建
----------------------------------------------------------------
--
七、从源码构建
--
前置:Godot 4.7.x + 对应版本的 export templates
@@ -115,13 +218,13 @@ OpenDance Studio —— Windows 客户端 v0.1.0 使用说明
运行测试:
GODOT=/path/to/godot
$GODOT --headless --path . --import
$GODOT --headless --path . --script res://tests/run_tests.gd
$GODOT --headless --path . --quit-after 180
$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 导出预设(必须在项目根目录)
@@ -133,21 +236,21 @@ OpenDance Studio —— Windows 客户端 v0.1.0 使用说明
src/autoload/ AppConfig / CatalogStore / PoseReceiver / ScoreService
src/net/ 239 字节编解码、WebSocket 服务、REST 客户端
src/model/ VRM 加载、humanoid 骨骼映射
src/anim/ 关键点 → 骨骼重定向
src/anim/ 关键点 -> 骨骼重定向
src/score/ 特征提取、DTW、评分规则
src/ui/ 主界面 / 游戏界面 / 结算 / 关于 / 来源展示
tests/ 无头测试框架与用例
build/ 导出脚本
----------------------------------------------------------------
八、常见问题
----------------------------------------------------------------
--
九、常见问题
--
Q: 主界面显示「服务端不可达」
A: 检查 Web 服务端是否已启动,以及「服务端地址」是否正确(含端口)。
Q: 目录里没有可用的动作
A: 动作必须先在管理端完成「预处理」(BVH → 关键点序列)并「审核通过」,
A: 动作必须先在管理端完成「预处理」(BVH -> 关键点序列)并「审核通过」,
才会出现在客户端。
Q: 模型加载失败 / 动作不完整
@@ -160,9 +263,9 @@ A: 确认手机端已开始推流;主界面的「查看接收状态」会显
Q: 关于页面的开源声明
A: 与 THIRD_PARTY.md、contracts/open-source-credits.md 三处保持一致。
----------------------------------------------------------------
九、许可
----------------------------------------------------------------
--
十、许可
--
本客户端代码以 MIT 许可发布。第三方开源项目声明见 THIRD_PARTY.md。
未使用 SMPL 模型及其衍生拓扑,统一采用 MediaPipe 33 点拓扑。
+4 -4
View File
@@ -1,8 +1,8 @@
# OpenDance Studio 契约版本
# 规则:任何字段级变更必须提升对应条目版本,并回到 opendance-web 修改后再向另两端建迁移任务。
rest-api: 0.1.0
rest-api: 0.4.0
websocket-keypoints: 0.1.0
data-models: 0.1.0
data-models: 0.4.0
skeleton-topology: 0.1.0
model-formats: 0.1.0
database: 0.1.0
model-formats: 0.4.0
database: 0.3.0
+31 -19
View File
@@ -1,4 +1,4 @@
# contracts/data-models.md — 数据模型契约 v0.1.0
# contracts/data-models.md - 数据模型契约 v0.4.0
Web 服务端是唯一权威定义,本文件是另两端消费的**稳定视图**。
字段名在 JSON 中统一 `camelCase`;数据库列名 `snake_case`(见 `database.md`)。
@@ -16,13 +16,13 @@ Web 服务端是唯一权威定义,本文件是另两端消费的**稳定视
- 错误响应统一:
```json
{ "statusCode": 400, "message": "…", "error": "Bad Request", "details": {} }
{ "statusCode": 400, "message": "...", "error": "Bad Request", "details": {} }
```
- **来源(出处)五元组**是必填项,出现在 `songs` / `motions` / `models` 三处:
| 字段 | 类型 | 说明 |
|---|---|---|
|--|--|--|
| `sourceType` | enum | 见 1.1 |
| `sourceName` | string | 来源名称,如「官方曲库」「作者名」 |
| `sourceUrl` | string | 来源链接,必须为 http(s) |
@@ -52,11 +52,11 @@ rejected 已驳回
## 2. `Song`
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
|--|--|--|--|
| `id` | number | 读 | |
| `title` | string | ✅ | 曲名,≤200 |
| `artist` | string | ✅ | 演唱者,≤200 |
| `album` | string | | 专辑,≤200 |
| `title` | string | ✅ | 曲名,<=200 |
| `artist` | string | ✅ | 演唱者,<=200 |
| `album` | string | | 专辑,<=200 |
| `language` | string | | `zh` / `en` / `ja` / `ko` / `instrumental` |
| `durationMs` | number | | 时长毫秒 |
| `bpm` | number | | 节拍 |
@@ -71,18 +71,18 @@ rejected 已驳回
## 3. `Motion`(动作数据)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
|--|--|--|--|
| `id` | number | 读 | |
| `songId` | number\|null | | 关联曲目 |
| `name` | string | ✅ | 动作名称,≤200 |
| `format` | enum | ✅ | `bvh` / `fbx` / `gltf` / `glb` / `keypoints-json` |
| `name` | string | ✅ | 动作名称,<=200 |
| `format` | enum | ✅ | `bvh` / `vmd` / `fbx` / `gltf` / `glb` / `keypoints-json` |
| `fileUrl` | string | 读 | 原始动作文件 |
| `fileSize` | number | 读 | 字节 |
| `fps` | number\|null | | 原始帧率 |
| `frameCount` | number\|null | | 原始帧数 |
| `durationMs` | number\|null | | |
| `keypointFormat` | string\|null | 读 | 预处理后固定为 `mediapipe33@0.1.0` |
| `keypointUrl` | string\|null | 读 | 预处理产物(NDJSON 或 .npz 的 JSON 封装) |
| `keypointUrl` | string\|null | 读 | 预处理产物(NDJSON 或.npz 的 JSON 封装) |
| `keypointFrames` | number\|null | 读 | 关键点帧数 |
| `preprocessState` | enum | 读 | `none` / `running` / `done` / `failed` |
| `preprocessError` | string\|null | 读 | |
@@ -93,37 +93,49 @@ rejected 已驳回
## 4. `Model`(角色模型,VRM 优先)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
|--|--|--|--|
| `id` | number | 读 | |
| `name` | string | ✅ | 模型名称,≤200 |
| `format` | enum | ✅ | `vrm` / `glb` / `gltf` / `fbx` |
| `name` | string | ✅ | 模型名称,<=200 |
| `format` | enum | ✅ | `glb` / `vrm`(主力)/ `gltf` / `fbx`(历史数据)。上传的 PMX/FBX 会被服务端转换成 glb 或 vrm 再入库,见 model-formats.md 1.1 |
| `vrmVersion` | string\|null | 读 | `0.x` / `1.0`,非 VRM 为 null |
| `isPrimary` | boolean | | 全局默认模型;**同一时刻全库至多一个 `true`** |
| `fileUrl` | string | 读 | |
| `fileSize` | number | 读 | |
| `thumbnailUrl` | string\|null | 读 | |
| `conversionNote` | string\|null | 读 | 由压缩包转换而来时的说明(`Hatsune Miku.pmx -> GLB,已内嵌 2 张贴图`);直传模型为 null |
| `fileSha256` | string\|null | 读 | 模型文件(转换产物)的 sha256 -- **每次转换都会变**,仅作记录 |
| `sourceSha256` | string\|null | 读 | 上传压缩包的 sha256(稳定);默认模型匹配以它为准,非压缩包上传为 null |
| `stats` | object\|null | 读 | 模型分析结果:面数 / 顶点数 / 贴图 / **骨骼树** / humanoid 覆盖 / 可驱动性,见 model-formats.md 1.1 |
| `source*` | | ✅ | 来源五元组 |
| `status` | `PublishStatus` | | |
| `createdAt` / `updatedAt` | string | 读 | |
### 4.1 VRM 优先规则(冻结)
### 4.1 默认模型选择规则(冻结)
客户端选择默认模型的顺序:
服务端 `GET /api/catalog/export` 的 `defaultModelId` 按以下顺序解析:
```
1. 显式指定 modelId(曲目动作绑定)
0. 环境变量 DEFAULT_MODEL_ID 指定,且该模型存在且已发布
1. 源码内写死的默认模型种子(web-server/src/config/default-model.ts 的
DEFAULT_MODEL_SEED):按 **sourceSha256(上传压缩包的 sha256)** 匹配已发布模型,
换库/重建库后仍然有效;没有压缩包时退回按产物 sha256,最后按名称兜底。
⚠ 不能用产物 sha256 当主键:实测 Assimp 的 GLB 导出不是字节级确定的,
同一份 zip 每次转换得到的文件 sha256 都不同(BIN chunk 有十几字节浮动)
2. models 中 format = 'vrm' 且 status = 'published' 且 is_primary = 1
3. models 中 format = 'vrm' 且 status = 'published',按 id 升序取第一个
4. models 中 status = 'published',按 (format='vrm' DESC, id ASC) 取第一个
5. 无可用模型 → 客户端使用内置占位模型
5. 无可用模型 -> 客户端使用内置占位模型
```
第 0/1 步是 v0.3.0 新增:上传 MMD 模型压缩包并转换成功后,服务端会把该模型
`is_primary=1` + `status=published`,并把身份写进源码,使它成为开箱默认模型。
`isPrimary` 通过 `POST /api/models/:id/primary` 设置,服务端保证互斥(同一事务内先清零)。
## 5. `ReviewRecord`(审核流水)
| 字段 | 类型 | 说明 |
|---|---|---|
|--|--|--|
| `id` | number | |
| `targetType` | enum | `song` / `motion` / `model` |
| `targetId` | number | |
+17 -10
View File
@@ -1,11 +1,11 @@
# contracts/database.md — 数据库契约 v0.1.0
# contracts/database.md - 数据库契约 v0.1.0
## 环境
- 类型:MySQL 兼容实例(外部提供)。**当前部署实际为 MariaDB 10.11.19**,TypeORM
- 类型:MySQL 兼容实例(外部提供,含 MariaDB 10.5+),TypeORM
使用 `mysql` driver(`mysql2`)连接,SQL 语法限制在 MySQL 5.7 / MariaDB 10.5+
共同子集内,不使用 MySQL 8 专有语法(窗口函数 CTE 递归除外)。
- 数据库名:`dpendance`(由部署方提供,通过 `DB_NAME` 注入;文档默认值 `opendance` 可覆盖)
- 数据库名:由部署方提供,通过 `DB_NAME` 注入(本文档示例统一用 `opendance`)
- 字符集:`utf8mb4` / `utf8mb4_general_ci`(沿用现有实例默认排序规则)
- 迁移工具:TypeORM migration,**不使用 `DB_SYNCHRONIZE`**
@@ -14,11 +14,11 @@
host / port / user / password 一律通过环境变量注入,**不入库、不入仓**。
```bash
DB_HOST=192.168.150.10
DB_HOST=<数据库主机>
DB_PORT=3306
DB_USER=dpendance
DB_PASS=<部署方提供>
DB_NAME=dpendance
DB_USER=opendance_app
DB_PASS=<数据库口令>
DB_NAME=opendance
DB_SYNCHRONIZE=false
DB_LOGGING=false
```
@@ -36,14 +36,21 @@ DB_LOGGING=false
- 每个 migration 必须可逆(提供 `down` 方法)
- 迁移文件名格式:`<timestamp>-<kebab-name>.ts`
## 增量迁移(v0.2.0 / v0.3.0)
| 迁移 | 变更 |
|--|--|
| `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)
| 表 | 用途 | 任务 |
|---|---|---|
|--|--|--|
| `songs` | 曲目元数据 | T-WEB-003 |
| `assets` | 上传文件元数据(去重、sha256、物理路径) | T-WEB-004 |
| `motions` | 动作数据(BVH/FBX…)+ 来源 + 预处理产物 | T-WEB-004 / 006 |
| `models` | 角色模型(VRM 优先)+ 来源 | T-WEB-005 |
| `motions` | 动作数据(BVH/FBX...)+ 来源 + 预处理产物 | T-WEB-004 / 006 |
| `models` | 角色模型(VRM 优先)+ 来源 + 压缩包转换溯源 | T-WEB-005 |
| `review_records` | 上传审核流水 | T-WEB-007 |
## 状态机(`status` 列,v0.1.0)
+117 -19
View File
@@ -1,15 +1,71 @@
# contracts/model-formats.md — 模型与动作格式契约 v0.1.0
# contracts/model-formats.md - 模型与动作格式契约 v0.4.0
## 1. 角色模型(models)
| 格式 | 扩展名 | 支持级别 | 说明 |
|---|---|---|---|
| VRM | `.vrm` | **首选(P0)** | glTF 2.0 二进制 + VRM 扩展;Windows 端 godot-vrm 直接加载 |
| glTF Binary | `.glb` | P1 | 需含骨骼(skin);无骨骼则仅作静态展示 |
| glTF | `.gltf` (+ `.bin`, 贴图) | P2 | 多文件,管理端需打包为 zip 上传 |
| FBX | `.fbx` | P2 | 需 Assimp 转换,管理端仅归档,不保证重定向质量 |
**系统以 GLB / VRM 为主力格式**(两个客户端都原生加载),其余格式一律在服务端转换。
**VRM 优先**的选取顺序见 `data-models.md` §4.1。
| 格式 | 扩展名 | 支持级别 | 服务端行为 |
|--|--|--|--|
| 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) |
### 1.1 上传与转换策略
**`.glb` / `.vrm` / `.gltf` 直接导入**(不重编码,零损失);**`.pmx` / `.fbx` 由服务端转换**:
**优先 GLB,其次 VRM**。
#### 单文件上传
| 输入 | 行为 |
|--|--|
| `.glb` / `.vrm` | 直接导入;`.glb` 若含 `VRMC_vrm`/`VRM` 扩展会被识别为 VRM |
| `.gltf` | 直接导入(外部 `.bin` / 贴图不在同一文件里,会缺资源) |
| `.pmx` / `.fbx` | 转换:`ASSIMP_BIN` 导出 -> 回读校验(见下) |
#### 压缩包上传(自动搜索)
不再"只找 `.pmx`",而是**按格式优先级自动搜索包内可用模型**:
```
.vrm ->.glb ->.gltf (直接导入,优先)
.pmx ->.fbx (需要转换)
```
同一优先级内:`MODEL_CONVERT_PMX` 指定的文件名优先,其次目录最浅,最后文件最大。
压缩包原件以 `model-archive` 资产留存并写入 `models.source_asset_id`。
#### 转换与校验(PMX / FBX)
| 步骤 | 规则 |
|--|--|
| 1. GLB 直出 | `assimp export ->.glb`,成功后把 Assimp 写成外部 URI 的贴图**内嵌**进 BIN chunk |
| 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 张贴图`) |
| 失败 | 包内没有可用模型、Assimp 不可用、两条路径都失败 -> **HTTP 422**,不产生模型记录 |
#### 入库后(自动)
| 项 | 说明 |
|--|--|
| 状态 | **一律 `pending`(待审核)**:模型必须审核通过后才可能被客户端消费,见 `database.md` 状态机 |
| 分析 | `model-inspector.ts` 计算面数 / 顶点数 / 贴图 / **骨骼树** / humanoid 覆盖 / 可驱动性,写入 `models.stats`,供审核界面展示 |
| 缩略图 | `model-thumbnail.ts` 纯 TS 光栅化生成 PNG,写入 `models.thumbnail_asset_id`(失败不影响入库) |
| 默认模型 | 只在**审核通过并勾选"同时设为默认"**(或 `POST /api/models/:id/primary`)时设置,并把身份写进源码 `src/config/default-model.ts`,见 `data-models.md` 4.1 |
#### 其它约定
| 项 | 规则 |
|--|--|
| 确定性 | **转换产物不是字节级确定的**:同一份 zip 连续转换 3 次得到 3 个不同 sha256(大小相同,BIN chunk 有十几字节浮动)。因此默认模型种子以**上传压缩包的 sha256** 为匹配键,产物 sha256 只作记录 |
| 安全 | 解压前逐条校验路径,拒绝绝对路径与 `..`(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`。
@@ -17,8 +73,9 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)
## 2. 动作数据(motions)
| 格式 | 扩展名 | 预处理支持 | 说明 |
|---|---|---|---|
| BVH | `.bvh` | ✅ 内置纯 TS 解析器 | 文本格式,服务端直接解析 → FK → MediaPipe33 |
|--|--|--|--|
| BVH | `.bvh` | ✅ 内置纯 TS 解析器 | 文本格式,服务端直接解析 -> FK -> MediaPipe33 |
| VMD | `.vmd` | ✅ 内置纯 TS 解析器 | MikuMikuDance 动作数据(二进制,Shift-JIS 骨骼名 + 四元数关键帧);服务端按标准 MMD 骨架做 FK + 腿部 IK 求解 -> MediaPipe33,见 2.3 |
| FBX | `.fbx` | ⚠️ 需 Assimp | 服务端调用 `assimp` 可执行文件导出为 BVH/glTF 后再解析;未安装则 `preprocessState=failed` |
| glTF/GLB | `.gltf` / `.glb` | ⚠️ 需 Assimp | 同上 |
| 关键点 JSON | `.json` | ✅ 直通校验 | 已是 `mediapipe33@0.1.0` 结构,仅做校验与归一化 |
@@ -29,11 +86,11 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)
- 一行结构:
```json
{"frame":0,"timeMs":0.0,"points":[[0.5,0.5,0.0,1.0],"…33 项"],"angles":[1.0,"…9 项"]}
{"frame":0,"timeMs":0.0,"points":[[0.5,0.5,0.0,1.0],"...33 项"],"angles":[1.0,"...9 项"]}
```
- `points[i] = [x, y, z, confidence]`,语义与 `websocket-keypoints.md` 解码后完全一致
- `angles` 为 `skeleton-topology.md` §5 定义的 9 个关节角余弦,顺序固定:
- `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 轴向上、右手系、骨盆为原点**,
@@ -45,7 +102,7 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)
### 2.2 BVH 关节名映射(默认表,可在 `web-server/src/preprocess/joint-map.ts` 扩展)
| MediaPipe 点 | 常见 BVH 关节名(不区分大小写,按序匹配) |
|---|---|
|--|--|
| 0 nose | `Head`, `Neck1`, `Head_Nub` |
| 11 left_shoulder | `LeftShoulder`, `LeftArm`, `LShoulder` |
| 12 right_shoulder | `RightShoulder`, `RightArm`, `RShoulder` |
@@ -62,13 +119,54 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)
| 31 left_foot_index | `LeftToeBase`, `LeftToe`, `LToe` |
| 32 right_foot_index | `RightToeBase`, `RightToe`, `RToe` |
未映射到的点按 `skeleton-topology.md` §5 的层级由父节点推算,
未映射到的点按 `skeleton-topology.md` 5 的层级由父节点推算,
并置 `confidence` 降低(`0.5`),表示估算而非实测。
### 2.3 VMD(MikuMikuDance)解析约定
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` 的
`worldFramesToKeypointFrames()`(与 BVH 完全共用骨盆归零 / 自适应缩放 / 图像空间投影 /
9 个关节角)。固定 **30 fps**(MMD 帧率恒定,上传时填写的 `fps` 被忽略)。
| MediaPipe 点 | MMD 标准骨骼 | 说明 |
|--|--|--|
| 0 nose | `頭` | 取头骨根部再上移,近似面部中心 |
| 11 / 12 left/right_shoulder | `左腕` / `右腕` | 肩关节(不是 `左肩` 骨骼) |
| 13 / 14 left/right_elbow | `左ひじ` / `右ひじ` | |
| 15 / 16 left/right_wrist | `左手首` / `右手首` | |
| 23 / 24 left/right_hip | `左足` / `右足` | 大腿根部 |
| 25 / 26 left/right_knee | `左ひざ` / `右ひざ` | |
| 27 / 28 left/right_ankle | `左足首` / `右足首` | |
| 31 / 32 left/right_foot_index | `左つま先` / `右つま先` | |
骨骼名按 `mmd-skeleton.ts` 的别名表归一(含全角 `IK` / 半角 `IK` 两种写法);
未纳入标准骨架的骨骼(裙摆、物理、表情等)被忽略但会计数并写入预处理日志。
**腿部驱动判定**:`足IK` 轨道存在非零位移 -> 走 IK(用两骨骼解析 IK 把踝拉到 IK
目标点,膝盖朝角色正面弯,不可达时收敛为伸直);否则走 FK(部分动作数据把腿烘焙成
FK 旋转)。两份示例文件分别命中两种路径。
**坐标系**: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`)。
**标准骨架数值来源**:解析参考模型的 PMX 骨骼段实测得到(而非凭经验估计),
再去掉模型自身原点偏移并抹平左右不对称。关键比例:足首 1.43 / ひざ 6.57 /
髋 11.50 / センター 10.88 / 首 16.21 / 頭 17.16(总高约 19.4,髋高比约 59%);
大腿 4.94 + 小腿 5.15 ~ 腿长 10.07。
## 3. 上传限制
| 项 | 值 |
|---|---|
|--|--|
| 单文件上限 | 512 MiB(`UPLOAD_MAX_BYTES` 可覆盖) |
| 允许扩展名 | 上表全部;其他一律 422 |
| 存储路径 | `${STORAGE_DIR}/{kind}/{yyyy}/{mm}/{sha256前2位}/{sha256}.{ext}` |
@@ -77,7 +175,7 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)
## 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 预览
+3 -3
View File
@@ -1,10 +1,10 @@
# contracts/open-source-credits.md — 开源声明锚点 v0.1.0
# contracts/open-source-credits.md - 开源声明锚点 v0.1.0
三端发布产物中的 `THIRD_PARTY.md` 与各自「关于」页面必须包含下表全部条目。
本文件是**唯一来源**,任一端新增依赖须回到 `opendance-web` 更新此表。
| # | 项目 | 许可证 | 源链接 | 使用端 |
|---|---|---|---|---|
|--|--|--|--|--|
| 1 | MediaPipe | Apache License 2.0 | https://github.com/google-ai-edge/mediapipe | Android |
| 2 | Three.js | MIT | https://github.com/mrdoob/three.js | Web 管理端 |
| 3 | @pixiv/three-vrm | MIT | https://github.com/pixiv/three-vrm | Web 管理端 |
@@ -21,7 +21,7 @@
| 14 | Jetpack Compose | Apache License 2.0 | https://developer.android.com/jetpack/compose | Android |
| 15 | mysql2 | MIT | https://github.com/sidorares/node-mysql2 | Web 服务端 |
> 注意:文档模板中列出的 10 项为**最低要求**;第 11–15 项为实际引入的构建期/运行时依赖,
> 注意:文档模板中列出的 10 项为**最低要求**;第 11-15 项为实际引入的构建期/运行时依赖,
> 一并声明更完整。发布时必须保留各许可证副本(见 `web-server/LICENSE-THIRD-PARTY/`)。
## 版权声明补充
+74 -12
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: OpenDance Studio Web 管理服务端 API
version: 0.1.0
version: 0.4.0
description: |
OpenDance Studio 的唯一数据源与契约权威。
契约版本见 contracts/VERSION。字段语义见 contracts/data-models.md。
@@ -11,9 +11,14 @@ info:
- 来源五元组(sourceType/sourceName/sourceUrl/license/author)在三类资产上均为必填
servers:
- url: http://localhost:3000
description: 本地开发
description: 本地开发(软件 API 端口)
- url: http://<host>:3000
description: Docker 部署
description: >-
Linux 业务服务部署的**软件 API 端口**(`PORT`,默认 3000)。Windows/Godot 客户端与
Android App 调用本端口的 /api、/health、/static、/contracts。
管理端页面在**管理端口**(`ADMIN_PORT`,默认 3001),该端口同时提供全部接口;
API 端口不提供管理页面,未命中的路径一律 404。
反向代理由另外的服务提供(非本项目交付物),按用途分别转发到对应端口即可。
tags:
- name: health
@@ -178,7 +183,7 @@ paths:
schema: { type: integer, format: int64 }
- name: format
in: query
schema: { type: string, enum: [bvh, fbx, gltf, glb, keypoints-json] }
schema: { type: string, enum: [bvh, vmd, fbx, gltf, glb, keypoints-json] }
- name: status
in: query
schema: { $ref: '#/components/schemas/PublishStatus' }
@@ -257,7 +262,7 @@ paths:
parameters: [{ $ref: '#/components/parameters/Id' }]
post:
tags: [motions]
summary: 触发 BVH/FBX → MediaPipe33 关键点序列预处理(同步执行)
summary: 触发 BVH/FBX -> MediaPipe33 关键点序列预处理(同步执行)
description: |
同步执行并返回最终状态:成功时 preprocessState=done;
失败时返回 422,且 Motion.preprocessError 记录可读原因。
@@ -280,7 +285,7 @@ paths:
'200':
description: |
application/x-ndjson,每行一个帧对象:
{"frame":0,"timeMs":0,"points":[[x,y,z,conf], …33],"angles":[9 个余弦]}
{"frame":0,"timeMs":0,"points":[[x,y,z,conf], ...33],"angles":[9 个余弦]}
content:
application/x-ndjson:
schema: { type: string }
@@ -312,7 +317,26 @@ paths:
schema: { $ref: '#/components/schemas/ModelPage' }
post:
tags: [models]
summary: 上传模型 + 来源信息(VRM 优先)
summary: 上传模型(GLB/VRM 直接导入;PMX/FBX 服务端转 GLB 或 VRM)
description: |
系统以 **GLB / VRM 为主力格式**(两个客户端都原生加载):
- `.glb` / `.vrm` / `.gltf`:**直接导入**,不重编码
- `.pmx` / `.fbx`:服务端转换,**优先 GLB、其次 VRM**
- `.zip`:**自动搜索包内可用模型** -- `.vrm` -> `.glb` -> `.gltf`(直接导入)
优先,其次 `.pmx` -> `.fbx`(转换)。压缩包原件作为 `model-archive` 资产留存。
转换链路(`model-convert.ts`):`assimp export -> .glb` ->(失败)`-> .gltf` 后
由服务端自行打包成 GLB ->(`MODEL_CONVERT_FORMAT=vrm` 时)在 GLB 之上补
`VRMC_vrm` 扩展产出 VRM 1.0。每一步都 `assimp info` **回读校验**,
并把过程写进 `conversionNote`;贴图会内嵌进 GLB,保证单文件可用。
入库时自动做两件事并写入 `stats` / `thumbnailUrl`:模型分析(面数、骨骼树、
humanoid 覆盖、可驱动性)与缩略图生成。
**状态一律 `pending`(待审核)**:模型必须审核通过后才可能被客户端消费;
设为默认模型请在审核通过时带 `setPrimary=true`,或调用
`POST /api/models/{id}/primary`(仅已发布模型可设)。
requestBody:
required: true
content:
@@ -324,6 +348,7 @@ paths:
file: { type: string, format: binary }
name: { type: string }
isPrimary: { type: boolean }
# 注意:上传不再提供"自动发布/自动设为默认",模型一律进入待审核
sourceType: { $ref: '#/components/schemas/SourceType' }
sourceName: { type: string }
sourceUrl: { type: string }
@@ -373,6 +398,25 @@ paths:
responses:
'204': { description: 已删除 }
/api/models/{id}/reanalyze:
parameters: [{ $ref: '#/components/parameters/Id' }]
post:
tags: [models]
summary: 重新分析模型并重建缩略图(升级回填 / 文件替换后刷新)
description: |
重新计算 `stats`(面数、骨骼树、humanoid 覆盖、可驱动性)并重出缩略图,
只改这两个字段。用于:升级到带 `stats` 的版本后回填历史模型、
模型文件被替换后刷新、缩略图渲染器升级后重出图。
批量回填用 `node scripts/reanalyze-models.mjs --url http://127.0.0.1:3000`。
responses:
'200':
description: 重新分析后的模型
content:
application/json:
schema: { $ref: '#/components/schemas/Model' }
'404': { $ref: '#/components/responses/NotFound' }
'409': { description: 模型没有文件资产,无法分析 }
/api/models/{id}/primary:
parameters: [{ $ref: '#/components/parameters/Id' }]
post:
@@ -444,7 +488,7 @@ paths:
- $ref: '#/components/parameters/Id'
post:
tags: [reviews]
summary: 审核通过 → published
summary: 审核通过 -> published
requestBody:
content:
application/json:
@@ -466,7 +510,7 @@ paths:
- $ref: '#/components/parameters/Id'
post:
tags: [reviews]
summary: 撤回审核 → draft
summary: 撤回审核 -> draft
requestBody:
content:
application/json:
@@ -508,7 +552,7 @@ paths:
- $ref: '#/components/parameters/Id'
post:
tags: [reviews]
summary: 审核驳回 → rejected
summary: 审核驳回 -> rejected
requestBody:
required: true
content:
@@ -587,7 +631,7 @@ components:
enum: [draft, pending, published, rejected]
MotionFormat:
type: string
enum: [bvh, fbx, gltf, glb, keypoints-json]
enum: [bvh, vmd, fbx, gltf, glb, keypoints-json]
ModelFormat:
type: string
enum: [vrm, glb, gltf, fbx]
@@ -620,7 +664,7 @@ components:
type: object
properties:
connected: { type: boolean }
server: { type: string, description: '如 MariaDB 10.11.19' }
server: { type: string, description: '如 MariaDB 10.5+' }
database: { type: string }
migrationsPending: { type: integer }
@@ -744,6 +788,24 @@ components:
fileUrl: { type: string, nullable: true }
fileSize: { type: integer, nullable: true }
thumbnailUrl: { type: string, nullable: true }
conversionNote:
type: string
nullable: true
description: 由压缩包转换而来时的说明;直传模型为 null
fileSha256:
type: string
nullable: true
description: 模型文件(转换产物)的 sha256 -- 每次转换都会变(Assimp 导出非字节级确定),仅作记录
stats:
type: object
nullable: true
description: >-
模型分析结果(面数 / 顶点数 / 贴图 / 骨骼树 / humanoid 覆盖 / 可驱动性),
审核界面据此判断"能不能驱动与评分"。结构见 contracts/model-formats.md 1.1
sourceSha256:
type: string
nullable: true
description: 上传压缩包的 sha256(稳定);默认模型匹配以它为准,非压缩包上传为 null
status: { $ref: '#/components/schemas/PublishStatus' }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
+4 -4
View File
@@ -1,11 +1,11 @@
# contracts/skeleton-topology.md — 骨骼拓扑契约 v0.1.0
# contracts/skeleton-topology.md - 骨骼拓扑契约 v0.1.0
**唯一拓扑:MediaPipe Pose 33 点。禁止引入 SMPL 及其衍生拓扑(许可风险)。**
## 1. 33 点定义与索引
| idx | name | 中文 | 说明 |
|---|---|---|---|
|--|--|--|--|
| 0 | nose | 鼻 | |
| 1 | left_eye_inner | 左眼内 | |
| 2 | left_eye | 左眼 | |
@@ -43,7 +43,7 @@
## 2. 虚拟关节(由 33 点派生,索引 33/34,仅用于层级与角度特征)
| idx | name | 计算 |
|---|---|---|
|--|--|--|
| 33 | neck | `midpoint(left_shoulder(11), right_shoulder(12))` |
| 34 | pelvis | `midpoint(left_hip(23), right_hip(24))` |
@@ -121,5 +121,5 @@ spine = angle(neck, pelvis, midpoint(left_ankle, right_ankle))
`angle(a, b, c)` = 向量 `ba` 与 `bc` 的夹角余弦,即
`dot(normalize(a-b), normalize(c-b))`,落在 `[-1, 1]`。
**DTW 序列结构(冻结)**:每帧 = `[33 × (x, y, z), 9 个关节角余弦] = 108 维`。
**DTW 序列结构(冻结)**:每帧 = `[33 x (x, y, z), 9 个关节角余弦] = 108 维`。
低置信度点(`visibility < 0.5`)在插值阶段由相邻帧线性补全,不参与代价计算。
+10 -10
View File
@@ -1,6 +1,6 @@
# contracts/websocket-keypoints.md — 关键点传输契约 v0.1.0
# contracts/websocket-keypoints.md - 关键点传输契约 v0.1.0
Android 辅助 App(发送端)→ Windows 客户端(接收端)。
Android 辅助 App(发送端)-> Windows 客户端(接收端)。
服务端不参与实时链路,但 T-WEB-006 预处理产出的序列必须与此结构同构。
## 1. 端点
@@ -27,8 +27,8 @@ ws://<windows-host>:8765/pose
bit2 = 上半身模式(仅 0..22 有效)
bit3-7 保留,必须为 0
4 4 frame_id u32 LE 单调递增,从 0 开始
8 231 keypoints 33 × 7B 见下
--------------------------------------------------------------
8 231 keypoints 33 x 7B 见下
--
合计 239
```
@@ -70,7 +70,7 @@ func decode(buf: PackedByteArray) -> Dictionary:
## 3. 文本控制帧(JSON,UTF-8)
### 3.1 `hello` — Android → Windows,连接后立即发送
### 3.1 `hello` - Android -> Windows,连接后立即发送
```json
{
@@ -84,25 +84,25 @@ func decode(buf: PackedByteArray) -> Dictionary:
}
```
### 3.2 `welcome` — Windows → Android
### 3.2 `welcome` - Windows -> Android
```json
{ "type": "welcome", "protocol": "opendance.pose.v1", "sessionId": "uuid", "expectedFps": 30 }
```
### 3.3 `config` — Windows → Android,运行中调整
### 3.3 `config` - Windows -> Android,运行中调整
```json
{ "type": "config", "mirror": true, "targetFps": 30, "upperBodyOnly": false, "resolution": "640x480" }
```
### 3.4 `stats` — Android → Windows,每秒一次
### 3.4 `stats` - Android -> Windows,每秒一次
```json
{ "type": "stats", "fps": 29.6, "dropped": 3, "latencyMs": 42, "confidenceAvg": 0.93 }
```
### 3.5 `bye` — 双向
### 3.5 `bye` - 双向
```json
{ "type": "bye", "reason": "user_stop" }
@@ -117,7 +117,7 @@ func decode(buf: PackedByteArray) -> Dictionary:
"frameId": 1024,
"timestampMs": 1730000000000,
"flags": { "mirrored": false, "upperBodyOnly": false },
"points": [[0.51, 0.22, -0.03, 0.99], "…共 33 项,顺序固定 [x, y, z, conf]"]
"points": [[0.51, 0.22, -0.03, 0.99], "...共 33 项,顺序固定 [x, y, z, conf]"]
}
```
+2 -2
View File
@@ -5,7 +5,7 @@ default_branch: main
engine: Godot 4.7.2
contracts_version: 0.1.0
# 契约变更流程(文档 §7.2):
# 契约变更流程(文档 7.2):
# 任何跨端契约变更必须先在 opendance-web 修改 contracts/ 并提升版本,
# 再在本项目创建适配任务。本项目内的 contracts/ 只是只读快照。
@@ -42,7 +42,7 @@ tasks:
status: done
- id: T-WIN-006
file: tasks/006-skeleton-retarget.md
scope: 关键点 → VRM 骨骼重定向
scope: 关键点 -> VRM 骨骼重定向
status: done
- id: T-WIN-007
file: tasks/007-scoring-dtw.md
+4 -4
View File
@@ -20,9 +20,9 @@ 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` | 目录缓存 + 模型下载缓存 + 关键点序列缓存 |
@@ -32,8 +32,8 @@ scenes/main.tscn
## 验收
```bash
GODOT --headless --path . --import # 无 SCRIPT ERROR
GODOT --headless --path . --quit-after 180 # 应用可启动,UI 构建无报错
GODOT --headless --path. --import # 无 SCRIPT ERROR
GODOT --headless --path. --quit-after 180 # 应用可启动,UI 构建无报错
```
## 结果
+3 -3
View File
@@ -12,7 +12,7 @@
```
DSH.md
contracts/rest-api.yaml # 锚点:#/paths/~1api~1catalog
contracts/data-models.md # 锚点:§2 Song、§3 Motion、§4 Model、§6 CatalogBundle
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
@@ -21,10 +21,10 @@ 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/net/keypoint_sequence.gd` | NDJSON 关键点序列 -> 特征向量序列 |
| `src/autoload/catalog_store.gd` | 目录刷新、模型文件下载缓存、序列缓存 |
## 关键实现
+3 -3
View File
@@ -11,7 +11,7 @@
```
DSH.md
contracts/websocket-keypoints.md # 锚点:§1 端点与子协议、§3 文本控制帧
contracts/websocket-keypoints.md # 锚点:1 端点与子协议、3 文本控制帧
src/net/pose_server.gd
src/autoload/pose_receiver.gd
```
@@ -24,9 +24,9 @@ src/autoload/pose_receiver.gd
## 能力
| 项 | 说明 |
|---|---|
|--|--|
| 子协议 | 协商 `opendance.pose.v1`,客户端选择不符时告警 |
| 控制帧 | `hello` → 回 `welcome`(含 sessionId);`bye` → 关闭 |
| 控制帧 | `hello` -> 回 `welcome`(含 sessionId);`bye` -> 关闭 |
| 协议不匹配 | 回 `bye{reason: protocol_mismatch}` 并以 1002 关闭 |
| 二进制帧 | 严格校验 239 字节与 magic,畸形帧计数但不影响连接 |
| 丢包统计 | 按 `frame_id` 跳号计数(含 u32 回绕保护) |
+4 -4
View File
@@ -11,8 +11,8 @@
```
DSH.md
contracts/websocket-keypoints.md # 锚点:§2 二进制帧(冻结)
contracts/skeleton-topology.md # 锚点:§4 坐标系
contracts/websocket-keypoints.md # 锚点:2 二进制帧(冻结)
contracts/skeleton-topology.md # 锚点:4 坐标系
src/net/keypoint_frame.gd
```
@@ -24,7 +24,7 @@ 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 × 7B:s16 x, s16 y, s16 z, u8 conf
8 231 keypoints 33 x 7B:s16 x, s16 y, s16 z, u8 conf
合计 239
```
@@ -35,7 +35,7 @@ src/net/keypoint_frame.gd
## 关键实现
- 归一化系数统一 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()`
- 极值(±1.0)不溢出 int16
+3 -3
View File
@@ -11,8 +11,8 @@
```
DSH.md
contracts/model-formats.md # 锚点:§1 角色模型
contracts/data-models.md # 锚点:§4.1 VRM 优先规则
contracts/model-formats.md # 锚点:1 角色模型
contracts/data-models.md # 锚点:4.1 VRM 优先规则
src/model/vrm_loader.gd
src/model/humanoid_map.gd
```
@@ -29,7 +29,7 @@ src/model/humanoid_map.gd
`HumanoidMap` 覆盖 VRoid / VRM 0.x / Mixamo 三类命名,归一化后精确比较:
- 剥离前缀 `jbip` / `mixamorig` / `bip01`(**归一化之后**的形态)
- 生成 compact(left→l)与 expanded(l→left)两种变体比较
- 生成 compact(left->l)与 expanded(l->left)两种变体比较
- **故意不剥离 `J_Aim_` / `J_Roll_` / `J_Sec_`**:VRoid 用它们标记扭转辅助骨,
剥离后 `J_Aim_L_UpperLeg` 会与真正的 `J_Bip_L_UpperLeg` 撞名
+9 -9
View File
@@ -1,4 +1,4 @@
# T-WIN-006 关键点 → VRM 骨骼重定向
# T-WIN-006 关键点 -> VRM 骨骼重定向
- **状态**:done
- **依赖**:T-WIN-005、T-WIN-004
@@ -11,7 +11,7 @@
```
DSH.md
contracts/skeleton-topology.md # 锚点:§1 33 点、§2 虚拟关节、§4 坐标系
contracts/skeleton-topology.md # 锚点:1 33 点、2 虚拟关节、4 坐标系
src/anim/skeleton_retarget.gd
```
@@ -20,9 +20,9 @@ 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`
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 的合成顺序,
在真实 VRoid 模型上暴力枚举 5 种候选公式,只有该式误差为 1.9e-8(其余 ~0.14)。
@@ -30,15 +30,15 @@ src/anim/skeleton_retarget.gd
## 坐标转换
契约 §4 的 MediaPipe 归一化坐标 → Godot 世界方向:
`Vector3(x × aspect, −y, −z × aspect)`(x/z 以图像宽为基准故乘宽高比)。
契约 4 的 MediaPipe 归一化坐标 -> Godot 世界方向:
`Vector3(x x aspect, -y, -z x aspect)`(x/z 以图像宽为基准故乘宽高比)。
## 验收
`tests/test_retarget.gd` —— 最关键的是**功能性**断言:
`tests/test_retarget.gd` -- 最关键的是**功能性**断言:
```
→ 左上臂方向点积 1.0000,左前臂方向点积 1.0000
-> 左上臂方向点积 1.0000,左前臂方向点积 1.0000
```
即:给定一组关键点,重定向后骨骼在世界空间的**实际指向**与关键点方向一致;
+8 -8
View File
@@ -11,29 +11,29 @@
```
DSH.md
contracts/skeleton-topology.md # 锚点:§5 派生特征
contracts/model-formats.md # 锚点:§2.1 预处理产物
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 冻结)
## 特征(契约 5 冻结)
每帧 = `[33 × (x,y,z), 9 个关节角余弦]` = **108 维**。
每帧 = `[33 x (x,y,z), 9 个关节角余弦]` = **108 维**。
低置信度点(conf < 0.5)对应维度**权重置 0**,不参与代价计算。
## DTW
- Sakoe-Chiba 带状约束 `|i − j| ≤ band`(默认 15 帧 ≈ 0.5 秒)
- Sakoe-Chiba 带状约束 `|i - j| <= band`(默认 15 帧 ~ 0.5 秒)
- `DtwMatcher.compute_full()` 离线段;`StreamingDtw` 逐帧流式推进,
每帧只计算带内一行,总体复杂度 O(n·m·band) 但摊到每帧 O(m·band)
每帧只计算带内一行,总体复杂度 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。
`score = clamp(1 - cost/tolerance, 0, 1) x 100`,EMA 平滑抑制抖动,
等级 S(>=95) / A(>=85) / B(>=70) / C(>=55) / D,连击阈值 70。
## 验收
+4 -4
View File
@@ -5,7 +5,7 @@
## 目标
三页式体验:主菜单(选曲/选动作/选模型)→ 游戏页(3D 角色 + 实时评分 HUD)→ 结算页。
三页式体验:主菜单(选曲/选动作/选模型)-> 游戏页(3D 角色 + 实时评分 HUD)-> 结算页。
## load
@@ -21,14 +21,14 @@ src/ui/ui_kit.gd
## 页面
| 页面 | 能力 |
|---|---|
|--|--|
| 主菜单 | 三栏选择(曲目 / 可评分动作 / 模型)、服务端地址编辑与测试、目录刷新、接收服务状态与重启 |
| 游戏页 | SubViewport 3D 角色、自动定机位、实时分数/等级/连击、进度条、连接状态提示、来源展示 |
| 结算 | 等级大字、平均/最终分、完成度、最高单帧、最长连击、评语、来源展示 |
## 关键实现
- 界面全部代码构建(见 DSH.md §6),主菜单只列出 `preprocessState == "done"` 的动作
- 界面全部代码构建(见 DSH.md 6),主菜单只列出 `preprocessState == "done"` 的动作
- 游戏页订阅 `PoseReceiver.frame_received` 驱动骨骼,`ScoreService` 独立订阅做评分
- 未连接设备时给出明确提示而非静默失败;模型/序列拉取失败时显示可读原因并允许重试
- 参考序列走完自动结算
@@ -36,5 +36,5 @@ src/ui/ui_kit.gd
## 验收
`GODOT --headless --path . --quit-after 180` 启动无运行时错误;
`GODOT --headless --path. --quit-after 180` 启动无运行时错误;
主菜单、游戏页、结算页的构造路径均被该启动流程覆盖。
+2 -2
View File
@@ -11,7 +11,7 @@
```
DSH.md
contracts/data-models.md # 锚点:§1 来源五元组
contracts/data-models.md # 锚点:1 来源五元组
src/ui/source_panel.gd
src/net/catalog_types.gd
```
@@ -23,7 +23,7 @@ src/net/catalog_types.gd
## 展示规则
- 标题 + 来源类型中文标签(原创 / 已获授权 / Creative Commons / 公共领域 / 用户上传 / 其他)
- 资产名 | 来源名称
- 资产名 | 来源名称
- 作者
- 许可 + **可点击外链**跳转到 `sourceUrl`
- **五元组不齐备时显式告警**("⚠ 来源信息不完整,请联系管理员补全后再使用"),不静默展示
+4 -4
View File
@@ -19,9 +19,9 @@ export_presets.cfg
## 交付物
| 文件 | 说明 |
|---|---|
|--|--|
| `export_presets.cfg` | 导出预设(**必须在项目根目录**,Godot 硬性要求) |
| `build/export-windows.sh` | Linux 导出:定位 Godot → 检查模板 → 导入 → 导出 → **校验 PE 头** |
| `build/export-windows.sh` | Linux 导出:定位 Godot -> 检查模板 -> 导入 -> 导出 -> **校验 PE 头** |
| `build/export-windows.ps1` | Windows 兜底:同上,产物路径与校验逻辑一致 |
## 脚本健壮性
@@ -31,10 +31,10 @@ export_presets.cfg
- 导出命令返回非 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,未实机执行 |
| 生成的 exe 在 Windows 10/11 可运行 | ⚠️ 本环境无 Windows;已校验为合法 PE32+ x86_64 可执行文件 |