Files
opendance-web/contracts/data-models.md
T
opendance 39bfd390e3 feat(models): 模型一律转 VRM(契约 0.5.0)+ 可用性门槛 + 管理端版本号展示
按《DSH 开发文档 v1.6》4.9 与第十一章实现,Web 端第一步:

1. 契约先行(先改 contracts/ 再同步三端):
   * model-formats.md -> 0.5.0:模型主力格式改为 VRM,所有上传模型一律转 VRM
     (.pmx/.fbx: assimp -> GLB -> VRM;glb/gltf: 包装为 VRM);
     新增 humanoid 硬门槛与"不达标回退 GLB + usable=false"的交付表;
   * data-models.md -> 0.5.0:Model 新增 usable / unusableReason,明确
     usable=false 不进入 catalog/export;format 说明改为主力 vrm;
   * VERSION 与 contract-versions.ts 同步(三端契约文件逐字节一致)。
2. 实现:
   * MODEL_CONVERT_FORMAT 默认改为 vrm(deploy/.env.example 同步);
   * 新增 isUsableModel()/unusableReason():usable 等价于"已产出 VRM",
     原因从 stats 里带出 humanoid 覆盖度;
   * catalog/export 与 catalog/version 指纹都只统计 format=vrm 的已发布模型,
     resolveDefaultModelId 同样只在可用模型中选;
   * 管理端:模型列表与审核面板显示「不可用」及原因;
   * 管理端左下角展示当前版本号(构建期从 web-server/package.json 注入
     __APP_VERSION__,与 GET /health 同源),并去掉写死的"契约 rest-api 0.1.0"。
3. 测试:新增 test/model-usability.spec.ts 5 项;规模 16 文件 / 225 项。
4. 版本 0.3.0。
2026-09-17 00:05:02 +08:00

6.9 KiB

contracts/data-models.md - 数据模型契约 v0.5.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。所有上传模型一律转换为 VRM,只有 humanoid 不达标时回退为 glb 并置 usable=false,见 model-formats.md 1.1
vrmVersion string|null 读 0.x / 1.0,非 VRM 为 null
usable boolean 读 是否可被客户端使用:等价于"已产出 VRM"(format=vrm)。false 的模型不出现在 catalog/export(v0.5.0)
unusableReason string|null 读 usable=false 时的原因,例如 humanoid 不达标(0/22),未产出 VRM
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 默认模型选择规则(冻结)

服务端 GET /api/catalog/export 的 defaultModelId 按以下顺序解析:

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. 无可用模型 -> 客户端使用内置占位模型

第 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
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 的规则解析得出,客户端无需自行实现优先级。