拆分编辑器生成提交模型
抽出图片生成请求与结果快照构建逻辑 补充生成提交模型单测 更新 TRACKING.md 记录第三十五阶段验证
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
import { buildImageGenerationSubmissionPlan } from './ImageCanvasGenerationSubmissionModel';
|
||||
|
||||
function createLayer(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: 1024,
|
||||
originalHeight: 768,
|
||||
zIndex: 2,
|
||||
sourceType: 'uploaded',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
it('builds normal image generation submission plans', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
mode: 'generate',
|
||||
prompt: ' 一张发光主视觉 ',
|
||||
status: 'idle',
|
||||
},
|
||||
layers: [],
|
||||
nextGeneratedIndex: 3,
|
||||
});
|
||||
|
||||
expect(plan).toEqual({
|
||||
kind: 'image',
|
||||
normalizedPrompt: '一张发光主视觉',
|
||||
input: {
|
||||
prompt: '一张发光主视觉',
|
||||
},
|
||||
result: {
|
||||
generationInputs: {
|
||||
fields: [{ title: '生成提示词', value: '一张发光主视觉' }],
|
||||
references: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('builds edit submission plans with the source layer snapshot', () => {
|
||||
const sourceLayer = createLayer();
|
||||
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
mode: 'edit',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
sourceLayerId: sourceLayer.id,
|
||||
},
|
||||
layers: [sourceLayer],
|
||||
nextGeneratedIndex: 4,
|
||||
});
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
kind: 'edit',
|
||||
normalizedPrompt: '修改当前图片',
|
||||
sourceLayer,
|
||||
generationInputs: {
|
||||
fields: [{ title: '修改要求', value: '修改当前图片' }],
|
||||
references: [
|
||||
{
|
||||
title: '参考图',
|
||||
label: '源图',
|
||||
src: 'data:image/png;base64,source',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('builds spec generation plans with reference prompt semantics', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
mode: 'spec',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
specType: 'icon',
|
||||
specValues: {
|
||||
playSetting: '休闲消除',
|
||||
artStyle: '清爽卡通',
|
||||
bodyRatio: '3',
|
||||
characterView: '',
|
||||
customPrompt: '',
|
||||
},
|
||||
specReference: {
|
||||
id: 'spec-ref',
|
||||
label: '参考.png',
|
||||
src: 'data:image/png;base64,ref',
|
||||
},
|
||||
},
|
||||
layers: [],
|
||||
nextGeneratedIndex: 8,
|
||||
});
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
kind: 'image',
|
||||
normalizedPrompt: 'AI 生成图片',
|
||||
input: {
|
||||
size: '2048x1152',
|
||||
kind: 'spec',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
prompt: expect.stringContaining('参考图生成规范'),
|
||||
},
|
||||
result: {
|
||||
assetKind: 'icon-spec',
|
||||
title: '图标素材规范 8',
|
||||
},
|
||||
});
|
||||
expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual({
|
||||
fields: [
|
||||
{ title: '玩法设定', value: '休闲消除' },
|
||||
{ title: '美术风格', value: '清爽卡通' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
title: '参考图',
|
||||
label: '参考.png',
|
||||
src: 'data:image/png;base64,ref',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('builds character plans with references and remembered model', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
mode: 'character',
|
||||
prompt: ' 白发骑士 ',
|
||||
status: 'idle',
|
||||
imageModel: 'gpt-image-2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
characterSpecReference: {
|
||||
id: 'spec',
|
||||
label: '角色规范',
|
||||
src: 'data:image/png;base64,spec',
|
||||
},
|
||||
characterReferences: [
|
||||
{
|
||||
id: 'ref-1',
|
||||
label: '盔甲参考',
|
||||
src: 'data:image/png;base64,armor',
|
||||
},
|
||||
],
|
||||
},
|
||||
layers: [],
|
||||
nextGeneratedIndex: 9,
|
||||
});
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
kind: 'image',
|
||||
normalizedPrompt: '白发骑士',
|
||||
input: {
|
||||
prompt: '白发骑士',
|
||||
kind: 'character',
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '2:3',
|
||||
imageSize: '2K',
|
||||
referenceImageSrcs: [
|
||||
'data:image/png;base64,spec',
|
||||
'data:image/png;base64,armor',
|
||||
],
|
||||
},
|
||||
result: {
|
||||
assetKind: 'character',
|
||||
title: '角色形象 9',
|
||||
},
|
||||
rememberImageModel: 'gpt-image-2',
|
||||
});
|
||||
expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual({
|
||||
fields: [{ title: '角色设定', value: '白发骑士' }],
|
||||
references: [
|
||||
{
|
||||
title: '角色形象规范',
|
||||
label: '角色规范',
|
||||
src: 'data:image/png;base64,spec',
|
||||
},
|
||||
{
|
||||
title: '常规参考图 1',
|
||||
label: '盔甲参考',
|
||||
src: 'data:image/png;base64,armor',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when edit source layer is missing', () => {
|
||||
expect(() =>
|
||||
buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
mode: 'edit',
|
||||
prompt: '修图',
|
||||
status: 'idle',
|
||||
sourceLayerId: 'missing-layer',
|
||||
},
|
||||
layers: [],
|
||||
nextGeneratedIndex: 1,
|
||||
}),
|
||||
).toThrow('未找到要修改的图片');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
import type {
|
||||
EditorImageGenerationInput,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import type {
|
||||
CanvasGenerationInputs,
|
||||
CanvasLayer,
|
||||
GenerateDialogState,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
DEFAULT_IMAGE_MODEL,
|
||||
DEFAULT_SPEC_FORM_VALUES,
|
||||
SPEC_GENERATION_SIZE,
|
||||
SPEC_TYPE_LABEL,
|
||||
buildCharacterGenerationInputs,
|
||||
buildEditGenerationInputs,
|
||||
buildImageGenerationInputs,
|
||||
buildSpecGenerationInputs,
|
||||
buildSpecPrompt,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
|
||||
type ImageGenerationSubmissionOptions = {
|
||||
dialog: GenerateDialogState;
|
||||
layers: CanvasLayer[];
|
||||
nextGeneratedIndex: number;
|
||||
};
|
||||
|
||||
export type ImageGenerationSubmissionPlan =
|
||||
| {
|
||||
kind: 'edit';
|
||||
normalizedPrompt: string;
|
||||
sourceLayer: CanvasLayer;
|
||||
generationInputs: CanvasGenerationInputs;
|
||||
}
|
||||
| {
|
||||
kind: 'image';
|
||||
normalizedPrompt: string;
|
||||
input: EditorImageGenerationInput;
|
||||
result: {
|
||||
assetKind?: CanvasLayer['assetKind'];
|
||||
title?: string;
|
||||
generationInputs: CanvasGenerationInputs;
|
||||
};
|
||||
rememberImageModel?: string;
|
||||
};
|
||||
|
||||
export function buildImageGenerationSubmissionPlan({
|
||||
dialog,
|
||||
layers,
|
||||
nextGeneratedIndex,
|
||||
}: ImageGenerationSubmissionOptions): ImageGenerationSubmissionPlan {
|
||||
const normalizedPrompt =
|
||||
dialog.prompt.trim() ||
|
||||
(dialog.mode === 'edit' ? '修改当前图片' : 'AI 生成图片');
|
||||
|
||||
if (dialog.mode === 'edit') {
|
||||
const sourceLayer = layers.find((layer) => layer.id === dialog.sourceLayerId);
|
||||
if (!sourceLayer) {
|
||||
throw new Error('未找到要修改的图片');
|
||||
}
|
||||
return {
|
||||
kind: 'edit',
|
||||
normalizedPrompt,
|
||||
sourceLayer,
|
||||
generationInputs: buildEditGenerationInputs(
|
||||
'修改要求',
|
||||
normalizedPrompt,
|
||||
sourceLayer,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (dialog.mode === 'spec') {
|
||||
const specType = dialog.specType ?? 'custom';
|
||||
const specValues = dialog.specValues ?? DEFAULT_SPEC_FORM_VALUES[specType];
|
||||
return {
|
||||
kind: 'image',
|
||||
normalizedPrompt,
|
||||
input: {
|
||||
prompt: buildSpecPrompt(
|
||||
specType,
|
||||
specValues,
|
||||
Boolean(dialog.specReference?.src),
|
||||
),
|
||||
size: SPEC_GENERATION_SIZE,
|
||||
model: DEFAULT_IMAGE_MODEL,
|
||||
kind: 'spec',
|
||||
...(dialog.specReference?.src
|
||||
? { referenceImageSrcs: [dialog.specReference.src] }
|
||||
: {}),
|
||||
},
|
||||
result: {
|
||||
assetKind: specType === 'icon' ? 'icon-spec' : 'spec',
|
||||
title: `${SPEC_TYPE_LABEL[specType]} ${nextGeneratedIndex}`,
|
||||
generationInputs: buildSpecGenerationInputs(
|
||||
specType,
|
||||
specValues,
|
||||
dialog.specReference,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (dialog.mode === 'character') {
|
||||
const referenceImageSrcs = [
|
||||
dialog.characterSpecReference?.src,
|
||||
...(dialog.characterReferences ?? []).map((reference) => reference.src),
|
||||
].filter((src): src is string => Boolean(src));
|
||||
const imageModel = dialog.imageModel ?? DEFAULT_IMAGE_MODEL;
|
||||
return {
|
||||
kind: 'image',
|
||||
normalizedPrompt,
|
||||
input: {
|
||||
prompt: normalizedPrompt,
|
||||
kind: 'character',
|
||||
model: imageModel,
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||
},
|
||||
result: {
|
||||
assetKind: 'character',
|
||||
title: `角色形象 ${nextGeneratedIndex}`,
|
||||
generationInputs: buildCharacterGenerationInputs(
|
||||
normalizedPrompt,
|
||||
dialog.characterSpecReference,
|
||||
dialog.characterReferences,
|
||||
),
|
||||
},
|
||||
rememberImageModel: imageModel,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'image',
|
||||
normalizedPrompt,
|
||||
input: {
|
||||
prompt: normalizedPrompt,
|
||||
},
|
||||
result: {
|
||||
generationInputs: buildImageGenerationInputs(normalizedPrompt),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -36,22 +36,17 @@ import {
|
||||
ICON_FRAME_ORIGINAL_SIZE,
|
||||
SPEC_FRAME_DISPLAY_SIZE,
|
||||
SPEC_FRAME_ORIGINAL_SIZE,
|
||||
SPEC_GENERATION_SIZE,
|
||||
SPEC_TYPE_LABEL,
|
||||
buildCharacterGenerationInputs,
|
||||
buildEditGenerationInputs,
|
||||
buildIconGenerationInputs,
|
||||
buildImageGenerationInputs,
|
||||
buildQuickEditModelOptions,
|
||||
buildQuickEditSizeOptions,
|
||||
buildSpecGenerationInputs,
|
||||
buildSpecPrompt,
|
||||
calculateCharacterAnimationPrice,
|
||||
createCanvasLayerReference,
|
||||
isCanvasGenerationDialog,
|
||||
resolveCharacterAnimationSourceImageSrc,
|
||||
resolveImageGenerationErrorMessage,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
import { buildImageGenerationSubmissionPlan } from './ImageCanvasGenerationSubmissionModel';
|
||||
import { formatImageSizeValue } from './ImageCanvasEditorModel';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
@@ -801,92 +796,34 @@ export function useImageCanvasGenerationWorkflow({
|
||||
}
|
||||
|
||||
try {
|
||||
if (dialog.mode === 'edit') {
|
||||
const sourceLayer = layers.find(
|
||||
(layer) => layer.id === dialog.sourceLayerId,
|
||||
);
|
||||
if (!sourceLayer) {
|
||||
throw new Error('未找到要修改的图片');
|
||||
}
|
||||
const submissionPlan = buildImageGenerationSubmissionPlan({
|
||||
dialog,
|
||||
layers,
|
||||
nextGeneratedIndex: layerCounterRef.current + 1,
|
||||
});
|
||||
if (submissionPlan.kind === 'edit') {
|
||||
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
|
||||
sourceLayer.src,
|
||||
submissionPlan.sourceLayer.src,
|
||||
);
|
||||
const generated = await editEditorImage({
|
||||
prompt: normalizedPrompt,
|
||||
prompt: submissionPlan.normalizedPrompt,
|
||||
sourceImageSrc: referenceImageSrc,
|
||||
});
|
||||
addGeneratedResultLayer(generated, {
|
||||
sourceLayer,
|
||||
generationInputs: buildEditGenerationInputs(
|
||||
'修改要求',
|
||||
normalizedPrompt,
|
||||
sourceLayer,
|
||||
),
|
||||
});
|
||||
} else if (dialog.mode === 'spec') {
|
||||
const specType = dialog.specType ?? 'custom';
|
||||
const specValues =
|
||||
dialog.specValues ?? DEFAULT_SPEC_FORM_VALUES[specType];
|
||||
const specPrompt = buildSpecPrompt(
|
||||
specType,
|
||||
specValues,
|
||||
Boolean(dialog.specReference?.src),
|
||||
);
|
||||
const generated = await generateEditorImage({
|
||||
prompt: specPrompt,
|
||||
size: SPEC_GENERATION_SIZE,
|
||||
model: DEFAULT_IMAGE_MODEL,
|
||||
kind: 'spec',
|
||||
...(dialog.specReference?.src
|
||||
? { referenceImageSrcs: [dialog.specReference.src] }
|
||||
: {}),
|
||||
});
|
||||
addGeneratedResultLayer(generated, {
|
||||
frame: getGeneratingDialogPlaceholder(dialog),
|
||||
assetKind: specType === 'icon' ? 'icon-spec' : 'spec',
|
||||
title: `${SPEC_TYPE_LABEL[specType]} ${layerCounterRef.current + 1}`,
|
||||
dialogId: canvasDialog?.id,
|
||||
generationInputs: buildSpecGenerationInputs(
|
||||
specType,
|
||||
specValues,
|
||||
dialog.specReference,
|
||||
),
|
||||
});
|
||||
} else if (dialog.mode === 'character') {
|
||||
const referenceImageSrcs = [
|
||||
dialog.characterSpecReference?.src,
|
||||
...(dialog.characterReferences ?? []).map(
|
||||
(reference) => reference.src,
|
||||
),
|
||||
].filter((src): src is string => Boolean(src));
|
||||
const generated = await generateEditorImage({
|
||||
prompt: normalizedPrompt,
|
||||
kind: 'character',
|
||||
model: dialog.imageModel ?? DEFAULT_IMAGE_MODEL,
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||
});
|
||||
setLastImageModel(dialog.imageModel ?? DEFAULT_IMAGE_MODEL);
|
||||
addGeneratedResultLayer(generated, {
|
||||
frame: getGeneratingDialogPlaceholder(dialog),
|
||||
assetKind: 'character',
|
||||
title: `角色形象 ${layerCounterRef.current + 1}`,
|
||||
dialogId: canvasDialog?.id,
|
||||
generationInputs: buildCharacterGenerationInputs(
|
||||
normalizedPrompt,
|
||||
dialog.characterSpecReference,
|
||||
dialog.characterReferences,
|
||||
),
|
||||
sourceLayer: submissionPlan.sourceLayer,
|
||||
generationInputs: submissionPlan.generationInputs,
|
||||
});
|
||||
} else {
|
||||
const generated = await generateEditorImage({
|
||||
prompt: normalizedPrompt,
|
||||
});
|
||||
const generated = await generateEditorImage(submissionPlan.input);
|
||||
if (submissionPlan.rememberImageModel) {
|
||||
setLastImageModel(submissionPlan.rememberImageModel);
|
||||
}
|
||||
addGeneratedResultLayer(generated, {
|
||||
frame: getGeneratingDialogPlaceholder(dialog),
|
||||
assetKind: submissionPlan.result.assetKind,
|
||||
title: submissionPlan.result.title,
|
||||
dialogId: canvasDialog?.id,
|
||||
generationInputs: buildImageGenerationInputs(normalizedPrompt),
|
||||
generationInputs: submissionPlan.result.generationInputs,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user