Files
Genarrative/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts
T
kdletters 870d723eaf 修复画板音视频素材保存
生成视频持久化到 OSS 并回传 objectKey 与 assetObjectId

生成音频透传 OSS 对象身份并支持保存到素材库

普通素材上传支持 MP3 和 MP4 直传 OSS 后确认资产对象

素材库和画布恢复音频视频媒体类型与图层播放

更新画板音视频素材保存相关文档与测试
2026-06-19 15:11:42 +08:00

968 lines
29 KiB
TypeScript

import {
type Dispatch,
type MutableRefObject,
type SetStateAction,
useCallback,
useMemo,
} from 'react';
import {
resolveEditorImageReferenceDataUrl,
resolveEditorImageReferenceDataUrlForGeneration,
} from '../../services/image-editor/editorImageReference';
import {
editEditorImage,
extractEditorUiDesignAssets,
generateEditorBackgroundMusic,
generateEditorCharacterAnimation,
generateEditorIconSpritesheet,
generateEditorImage,
generateEditorSoundEffect,
generateEditorVideo,
} from '../../services/image-editor/editorProjectClient';
import type {
CanvasGenerationDialogState,
CanvasGenerationInputs,
CanvasLayer,
CanvasTool,
CanvasViewport,
CharacterAnimationPanelState,
GenerateDialogState,
QuickEditPanelState,
SidebarPanel,
} from './ImageCanvasEditorTypes';
import {
createAudioResultLayer,
createCharacterAnimationResultLayer,
createGeneratedResultLayer,
createIconSpritesheetResultLayers,
createQuickEditResultLayer,
createVideoResultLayer,
} from './ImageCanvasGenerationLayerModel';
import {
buildCharacterAnimationGenerationInputs,
buildEditGenerationInputs,
buildQuickEditGenerationInputs,
buildUiDesignAssetExtractionGenerationInputs,
ICON_FRAME_DISPLAY_SIZE,
ICON_FRAME_ORIGINAL_SIZE,
isCanvasGenerationDialog,
normalizeEditorImageModel,
normalizeQuickEditPromptSourceReference,
resolveImageGenerationErrorMessage,
} from './ImageCanvasGenerationModel';
import { createQuickEditGenerationDialogDraft } from './ImageCanvasGenerationDialogModel';
import {
buildCharacterAnimationSubmissionPlan,
buildIconSpritesheetGenerationSubmissionPlan,
buildImageGenerationSubmissionPlan,
} from './ImageCanvasGenerationSubmissionModel';
type CanvasSize = { width: number; height: number };
type CanvasGenerationDialogUpdater = (
dialog: CanvasGenerationDialogState,
) => CanvasGenerationDialogState | null;
type GenerationSubmissionWorkflowOptions = {
layers: CanvasLayer[];
canvasSize: CanvasSize;
viewport: CanvasViewport;
layerCounterRef: MutableRefObject<number>;
quickEditPanel: QuickEditPanelState | null;
quickEditSourceLayer: CanvasLayer | null;
setQuickEditPanel: Dispatch<SetStateAction<QuickEditPanelState | null>>;
characterAnimationPanel: CharacterAnimationPanelState | null;
characterAnimationDialog: CanvasGenerationDialogState | null;
characterAnimationSourceLayer: CanvasLayer | null;
setCharacterAnimationPanel: Dispatch<
SetStateAction<CharacterAnimationPanelState | null>
>;
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
openCanvasGenerationDialog: (
dialog: Omit<CanvasGenerationDialogState, 'id'>,
) => string;
updateCanvasGenerationDialogById: (
dialogId: string,
updater: CanvasGenerationDialogUpdater,
) => void;
hasCanvasGenerationDialogById: (dialogId: string) => boolean;
getGeneratingDialogPlaceholder: (
dialog: GenerateDialogState,
) => GenerateDialogState['placeholder'];
appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void;
selectSingleLayer: (layerId: string | null) => void;
fitLayers: (targetLayers?: CanvasLayer[]) => void;
setActiveTool: Dispatch<SetStateAction<CanvasTool>>;
setActiveSidebarPanel: Dispatch<SetStateAction<SidebarPanel | null>>;
rememberImageModel: (imageModel: string) => void;
persistGeneratedAsset?: (layer: CanvasLayer) => void;
};
async function normalizePublicationReferenceImages(
input: Parameters<typeof generateEditorImage>[0],
) {
if (
input.kind !== 'publication-material' ||
!input.referenceImageSrcs?.length
) {
return input;
}
return {
...input,
referenceImageSrcs: await Promise.all(
input.referenceImageSrcs.map((referenceImageSrc) =>
resolveEditorImageReferenceDataUrlForGeneration(referenceImageSrc),
),
),
};
}
export function useImageCanvasGenerationSubmissionWorkflow({
layers,
canvasSize,
viewport,
layerCounterRef,
quickEditPanel,
quickEditSourceLayer,
setQuickEditPanel,
characterAnimationPanel,
characterAnimationDialog,
characterAnimationSourceLayer,
setCharacterAnimationPanel,
setGenerateDialog,
openCanvasGenerationDialog,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
getGeneratingDialogPlaceholder,
appendCanvasLayersWithResources,
selectSingleLayer,
fitLayers,
setActiveTool,
setActiveSidebarPanel,
rememberImageModel,
persistGeneratedAsset,
}: GenerationSubmissionWorkflowOptions) {
const addGeneratedLayersToCanvas = useCallback(
(nextLayers: CanvasLayer[]) => {
appendCanvasLayersWithResources(nextLayers);
nextLayers.forEach((layer) => persistGeneratedAsset?.(layer));
},
[appendCanvasLayersWithResources, persistGeneratedAsset],
);
const addGeneratedResultLayer = useCallback(
(
generated: Parameters<typeof createGeneratedResultLayer>[0]['generated'],
options: {
sourceLayer?: CanvasLayer;
frame?: GenerateDialogState['placeholder'];
assetKind?: CanvasLayer['assetKind'];
title?: string;
dialogId?: string;
generationInputs?: CanvasGenerationInputs;
} = {},
) => {
if (options.dialogId && !hasCanvasGenerationDialogById(options.dialogId)) {
return;
}
layerCounterRef.current += 1;
const generatedIndex = layerCounterRef.current;
const nextLayer = createGeneratedResultLayer({
generated,
generatedIndex,
canvasSize,
viewport,
sourceLayer: options.sourceLayer,
frame: options.frame,
assetKind: options.assetKind,
title: options.title,
generationInputs: options.generationInputs,
});
addGeneratedLayersToCanvas([nextLayer]);
selectSingleLayer(nextLayer.id);
setActiveSidebarPanel('layers');
if (options.sourceLayer) {
setGenerateDialog(null);
setActiveTool('select');
} else if (options.dialogId) {
updateCanvasGenerationDialogById(options.dialogId, (currentDialog) => ({
...currentDialog,
status: 'idle',
composerOpen: true,
generatedLayerId: nextLayer.id,
errorMessage: undefined,
}));
}
if (options.sourceLayer) {
fitLayers([options.sourceLayer, nextLayer]);
}
},
[
addGeneratedLayersToCanvas,
canvasSize,
fitLayers,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
setGenerateDialog,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
viewport,
],
);
const addQuickEditResultLayer = useCallback(
(
generated: Parameters<typeof createQuickEditResultLayer>[0]['generated'],
sourceLayer: CanvasLayer,
generationInputs: CanvasGenerationInputs,
mode: QuickEditPanelState['mode'] = 'quick-edit',
options: {
frame?: GenerateDialogState['placeholder'];
dialogId?: string;
} = {},
) => {
if (options.dialogId && !hasCanvasGenerationDialogById(options.dialogId)) {
return;
}
layerCounterRef.current += 1;
const generatedIndex = layerCounterRef.current;
const nextLayer = createQuickEditResultLayer({
generated,
generatedIndex,
sourceLayer,
generationInputs,
mode,
frame: options.frame,
});
addGeneratedLayersToCanvas([nextLayer]);
selectSingleLayer(mode === 'redraw' ? sourceLayer.id : nextLayer.id);
setActiveSidebarPanel('layers');
if (mode === 'redraw') {
setQuickEditPanel((currentPanel) =>
currentPanel?.sourceLayerId === sourceLayer.id
? {
...currentPanel,
mode: 'redraw',
status: 'idle',
errorMessage: undefined,
}
: currentPanel,
);
} else {
setQuickEditPanel(null);
setActiveTool('select');
if (options.dialogId) {
updateCanvasGenerationDialogById(options.dialogId, (currentDialog) => ({
...currentDialog,
status: 'idle',
composerOpen: false,
generatedLayerId: nextLayer.id,
errorMessage: undefined,
}));
}
}
fitLayers([sourceLayer, nextLayer]);
},
[
addGeneratedLayersToCanvas,
fitLayers,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
setQuickEditPanel,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
],
);
const addIconSpritesheetResultLayers = useCallback(
(
generated: Parameters<
typeof createIconSpritesheetResultLayers
>[0]['generated'],
iconResults: Parameters<
typeof createIconSpritesheetResultLayers
>[0]['iconResults'],
generationInputs: CanvasGenerationInputs,
frame?: GenerateDialogState['placeholder'],
dialogId?: string,
options: {
spritesheetTitle?: string;
} = {},
) => {
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
return [];
}
const startIndex = layerCounterRef.current + 1;
const nextLayers = createIconSpritesheetResultLayers({
generated,
iconResults,
startIndex,
canvasSize,
viewport,
generationInputs,
frame,
spritesheetTitle: options.spritesheetTitle,
});
if (!nextLayers.length) {
return [];
}
layerCounterRef.current += nextLayers.length;
addGeneratedLayersToCanvas(nextLayers);
selectSingleLayer(nextLayers[0]?.id ?? null);
setActiveSidebarPanel('layers');
if (dialogId) {
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
...currentDialog,
status: 'idle',
composerOpen: true,
generatedLayerId: nextLayers[0]?.id,
errorMessage: undefined,
}));
}
setActiveTool('select');
return nextLayers;
},
[
addGeneratedLayersToCanvas,
canvasSize,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
viewport,
],
);
const addVideoResultLayer = useCallback(
(
generated: Parameters<typeof createVideoResultLayer>[0]['generated'],
title: string,
generationInputs: CanvasGenerationInputs,
frame?: GenerateDialogState['placeholder'],
dialogId?: string,
) => {
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
return;
}
layerCounterRef.current += 1;
const generatedIndex = layerCounterRef.current;
const nextLayer = createVideoResultLayer({
generated,
generatedIndex,
title,
canvasSize,
viewport,
generationInputs,
frame,
});
addGeneratedLayersToCanvas([nextLayer]);
selectSingleLayer(nextLayer.id);
setActiveSidebarPanel('layers');
if (dialogId) {
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
...currentDialog,
status: 'idle',
composerOpen: true,
generatedLayerId: nextLayer.id,
errorMessage: undefined,
}));
}
setActiveTool('select');
},
[
addGeneratedLayersToCanvas,
canvasSize,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
viewport,
],
);
const addAudioResultLayer = useCallback(
(
generated: Parameters<typeof createAudioResultLayer>[0]['generated'],
title: string,
generationInputs: CanvasGenerationInputs,
frame?: GenerateDialogState['placeholder'],
dialogId?: string,
) => {
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
return;
}
layerCounterRef.current += 1;
const generatedIndex = layerCounterRef.current;
const nextLayer = createAudioResultLayer({
generated,
generatedIndex,
title,
canvasSize,
viewport,
generationInputs,
frame,
});
addGeneratedLayersToCanvas([nextLayer]);
selectSingleLayer(nextLayer.id);
setActiveSidebarPanel('layers');
if (dialogId) {
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
...currentDialog,
status: 'idle',
composerOpen: true,
generatedLayerId: nextLayer.id,
errorMessage: undefined,
}));
}
setActiveTool('select');
},
[
addGeneratedLayersToCanvas,
canvasSize,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
viewport,
],
);
const addCharacterAnimationResultLayer = useCallback(
(
generated: Parameters<
typeof createCharacterAnimationResultLayer
>[0]['generated'],
title: string,
generationInputs: CanvasGenerationInputs,
frame?: GenerateDialogState['placeholder'],
dialogId?: string,
) => {
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
return;
}
layerCounterRef.current += 1;
const generatedIndex = layerCounterRef.current;
const nextLayer = createCharacterAnimationResultLayer({
generated,
generatedIndex,
title,
canvasSize,
viewport,
generationInputs,
frame,
});
if (!nextLayer) {
return;
}
addGeneratedLayersToCanvas([nextLayer]);
selectSingleLayer(nextLayer.id);
setActiveSidebarPanel('layers');
if (dialogId) {
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
...currentDialog,
status: 'idle',
composerOpen: true,
generatedLayerId: nextLayer.id,
characterAnimationResult: generated,
errorMessage: undefined,
}));
}
setActiveTool('select');
},
[
addGeneratedLayersToCanvas,
canvasSize,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
viewport,
],
);
const extractUiDesignAssets = useCallback(
async (sourceLayer: CanvasLayer) => {
if (sourceLayer.assetKind !== 'ui-design') {
return;
}
try {
const sourceImageSrc = await resolveEditorImageReferenceDataUrl(
sourceLayer.objectKey?.trim() || sourceLayer.src,
);
const generated = await extractEditorUiDesignAssets({
sourceImageSrc,
});
const extractionFrame = {
x: sourceLayer.x + sourceLayer.width + 32,
y: sourceLayer.y,
width: ICON_FRAME_DISPLAY_SIZE.width,
height: ICON_FRAME_DISPLAY_SIZE.height,
originalWidth: ICON_FRAME_ORIGINAL_SIZE.width,
originalHeight: ICON_FRAME_ORIGINAL_SIZE.height,
};
const nextLayers = addIconSpritesheetResultLayers(
generated,
generated.iconImageSrcs,
buildUiDesignAssetExtractionGenerationInputs(sourceLayer),
extractionFrame,
undefined,
{
spritesheetTitle: `${sourceLayer.title} 素材图集`,
},
);
if (nextLayers.length) {
fitLayers([sourceLayer, ...nextLayers]);
}
} catch (error) {
window.alert(
error instanceof Error && error.message.trim()
? error.message
: '提取素材失败',
);
}
},
[addIconSpritesheetResultLayers, fitLayers],
);
const submitIconSpritesheetGeneration = useCallback(
async (dialog: GenerateDialogState) => {
if (dialog.mode !== 'icon') {
return;
}
const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null;
const setSubmittingIconDialog = (
nextDialog: CanvasGenerationDialogState,
) => {
updateCanvasGenerationDialogById(nextDialog.id, () => nextDialog);
};
const submissionPlan =
buildIconSpritesheetGenerationSubmissionPlan(dialog);
if (submissionPlan.ok === false) {
if (canvasDialog) {
setSubmittingIconDialog({
...canvasDialog,
status: 'failed',
composerOpen: true,
errorMessage: submissionPlan.errorMessage,
});
}
return;
}
if (!canvasDialog) {
return;
}
setSubmittingIconDialog({
...canvasDialog,
iconDescriptions: submissionPlan.iconDescriptions,
status: 'generating',
composerOpen: false,
errorMessage: undefined,
});
try {
const generated = await generateEditorIconSpritesheet(
submissionPlan.input,
);
rememberImageModel(submissionPlan.rememberImageModel);
addIconSpritesheetResultLayers(
generated,
generated.iconImageSrcs,
submissionPlan.generationInputs,
getGeneratingDialogPlaceholder(dialog),
canvasDialog.id,
);
} catch (error) {
setSubmittingIconDialog({
...canvasDialog,
iconDescriptions: submissionPlan.iconDescriptions,
status: 'failed',
composerOpen: true,
errorMessage: resolveImageGenerationErrorMessage(error),
});
}
},
[
addIconSpritesheetResultLayers,
getGeneratingDialogPlaceholder,
rememberImageModel,
updateCanvasGenerationDialogById,
],
);
const submitQuickEdit = useCallback(async () => {
if (!quickEditPanel || !quickEditSourceLayer) {
return;
}
const panelMode = quickEditPanel.mode ?? 'quick-edit';
const quickEditReferences =
panelMode === 'redraw' ? [] : (quickEditPanel.quickEditReferences ?? []);
const sourceReferenceIndex = quickEditReferences.length + 1;
const basePrompt =
quickEditPanel.prompt.trim() ||
(panelMode === 'redraw' ? '重绘图片' : '快速编辑图片');
const normalizedPrompt =
panelMode === 'redraw'
? basePrompt
: normalizeQuickEditPromptSourceReference(
basePrompt,
sourceReferenceIndex,
);
let quickEditDialogId: string | undefined;
if (panelMode === 'quick-edit') {
quickEditDialogId = openCanvasGenerationDialog(
createQuickEditGenerationDialogDraft({
sourceLayer: quickEditSourceLayer,
prompt: normalizedPrompt,
status: 'generating',
references: quickEditReferences,
}),
);
setQuickEditPanel(null);
selectSingleLayer(null);
} else {
setQuickEditPanel({
...quickEditPanel,
prompt: normalizedPrompt,
status: 'generating',
errorMessage: undefined,
});
}
try {
const extraReferenceImageSrcs = await Promise.all(
quickEditReferences.map((reference) =>
resolveEditorImageReferenceDataUrl(
reference.objectKey?.trim() || reference.src,
),
),
);
const sourceReferenceImageSrc = await resolveEditorImageReferenceDataUrl(
quickEditSourceLayer.objectKey?.trim() || quickEditSourceLayer.src,
);
const generated = await generateEditorImage({
prompt: normalizedPrompt,
size: quickEditPanel.size,
kind: 'quick-edit',
model: normalizeEditorImageModel(quickEditPanel.model),
referenceImageSrcs: [
...extraReferenceImageSrcs,
sourceReferenceImageSrc,
],
});
addQuickEditResultLayer(
generated,
quickEditSourceLayer,
panelMode === 'redraw'
? buildEditGenerationInputs(
'重绘提示词',
normalizedPrompt,
quickEditSourceLayer,
)
: buildQuickEditGenerationInputs(
'快速编辑提示词',
normalizedPrompt,
quickEditSourceLayer,
quickEditReferences,
),
panelMode,
quickEditDialogId
? {
frame: getGeneratingDialogPlaceholder({
id: quickEditDialogId,
mode: 'quick-edit',
prompt: normalizedPrompt,
status: 'generating',
}),
dialogId: quickEditDialogId,
}
: {},
);
} catch (error) {
if (quickEditDialogId) {
updateCanvasGenerationDialogById(quickEditDialogId, (currentDialog) => ({
...currentDialog,
prompt: normalizedPrompt,
status: 'failed',
composerOpen: false,
errorMessage: resolveImageGenerationErrorMessage(error),
}));
}
setQuickEditPanel({
...quickEditPanel,
prompt: normalizedPrompt,
status: 'failed',
errorMessage: resolveImageGenerationErrorMessage(error),
});
}
}, [
addQuickEditResultLayer,
getGeneratingDialogPlaceholder,
openCanvasGenerationDialog,
quickEditPanel,
quickEditSourceLayer,
setQuickEditPanel,
updateCanvasGenerationDialogById,
]);
const submitImageGeneration = useCallback(
async (dialog: GenerateDialogState) => {
const normalizedPrompt =
dialog.prompt.trim() ||
(dialog.mode === 'edit'
? '修改当前图片'
: dialog.mode === 'audio-sound-effect'
? '游戏音效'
: dialog.mode === 'audio-background-music'
? '游戏背景音乐'
: 'AI 生成图片');
const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null;
if (canvasDialog) {
updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({
...currentDialog,
prompt: normalizedPrompt,
status: 'generating',
composerOpen: false,
}));
} else {
setGenerateDialog({
...dialog,
prompt: normalizedPrompt,
status: 'generating',
composerOpen: dialog.mode === 'edit',
});
}
try {
const submissionPlan = buildImageGenerationSubmissionPlan({
dialog,
layers,
nextGeneratedIndex: layerCounterRef.current + 1,
});
if (submissionPlan.kind === 'edit') {
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
submissionPlan.sourceLayer.src,
);
const generated = await editEditorImage({
prompt: submissionPlan.normalizedPrompt,
sourceImageSrc: referenceImageSrc,
});
addGeneratedResultLayer(generated, {
sourceLayer: submissionPlan.sourceLayer,
generationInputs: submissionPlan.generationInputs,
});
} else if (submissionPlan.kind === 'video') {
const generated = await generateEditorVideo(submissionPlan.input);
addVideoResultLayer(
generated,
submissionPlan.result.title,
submissionPlan.result.generationInputs,
getGeneratingDialogPlaceholder(dialog),
canvasDialog?.id,
);
} else if (submissionPlan.kind === 'audio') {
const generated =
submissionPlan.audioKind === 'sound-effect'
? await generateEditorSoundEffect(submissionPlan.input)
: await generateEditorBackgroundMusic(submissionPlan.input);
addAudioResultLayer(
generated,
submissionPlan.result.title,
submissionPlan.result.generationInputs,
getGeneratingDialogPlaceholder(dialog),
canvasDialog?.id,
);
} else {
const imageGenerationInput =
submissionPlan.input.kind === 'publication-material'
? await normalizePublicationReferenceImages(submissionPlan.input)
: submissionPlan.input;
const generated = await generateEditorImage(imageGenerationInput);
if (submissionPlan.rememberImageModel) {
rememberImageModel(submissionPlan.rememberImageModel);
}
addGeneratedResultLayer(generated, {
frame: getGeneratingDialogPlaceholder(dialog),
assetKind: submissionPlan.result.assetKind,
title: submissionPlan.result.title,
dialogId: canvasDialog?.id,
generationInputs: submissionPlan.result.generationInputs,
});
}
} catch (error) {
if (canvasDialog) {
updateCanvasGenerationDialogById(canvasDialog.id, () => ({
...canvasDialog,
prompt: normalizedPrompt,
status: 'failed',
composerOpen: true,
errorMessage: resolveImageGenerationErrorMessage(error),
}));
} else {
setGenerateDialog({
...dialog,
prompt: normalizedPrompt,
status: 'failed',
composerOpen: true,
errorMessage: resolveImageGenerationErrorMessage(error),
});
}
}
},
[
addGeneratedResultLayer,
addAudioResultLayer,
addVideoResultLayer,
getGeneratingDialogPlaceholder,
layerCounterRef,
layers,
rememberImageModel,
setGenerateDialog,
updateCanvasGenerationDialogById,
],
);
const submitCharacterAnimation = useCallback(async () => {
if (!characterAnimationPanel || !characterAnimationSourceLayer) {
return;
}
const submissionPlan = buildCharacterAnimationSubmissionPlan({
panel: characterAnimationPanel,
sourceLayer: characterAnimationSourceLayer,
});
const canvasDialog = characterAnimationDialog;
if (canvasDialog?.mode === 'character-animation') {
updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({
...currentDialog,
prompt: submissionPlan.promptText,
characterAnimationResolution: characterAnimationPanel.resolution,
characterAnimationRatio: characterAnimationPanel.ratio,
characterAnimationFrameCount: characterAnimationPanel.frameCount,
characterAnimationDurationSeconds:
characterAnimationPanel.durationSeconds,
characterAnimationResult: undefined,
status: 'generating',
composerOpen: false,
errorMessage: undefined,
}));
}
const nextPanel = {
...characterAnimationPanel,
promptText: submissionPlan.promptText,
status: 'generating' as const,
errorMessage: undefined,
result: undefined,
};
if (!canvasDialog) {
setCharacterAnimationPanel(nextPanel);
}
try {
const result = await generateEditorCharacterAnimation(
submissionPlan.input,
);
if (canvasDialog) {
addCharacterAnimationResultLayer(
result,
'角色动作',
buildCharacterAnimationGenerationInputs(
submissionPlan.promptText,
characterAnimationSourceLayer,
),
getGeneratingDialogPlaceholder(canvasDialog),
canvasDialog.id,
);
return;
}
setCharacterAnimationPanel((currentPanel) =>
currentPanel
? {
...currentPanel,
status: 'completed',
result,
}
: currentPanel,
);
} catch (error) {
if (canvasDialog) {
updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({
...currentDialog,
prompt: submissionPlan.promptText,
characterAnimationResolution: characterAnimationPanel.resolution,
characterAnimationRatio: characterAnimationPanel.ratio,
characterAnimationFrameCount: characterAnimationPanel.frameCount,
characterAnimationDurationSeconds:
characterAnimationPanel.durationSeconds,
status: 'failed',
composerOpen: true,
errorMessage:
error instanceof Error && error.message.trim()
? error.message
: '生成角色动画失败',
}));
return;
}
setCharacterAnimationPanel((currentPanel) =>
currentPanel
? {
...currentPanel,
status: 'failed',
errorMessage:
error instanceof Error && error.message.trim()
? error.message
: '生成角色动画失败',
}
: currentPanel,
);
}
}, [
addCharacterAnimationResultLayer,
characterAnimationDialog,
characterAnimationPanel,
characterAnimationSourceLayer,
getGeneratingDialogPlaceholder,
setCharacterAnimationPanel,
updateCanvasGenerationDialogById,
]);
return useMemo(
() => ({
submitIconSpritesheetGeneration,
extractUiDesignAssets,
submitQuickEdit,
submitImageGeneration,
submitCharacterAnimation,
}),
[
submitCharacterAnimation,
extractUiDesignAssets,
submitIconSpritesheetGeneration,
submitImageGeneration,
submitQuickEdit,
],
);
}