Files
opendance-web/contracts/data-models.md
T
zijunchiang bba5bcf59e 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 越界(测试数据已从数据库清理)。
2026-09-15 08:47:37 +08:00

5.1 KiB
Raw Blame History

contracts/data-models.md — 数据模型契约 v0.2.0

Web 服务端是唯一权威定义,本文件是另两端消费的稳定视图。 字段名在 JSON 中统一 camelCase;数据库列名 snake_case(见 database.md)。

1. 通用约定

  • 所有资源返回 id: number(BIGINT,超出 2^53 前不会发生)。
  • 所有时间为 ISO 8601 UTC 字符串,例:2026-09-14T10:00:00.000Z。
  • 列表接口统一分页响应:
{ "items": [], "total": 0, "page": 1, "pageSize": 20 }
  • 错误响应统一:
{ "statusCode": 400, "message": "…", "error": "Bad Request", "details": {} }
  • 来源(出处)五元组是必填项,出现在 songs / motions / models 三处:
字段 类型 说明
sourceType enum 见 1.1
sourceName string 来源名称,如「官方曲库」「作者名」
sourceUrl string 来源链接,必须为 http(s)
license string 许可标识,如 CC-BY-4.0、MIT、proprietary、unknown
author string 作者 / 权利人

1.1 SourceType 枚举

original      原创
licensed      已获授权
cc            Creative Commons
public_domain 公共领域
user_upload   用户上传(需审核)
other         其他(须在 sourceName 说明)

1.2 PublishStatus 枚举

draft      草稿,仅管理端可见
pending    待审核
published  已发布,客户端可见
rejected   已驳回

2. Song

字段 类型 必填 说明
id number 读
title string ✅ 曲名,≤200
artist string ✅ 演唱者,≤200
album string 专辑,≤200
language string zh / en / ja / ko / instrumental
durationMs number 时长毫秒
bpm number 节拍
difficulty number 1..5
tags string[]
coverUrl string|null 封面访问地址
audioUrl string|null 音频访问地址
source* ✅ 来源五元组
status PublishStatus 默认 draft
createdAt / updatedAt string 读

3. Motion(动作数据)

字段 类型 必填 说明
id number 读
songId number|null 关联曲目
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 封装)
keypointFrames number|null 读 关键点帧数
preprocessState enum 读 none / running / done / failed
preprocessError string|null 读
source* ✅ 来源五元组
status PublishStatus
createdAt / updatedAt string 读

4. Model(角色模型,VRM 优先)

字段 类型 必填 说明
id number 读
name string ✅ 模型名称,≤200
format enum ✅ vrm / glb / gltf / fbx
vrmVersion string|null 读 0.x / 1.0,非 VRM 为 null
isPrimary boolean 全局默认模型;同一时刻全库至多一个 true
fileUrl string 读
fileSize number 读
thumbnailUrl string|null 读
source* ✅ 来源五元组
status PublishStatus
createdAt / updatedAt string 读

4.1 VRM 优先规则(冻结)

客户端选择默认模型的顺序:

1. 显式指定 modelId(曲目动作绑定)
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. 无可用模型 → 客户端使用内置占位模型

isPrimary 通过 POST /api/models/:id/primary 设置,服务端保证互斥(同一事务内先清零)。

5. ReviewRecord(审核流水)

字段 类型 说明
id number
targetType enum song / motion / model
targetId number
action enum submit / approve / reject / withdraw
fromStatus / toStatus PublishStatus
reviewer string|null 审核人标识
comment string|null 审核意见
createdAt string

6. CatalogBundle(客户端一次性拉取,GET /api/catalog/export)

{
  "contractVersion": { "rest-api": "0.1.0", "data-models": "0.1.0", "skeleton-topology": "0.1.0", "websocket-keypoints": "0.1.0" },
  "generatedAt": "2026-09-14T10:00:00.000Z",
  "songs": [],
  "motions": [],
  "models": [],
  "defaultModelId": 3
}
  • 仅包含 status = 'published' 的记录。
  • defaultModelId 由 4.1 的规则解析得出,客户端无需自行实现优先级。