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, }, }; }