diff --git a/DSH.md b/DSH.md index 1fc7cad..2e9eab1 100644 --- a/DSH.md +++ b/DSH.md @@ -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,7 +52,7 @@ Web 服务端 ──GET /api/catalog/export──────┤ ## 4. 上下文预算 | 项 | 建议值 | -|---|---| +|--|--| | 单任务文件数 | ≤ 6 | | 单任务代码行数 | ≤ 1500 | | 单任务 Token | ≤ 12K | @@ -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,7 +99,7 @@ opendance-windows/ 1. **绝不让视频帧离开设备**:本端只接收 33 个关键点的坐标,不接收也不存储图像。 2. **契约先行**:改交互先改 `opendance-web` 的 `contracts/`,再回本仓库同步快照。 -3. **不假设骨骼轴向约定**:重定向基于「静止朝向 → 目标朝向」的最小旋转, +3. **不假设骨骼轴向约定**:重定向基于「静止朝向 -> 目标朝向」的最小旋转, `pose = parent_global_basis⁻¹ × 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. 上下文超标时的拆分策略 | 超标原因 | 拆分方式 | -|---|---| +|--|--| | 单任务涉及多个界面 | 按界面拆(主/游戏/结算/关于) | | 单文件过大 | 按类拆,先生成接口再分实现 | | 契约全文加载 | 改用锚点引用 | diff --git a/README.md b/README.md index 8077cbe..10b522e 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ OpenDance Studio **阶段二**:Windows 客户端(Godot 4)。 ## 状态 -**阶段二已完成**:T-WIN-001 → T-WIN-011 全部落地。 +**阶段二已完成**:T-WIN-001 -> T-WIN-011 全部落地。 | 验收项 | 结果 | -|---|---| +|--|--| | 脚本编译 | ✓ 全部通过(无 SCRIPT ERROR) | | 无头测试 | ✓ **580 项全部通过**(8 个测试套件) | | 应用启动 | ✓ 180 帧无运行时错误 | @@ -24,7 +24,7 @@ OpenDance Studio **阶段二**:Windows 客户端(Godot 4)。 ## 快速开始 ```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:////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:////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),版权原因不入库; diff --git a/README.txt b/README.txt index a60c1ab..6ad6f64 100644 --- a/README.txt +++ b/README.txt @@ -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:////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 使用说明 关于页面 - 版本与契约版本、隐私说明、完整可滚动的第三方开源项目声明 ----------------------------------------------------------------- -三、配置文件 ----------------------------------------------------------------- +-- +四、配置文件 +-- 首次运行会在以下位置生成配置文件,可直接用文本编辑器修改: @@ -74,9 +177,9 @@ OpenDance Studio —— Windows 客户端 v0.1.0 使用说明 score_tolerance 评分容差:平均每步代价达到该值即 0 分(默认 0.25) mirror_preview 镜像预览 ----------------------------------------------------------------- -四、防火墙与网络 ----------------------------------------------------------------- +-- +五、防火墙与网络 +-- 本客户端需要接受 Android 设备的入站连接: @@ -86,9 +189,9 @@ OpenDance Studio —— Windows 客户端 v0.1.0 使用说明 若端口被占用,可修改 opendance.cfg 中的 listen_port 后重启。 ----------------------------------------------------------------- -五、评分说明 ----------------------------------------------------------------- +-- +六、评分说明 +-- - 每帧特征 = 33 点 × (x,y,z) + 9 个关节角余弦 = 108 维 (与 Web 服务端预处理产物、Android 端推流结构完全一致) @@ -97,9 +200,9 @@ OpenDance Studio —— Windows 客户端 v0.1.0 使用说明 - 得分 = clamp(1 − 平均代价 / 容差, 0, 1) × 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 点拓扑。 diff --git a/contracts/data-models.md b/contracts/data-models.md index d4d7c13..978f5f3 100644 --- a/contracts/data-models.md +++ b/contracts/data-models.md @@ -1,4 +1,4 @@ -# contracts/data-models.md — 数据模型契约 v0.4.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,7 +52,7 @@ rejected 已驳回 ## 2. `Song` | 字段 | 类型 | 必填 | 说明 | -|---|---|---|---| +|--|--|--|--| | `id` | number | 读 | | | `title` | string | ✅ | 曲名,≤200 | | `artist` | string | ✅ | 演唱者,≤200 | @@ -71,7 +71,7 @@ rejected 已驳回 ## 3. `Motion`(动作数据) | 字段 | 类型 | 必填 | 说明 | -|---|---|---|---| +|--|--|--|--| | `id` | number | 读 | | | `songId` | number\|null | | 关联曲目 | | `name` | string | ✅ | 动作名称,≤200 | @@ -82,7 +82,7 @@ rejected 已驳回 | `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,19 +93,19 @@ rejected 已驳回 ## 4. `Model`(角色模型,VRM 优先) | 字段 | 类型 | 必填 | 说明 | -|---|---|---|---| +|--|--|--|--| | `id` | number | 读 | | | `name` | string | ✅ | 模型名称,≤200 | -| `format` | enum | ✅ | `glb` / `vrm`(主力)/ `gltf` / `fbx`(历史数据)。上传的 PMX/FBX 会被服务端转换成 glb 或 vrm 再入库,见 model-formats.md §1.1 | +| `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 —— **每次转换都会变**,仅作记录 | +| `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 | +| `stats` | object\|null | 读 | 模型分析结果:面数 / 顶点数 / 贴图 / **骨骼树** / humanoid 覆盖 / 可驱动性,见 model-formats.md 1.1 | | `source*` | | ✅ | 来源五元组 | | `status` | `PublishStatus` | | | | `createdAt` / `updatedAt` | string | 读 | | @@ -124,7 +124,7 @@ rejected 已驳回 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 模型压缩包并转换成功后,服务端会把该模型 @@ -135,7 +135,7 @@ rejected 已驳回 ## 5. `ReviewRecord`(审核流水) | 字段 | 类型 | 说明 | -|---|---|---| +|--|--|--| | `id` | number | | | `targetType` | enum | `song` / `motion` / `model` | | `targetId` | number | | diff --git a/contracts/database.md b/contracts/database.md index 7860b4e..161f404 100644 --- a/contracts/database.md +++ b/contracts/database.md @@ -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 ``` @@ -39,17 +39,17 @@ DB_LOGGING=false ## 增量迁移(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) | 表 | 用途 | 任务 | -|---|---|---| +|--|--|--| | `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 | diff --git a/contracts/model-formats.md b/contracts/model-formats.md index ad0a74b..c8db38d 100644 --- a/contracts/model-formats.md +++ b/contracts/model-formats.md @@ -1,15 +1,15 @@ -# contracts/model-formats.md — 模型与动作格式契约 v0.4.0 +# contracts/model-formats.md - 模型与动作格式契约 v0.4.0 ## 1. 角色模型(models) **系统以 GLB / VRM 为主力格式**(两个客户端都原生加载),其余格式一律在服务端转换。 | 格式 | 扩展名 | 支持级别 | 服务端行为 | -|---|---|---|---| +|--|--|--|--| | 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 | +| 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) | @@ -21,18 +21,18 @@ #### 单文件上传 | 输入 | 行为 | -|---|---| +|--|--| | `.glb` / `.vrm` | 直接导入;`.glb` 若含 `VRMC_vrm`/`VRM` 扩展会被识别为 VRM | | `.gltf` | 直接导入(外部 `.bin` / 贴图不在同一文件里,会缺资源) | -| `.pmx` / `.fbx` | 转换:`ASSIMP_BIN` 导出 → 回读校验(见下) | +| `.pmx` / `.fbx` | 转换:`ASSIMP_BIN` 导出 -> 回读校验(见下) | #### 压缩包上传(自动搜索) 不再"只找 `.pmx`",而是**按格式优先级自动搜索包内可用模型**: ``` -.vrm → .glb → .gltf (直接导入,优先) -.pmx → .fbx (需要转换) +.vrm ->.glb ->.gltf (直接导入,优先) +.pmx ->.fbx (需要转换) ``` 同一优先级内:`MODEL_CONVERT_PMX` 指定的文件名优先,其次目录最浅,最后文件最大。 @@ -41,31 +41,31 @@ #### 转换与校验(PMX / FBX) | 步骤 | 规则 | -|---|---| -| 1. GLB 直出 | `assimp export → .glb`,成功后把 Assimp 写成外部 URI 的贴图**内嵌**进 BIN chunk | -| 2. glTF 打包 | 上一步失败时 `assimp export → .gltf`,由 `gltf-packer.ts` 把 `.gltf` + `.bin` + 贴图自行合成 GLB | +|--|--| +| 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**,不产生模型记录 | +| 说明 | 每次尝试都写进 `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 | +| 默认模型 | 只在**审核通过并勾选"同时设为默认"**(或 `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 优先**的选取顺序见 `data-models.md` 4.1。 VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)或 `extensions.VRMC_vrm`(1.0),写入 `models.vrm_version`。 @@ -73,9 +73,9 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x) ## 2. 动作数据(motions) | 格式 | 扩展名 | 预处理支持 | 说明 | -|---|---|---|---| -| BVH | `.bvh` | ✅ 内置纯 TS 解析器 | 文本格式,服务端直接解析 → FK → MediaPipe33 | -| VMD | `.vmd` | ✅ 内置纯 TS 解析器 | MikuMikuDance 动作数据(二进制,Shift-JIS 骨骼名 + 四元数关键帧);服务端按标准 MMD 骨架做 FK + 腿部 IK 求解 → MediaPipe33,见 §2.3 | +|--|--|--|--| +| 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` 结构,仅做校验与归一化 | @@ -86,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 轴向上、右手系、骨盆为原点**, @@ -102,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` | @@ -119,7 +119,7 @@ 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)解析约定 @@ -130,13 +130,13 @@ float32×3 位移、float32×4 旋转四元数、64 字节贝塞尔插值参数 灯光 / 自阴影 / IK / 可见性段落。**VMD 不含骨骼长度**,只有旋转(少数骨骼带位移), 因此服务端必须自带一套标准 MMD 骨架才能做 FK。 -处理链路:`vmd-parser.ts`(二进制 + Shift-JIS + 贝塞尔插值)→ `mmd-skeleton.ts` -(标准骨架)→ `mmd-pose.ts`(FK + 腿部 IK)→ `retarget.ts` 的 +处理链路:`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 | `左ひじ` / `右ひじ` | | @@ -149,12 +149,12 @@ float32×3 位移、float32×4 旋转四元数、64 字节贝塞尔插值参数 骨骼名按 `mmd-skeleton.ts` 的别名表归一(含全角 `IK` / 半角 `IK` 两种写法); 未纳入标准骨架的骨骼(裙摆、物理、表情等)被忽略但会计数并写入预处理日志。 -**腿部驱动判定**:`足IK` 轨道存在非零位移 → 走 IK(用两骨骼解析 IK 把踝拉到 IK +**腿部驱动判定**:`足IK` 轨道存在非零位移 -> 走 IK(用两骨骼解析 IK 把踝拉到 IK 目标点,膝盖朝角色正面弯,不可达时收敛为伸直);否则走 FK(部分动作数据把腿烘焙成 FK 旋转)。两份示例文件分别命中两种路径。 **坐标系**:MMD 原生是左手系(正面为 -Z),本项目通路是右手系(正面为 +Z、+X 为角色 -左侧)。VMD 的四元数与位移统一做 Z 镜像(`(x,y,z,w) → (-x,-y,z,w)`、位移 z 取反)后 +左侧)。VMD 的四元数与位移统一做 Z 镜像(`(x,y,z,w) -> (-x,-y,z,w)`、位移 z 取反)后 送入标准骨架。该选择由真实数据对照实验确定:腿部烘焙为 FK 的样本中,`zMirrored` 下 膝盖朝前占比 94.1%,不转换时仅 5.9%(见 `test/mmd-vmd.spec.ts`)。 @@ -166,7 +166,7 @@ FK 旋转)。两份示例文件分别命中两种路径。 ## 3. 上传限制 | 项 | 值 | -|---|---| +|--|--| | 单文件上限 | 512 MiB(`UPLOAD_MAX_BYTES` 可覆盖) | | 允许扩展名 | 上表全部;其他一律 422 | | 存储路径 | `${STORAGE_DIR}/{kind}/{yyyy}/{mm}/{sha256前2位}/{sha256}.{ext}` | @@ -175,7 +175,7 @@ FK 旋转)。两份示例文件分别命中两种路径。 ## 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 预览 diff --git a/contracts/open-source-credits.md b/contracts/open-source-credits.md index 4a6d166..473d251 100644 --- a/contracts/open-source-credits.md +++ b/contracts/open-source-credits.md @@ -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/`)。 ## 版权声明补充 diff --git a/contracts/rest-api.yaml b/contracts/rest-api.yaml index 66dc71b..e0b9557 100644 --- a/contracts/rest-api.yaml +++ b/contracts/rest-api.yaml @@ -262,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 记录可读原因。 @@ -285,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 } @@ -323,11 +323,11 @@ paths: - `.glb` / `.vrm` / `.gltf`:**直接导入**,不重编码 - `.pmx` / `.fbx`:服务端转换,**优先 GLB、其次 VRM** - - `.zip`:**自动搜索包内可用模型** —— `.vrm` → `.glb` → `.gltf`(直接导入) - 优先,其次 `.pmx` → `.fbx`(转换)。压缩包原件作为 `model-archive` 资产留存。 + - `.zip`:**自动搜索包内可用模型** -- `.vrm` -> `.glb` -> `.gltf`(直接导入) + 优先,其次 `.pmx` -> `.fbx`(转换)。压缩包原件作为 `model-archive` 资产留存。 - 转换链路(`model-convert.ts`):`assimp export → .glb` →(失败)`→ .gltf` 后 - 由服务端自行打包成 GLB →(`MODEL_CONVERT_FORMAT=vrm` 时)在 GLB 之上补 + 转换链路(`model-convert.ts`):`assimp export -> .glb` ->(失败)`-> .gltf` 后 + 由服务端自行打包成 GLB ->(`MODEL_CONVERT_FORMAT=vrm` 时)在 GLB 之上补 `VRMC_vrm` 扩展产出 VRM 1.0。每一步都 `assimp info` **回读校验**, 并把过程写进 `conversionNote`;贴图会内嵌进 GLB,保证单文件可用。 @@ -488,7 +488,7 @@ paths: - $ref: '#/components/parameters/Id' post: tags: [reviews] - summary: 审核通过 → published + summary: 审核通过 -> published requestBody: content: application/json: @@ -510,7 +510,7 @@ paths: - $ref: '#/components/parameters/Id' post: tags: [reviews] - summary: 撤回审核 → draft + summary: 撤回审核 -> draft requestBody: content: application/json: @@ -552,7 +552,7 @@ paths: - $ref: '#/components/parameters/Id' post: tags: [reviews] - summary: 审核驳回 → rejected + summary: 审核驳回 -> rejected requestBody: required: true content: @@ -664,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 } @@ -795,13 +795,13 @@ components: fileSha256: type: string nullable: true - description: 模型文件(转换产物)的 sha256 —— 每次转换都会变(Assimp 导出非字节级确定),仅作记录 + description: 模型文件(转换产物)的 sha256 -- 每次转换都会变(Assimp 导出非字节级确定),仅作记录 stats: type: object nullable: true description: >- 模型分析结果(面数 / 顶点数 / 贴图 / 骨骼树 / humanoid 覆盖 / 可驱动性), - 审核界面据此判断"能不能驱动与评分"。结构见 contracts/model-formats.md §1.1 + 审核界面据此判断"能不能驱动与评分"。结构见 contracts/model-formats.md 1.1 sourceSha256: type: string nullable: true diff --git a/contracts/skeleton-topology.md b/contracts/skeleton-topology.md index c4ba13c..d8c9db0 100644 --- a/contracts/skeleton-topology.md +++ b/contracts/skeleton-topology.md @@ -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))` | diff --git a/contracts/websocket-keypoints.md b/contracts/websocket-keypoints.md index 4930921..fd40f48 100644 --- a/contracts/websocket-keypoints.md +++ b/contracts/websocket-keypoints.md @@ -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. 端点 @@ -28,7 +28,7 @@ ws://:8765/pose bit3-7 保留,必须为 0 4 4 frame_id u32 LE 单调递增,从 0 开始 8 231 keypoints 33 × 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]"] } ``` diff --git a/project.yaml b/project.yaml index 14a07e5..3bda3ba 100644 --- a/project.yaml +++ b/project.yaml @@ -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 diff --git a/tasks/001-scaffold.md b/tasks/001-scaffold.md index 7dbc75e..b325652 100644 --- a/tasks/001-scaffold.md +++ b/tasks/001-scaffold.md @@ -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 构建无报错 ``` ## 结果 diff --git a/tasks/002-rest-client.md b/tasks/002-rest-client.md index a461e60..f2b0e33 100644 --- a/tasks/002-rest-client.md +++ b/tasks/002-rest-client.md @@ -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` | 目录刷新、模型文件下载缓存、序列缓存 | ## 关键实现 diff --git a/tasks/003-websocket-receiver.md b/tasks/003-websocket-receiver.md index 84c06f9..766597d 100644 --- a/tasks/003-websocket-receiver.md +++ b/tasks/003-websocket-receiver.md @@ -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 回绕保护) | diff --git a/tasks/004-keypoint-deserialize.md b/tasks/004-keypoint-deserialize.md index d2fb237..e28742e 100644 --- a/tasks/004-keypoint-deserialize.md +++ b/tasks/004-keypoint-deserialize.md @@ -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 ``` diff --git a/tasks/005-vrm-loader.md b/tasks/005-vrm-loader.md index 6cdabb0..4ac14e9 100644 --- a/tasks/005-vrm-loader.md +++ b/tasks/005-vrm-loader.md @@ -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` 撞名 diff --git a/tasks/006-skeleton-retarget.md b/tasks/006-skeleton-retarget.md index 171198c..6ac52d4 100644 --- a/tasks/006-skeleton-retarget.md +++ b/tasks/006-skeleton-retarget.md @@ -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,7 +20,7 @@ src/anim/skeleton_retarget.gd 1. 每根骨骼指定一条骨链(起点关键点,终点关键点) 2. 取骨骼静止朝向 `rest_dir`(由该骨与首个子骨的位置差得出,**不假设模型骨骼轴向**) 3. 目标朝向由两个关键点在世界空间的方向得出 -4. `delta` = rest_dir → target_dir 的最小旋转四元数(保留扭转/roll) +4. `delta` = rest_dir -> target_dir 的最小旋转四元数(保留扭转/roll) 5. `desired = Basis(delta) × rest_global_basis` 6. `pose_local = parent_global_basis⁻¹ × desired` @@ -30,15 +30,15 @@ src/anim/skeleton_retarget.gd ## 坐标转换 -契约 §4 的 MediaPipe 归一化坐标 → Godot 世界方向: +契约 4 的 MediaPipe 归一化坐标 -> Godot 世界方向: `Vector3(x × aspect, −y, −z × aspect)`(x/z 以图像宽为基准故乘宽高比)。 ## 验收 -`tests/test_retarget.gd` —— 最关键的是**功能性**断言: +`tests/test_retarget.gd` -- 最关键的是**功能性**断言: ``` -→ 左上臂方向点积 1.0000,左前臂方向点积 1.0000 +-> 左上臂方向点积 1.0000,左前臂方向点积 1.0000 ``` 即:给定一组关键点,重定向后骨骼在世界空间的**实际指向**与关键点方向一致; diff --git a/tasks/007-scoring-dtw.md b/tasks/007-scoring-dtw.md index 3d574e0..0def0e6 100644 --- a/tasks/007-scoring-dtw.md +++ b/tasks/007-scoring-dtw.md @@ -11,14 +11,14 @@ ``` 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 维**。 低置信度点(conf < 0.5)对应维度**权重置 0**,不参与代价计算。 diff --git a/tasks/008-game-ui.md b/tasks/008-game-ui.md index 110598f..13659e9 100644 --- a/tasks/008-game-ui.md +++ b/tasks/008-game-ui.md @@ -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` 启动无运行时错误; 主菜单、游戏页、结算页的构造路径均被该启动流程覆盖。 diff --git a/tasks/009-source-display.md b/tasks/009-source-display.md index e958682..82d2712 100644 --- a/tasks/009-source-display.md +++ b/tasks/009-source-display.md @@ -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 ``` diff --git a/tasks/011-build-script.md b/tasks/011-build-script.md index ee84752..4780635 100644 --- a/tasks/011-build-script.md +++ b/tasks/011-build-script.md @@ -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 可执行文件 |