修复图片修改参数恢复与提交一致性

- 图片修改面板优先恢复历史模型、比例和尺寸,并保留显式设置优先级

- 图片修改提交计划统一归一化参数并下发 model、aspectRatio、imageSize 和 size

- 画布完成占位使用实际目标输出尺寸

- 补充参数恢复回归测试与编辑器方案说明
This commit is contained in:
2026-08-04 18:00:23 +08:00
parent 78307de9a1
commit 59a69f063d
6 changed files with 294 additions and 25 deletions
@@ -419,6 +419,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({
@@ -1682,21 +1682,30 @@ export function createEditDialogDraft(
imageSize?: string;
} = {},
): GenerateDialogState {
const imageModel = normalizeEditorImageModel(
options.imageModel ?? sourceLayer.model,
const decodedInputs = decodeCanvasGenerationInputs(
sourceLayer.generationInputs,
);
const aspectRatio =
options.aspectRatio ??
inferEditorImageAspectRatio(
sourceLayer.originalWidth,
sourceLayer.originalHeight,
);
const imageSize =
options.imageSize ??
inferEditorImageSizeLabel(
sourceLayer.originalWidth,
sourceLayer.originalHeight,
);
const normalizedSourceLayer = decodedInputs.ok
? { ...sourceLayer, generationInputs: decodedInputs.inputs }
: sourceLayer;
const persistedImageModel = decodedInputs.ok
? getNormalizedString(normalizedSourceLayer, 'model') || undefined
: undefined;
const persistedAspectRatio = decodedInputs.ok
? getNormalizedString(normalizedSourceLayer, 'aspectRatio') || undefined
: undefined;
const persistedImageSize = decodedInputs.ok
? getNormalizedString(normalizedSourceLayer, 'imageSize') || undefined
: undefined;
const resolvedOptions = createQuickEditPanelDraft(sourceLayer, {
imageModel:
options.imageModel ??
persistedImageModel ??
sourceLayer.model ??
undefined,
aspectRatio: options.aspectRatio ?? persistedAspectRatio,
imageSize: options.imageSize ?? persistedImageSize,
});
return {
id: `edit-dialog:${sourceLayer.id}`,
mode: 'edit',
@@ -1704,9 +1713,9 @@ export function createEditDialogDraft(
status: 'idle',
composerOpen: true,
sourceLayerId: sourceLayer.id,
imageModel,
aspectRatio,
imageSize,
imageModel: resolvedOptions.model,
aspectRatio: resolvedOptions.aspectRatio,
imageSize: resolvedOptions.imageSize,
};
}
@@ -119,6 +119,9 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
prompt: '',
status: 'idle',
sourceLayerId: sourceLayer.id,
imageModel: 'unknown-model',
aspectRatio: '7:5',
imageSize: '8K',
},
layers: [sourceLayer],
nextGeneratedIndex: 4,
@@ -128,6 +131,12 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
kind: 'edit',
normalizedPrompt: '修改当前图片',
sourceLayer,
editInput: {
model: IMAGE_MODEL_NANOBANANA2,
aspectRatio: '4:3',
imageSize: '1K',
size: '1024x768',
},
generationInputs: expect.objectContaining({
version: 2,
action: 'image.edit',
@@ -147,6 +156,64 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
});
});
it('builds edit submission plans from persisted image-edit inputs', () => {
const sourceLayer = createLayer({
generationInputs: {
version: 2,
action: 'image.edit',
fields: [
{ id: 'prompt', title: '修改要求', value: 'persisted' },
{ id: 'model', title: '模型', value: IMAGE_MODEL_GPT_IMAGE_2 },
{ id: 'aspectRatio', title: '比例', value: '2:3' },
{ id: 'imageSize', title: '尺寸', value: '2K' },
],
references: [
{
id: 'source',
title: '参考图',
label: '源图',
refType: 'project-resource',
refId: 'resource-source',
},
],
},
model: IMAGE_MODEL_GPT_IMAGE_2,
originalWidth: 2048,
originalHeight: 1152,
});
const plan = buildImageGenerationSubmissionPlan({
dialog: {
mode: 'edit',
prompt: '',
status: 'idle',
sourceLayerId: sourceLayer.id,
imageModel: 'unknown-model',
aspectRatio: '7:5',
imageSize: '8K',
},
layers: [sourceLayer],
nextGeneratedIndex: 4,
});
expect(plan).toMatchObject({
kind: 'edit',
editInput: {
model: IMAGE_MODEL_GPT_IMAGE_2,
aspectRatio: '2:3',
imageSize: '2K',
size: '1365x2048',
},
generationInputs: expect.objectContaining({
fields: expect.arrayContaining([
{ id: 'model', title: '模型', value: IMAGE_MODEL_GPT_IMAGE_2 },
{ id: 'aspectRatio', title: '比例', value: '2:3' },
{ id: 'imageSize', title: '尺寸', value: '2K' },
]),
}),
});
});
it('builds quick-edit canvas generator plans as direct source edits', () => {
const sourceLayer = createLayer({
objectKey: 'generated-character-drafts/editor/source.png',
@@ -31,6 +31,7 @@ import {
CHARACTER_ANIMATION_MODEL,
DEFAULT_EDITOR_BGFILTER_SEG_MODEL,
DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
DEFAULT_IMAGE_MODEL,
DEFAULT_SOUND_EFFECT_DURATION_SECONDS,
DEFAULT_SOUND_EFFECT_MODEL,
DEFAULT_SPEC_FORM_VALUES,
@@ -165,12 +166,151 @@ function resolveOptionalFieldWithFallback<T extends string>(
);
}
function resolveSupportedFieldValue({
preferredValues,
supportedValues,
fallbackValues,
defaultValue,
}: {
preferredValues: Array<string | null | undefined>;
supportedValues: readonly string[];
fallbackValues: readonly string[];
defaultValue: string;
}) {
for (const value of preferredValues) {
const normalized = value?.trim();
if (normalized && supportedValues.includes(normalized)) {
return normalized;
}
}
return (
fallbackValues.find((fallbackValue) =>
supportedValues.includes(fallbackValue),
) ?? defaultValue
);
}
function resolveImageEditFieldValueFromSourceLayer(
sourceLayer: CanvasLayer,
fieldId: string,
) {
const rawValue = sourceLayer.generationInputs?.fields.find(
(field) => field.id === fieldId,
)?.value;
return typeof rawValue === 'string' ? rawValue : null;
}
function resolveImageEditModel({
sourceLayer,
dialogModel,
}: {
sourceLayer: CanvasLayer;
dialogModel?: string | null;
}) {
const normalizedDialogModel = dialogModel
? normalizeEditorImageModel(dialogModel)
: null;
const normalizedSourceLayerModel = sourceLayer.model
? normalizeEditorImageModel(sourceLayer.model)
: null;
const resolvedModelFromLayer = resolveImageEditFieldValueFromSourceLayer(
sourceLayer,
'model',
);
const normalizedLayerPersistedModel = resolvedModelFromLayer
? normalizeEditorImageModel(resolvedModelFromLayer)
: null;
return [
normalizedDialogModel,
normalizedLayerPersistedModel,
normalizedSourceLayerModel,
].find((candidateModel) =>
EDITOR_IMAGE_MODEL_OPTIONS.some(
(option) => option.value === candidateModel,
),
);
}
function resolveImageEditGeometry({
sourceLayer,
dialogImageModel,
dialogAspectRatio,
dialogImageSize,
}: {
sourceLayer: CanvasLayer;
dialogImageModel?: string | null;
dialogAspectRatio?: string | null;
dialogImageSize?: string | null;
}) {
const model = resolveImageEditModel({
sourceLayer,
dialogModel: dialogImageModel,
});
const selectedModel = model ?? DEFAULT_IMAGE_MODEL;
const dimensionOptions =
EDITOR_IMAGE_DIMENSION_OPTIONS[
selectedModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
] ??
EDITOR_IMAGE_DIMENSION_OPTIONS[
DEFAULT_IMAGE_MODEL as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
];
const supportedAspectRatios =
dimensionOptions.aspectRatios as readonly string[];
const supportedImageSizes = dimensionOptions.imageSizes as readonly string[];
const persistedAspectRatio = resolveImageEditFieldValueFromSourceLayer(
sourceLayer,
'aspectRatio',
);
const persistedImageSize = resolveImageEditFieldValueFromSourceLayer(
sourceLayer,
'imageSize',
);
const inferredAspectRatio = inferEditorImageAspectRatio(
sourceLayer.originalWidth,
sourceLayer.originalHeight,
);
const inferredImageSize = inferEditorImageSizeLabel(
sourceLayer.originalWidth,
sourceLayer.originalHeight,
);
const defaultImageSize = supportedImageSizes.includes('1K')
? '1K'
: (supportedImageSizes[0] ?? '1K');
const aspectRatio = resolveSupportedFieldValue({
preferredValues: [dialogAspectRatio, persistedAspectRatio],
supportedValues: supportedAspectRatios,
fallbackValues: [inferredAspectRatio, ...dimensionOptions.aspectRatios],
defaultValue: '1:1',
});
const imageSize = resolveSupportedFieldValue({
preferredValues: [dialogImageSize, persistedImageSize],
supportedValues: supportedImageSizes,
fallbackValues: [inferredImageSize, ...dimensionOptions.imageSizes],
defaultValue: defaultImageSize,
});
const outputSize = resolveEditorImageGenerationPixelSize({
model: selectedModel,
aspectRatio,
imageSize,
});
return {
model: selectedModel,
aspectRatio,
imageSize,
outputSize,
};
}
export type ImageGenerationSubmissionPlan =
| {
kind: 'edit';
normalizedPrompt: string;
sourceLayer: CanvasLayer;
resultTitle: string;
editInput: Pick<
EditorImageEditInput,
'size' | 'model' | 'aspectRatio' | 'imageSize'
>;
generationInputs: CanvasGenerationInputs;
}
| {
@@ -268,6 +408,12 @@ export function buildImageGenerationSubmissionPlan({
if (!sourceLayer) {
throw new Error('未找到要修改的图片');
}
const geometry = resolveImageEditGeometry({
sourceLayer,
dialogImageModel: dialog.imageModel,
dialogAspectRatio: dialog.aspectRatio,
dialogImageSize: dialog.imageSize,
});
return {
kind: 'edit',
normalizedPrompt,
@@ -276,14 +422,20 @@ export function buildImageGenerationSubmissionPlan({
dialog.assetLabel,
`${sourceLayer.title} 修改结果`,
),
editInput: {
size: `${geometry.outputSize.width}x${geometry.outputSize.height}`,
model: geometry.model,
aspectRatio: geometry.aspectRatio,
imageSize: geometry.imageSize,
},
generationInputs: buildEditGenerationInputs(
'修改要求',
normalizedPrompt,
sourceLayer,
{
model: dialog.imageModel,
aspectRatio: dialog.aspectRatio,
imageSize: dialog.imageSize,
model: geometry.model,
aspectRatio: geometry.aspectRatio,
imageSize: geometry.imageSize,
},
),
};
@@ -1835,6 +1835,11 @@ export function useImageCanvasGenerationSubmissionWorkflow({
'image',
projectId,
);
const editPlaceholderSize =
getCanvasCompletionPlaceholderSizeFromPlan({
sourceLayer: submissionPlan.sourceLayer,
outputSize: submissionPlan.editInput.size,
});
const canvasCompletionPlaceholder =
getGeneratingDialogPlaceholder(dialog);
const editCanvasCompletionPlaceholder =
@@ -1842,17 +1847,17 @@ export function useImageCanvasGenerationSubmissionWorkflow({
buildRightSideCanvasCompletionPlaceholder(
submissionPlan.sourceLayer,
{
width: submissionPlan.sourceLayer.originalWidth,
height: submissionPlan.sourceLayer.originalHeight,
originalWidth: submissionPlan.sourceLayer.originalWidth,
originalHeight: submissionPlan.sourceLayer.originalHeight,
width: editPlaceholderSize.width,
height: editPlaceholderSize.height,
originalWidth: editPlaceholderSize.width,
originalHeight: editPlaceholderSize.height,
},
);
const generated = await runEditorGenerationWithWalletRefresh(
editEditorImage({
prompt: submissionPlan.normalizedPrompt,
sourceImageSrc: referenceImageSrc,
size: `${submissionPlan.sourceLayer.originalWidth}x${submissionPlan.sourceLayer.originalHeight}`,
...submissionPlan.editInput,
projectId,
assetKind: submissionPlan.sourceLayer.assetKind,
generationInputs: submissionPlan.generationInputs,