cd80c085ff
统一前后端 SFX Prompt Unicode canonicalization、字符计数和 2048 边界 固化 52 个音效预设、追加规则及跨语言测试向量 演进共享音频请求响应、duration、Loop 和 V2 metadata 契约 增加 External model 输入矩阵纯函数并保持正式接线归属 T5 补齐前端提交、读取、深拷贝与请求边界回归测试 更新共享计划并标记 T1 完成且当前不可发布
800 lines
24 KiB
TypeScript
800 lines
24 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import type {
|
|
CanvasGenerationInputs,
|
|
CanvasLayer,
|
|
} from './ImageCanvasEditorTypes';
|
|
import {
|
|
applyImageEditResultToSourceLayer,
|
|
createAudioResultLayer,
|
|
createCharacterAnimationResultLayer,
|
|
createGeneratedResultLayer,
|
|
createIconSpritesheetResultLayers,
|
|
createQuickEditResultLayer,
|
|
createVideoResultLayer,
|
|
} from './ImageCanvasGenerationLayerModel';
|
|
|
|
function createGenerated(overrides = {}) {
|
|
return {
|
|
imageSrc: 'data:image/png;base64,generated',
|
|
width: 1024,
|
|
height: 1024,
|
|
sourceType: 'generated' as const,
|
|
prompt: '生成提示词',
|
|
actualPrompt: '实际提示词',
|
|
model: 'gpt-image-2',
|
|
provider: 'VectorEngine',
|
|
taskId: 'task-generated',
|
|
objectKey: 'generated/object.png',
|
|
assetObjectId: 'asset-object-generated',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createSourceLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
|
return {
|
|
id: 'layer-source',
|
|
resourceId: 'resource-source',
|
|
title: '源图',
|
|
src: 'data:image/png;base64,source',
|
|
x: 120,
|
|
y: 140,
|
|
width: 320,
|
|
height: 240,
|
|
originalWidth: 1536,
|
|
originalHeight: 1024,
|
|
zIndex: 2,
|
|
sourceType: 'generated',
|
|
groupId: 'group-a',
|
|
assetKind: 'spec',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createGenerationInputs(): CanvasGenerationInputs {
|
|
return {
|
|
fields: [{ title: '生成提示词', value: '生成提示词' }],
|
|
references: [],
|
|
};
|
|
}
|
|
|
|
describe('ImageCanvasGenerationLayerModel', () => {
|
|
it('creates generated result layers centered on the active placeholder', () => {
|
|
const layer = createGeneratedResultLayer({
|
|
generated: createGenerated({ width: 2048, height: 1152 }),
|
|
generatedIndex: 7,
|
|
canvasSize: { width: 900, height: 640 },
|
|
viewport: { x: 10, y: 20, scale: 2 },
|
|
frame: {
|
|
x: 80,
|
|
y: 60,
|
|
width: 560,
|
|
height: 315,
|
|
originalWidth: 2048,
|
|
originalHeight: 1152,
|
|
},
|
|
assetKind: 'spec',
|
|
title: '图标规范 7',
|
|
generationInputs: createGenerationInputs(),
|
|
});
|
|
|
|
expect(layer).toMatchObject({
|
|
id: 'layer-generated-7',
|
|
resourceId: 'local-resource-generated-7',
|
|
title: '图标规范 7',
|
|
x: -664,
|
|
y: -358.5,
|
|
width: 2048,
|
|
height: 1152,
|
|
originalWidth: 2048,
|
|
originalHeight: 1152,
|
|
zIndex: 17,
|
|
sourceType: 'generated',
|
|
assetKind: 'spec',
|
|
prompt: '生成提示词',
|
|
actualPrompt: '实际提示词',
|
|
objectKey: 'generated/object.png',
|
|
assetObjectId: 'asset-object-generated',
|
|
});
|
|
expect(layer.generationInputs?.fields[0]?.value).toBe('生成提示词');
|
|
});
|
|
|
|
it('keeps publication material result dimensions constrained by the active placeholder', () => {
|
|
const layer = createGeneratedResultLayer({
|
|
generated: createGenerated({ width: 1024, height: 1024 }),
|
|
generatedIndex: 8,
|
|
canvasSize: { width: 900, height: 640 },
|
|
viewport: { x: 10, y: 20, scale: 2 },
|
|
frame: {
|
|
x: 180,
|
|
y: 120,
|
|
width: 360,
|
|
height: 640,
|
|
originalWidth: 720,
|
|
originalHeight: 1280,
|
|
},
|
|
assetKind: 'publication-material',
|
|
title: '8 宣发素材',
|
|
generationInputs: createGenerationInputs(),
|
|
});
|
|
|
|
expect(layer).toMatchObject({
|
|
id: 'layer-generated-8',
|
|
title: '8 宣发素材',
|
|
width: 720,
|
|
height: 1280,
|
|
originalWidth: 720,
|
|
originalHeight: 1280,
|
|
assetKind: 'publication-material',
|
|
});
|
|
});
|
|
|
|
it('keeps publication cover image results at the 4:3 workflow dimensions', () => {
|
|
const layer = createGeneratedResultLayer({
|
|
generated: createGenerated({ width: 1024, height: 1024 }),
|
|
generatedIndex: 9,
|
|
canvasSize: { width: 900, height: 640 },
|
|
viewport: { x: 10, y: 20, scale: 2 },
|
|
frame: {
|
|
x: 100,
|
|
y: 100,
|
|
width: 480,
|
|
height: 360,
|
|
originalWidth: 720,
|
|
originalHeight: 540,
|
|
},
|
|
assetKind: 'publication-material',
|
|
title: '9 宣发素材',
|
|
generationInputs: createGenerationInputs(),
|
|
});
|
|
|
|
expect(layer).toMatchObject({
|
|
width: 720,
|
|
height: 540,
|
|
originalWidth: 720,
|
|
originalHeight: 540,
|
|
assetKind: 'publication-material',
|
|
});
|
|
});
|
|
|
|
it('uses backend-persisted resource and asset snapshots for generated layers', () => {
|
|
const generationInputs = createGenerationInputs();
|
|
const layer = createGeneratedResultLayer({
|
|
generated: createGenerated({
|
|
resource: {
|
|
resourceId: 'resource-generated-bff',
|
|
projectId: 'editor-project-1',
|
|
imageSrc: 'data:image/png;base64,generated',
|
|
objectKey: 'generated/from-resource.png',
|
|
assetObjectId: 'asset-object-from-resource',
|
|
width: 1024,
|
|
height: 1024,
|
|
sourceType: 'generated',
|
|
assetKind: 'character',
|
|
generationInputs,
|
|
},
|
|
asset: {
|
|
assetId: 'asset-generated-bff',
|
|
folderId: 'user-1:asset-folder:project',
|
|
label: '角色形象',
|
|
imageSrc: 'data:image/png;base64,generated',
|
|
objectKey: 'generated/from-asset.png',
|
|
assetObjectId: 'asset-object-from-asset',
|
|
width: 1024,
|
|
height: 1024,
|
|
sourceType: 'generated',
|
|
assetKind: 'character',
|
|
generationInputs,
|
|
},
|
|
}),
|
|
generatedIndex: 9,
|
|
canvasSize: { width: 900, height: 640 },
|
|
viewport: { x: 0, y: 0, scale: 1 },
|
|
assetKind: 'spec',
|
|
generationInputs: {
|
|
fields: [{ title: '旧输入', value: '本地兜底' }],
|
|
references: [],
|
|
},
|
|
});
|
|
|
|
expect(layer).toMatchObject({
|
|
resourceId: 'resource-generated-bff',
|
|
sourceAssetId: 'asset-generated-bff',
|
|
objectKey: 'generated/from-resource.png',
|
|
assetObjectId: 'asset-object-from-resource',
|
|
assetKind: 'character',
|
|
});
|
|
expect(layer.generationInputs?.fields[0]?.value).toBe('生成提示词');
|
|
expect(layer.generatedAssetSnapshot?.assetId).toBe('asset-generated-bff');
|
|
});
|
|
|
|
it('creates edit result layers beside the source image', () => {
|
|
const sourceLayer = createSourceLayer();
|
|
const layer = createGeneratedResultLayer({
|
|
generated: createGenerated({ prompt: '修改要求' }),
|
|
generatedIndex: 8,
|
|
canvasSize: { width: 900, height: 640 },
|
|
viewport: { x: 0, y: 0, scale: 1 },
|
|
sourceLayer,
|
|
generationInputs: createGenerationInputs(),
|
|
});
|
|
|
|
expect(layer).toMatchObject({
|
|
id: 'layer-edit-8',
|
|
resourceId: 'local-resource-edit-8',
|
|
title: '源图 修改结果',
|
|
x: 472,
|
|
y: 140,
|
|
width: 1024,
|
|
height: 1024,
|
|
sourceResourceId: 'resource-source',
|
|
prompt: '修改要求',
|
|
});
|
|
});
|
|
|
|
it('creates quick edit layers centered on the generating placeholder', () => {
|
|
const sourceLayer = createSourceLayer();
|
|
const layer = createQuickEditResultLayer({
|
|
generated: createGenerated({ width: 1536, height: 1024 }),
|
|
generatedIndex: 10,
|
|
sourceLayer,
|
|
generationInputs: createGenerationInputs(),
|
|
frame: {
|
|
x: 600,
|
|
y: 300,
|
|
width: 320,
|
|
height: 240,
|
|
originalWidth: 1536,
|
|
originalHeight: 1024,
|
|
},
|
|
});
|
|
|
|
expect(layer).toMatchObject({
|
|
id: 'layer-quick-edit-10',
|
|
x: -8,
|
|
y: -92,
|
|
width: 1536,
|
|
height: 1024,
|
|
});
|
|
});
|
|
|
|
it('creates quick edit layers with source group and asset kind preserved', () => {
|
|
const sourceLayer = createSourceLayer();
|
|
const layer = createQuickEditResultLayer({
|
|
generated: createGenerated({ width: 1536, height: 1024 }),
|
|
generatedIndex: 9,
|
|
sourceLayer,
|
|
generationInputs: createGenerationInputs(),
|
|
});
|
|
|
|
expect(layer).toMatchObject({
|
|
id: 'layer-quick-edit-9',
|
|
resourceId: 'local-resource-quick-edit-9',
|
|
title: '源图 快速编辑',
|
|
x: 472,
|
|
y: 140,
|
|
width: 1536,
|
|
height: 1024,
|
|
originalWidth: 1536,
|
|
originalHeight: 1024,
|
|
sourceResourceId: 'resource-source',
|
|
groupId: 'group-a',
|
|
assetKind: 'spec',
|
|
});
|
|
});
|
|
|
|
it('applies quick edits to the source layer in place', () => {
|
|
const sourceLayer = createSourceLayer({
|
|
width: 320,
|
|
height: 240,
|
|
originalWidth: 1537,
|
|
originalHeight: 1025,
|
|
});
|
|
const layer = applyImageEditResultToSourceLayer({
|
|
generated: createGenerated({
|
|
imageSrc: 'data:image/png;base64,edited',
|
|
width: 1536,
|
|
height: 1024,
|
|
resource: {
|
|
resourceId: 'resource-edited',
|
|
objectKey: 'generated/edited.png',
|
|
assetObjectId: 'asset-object-edited',
|
|
width: 1536,
|
|
height: 1024,
|
|
sourceType: 'generated',
|
|
generationInputs: createGenerationInputs(),
|
|
},
|
|
}),
|
|
sourceLayer,
|
|
generationInputs: createGenerationInputs(),
|
|
});
|
|
|
|
expect(layer).toMatchObject({
|
|
id: 'layer-source',
|
|
title: '源图',
|
|
src: 'data:image/png;base64,edited',
|
|
x: -488,
|
|
y: -252,
|
|
width: 1536,
|
|
height: 1024,
|
|
originalWidth: 1536,
|
|
originalHeight: 1024,
|
|
resourceId: 'resource-edited',
|
|
sourceResourceId: 'resource-source',
|
|
objectKey: 'generated/edited.png',
|
|
});
|
|
expect(layer.x + layer.width / 2).toBe(
|
|
sourceLayer.x + sourceLayer.width / 2,
|
|
);
|
|
expect(layer.y + layer.height / 2).toBe(
|
|
sourceLayer.y + sourceLayer.height / 2,
|
|
);
|
|
});
|
|
|
|
it('creates wrapped icon layers with icon metadata', () => {
|
|
const layers = createIconSpritesheetResultLayers({
|
|
generated: {
|
|
spritesheetImageSrc: 'data:image/png;base64,sheet',
|
|
spritesheetWidth: 512,
|
|
spritesheetHeight: 512,
|
|
iconImageSrcs: [],
|
|
prompt: '图标 prompt',
|
|
actualPrompt: '图标 actual prompt',
|
|
model: 'gpt-image-2',
|
|
taskId: 'task-icons',
|
|
priceMudPoints: 20,
|
|
},
|
|
iconResults: [
|
|
{
|
|
name: '返回按钮',
|
|
imageSrc: 'data:image/png;base64,back',
|
|
width: 256,
|
|
height: 256,
|
|
},
|
|
{
|
|
name: '设置按钮',
|
|
imageSrc: 'data:image/png;base64,settings',
|
|
width: 512,
|
|
height: 256,
|
|
},
|
|
{
|
|
name: '提示按钮',
|
|
imageSrc: 'data:image/png;base64,hint',
|
|
width: 128,
|
|
height: 128,
|
|
},
|
|
],
|
|
startIndex: 11,
|
|
canvasSize: { width: 900, height: 640 },
|
|
viewport: { x: 0, y: 0, scale: 1 },
|
|
frame: {
|
|
x: 200,
|
|
y: 100,
|
|
width: 360,
|
|
height: 360,
|
|
originalWidth: 512,
|
|
originalHeight: 512,
|
|
},
|
|
generationInputs: createGenerationInputs(),
|
|
});
|
|
|
|
expect(layers).toHaveLength(4);
|
|
expect(layers[0]).toMatchObject({
|
|
id: 'layer-icon-spritesheet-11',
|
|
title: '图标素材图集 11',
|
|
x: 200,
|
|
y: 100,
|
|
width: 512,
|
|
height: 512,
|
|
assetKind: 'icon-spritesheet',
|
|
prompt: '图标 prompt',
|
|
actualPrompt: '图标 actual prompt',
|
|
});
|
|
expect(layers[1]).toMatchObject({
|
|
id: 'layer-icon-12',
|
|
title: '返回按钮',
|
|
x: 744,
|
|
y: 100,
|
|
width: 256,
|
|
height: 256,
|
|
assetKind: 'icon',
|
|
prompt: '图标 prompt',
|
|
actualPrompt: '图标 actual prompt',
|
|
});
|
|
expect(layers[2]).toMatchObject({
|
|
id: 'layer-icon-13',
|
|
title: '设置按钮',
|
|
x: 744,
|
|
y: 380,
|
|
width: 512,
|
|
height: 256,
|
|
assetKind: 'icon',
|
|
});
|
|
expect(layers[3]).toMatchObject({
|
|
id: 'layer-icon-14',
|
|
title: '提示按钮',
|
|
x: 744,
|
|
y: 660,
|
|
width: 128,
|
|
height: 128,
|
|
assetKind: 'icon',
|
|
});
|
|
});
|
|
|
|
it('keeps the spritesheet layer when icon results are missing', () => {
|
|
const layers = createIconSpritesheetResultLayers({
|
|
generated: {
|
|
spritesheetImageSrc: 'data:image/png;base64,sheet',
|
|
spritesheetWidth: 512,
|
|
spritesheetHeight: 512,
|
|
iconImageSrcs: [],
|
|
prompt: '图标 prompt',
|
|
actualPrompt: '图标 actual prompt',
|
|
model: 'gpt-image-2',
|
|
taskId: 'task-icons',
|
|
priceMudPoints: 20,
|
|
},
|
|
iconResults: undefined,
|
|
startIndex: 11,
|
|
canvasSize: { width: 900, height: 640 },
|
|
viewport: { x: 0, y: 0, scale: 1 },
|
|
generationInputs: createGenerationInputs(),
|
|
});
|
|
|
|
expect(layers).toHaveLength(1);
|
|
expect(layers[0]).toMatchObject({
|
|
id: 'layer-icon-spritesheet-11',
|
|
title: '图标素材图集 11',
|
|
assetKind: 'icon-spritesheet',
|
|
src: 'data:image/png;base64,sheet',
|
|
});
|
|
});
|
|
|
|
it('creates audio result layers centered on the active placeholder', () => {
|
|
const layer = createAudioResultLayer({
|
|
generated: {
|
|
audioSrc: '/generated-character-drafts/editor-audios/sfx.mp3',
|
|
objectKey: 'generated-character-drafts/editor-audios/sfx.mp3',
|
|
assetObjectId: 'assetobj-audio-1',
|
|
width: 420,
|
|
height: 120,
|
|
sourceType: 'generated',
|
|
prompt: '金币掉落叮当声',
|
|
actualPrompt: '金币掉落叮当声',
|
|
model: 'audio1.0',
|
|
provider: 'VectorEngine',
|
|
taskId: 'sound-task-1',
|
|
priceMudPoints: 10,
|
|
audioKind: 'sound-effect',
|
|
durationSeconds: 75,
|
|
resource: {
|
|
resourceId: 'resource-audio-bff',
|
|
projectId: 'editor-project-1',
|
|
imageSrc: '/generated-character-drafts/editor-audios/sfx.mp3',
|
|
objectKey: 'generated-character-drafts/editor-audios/sfx.mp3',
|
|
assetObjectId: 'assetobj-audio-1',
|
|
width: 420,
|
|
height: 120,
|
|
sourceType: 'generated',
|
|
assetKind: 'sound-effect',
|
|
},
|
|
asset: {
|
|
assetId: 'asset-audio-bff',
|
|
folderId: 'user-1:asset-folder:project',
|
|
label: '游戏音效 14',
|
|
imageSrc: '/generated-character-drafts/editor-audios/sfx.mp3',
|
|
width: 420,
|
|
height: 120,
|
|
sourceType: 'generated',
|
|
assetKind: 'sound-effect',
|
|
},
|
|
},
|
|
generatedIndex: 14,
|
|
title: '游戏音效 14',
|
|
canvasSize: { width: 900, height: 640 },
|
|
viewport: { x: 10, y: 20, scale: 2 },
|
|
frame: {
|
|
x: 80,
|
|
y: 60,
|
|
width: 420,
|
|
height: 120,
|
|
originalWidth: 420,
|
|
originalHeight: 120,
|
|
},
|
|
generationInputs: {
|
|
fields: [
|
|
{ title: 'prompt', value: '金币掉落叮当声' },
|
|
{ title: '时长', value: '75秒' },
|
|
],
|
|
references: [],
|
|
},
|
|
});
|
|
|
|
expect(layer).toMatchObject({
|
|
id: 'layer-audio-14',
|
|
resourceId: 'resource-audio-bff',
|
|
title: '游戏音效 14',
|
|
src: '/generated-character-drafts/editor-audios/sfx.mp3',
|
|
mediaType: 'audio',
|
|
assetKind: 'sound-effect',
|
|
x: 80,
|
|
y: 60,
|
|
width: 420,
|
|
height: 120,
|
|
originalWidth: 420,
|
|
originalHeight: 120,
|
|
sourceType: 'generated',
|
|
prompt: '金币掉落叮当声',
|
|
model: 'audio1.0',
|
|
taskId: 'sound-task-1',
|
|
objectKey: 'generated-character-drafts/editor-audios/sfx.mp3',
|
|
assetObjectId: 'assetobj-audio-1',
|
|
sourceAssetId: 'asset-audio-bff',
|
|
generationInputs: {
|
|
fields: [
|
|
{ title: 'prompt', value: '金币掉落叮当声' },
|
|
{ title: '时长', value: '75秒' },
|
|
],
|
|
references: [],
|
|
},
|
|
generatedAssetSnapshot: expect.objectContaining({
|
|
assetId: 'asset-audio-bff',
|
|
}),
|
|
});
|
|
expect(layer).not.toHaveProperty('durationSeconds');
|
|
expect(layer.provider).toBeUndefined();
|
|
});
|
|
|
|
it('creates video result layers with OSS asset identity', () => {
|
|
const layer = createVideoResultLayer({
|
|
generated: {
|
|
videoSrc: '/generated-character-drafts/editor-videos/video.mp4',
|
|
objectKey: 'generated-character-drafts/editor-videos/video.mp4',
|
|
assetObjectId: 'assetobj-video-1',
|
|
width: 1280,
|
|
height: 720,
|
|
sourceType: 'generated',
|
|
prompt: '开场动画',
|
|
actualPrompt: '开场动画',
|
|
model: 'seedance2.0-fast',
|
|
taskId: 'video-task-1',
|
|
priceMudPoints: 40,
|
|
durationSeconds: 5,
|
|
thumbnailSrc: '/generated-character-drafts/editor-videos/cover.png',
|
|
asset: {
|
|
assetId: 'asset-video-bff',
|
|
folderId: 'user-1:asset-folder:project',
|
|
label: '生成视频 15',
|
|
imageSrc: '/generated-character-drafts/editor-videos/video.mp4',
|
|
thumbnailSrc: '/generated-character-drafts/editor-videos/cover.png',
|
|
objectKey: 'generated-character-drafts/editor-videos/video.mp4',
|
|
assetObjectId: 'assetobj-video-1',
|
|
width: 1280,
|
|
height: 720,
|
|
sourceType: 'generated',
|
|
assetKind: 'video',
|
|
},
|
|
},
|
|
generatedIndex: 15,
|
|
title: '生成视频 15',
|
|
canvasSize: { width: 900, height: 640 },
|
|
viewport: { x: 10, y: 20, scale: 2 },
|
|
generationInputs: createGenerationInputs(),
|
|
});
|
|
|
|
expect(layer).toMatchObject({
|
|
id: 'layer-video-15',
|
|
src: '/generated-character-drafts/editor-videos/video.mp4',
|
|
mediaType: 'video',
|
|
assetKind: 'video',
|
|
objectKey: 'generated-character-drafts/editor-videos/video.mp4',
|
|
assetObjectId: 'assetobj-video-1',
|
|
thumbnailSrc: '/generated-character-drafts/editor-videos/cover.png',
|
|
});
|
|
expect(layer).not.toHaveProperty('durationSeconds');
|
|
expect(layer.generatedAssetSnapshot?.assetId).toBe('asset-video-bff');
|
|
expect(layer.provider).toBeUndefined();
|
|
});
|
|
|
|
it('creates character animation result layers as playable image sequences', () => {
|
|
const frames = [
|
|
{
|
|
imageSrc: '/generated-character-drafts/editor/frame1.png',
|
|
width: 1024,
|
|
height: 1024,
|
|
},
|
|
{
|
|
imageSrc: '/generated-character-drafts/editor/frame2.png',
|
|
width: 1024,
|
|
height: 1024,
|
|
},
|
|
];
|
|
const layer = createCharacterAnimationResultLayer({
|
|
generated: {
|
|
taskId: 'character-animation-task-1',
|
|
model: 'seedance2.0-fast',
|
|
prompt: '待机动作',
|
|
previewVideoPath: '/generated-character-drafts/editor/preview.mp4',
|
|
frames,
|
|
frameCount: 2,
|
|
durationSeconds: 4,
|
|
fps: 8,
|
|
priceMudPoints: 40,
|
|
resource: {
|
|
resourceId: 'resource-character-animation-15',
|
|
projectId: 'project-1',
|
|
imageSrc: frames[0]!.imageSrc,
|
|
width: 1024,
|
|
height: 1024,
|
|
sourceType: 'generated',
|
|
assetKind: 'character-animation',
|
|
imageSequenceFrames: frames,
|
|
imageSequenceDurationMs: 4_000,
|
|
},
|
|
},
|
|
generatedIndex: 15,
|
|
title: '角色动作',
|
|
canvasSize: { width: 900, height: 640 },
|
|
viewport: { x: 10, y: 20, scale: 2 },
|
|
frame: {
|
|
x: 80,
|
|
y: 60,
|
|
width: 420,
|
|
height: 420,
|
|
originalWidth: 1024,
|
|
originalHeight: 1024,
|
|
},
|
|
generationInputs: {
|
|
fields: [{ title: '动作描述', value: '待机动作' }],
|
|
references: [],
|
|
},
|
|
});
|
|
|
|
expect(layer).toMatchObject({
|
|
id: 'layer-character-animation-15',
|
|
resourceId: 'resource-character-animation-15',
|
|
title: '角色动作',
|
|
src: '/generated-character-drafts/editor/frame1.png',
|
|
mediaType: 'image-sequence',
|
|
thumbnailSrc: '/generated-character-drafts/editor/frame1.png',
|
|
imageSequenceFrames: frames,
|
|
previewVideoPath: null,
|
|
x: -222,
|
|
y: -242,
|
|
width: 1024,
|
|
height: 1024,
|
|
originalWidth: 1024,
|
|
originalHeight: 1024,
|
|
zIndex: 25,
|
|
sourceType: 'generated',
|
|
prompt: '待机动作',
|
|
actualPrompt: '待机动作',
|
|
imageSequenceDurationMs: 4_000,
|
|
model: 'seedance2.0-fast',
|
|
taskId: 'character-animation-task-1',
|
|
assetKind: 'character-animation',
|
|
});
|
|
expect(layer?.generationInputs?.fields[0]?.value).toBe('待机动作');
|
|
expect(layer?.provider).toBeUndefined();
|
|
});
|
|
|
|
it('links a character animation result layer to its persisted project resource', () => {
|
|
const frames = [
|
|
{
|
|
imageSrc: '/generated-character-drafts/editor/frame1.png',
|
|
objectKey: 'generated-character-drafts/editor/frame1.png',
|
|
assetObjectId: 'asset-object-frame-1',
|
|
width: 512,
|
|
height: 768,
|
|
},
|
|
{
|
|
imageSrc: '/generated-character-drafts/editor/frame2.png',
|
|
objectKey: 'generated-character-drafts/editor/frame2.png',
|
|
assetObjectId: 'asset-object-frame-2',
|
|
width: 512,
|
|
height: 768,
|
|
},
|
|
];
|
|
const layer = createCharacterAnimationResultLayer({
|
|
generated: {
|
|
taskId: 'character-animation-task-persisted',
|
|
model: 'seedance2.0-fast',
|
|
prompt: '奔跑动作',
|
|
previewVideoPath: '/generated-character-drafts/editor/preview.mp4',
|
|
frames,
|
|
frameCount: 2,
|
|
durationSeconds: 4,
|
|
fps: 8,
|
|
priceMudPoints: 40,
|
|
resource: {
|
|
resourceId: 'resource-character-animation-final',
|
|
projectId: 'project-1',
|
|
imageSrc: frames[0]!.imageSrc,
|
|
objectKey: frames[0]!.objectKey,
|
|
assetObjectId: frames[0]!.assetObjectId,
|
|
width: 512,
|
|
height: 768,
|
|
sourceType: 'generated',
|
|
sourceResourceId: 'resource-character-animation-preview',
|
|
assetKind: 'character-animation',
|
|
imageSequenceFrames: frames,
|
|
imageSequenceDurationMs: 4_000,
|
|
},
|
|
},
|
|
generatedIndex: 16,
|
|
title: '角色奔跑',
|
|
canvasSize: { width: 900, height: 640 },
|
|
viewport: { x: 0, y: 0, scale: 1 },
|
|
generationInputs: createGenerationInputs(),
|
|
sourceLayer: createSourceLayer({
|
|
resourceId: 'resource-original-character',
|
|
}),
|
|
});
|
|
|
|
expect(layer).toMatchObject({
|
|
resourceId: 'resource-character-animation-final',
|
|
src: frames[0]!.imageSrc,
|
|
objectKey: frames[0]!.objectKey,
|
|
assetObjectId: frames[0]!.assetObjectId,
|
|
sourceResourceId: 'resource-character-animation-preview',
|
|
imageSequenceFrames: frames,
|
|
imageSequenceDurationMs: 4_000,
|
|
});
|
|
});
|
|
|
|
it('keeps persisted animation lineage when generation returns only an asset', () => {
|
|
const frames = [
|
|
{
|
|
imageSrc: '/generated-character-drafts/editor/frame1.png',
|
|
width: 512,
|
|
height: 768,
|
|
},
|
|
{
|
|
imageSrc: '/generated-character-drafts/editor/frame2.png',
|
|
width: 512,
|
|
height: 768,
|
|
},
|
|
];
|
|
const layer = createCharacterAnimationResultLayer({
|
|
generated: {
|
|
taskId: 'character-animation-task-asset-only',
|
|
model: 'seedance2.0-fast',
|
|
prompt: '跳跃动作',
|
|
previewVideoPath: '/generated-character-drafts/editor/preview.mp4',
|
|
frames,
|
|
frameCount: 2,
|
|
durationSeconds: 4,
|
|
fps: 8,
|
|
priceMudPoints: 40,
|
|
asset: {
|
|
assetId: 'asset-character-animation-final',
|
|
folderId: 'user-1:asset-folder:project',
|
|
label: '角色跳跃',
|
|
imageSrc: frames[0]!.imageSrc,
|
|
width: 512,
|
|
height: 768,
|
|
sourceType: 'generated',
|
|
sourceResourceId: 'resource-character-animation-preview',
|
|
assetKind: 'character-animation',
|
|
imageSequenceFrames: frames,
|
|
imageSequenceDurationMs: 4_000,
|
|
},
|
|
},
|
|
generatedIndex: 17,
|
|
title: '角色跳跃',
|
|
canvasSize: { width: 900, height: 640 },
|
|
viewport: { x: 0, y: 0, scale: 1 },
|
|
generationInputs: createGenerationInputs(),
|
|
sourceLayer: createSourceLayer({
|
|
resourceId: 'resource-original-character',
|
|
}),
|
|
});
|
|
|
|
expect(layer).toMatchObject({
|
|
resourceId: 'local-resource-asset-character-animation-final',
|
|
sourceResourceId: 'resource-character-animation-preview',
|
|
sourceAssetId: 'asset-character-animation-final',
|
|
});
|
|
});
|
|
});
|