合并生成参数恢复与改造入口修复
合入 PR 135 的 GenerationInputs V2 稳定字段与严格恢复链路 统一缺失来源时保留改造入口并在点击后复检拒绝 保留主分支 SFX V2 自动时长、Loop 与权威元数据语义 将队列引用槽位与场景配方修复移植到 PR 137 的模块结构
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
||||
createSpecDialogDraft,
|
||||
createUiDesignGenerationDialogDraft,
|
||||
createVideoGenerationDialogDraft,
|
||||
getUnavailableNormalizedGenerationInputReferences,
|
||||
hideGeneratedLayerComposerAfterBlur,
|
||||
updateCharacterAnimationDurationPanel,
|
||||
updateIconDescriptionsTextInDialog,
|
||||
@@ -60,6 +61,22 @@ function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||
}
|
||||
|
||||
describe('ImageCanvasGenerationDialogModel', () => {
|
||||
it('does not legacy-fallback from a hydrated invalid versioned recipe', () => {
|
||||
expect(
|
||||
createSameSourceGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
sourceType: 'generated',
|
||||
generationInputs: null,
|
||||
generationInputsHydrationState: 'invalid-versioned',
|
||||
prompt: '不能误当旧版配方',
|
||||
}),
|
||||
canvasSize: { width: 960, height: 720 },
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
mode: 'redraw',
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('creates centered drafts for image and spec generation dialogs', () => {
|
||||
const canvasSize = { width: 1000, height: 800 };
|
||||
const viewport = { x: 100, y: 40, scale: 2 };
|
||||
@@ -475,6 +492,41 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
expect(createCharacterAnimationPanelDraft(createLayer())).toBeNull();
|
||||
});
|
||||
|
||||
it('restores persisted image parameters when creating edit drafts', () => {
|
||||
const sourceLayer = createLayer({
|
||||
model: IMAGE_MODEL_NANOBANANA2,
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'image.edit',
|
||||
fields: [
|
||||
{ id: 'prompt', title: '修改要求', value: '改成雨天' },
|
||||
{ id: 'model', title: '模型', value: IMAGE_MODEL_GPT_IMAGE_2 },
|
||||
{ id: 'aspectRatio', title: '比例', value: '2:3' },
|
||||
{ id: 'imageSize', title: '尺寸', value: '2K' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(createEditDialogDraft(sourceLayer)).toMatchObject({
|
||||
prompt: '改成雨天',
|
||||
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
});
|
||||
expect(
|
||||
createEditDialogDraft(sourceLayer, {
|
||||
imageModel: IMAGE_MODEL_NANOBANANA2,
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '1K',
|
||||
}),
|
||||
).toMatchObject({
|
||||
imageModel: IMAGE_MODEL_NANOBANANA2,
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '1K',
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves the resolved asset label in quick-edit dialog snapshots', () => {
|
||||
expect(
|
||||
createQuickEditGenerationDialogDraft({
|
||||
@@ -882,6 +934,408 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('restores V2 video and sound parameters by stable ids instead of display labels', () => {
|
||||
const imageReference = createLayer({
|
||||
id: 'layer-image-reference',
|
||||
resourceId: 'resource-image-reference',
|
||||
title: '视觉参考',
|
||||
});
|
||||
const audioReference = createLayer({
|
||||
id: 'layer-audio-reference',
|
||||
resourceId: 'resource-audio-reference',
|
||||
title: '音轨参考',
|
||||
mediaType: 'audio',
|
||||
});
|
||||
const videoReference = createLayer({
|
||||
id: 'layer-video-reference',
|
||||
resourceId: 'resource-video-reference',
|
||||
title: '源视频',
|
||||
mediaType: 'video',
|
||||
});
|
||||
const context = {
|
||||
canvasSize: { width: 960, height: 720 },
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
availableLayers: [imageReference, videoReference, audioReference],
|
||||
};
|
||||
|
||||
expect(
|
||||
createLayerGenerationDialogDraft({
|
||||
...context,
|
||||
sourceLayer: createLayer({
|
||||
sourceType: 'generated',
|
||||
mediaType: 'video',
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'video.generate',
|
||||
fields: [
|
||||
{ id: 'prompt', title: '可随时改名', value: '角色追逐镜头' },
|
||||
{ id: 'model', title: '不用作键', value: 'seedance2.0-fast' },
|
||||
{ id: 'aspectRatio', title: '显示快照', value: '9:16' },
|
||||
{ id: 'resolution', title: '显示快照', value: '1080p' },
|
||||
{ id: 'durationSeconds', title: '显示快照', value: 8 },
|
||||
{ id: 'sound', title: '显示快照', value: 'off' },
|
||||
{ id: 'webSearchEnabled', title: '显示快照', value: false },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
id: 'imageReference',
|
||||
title: '任意标题',
|
||||
label: '旧显示名',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-image-reference',
|
||||
},
|
||||
{
|
||||
id: 'videoReference',
|
||||
title: '任意标题',
|
||||
label: '旧显示名',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-video-reference',
|
||||
},
|
||||
{
|
||||
id: 'audioReference',
|
||||
title: '任意标题',
|
||||
label: '旧显示名',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-audio-reference',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: 'video',
|
||||
prompt: '角色追逐镜头',
|
||||
videoModel: 'seedance2.0-fast',
|
||||
videoAspectRatio: '9:16',
|
||||
videoResolution: '480p',
|
||||
videoDurationSeconds: 8,
|
||||
videoSound: 'off',
|
||||
videoWebSearchEnabled: false,
|
||||
generationReferences: [
|
||||
expect.objectContaining({
|
||||
id: 'canvas-layer-image-reference',
|
||||
label: '视觉参考',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'canvas-layer-video-reference',
|
||||
label: '源视频',
|
||||
mediaType: 'video',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'canvas-layer-audio-reference',
|
||||
label: '音轨参考',
|
||||
mediaType: 'audio',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
createLayerGenerationDialogDraft({
|
||||
...context,
|
||||
sourceLayer: createLayer({
|
||||
sourceType: 'generated',
|
||||
mediaType: 'audio',
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'audio.sound-effect.generate',
|
||||
fields: [
|
||||
{ id: 'prompt', title: '非 prompt 标题', value: '短促点击音' },
|
||||
{ id: 'model', title: '非 model 标题', value: 'audio1.0' },
|
||||
{ id: 'durationSeconds', title: '非 duration 标题', value: 7 },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: 'audio-sound-effect',
|
||||
prompt: '短促点击音',
|
||||
soundModel: 'audio1.0',
|
||||
soundDurationSeconds: 7,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats references outside the current canvas as unavailable', () => {
|
||||
const availableReference = createLayer({
|
||||
id: 'layer-available-reference',
|
||||
resourceId: 'resource-available-reference',
|
||||
});
|
||||
const sourceLayer = createLayer({
|
||||
sourceType: 'generated',
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'image.generate',
|
||||
fields: [{ id: 'prompt', title: '生成提示词', value: '场景图' }],
|
||||
references: [
|
||||
{
|
||||
id: 'reference',
|
||||
title: '画布参考图',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-available-reference',
|
||||
},
|
||||
{
|
||||
id: 'reference',
|
||||
title: '面板上传参考图',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-panel-upload',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
getUnavailableNormalizedGenerationInputReferences(sourceLayer, [
|
||||
availableReference,
|
||||
]),
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
title: '面板上传参考图',
|
||||
refId: 'resource-panel-upload',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps valid V2 fields ahead of conflicting legacy dialog and resource fallbacks', () => {
|
||||
expect(
|
||||
createLayerGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
sourceType: 'generated',
|
||||
prompt: '资源旧提示词',
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'image.generate',
|
||||
fields: [
|
||||
{
|
||||
id: 'model',
|
||||
title: '任意标题',
|
||||
value: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
},
|
||||
{ id: 'aspectRatio', title: '任意标题', value: '2:3' },
|
||||
{ id: 'imageSize', title: '任意标题', value: '2K' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
sourceDialog: {
|
||||
id: 'legacy-dialog',
|
||||
mode: 'generate',
|
||||
prompt: '历史对话框提示词',
|
||||
status: 'idle',
|
||||
imageModel: IMAGE_MODEL_NANOBANANA2,
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
},
|
||||
canvasSize: { width: 960, height: 720 },
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: 'generate',
|
||||
prompt: '',
|
||||
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
});
|
||||
});
|
||||
|
||||
it('restores scene.generate V2 inputs into the scene composer', () => {
|
||||
const sceneReference = createLayer({
|
||||
id: 'layer-scene-reference',
|
||||
resourceId: 'resource-scene-reference',
|
||||
title: '车站参考',
|
||||
});
|
||||
|
||||
expect(
|
||||
createSameSourceGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
id: 'layer-scene-result',
|
||||
resourceId: 'resource-scene-result',
|
||||
sourceType: 'generated',
|
||||
assetKind: 'scene',
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'scene.generate',
|
||||
fields: [
|
||||
{ id: 'prompt', title: '任意标题', value: '雨夜海边车站' },
|
||||
{ id: 'stylePreset', title: '任意标题', value: 'custom' },
|
||||
{ id: 'customStyle', title: '任意标题', value: '复古像素风' },
|
||||
{
|
||||
id: 'model',
|
||||
title: '任意标题',
|
||||
value: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
},
|
||||
{ id: 'aspectRatio', title: '任意标题', value: '16:9' },
|
||||
{ id: 'imageSize', title: '任意标题', value: '2K' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
id: 'reference',
|
||||
title: '任意标题',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-scene-reference',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
availableLayers: [sceneReference],
|
||||
canvasSize: { width: 960, height: 720 },
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
mode: 'redraw',
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: 'scene',
|
||||
prompt: '雨夜海边车站',
|
||||
sceneStylePreset: 'custom',
|
||||
sceneCustomStyle: '复古像素风',
|
||||
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '2K',
|
||||
generationReferences: [
|
||||
expect.objectContaining({
|
||||
id: 'canvas-layer-scene-reference',
|
||||
label: '车站参考',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('restores the narrowly scoped legacy scene recipe into the scene composer', () => {
|
||||
expect(
|
||||
createSameSourceGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
sourceType: 'generated',
|
||||
assetKind: 'scene',
|
||||
model: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
originalWidth: 2048,
|
||||
originalHeight: 1152,
|
||||
generationInputs: {
|
||||
fields: [
|
||||
{ title: '画面内容', value: '雨夜海边车站' },
|
||||
{ title: '视觉风格', value: '清透水彩' },
|
||||
],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
canvasSize: { width: 960, height: 720 },
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
mode: 'redraw',
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: 'scene',
|
||||
prompt: '雨夜海边车站',
|
||||
sceneStylePreset: 'watercolor',
|
||||
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '2K',
|
||||
});
|
||||
});
|
||||
|
||||
it('restores quick-edit references and the persisted image model', () => {
|
||||
const editSource = createLayer({
|
||||
id: 'layer-edit-source',
|
||||
resourceId: 'resource-edit-source',
|
||||
});
|
||||
const styleReference = createLayer({
|
||||
id: 'layer-style-reference',
|
||||
resourceId: 'resource-style-reference',
|
||||
title: '风格参考',
|
||||
});
|
||||
|
||||
expect(
|
||||
createLayerGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
id: 'layer-edit-result',
|
||||
resourceId: 'resource-edit-result',
|
||||
sourceType: 'generated',
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'image.edit',
|
||||
fields: [
|
||||
{ id: 'prompt', title: '提示词', value: '改成雨天' },
|
||||
{ id: 'model', title: '模型', value: IMAGE_MODEL_NANOBANANA2 },
|
||||
{ id: 'aspectRatio', title: '比例', value: '16:9' },
|
||||
{ id: 'imageSize', title: '尺寸', value: '2K' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
id: 'source',
|
||||
title: '原图',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-edit-source',
|
||||
},
|
||||
{
|
||||
id: 'reference',
|
||||
title: '参考图 1',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-style-reference',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
availableLayers: [editSource, styleReference],
|
||||
canvasSize: { width: 960, height: 720 },
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: 'quick-edit',
|
||||
prompt: '改成雨天',
|
||||
imageModel: IMAGE_MODEL_NANOBANANA2,
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '2K',
|
||||
generationReferences: [
|
||||
expect.objectContaining({
|
||||
id: 'canvas-layer-style-reference',
|
||||
label: '风格参考',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the V2 source reference ahead of a conflicting legacy animation source', () => {
|
||||
const v2Source = createLayer({
|
||||
id: 'layer-v2-character',
|
||||
resourceId: 'resource-v2-character',
|
||||
assetKind: 'character',
|
||||
});
|
||||
const legacySource = createLayer({
|
||||
id: 'layer-legacy-character',
|
||||
resourceId: 'resource-legacy-character',
|
||||
assetKind: 'character',
|
||||
});
|
||||
|
||||
expect(
|
||||
createSameSourceGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
id: 'layer-animation-result',
|
||||
sourceType: 'generated',
|
||||
assetKind: 'character-animation',
|
||||
sourceResourceId: 'resource-legacy-character',
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'character-animation.generate',
|
||||
fields: [{ id: 'prompt', title: '任意标题', value: '循环奔跑' }],
|
||||
references: [
|
||||
{
|
||||
id: 'source',
|
||||
title: '任意标题',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-v2-character',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
canvasSize: { width: 960, height: 720 },
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
mode: 'redraw',
|
||||
sourceAnimationLayer: legacySource,
|
||||
availableLayers: [legacySource, v2Source],
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: 'character-animation',
|
||||
prompt: '循环奔跑',
|
||||
sourceLayerId: 'layer-v2-character',
|
||||
});
|
||||
});
|
||||
|
||||
it('inherits and normalizes quick edit model parameters from the source', () => {
|
||||
expect(
|
||||
createQuickEditPanelDraft(
|
||||
@@ -1135,7 +1589,10 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
generationReferences: [{ label: '参考图' }],
|
||||
});
|
||||
expect(
|
||||
appendGenerationReference(sceneWithReference, createLayer({ id: 'other' })),
|
||||
appendGenerationReference(
|
||||
sceneWithReference,
|
||||
createLayer({ id: 'other' }),
|
||||
),
|
||||
).toMatchObject({
|
||||
generationReferences: [{ label: '参考图' }, { label: '源图' }],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user