需求 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 越界(测试数据已从数据库清理)。
84 lines
3.0 KiB
Markdown
84 lines
3.0 KiB
Markdown
# T-WEB-004 动作数据上传 + 来源字段
|
||
|
||
- **状态**:done
|
||
- **依赖**:T-WEB-002
|
||
|
||
## 目标
|
||
|
||
上传动作文件(BVH / VMD / FBX / glTF / GLB / keypoints-json),**强制填写来源五元组**。
|
||
|
||
## load
|
||
|
||
```
|
||
DSH.md
|
||
contracts/rest-api.yaml # 锚点:#/paths/~1api~1motions
|
||
contracts/data-models.md # 锚点:§3 Motion
|
||
contracts/model-formats.md # 锚点:§2 动作格式、§3 上传限制
|
||
web-server/src/motions/*
|
||
web-server/src/storage/storage.service.ts
|
||
web-server/src/preprocess/vmd-parser.ts # VMD 上传后的预处理链路
|
||
web-server/src/preprocess/mmd-skeleton.ts
|
||
web-server/src/preprocess/mmd-pose.ts
|
||
web-server/src/preprocess/retarget.ts # worldFramesToKeypointFrames()
|
||
```
|
||
|
||
## 端点
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|---|---|---|
|
||
| GET | `/api/motions` | 分页 + `q`/`songId`/`format`/`status` 过滤 |
|
||
| POST | `/api/motions` | multipart 上传 |
|
||
| GET | `/api/motions/:id` | 详情 |
|
||
| PATCH | `/api/motions/:id` | 更新元数据(含 `detachSong`) |
|
||
| DELETE | `/api/motions/:id` | 软删除 |
|
||
|
||
## 存储策略(`contracts/model-formats.md` §3)
|
||
|
||
```
|
||
${STORAGE_DIR}/{kind}/{yyyy}/{mm}/{sha256前2位}/{sha256}.{ext}
|
||
```
|
||
|
||
- 上传先落 `${STORAGE_DIR}/tmp`(multer diskStorage),避免大文件占用内存
|
||
- `StorageService` 流式计算 sha256;**相同内容只落盘一次**,复用 `assets` 记录
|
||
- 并发同文件上传靠 `assets.sha256` 唯一索引兜底,冲突后回退到已存在记录
|
||
- 路径永不使用原始文件名;`absolutePath()` 拒绝逃逸存储根目录
|
||
|
||
## 校验
|
||
|
||
- 扩展名白名单:`.bvh .vmd .fbx .gltf .glb .json`,其他 → 400
|
||
- 来源五元组缺任一项 → 400
|
||
- `songId` 指向不存在的曲目 → 400
|
||
- 单文件上限 `UPLOAD_MAX_BYTES`(默认 512 MiB)
|
||
|
||
## VMD 支持(`.vmd`)
|
||
|
||
- 管理端上传控件已支持 `.vmd`(accept、格式筛选、中文标签「VMD(MMD)」)
|
||
- 入库后 `format=vmd`;预处理走内置链路,不依赖 Assimp:
|
||
`vmd-parser.ts`(二进制 + Shift-JIS + 贝塞尔插值)→ `mmd-skeleton.ts`(标准骨架)
|
||
→ `mmd-pose.ts`(FK + 腿部 IK + Z 镜像)→ `retarget.ts` 的
|
||
`worldFramesToKeypointFrames()`(与 BVH 共用骨盆归零 / 自适应缩放 / 投影 / 关节角)
|
||
- 固定 30 fps;腿部按 `足IK` 是否带位移自动选 IK 或 FK
|
||
- 上传后处理失败与 BVH 一致:写入 `preprocessState=failed` 与可读错误
|
||
|
||
## 验收
|
||
|
||
```bash
|
||
npm run test:smoke
|
||
```
|
||
|
||
冒烟 [2] 段落:上传 201、自动识别 `format=bvh`、`fileSize` 记录、缺来源 400、
|
||
`evil.exe` 400。
|
||
|
||
VMD 实测(用 `../示例文件/` 下的两个样本):
|
||
|
||
| 样本 | 帧数 | 腿部驱动 | 结果 |
|
||
|---|---|---|---|
|
||
| `【动作】霜雪千年.vmd`(0.79 MiB) | 2901 | `足IK` 驱动(IK) | 0 NaN / 0 越界,预处理约 0.4s |
|
||
| `恋爱循环.vmd`(20.24 MiB) | 4604 | 烘焙 FK | 0 NaN / 0 越界,预处理约 2.5s |
|
||
|
||
上传前可先用诊断工具自查(只读):
|
||
|
||
```bash
|
||
node scripts/inspect-vmd.mjs 动作.vmd [--frames N] [--json out.json]
|
||
```
|