From f4cf2c70de6298f2fd9db1762eb15e8e73e7fd0f Mon Sep 17 00:00:00 2001 From: zijunchiang Date: Tue, 15 Sep 2026 13:55:59 +0800 Subject: [PATCH] =?UTF-8?q?chore(contracts):=20=E5=90=8C=E6=AD=A5=E5=A5=91?= =?UTF-8?q?=E7=BA=A6=E5=88=B0=20rest-api/data-models/model-formats=200.3.0?= =?UTF-8?q?=E3=80=81database=200.2.0=EF=BC=88=E6=A8=A1=E5=9E=8B=E5=8E=8B?= =?UTF-8?q?=E7=BC=A9=E5=8C=85=E8=BD=AC=E6=8D=A2=E4=B8=8E=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E8=A7=84=E5=88=99=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/VERSION | 8 ++++---- contracts/data-models.md | 15 +++++++++++---- contracts/database.md | 8 +++++++- contracts/model-formats.md | 20 +++++++++++++++++++- contracts/rest-api.yaml | 30 ++++++++++++++++++++++++++++-- 5 files changed, 69 insertions(+), 12 deletions(-) diff --git a/contracts/VERSION b/contracts/VERSION index 4044fac..29c3f7e 100644 --- a/contracts/VERSION +++ b/contracts/VERSION @@ -1,8 +1,8 @@ # OpenDance Studio 契约版本 # 规则:任何字段级变更必须提升对应条目版本,并回到 opendance-web 修改后再向另两端建迁移任务。 -rest-api: 0.2.0 +rest-api: 0.3.0 websocket-keypoints: 0.1.0 -data-models: 0.2.0 +data-models: 0.3.0 skeleton-topology: 0.1.0 -model-formats: 0.2.0 -database: 0.1.0 +model-formats: 0.3.0 +database: 0.2.0 diff --git a/contracts/data-models.md b/contracts/data-models.md index 5037968..da64438 100644 --- a/contracts/data-models.md +++ b/contracts/data-models.md @@ -1,4 +1,4 @@ -# contracts/data-models.md — 数据模型契约 v0.2.0 +# contracts/data-models.md — 数据模型契约 v0.3.0 Web 服务端是唯一权威定义,本文件是另两端消费的**稳定视图**。 字段名在 JSON 中统一 `camelCase`;数据库列名 `snake_case`(见 `database.md`)。 @@ -102,22 +102,29 @@ rejected 已驳回 | `fileUrl` | string | 读 | | | `fileSize` | number | 读 | | | `thumbnailUrl` | string\|null | 读 | | +| `conversionNote` | string\|null | 读 | 由压缩包转换而来时的说明(`Hatsune Miku.pmx → GLB,已内嵌 2 张贴图`);直传模型为 null | +| `fileSha256` | string\|null | 读 | 模型文件(转换产物)的 sha256,用于换库后定位默认模型 | | `source*` | | ✅ | 来源五元组 | | `status` | `PublishStatus` | | | | `createdAt` / `updatedAt` | string | 读 | | -### 4.1 VRM 优先规则(冻结) +### 4.1 默认模型选择规则(冻结) -客户端选择默认模型的顺序: +服务端 `GET /api/catalog/export` 的 `defaultModelId` 按以下顺序解析: ``` -1. 显式指定 modelId(曲目动作绑定) +0. 环境变量 DEFAULT_MODEL_ID 指定,且该模型存在且已发布 +1. 源码内写死的默认模型种子(web-server/src/config/default-model.ts 的 + DEFAULT_MODEL_SEED):按转换产物 sha256 匹配已发布模型 —— 换库/重建库后仍然有效 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`(审核流水) diff --git a/contracts/database.md b/contracts/database.md index a3596c4..44c0ecc 100644 --- a/contracts/database.md +++ b/contracts/database.md @@ -36,6 +36,12 @@ DB_LOGGING=false - 每个 migration 必须可逆(提供 `down` 方法) - 迁移文件名格式:`-.ts` +## 增量迁移(v0.2.0) + +| 迁移 | 变更 | +|---|---| +| `AddModelConversionFields` | `models` 增加 `source_asset_id BIGINT NULL`(上传的压缩包,用于溯源)与 `conversion_note VARCHAR(500) NULL`(转换说明,见 model-formats.md §1.1) | + ## 表清单(v0.1.0) | 表 | 用途 | 任务 | @@ -43,7 +49,7 @@ DB_LOGGING=false | `songs` | 曲目元数据 | T-WEB-003 | | `assets` | 上传文件元数据(去重、sha256、物理路径) | T-WEB-004 | | `motions` | 动作数据(BVH/FBX…)+ 来源 + 预处理产物 | T-WEB-004 / 006 | -| `models` | 角色模型(VRM 优先)+ 来源 | T-WEB-005 | +| `models` | 角色模型(VRM 优先)+ 来源 + 压缩包转换溯源 | T-WEB-005 | | `review_records` | 上传审核流水 | T-WEB-007 | ## 状态机(`status` 列,v0.1.0) diff --git a/contracts/model-formats.md b/contracts/model-formats.md index b31f67a..73c639e 100644 --- a/contracts/model-formats.md +++ b/contracts/model-formats.md @@ -1,4 +1,4 @@ -# contracts/model-formats.md — 模型与动作格式契约 v0.2.0 +# contracts/model-formats.md — 模型与动作格式契约 v0.3.0 ## 1. 角色模型(models) @@ -8,6 +8,24 @@ | glTF Binary | `.glb` | P1 | 需含骨骼(skin);无骨骼则仅作静态展示 | | glTF | `.gltf` (+ `.bin`, 贴图) | P2 | 多文件,管理端需打包为 zip 上传 | | FBX | `.fbx` | P2 | 需 Assimp 转换,管理端仅归档,不保证重定向质量 | +| **MMD 压缩包** | `.zip` | P1 | 内含 `.pmx`(+ `textures/`)。服务端自动解压并转换,见 §1.1 | + +### 1.1 模型压缩包转换(`.zip` → FBX / GLB) + +上传 `.zip` 时走"解压 → 选主模型 → 转换 → 入库",而不是把压缩包当模型文件存下来。 + +| 步骤 | 规则 | +|---|---| +| 选主模型 | 优先 `MODEL_CONVERT_PMX` 指定的文件名(默认 `Hatsune Miku.pmx`,大小写不敏感);否则取目录最浅、其次文件最大者。**只认 `.pmx`**(MMD 的 PMX 模型;PMD/PMX 之外的格式暂不支持) | +| 转换 | `MODEL_CONVERT_FORMAT`(默认 `fbx`)指定首选格式,调 `ASSIMP_BIN` 导出 | +| 校验 | **导出后必须回读校验**(`assimp info`)。Assimp 的 FBX 导出器对带蒙皮的大骨架模型会"写出损坏文件"(实测 714 骨骼的 PMX:导出退出码 11,产物回读报 `FBX-DOM no Objects dictionary found`),只信退出码会把坏文件入库 | +| 回退 | FBX 失败(导出失败或回读失败)→ 自动改用 **GLB**,并把两次尝试写进 `models.conversion_note` | +| 贴图 | GLB 产物里 Assimp 会把贴图写成外部 URI(还带 `\` 反斜杠),服务端把压缩包内的贴图**内嵌**进 GLB 的二进制块,保证单文件可加载 | +| 溯源 | 压缩包原件以 `model-archive` 资产留存,写入 `models.source_asset_id` | +| 默认模型 | 转换成功且 `autoDefault` 不为 false:`is_primary=1` + `status=published`,并把身份写进源码 `web-server/src/config/default-model.ts`,见 `data-models.md` §4.1 | +| 失败 | 压缩包里没有 `.pmx`、Assimp 不可用、两种格式都失败 → **HTTP 422**,不产生模型记录 | + +安全:解压前逐条校验路径,拒绝绝对路径与 `..`(zip-slip);拒绝加密条目;只支持 STORE / DEFLATE。 **VRM 优先**的选取顺序见 `data-models.md` §4.1。 diff --git a/contracts/rest-api.yaml b/contracts/rest-api.yaml index ed89909..6d9d835 100644 --- a/contracts/rest-api.yaml +++ b/contracts/rest-api.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: OpenDance Studio Web 管理服务端 API - version: 0.2.0 + version: 0.3.0 description: | OpenDance Studio 的唯一数据源与契约权威。 契约版本见 contracts/VERSION。字段语义见 contracts/data-models.md。 @@ -317,7 +317,22 @@ paths: schema: { $ref: '#/components/schemas/ModelPage' } post: tags: [models] - summary: 上传模型 + 来源信息(VRM 优先) + summary: 上传模型 + 来源信息(VRM 优先;支持 MMD 模型压缩包自动转换) + description: | + `file` 为单个文件: + + - `.vrm` / `.glb` / `.gltf` / `.fbx`:直接入库,`format` 由扩展名决定 + - `.zip`:视为 MMD 模型包。服务端解压 → 选取 `.pmx` + (优先 `MODEL_CONVERT_PMX` 指定的名字,默认 `Hatsune Miku.pmx`; + 否则取目录最浅、文件最大者)→ 调 Assimp 导出 **FBX**, + 导出后**回读校验**(Assimp 的 FBX 导出器对带蒙皮的大骨架模型会写出损坏文件), + 失败自动回退 **GLB**,并把过程写入 `conversionNote`。 + GLB 产物会把 Assimp 写成外部 URI 的贴图内嵌进二进制块,保证单文件可用。 + 压缩包原件作为 `model-archive` 资产留存以便溯源。 + + 转换成功且 `autoDefault` 不为 false 时,该模型会被置为全局默认模型 + (`isPrimary=1` + `status=published`),并把身份(名称/格式/产物 sha256) + 写进源码 `web-server/src/config/default-model.ts`。 requestBody: required: true content: @@ -329,6 +344,9 @@ paths: file: { type: string, format: binary } name: { type: string } isPrimary: { type: boolean } + autoDefault: + type: boolean + description: 仅对 `.zip` 生效;转换成功后是否自动设为默认模型,默认 true sourceType: { $ref: '#/components/schemas/SourceType' } sourceName: { type: string } sourceUrl: { type: string } @@ -749,6 +767,14 @@ components: fileUrl: { type: string, nullable: true } fileSize: { type: integer, nullable: true } thumbnailUrl: { type: string, nullable: true } + conversionNote: + type: string + nullable: true + description: 由压缩包转换而来时的说明;直传模型为 null + fileSha256: + type: string + nullable: true + description: 模型文件(转换产物)的 sha256,用于换库后定位默认模型 status: { $ref: '#/components/schemas/PublishStatus' } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time }