Files
Genarrative/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts
T
kdletters 6d964937db 完善画布生成面板交互
补齐普通生图参考图来源菜单和画布选择流程

接入UI设计图与视频生成面板的提交链路

让生成引用上传目标支持多种生成面板

统一图片信息弹窗断言并补充相关测试

修复图标按钮浮层锚点ref与视频生成类型契约
2026-06-17 21:49:32 +08:00

320 lines
8.5 KiB
TypeScript

import type {
EditorCharacterAnimationGenerationInput,
EditorIconSpritesheetGenerationInput,
EditorImageGenerationInput,
EditorVideoGenerationInput,
} from '../../services/image-editor/editorProjectClient';
import type {
CanvasGenerationInputs,
CanvasLayer,
CharacterAnimationPanelState,
GenerateDialogState,
} from './ImageCanvasEditorTypes';
import {
CHARACTER_ANIMATION_MODEL,
DEFAULT_IMAGE_MODEL,
DEFAULT_ICON_DESCRIPTIONS,
DEFAULT_SPEC_FORM_VALUES,
SPEC_GENERATION_SIZE,
SPEC_TYPE_LABEL,
buildIconGenerationInputs,
buildCharacterGenerationInputs,
buildEditGenerationInputs,
buildImageGenerationInputs,
buildSpecGenerationInputs,
buildSpecPrompt,
buildUiDesignGenerationInputs,
buildVideoGenerationInputs,
calculateCharacterAnimationPrice,
calculateEditorVideoPrice,
resolveCharacterAnimationSourceImageSrc,
} 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;
}
| {
kind: 'video';
normalizedPrompt: string;
input: EditorVideoGenerationInput;
result: {
title: string;
generationInputs: CanvasGenerationInputs;
};
};
export type IconSpritesheetGenerationSubmissionPlan =
| {
ok: false;
errorMessage: string;
}
| {
ok: true;
iconDescriptions: string[];
input: EditorIconSpritesheetGenerationInput;
generationInputs: CanvasGenerationInputs;
rememberImageModel: string;
};
export type CharacterAnimationSubmissionPlan = {
promptText: string;
input: EditorCharacterAnimationGenerationInput;
};
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,
};
}
if (dialog.mode === 'ui-design') {
const imageModel = dialog.imageModel ?? DEFAULT_IMAGE_MODEL;
const referenceImageSrcs = [dialog.uiDesignSpecReference?.src].filter(
(src): src is string => Boolean(src),
);
return {
kind: 'image',
normalizedPrompt,
input: {
prompt: normalizedPrompt,
kind: 'ui-design',
model: imageModel,
aspectRatio: dialog.aspectRatio ?? '16:9',
imageSize: dialog.imageSize ?? '1K',
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
},
result: {
assetKind: 'ui-design',
title: `UI设计图 ${nextGeneratedIndex}`,
generationInputs: buildUiDesignGenerationInputs(
normalizedPrompt,
dialog.uiDesignSpecReference,
),
},
rememberImageModel: imageModel,
};
}
if (dialog.mode === 'video') {
const resolution = dialog.videoResolution ?? '480p';
const durationSeconds = dialog.videoDurationSeconds ?? 4;
const model = dialog.videoModel ?? 'seedance2.0';
return {
kind: 'video',
normalizedPrompt,
input: {
prompt: normalizedPrompt,
model,
aspectRatio: '16:9',
durationSeconds,
resolution,
mode: 'std',
sound: 'off',
priceMudPoints: calculateEditorVideoPrice(
resolution,
durationSeconds,
),
},
result: {
title: `生成视频 ${nextGeneratedIndex}`,
generationInputs: buildVideoGenerationInputs(
normalizedPrompt,
dialog.generationReferences,
),
},
};
}
return {
kind: 'image',
normalizedPrompt,
input: {
prompt: normalizedPrompt,
...(dialog.generationReferences?.length
? {
referenceImageSrcs: dialog.generationReferences.map(
(reference) => reference.src,
),
}
: {}),
},
result: {
generationInputs: buildImageGenerationInputs(
normalizedPrompt,
dialog.generationReferences,
),
},
};
}
export function buildIconSpritesheetGenerationSubmissionPlan(
dialog: GenerateDialogState,
): IconSpritesheetGenerationSubmissionPlan {
const iconDescriptions = (dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS)
.map((description) => description.trim())
.filter(Boolean);
if (!dialog.iconSpecReference) {
return {
ok: false,
errorMessage: '请选择图标素材规范',
};
}
if (!iconDescriptions.length) {
return {
ok: false,
errorMessage: '请填写素材描述',
};
}
const rememberImageModel = dialog.imageModel ?? DEFAULT_IMAGE_MODEL;
return {
ok: true,
iconDescriptions,
input: {
referenceImageSrc: dialog.iconSpecReference.src,
iconDescriptions,
model: rememberImageModel,
aspectRatio: dialog.aspectRatio ?? '1:1',
imageSize: dialog.imageSize ?? '1K',
},
generationInputs: buildIconGenerationInputs(
iconDescriptions,
dialog.iconSpecReference,
),
rememberImageModel,
};
}
export function buildCharacterAnimationSubmissionPlan({
panel,
sourceLayer,
}: {
panel: CharacterAnimationPanelState;
sourceLayer: CanvasLayer;
}): CharacterAnimationSubmissionPlan {
const promptText = panel.promptText.trim();
return {
promptText,
input: {
sourceLayerId: sourceLayer.id,
sourceImageSrc: resolveCharacterAnimationSourceImageSrc(sourceLayer),
sourceWidth: sourceLayer.originalWidth,
sourceHeight: sourceLayer.originalHeight,
promptText,
resolution: panel.resolution,
ratio: panel.ratio,
frameCount: panel.frameCount,
durationSeconds: panel.durationSeconds,
priceMudPoints: calculateCharacterAnimationPrice(
panel.resolution,
panel.durationSeconds,
),
model: CHARACTER_ANIMATION_MODEL,
},
};
}