feat(web): 端口分离 + VMD 动作支持;契约升至 0.2.0

需求 1:管理端端口与软件 API 端口分离
- 软件 API 端口 PORT(默认 3000)只提供 /api、/health、/static、/contracts,
  不提供管理页面(未命中路径 404);管理端端口 ADMIN_PORT(默认 3001)
  提供管理页面与全部接口;ADMIN_HOST 可单独限制管理端来源
- main.ts 同进程监听两个端口,用 req.socket.localPort 区分端口行为;
  ADMIN_PORT=PORT 为单端口兼容模式(只创建一个 server,无 EADDRINUSE)
- 端口非法在生产环境中止启动;ADMIN_PORT=PORT 只 warn
- 启动日志分别打印两个端口的访问地址

需求 2:动作数据支持 VMD(MikuMikuDance)
- vmd-parser.ts:二进制解析(30 字节头 + Shift-JIS 模型名 + 111 字节/条骨骼
  关键帧 + morph 段)、MMD 贝塞尔插值、GBK 模型名回退、固定 30fps
- mmd-skeleton.ts:标准 MMD 骨架;静止比例从参考 PMX 骨骼段实测得到
  (足首 1.43 / 膝 6.57 / 髋 11.50 / 首 16.21 / 頭 17.16,腿长 10.09)
- mmd-pose.ts:FK + 两骨骼解析腿部 IK(膝盖朝正面,不可达收敛为伸直)
  + 左手系→右手系 Z 镜像(由真实数据对照实验确定:94.1% vs 5.9%)
- retarget.ts:抽出 worldFramesToKeypointFrames(),BVH 与 VMD 共用重定向
- MotionFormat 增 vmd、扩展名映射增 .vmd;管理端上传支持 .vmd
- scripts/inspect-vmd.mjs:上传前自查工具

契约:rest-api / data-models / model-formats → 0.2.0(另两库已同步逐字节一致)

验证:85 项单测通过(VMD 31 项);契约与 29 条路由一致;typecheck/build 通过;
真实样本经 HTTP 端到端:霜雪千年 2901 帧(IK 腿)、恋爱循环 4604 帧(FK 腿)
均 preprocessState=done、0 NaN、0 越界(测试数据已从数据库清理)。
This commit is contained in:
2026-09-15 08:47:37 +08:00
parent 76725e5ce3
commit bba5bcf59e
35 changed files with 2658 additions and 192 deletions
+43 -1
View File
@@ -1,4 +1,4 @@
# contracts/model-formats.md — 模型与动作格式契约 v0.1.0
# contracts/model-formats.md — 模型与动作格式契约 v0.2.0
## 1. 角色模型(models)
@@ -19,6 +19,7 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)
| 格式 | 扩展名 | 预处理支持 | 说明 |
|---|---|---|---|
| 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` 结构,仅做校验与归一化 |
@@ -65,6 +66,47 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)
未映射到的点按 `skeleton-topology.md` §5 的层级由父节点推算,
并置 `confidence` 降低(`0.5`),表示估算而非实测。
### 2.3 VMD(MikuMikuDance)解析约定
VMD 是二进制格式:文件头 `Vocaloid Motion Data 0002`(30 字节)+ 模型名(20 字节,
Shift-JIS)+ 骨骼关键帧数组(每条 **111 字节**:15 字节骨骼名、uint32 帧号、
float32×3 位移、float32×4 旋转四元数、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. 上传限制
| 项 | 值 |