Files
opendance-web/web-server/src/models/model.serializer.ts
T
zijunchiang ccced34dd6 fix(models): 默认模型改用「压缩包 sha256」匹配(转换产物非字节级确定)
实测发现:同一份 zip 连续转换三次得到三个不同的产物 sha256(大小都是 1,152,464
字节,差异集中在 BIN chunk 的十几字节),说明 Assimp 的 GLB 导出不是字节级确定的。
因此原先把「转换产物 sha256」当作默认模型匹配键的设计是错的——换一次转换就失效。

- default-model.ts / default-model-source.ts:种子拆成 sourceSha256(上传压缩包的
  sha256,稳定,主匹配键)与 fileSha256(产物 sha256,仅作记录)
- models.service.ts:resolveDefaultModelId 依次按 压缩包 sha256 → 产物 sha256 →
  名称 匹配,都落空才回落到 VRM 优先规则并告警
- ModelDto 增加 sourceSha256(非压缩包上传为 null)
- scripts/import-default-model.mjs:写种子时用 sourceSha256;顺带修掉位置参数解析
  (文件当第一个参数时识别不出,会打印用法)
- 契约与文档同步:data-models §4.1、model-formats §1.1 记录该确定性结论;
  README / DSH / tasks/005 的「按产物 sha256 匹配」表述已更正

另附:把模型格式能力的实测矩阵写进 Release 说明(原生只支持 glTF 系;
PMX→FBX 失败;VRM→GLB 丢 VRM 扩展;humanoid 骨骼命名覆盖率)。

验证:125 项单测通过;清理掉两个"文件只在本机临时目录"的模型(避免 sha256 去重
导致后续上传不落盘);种子按压缩包 sha256 命中后 defaultModelId 正确解析。
2026-09-15 19:36:37 +08:00

61 lines
1.9 KiB
TypeScript

import { ModelFormat, PublishStatus, SourceType } from '../common/enums';
import { CharacterModel } from '../entities/model.entity';
/** 契约 data-models.md §4 的对外表示 */
export interface ModelDto {
id: number;
name: string;
format: ModelFormat;
vrmVersion: string | null;
isPrimary: boolean;
fileUrl: string | null;
fileSize: number | null;
/** 模型文件(转换产物)的 sha256 —— 每次转换都会变,仅作记录 */
fileSha256: string | null;
/** 上传压缩包的 sha256(稳定);非压缩包上传为 null,此时以 fileSha256 为准 */
sourceSha256: string | null;
thumbnailUrl: string | null;
/** 由压缩包转换而来时的说明,例如 `Hatsune Miku.pmx → GLB,已内嵌 2 张贴图` */
conversionNote: string | null;
sourceType: SourceType;
sourceName: string;
sourceUrl: string;
license: string;
author: string;
status: PublishStatus;
createdAt: string;
updatedAt: string;
}
export interface ModelUrlLookup {
fileUrl: string | null;
thumbnailUrl: string | null;
fileSize?: number | null;
fileSha256?: string | null;
sourceSha256?: string | null;
}
export function toModelDto(model: CharacterModel, urls: ModelUrlLookup): ModelDto {
return {
id: model.id,
name: model.name,
format: model.format,
vrmVersion: model.vrmVersion,
isPrimary: model.isPrimary,
fileUrl: urls.fileUrl,
fileSize: urls.fileSize ?? null,
fileSha256: urls.fileSha256 ?? null,
sourceSha256: urls.sourceSha256 ?? null,
thumbnailUrl: urls.thumbnailUrl,
conversionNote: model.conversionNote ?? null,
sourceType: model.sourceType,
sourceName: model.sourceName,
sourceUrl: model.sourceUrl,
license: model.license,
author: model.author,
status: model.status,
createdAt: model.createdAt.toISOString(),
updatedAt: model.updatedAt.toISOString(),
};
}